feat(cli)!: subject-based command tree (tea-style) + global --output flag
BREAKING:
- Flat oo verb-noun layout replaced with subject → verb, mirroring the
gitea `tea` CLI. Every subject is one file in cmd/oo/:
oo calendar list | events | add | delete
oo projects list | get | milestones | create | update | delete
oo tasks list | get | create | update | delete | subtask add
oo users list | self (alias: oo whoami)
oo contacts list | get | delete | info-add
oo persons list | create | delete
oo companies list | create | delete
oo opportunities list | get | create | delete | stages | member-add
oo cases list | create | delete | member-add
oo crm-tasks list | create | delete | categories
oo applications sync
- Global `--output/-o table|json` flag on the root command; every list uses
a single printTable helper that flattens nested bidCurrency → abbreviation
for readability, truncates long cells, and dumps verbatim in JSON mode.
Library additions surfaced in the CLI:
- oo users / oo whoami via SelfUserID + GetUsers.
- oo projects full CRUD via Create/Update/Delete/GetProjectByID/Milestones.
- oo crm-tasks and oo cases via ListCRMTasks/CreateCRMTask/DeleteCRMTask/
ListTaskCategories/ListCases/CreateCase/DeleteCase/AddCaseMember.
- oo contacts info-add via AddContactInfo.
- oo opportunities stages/member-add via ListDealStages/AddOpportunityMember.
Internal:
- cmd/oo/common.go — rootCmd, newOO (uses AuthenticateContext), printTable,
printObject, fmtCell, deref* helpers.
- cmd/oo/crm.go deleted; content split into contacts.go, opportunities.go,
cases.go, crm_tasks.go for locality.
Docs:
- README: new subject→verb table + migration note.
- AGENTS.md: CLI section rewritten; new rule enforcing subject→verb and
printTable/printObject routing.
- CHANGELOG: 0.5.0 entry with full old→new mapping.
Made-with: Cursor
This commit is contained in:
+16
-5
@@ -7,19 +7,30 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func cmdAppsSync() *cobra.Command {
|
||||
var applicationsCmd = &cobra.Command{
|
||||
Use: "applications",
|
||||
Aliases: []string{"apps"},
|
||||
Short: "Job-application workflow (CV tree → CRM)",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(applicationsCmd)
|
||||
applicationsCmd.AddCommand(applicationsSyncCmd())
|
||||
}
|
||||
|
||||
func applicationsSyncCmd() *cobra.Command {
|
||||
var apply, verbose bool
|
||||
var base string
|
||||
cmd := &cobra.Command{
|
||||
Use: "applications-sync",
|
||||
Short: "Sync applications/*/README.md into CRM",
|
||||
Use: "sync",
|
||||
Short: "Sync applications/*/README.md into CRM (dry-run by default)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if base == "" {
|
||||
return fmt.Errorf("use --path to applications year dir (e.g. .../applications/2026)")
|
||||
return fmt.Errorf("--path is required (points at applications year dir, e.g. .../applications/2026)")
|
||||
}
|
||||
paths, err := applications.Discover(base)
|
||||
if err != nil {
|
||||
|
||||
+39
-24
@@ -6,12 +6,26 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func cmdCalList() *cobra.Command {
|
||||
var calendarCmd = &cobra.Command{
|
||||
Use: "calendar",
|
||||
Aliases: []string{"cal"},
|
||||
Short: "Calendars and events",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(calendarCmd)
|
||||
calendarCmd.AddCommand(calListCmd())
|
||||
calendarCmd.AddCommand(calEventsCmd())
|
||||
calendarCmd.AddCommand(calAddCmd())
|
||||
calendarCmd.AddCommand(calDeleteCmd())
|
||||
}
|
||||
|
||||
func calListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "cal-list",
|
||||
Short: "List calendars (default date span)",
|
||||
Use: "list",
|
||||
Short: "List calendars",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -19,19 +33,19 @@ func cmdCalList() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
printTable([]string{"objectId", "title", "textColor", "backgroundColor", "isEditable"}, out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cmdCalEvents() *cobra.Command {
|
||||
func calEventsCmd() *cobra.Command {
|
||||
var start, end string
|
||||
cmd := &cobra.Command{
|
||||
Use: "cal-events",
|
||||
Short: "List calendar data for period (default: next 7 days)",
|
||||
Use: "events",
|
||||
Short: "List calendar events for a date range (default: next 7 days)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -43,7 +57,7 @@ func cmdCalEvents() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
printTable([]string{"objectId", "title", "start", "end", "allDayLong"}, out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -52,15 +66,15 @@ func cmdCalEvents() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCalAdd() *cobra.Command {
|
||||
func calAddCmd() *cobra.Command {
|
||||
var cal, desc string
|
||||
var allDay bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "cal-add TITLE START END",
|
||||
Short: "Add calendar event",
|
||||
Use: "add TITLE START END",
|
||||
Short: "Add a calendar event",
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,23 +82,24 @@ func cmdCalAdd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(ev)
|
||||
printObject(ev)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&cal, "calendar", "", "calendar id (default from env)")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "")
|
||||
cmd.Flags().BoolVar(&allDay, "all-day", false, "")
|
||||
cmd.Flags().StringVar(&cal, "calendar", "", "calendar id (default from env OO_CALENDAR_ID)")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "event description")
|
||||
cmd.Flags().BoolVar(&allDay, "all-day", false, "mark as all-day event")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCalDel() *cobra.Command {
|
||||
func calDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "cal-delete EVENT_ID [EVENT_ID...]",
|
||||
Short: "Delete calendar events",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Use: "delete EVENT_ID [EVENT_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more calendar events",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,7 +108,7 @@ func cmdCalDel() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var casesCmd = &cobra.Command{
|
||||
Use: "cases",
|
||||
Aliases: []string{"case"},
|
||||
Short: "CRM cases",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(casesCmd)
|
||||
casesCmd.AddCommand(casesListCmd())
|
||||
casesCmd.AddCommand(casesCreateCmd())
|
||||
casesCmd.AddCommand(casesDeleteCmd())
|
||||
casesCmd.AddCommand(casesMemberAddCmd())
|
||||
}
|
||||
|
||||
func casesListCmd() *cobra.Command {
|
||||
var count, offset int
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List CRM cases",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, total, err := c.ListCases(cmd.Context(), count, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Printf("total: %d (shown: %d)\n", total, len(list))
|
||||
}
|
||||
printTable([]string{"id", "title", "isClosed", "created"}, list)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVar(&count, "count", 50, "")
|
||||
cmd.Flags().IntVar(&offset, "offset", 0, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func casesCreateCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "create TITLE",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create a case",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.CreateCase(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func casesDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete CASE_ID [CASE_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more cases",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range args {
|
||||
out, err := c.DeleteCase(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func casesMemberAddCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "member-add CASE_ID CONTACT_ID",
|
||||
Short: "Attach a contact to a case",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.AddCaseMember(cmd.Context(), args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+126
-22
@@ -5,40 +5,36 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// outputFormat is the value of the global --output/-o flag. Valid values:
|
||||
// "table" (default, tabwriter-rendered), "json" (machine-readable).
|
||||
var outputFormat = "table"
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "oo",
|
||||
Short: "OnlyOffice Workspace CLI (Go port of cv/bin/office)",
|
||||
Use: "oo",
|
||||
Short: "OnlyOffice Workspace CLI — subject-based command tree",
|
||||
Long: "oo is a thin CLI over github.com/eslider/go-onlyoffice.\nCommands are grouped by OnlyOffice subject (calendar, projects, tasks, users, persons, companies, opportunities, cases, crm-tasks, applications).",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", "output format: table|json")
|
||||
}
|
||||
|
||||
// execute runs the root command. Exported only to main.go in the same package.
|
||||
func execute() error { return rootCmd.Execute() }
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cmdCalList())
|
||||
rootCmd.AddCommand(cmdCalEvents())
|
||||
rootCmd.AddCommand(cmdCalAdd())
|
||||
rootCmd.AddCommand(cmdCalDel())
|
||||
rootCmd.AddCommand(cmdTaskList())
|
||||
rootCmd.AddCommand(cmdTaskAdd())
|
||||
rootCmd.AddCommand(cmdSubtaskAdd())
|
||||
rootCmd.AddCommand(cmdTaskUpdate())
|
||||
rootCmd.AddCommand(cmdCRMContacts())
|
||||
rootCmd.AddCommand(cmdCRMAddContact())
|
||||
rootCmd.AddCommand(cmdCRMDeals())
|
||||
rootCmd.AddCommand(cmdCRMAddDeal())
|
||||
rootCmd.AddCommand(cmdCRMCases())
|
||||
rootCmd.AddCommand(cmdAppsSync())
|
||||
}
|
||||
|
||||
// newOO loads env (incl. .env in CWD) and returns an authenticated client.
|
||||
// godotenv is a CLI-only concern; the library itself never loads dotfiles.
|
||||
func newOO() (*onlyoffice.Client, error) {
|
||||
func newOO(cmd *cobra.Command) (*onlyoffice.Client, error) {
|
||||
_ = godotenv.Load()
|
||||
creds := onlyoffice.GetEnvironmentCredentials()
|
||||
if creds.Url == "" || creds.User == "" || creds.Password == "" {
|
||||
@@ -46,20 +42,110 @@ func newOO() (*onlyoffice.Client, error) {
|
||||
}
|
||||
c := onlyoffice.NewClient(creds)
|
||||
c.SetDefaults(onlyoffice.GetEnvironmentDefaults())
|
||||
if err := c.Authenticate(); err != nil {
|
||||
if err := c.AuthenticateContext(cmd.Context()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// printJSON dumps any value as indented JSON.
|
||||
func printJSON(v any) {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
|
||||
// printObject renders a single value. JSON output dumps verbatim; table output
|
||||
// prints key/value pairs when v is a map, otherwise falls back to JSON.
|
||||
func printObject(v any) {
|
||||
if outputFormat == "json" {
|
||||
printJSON(v)
|
||||
return
|
||||
}
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
printJSON(v)
|
||||
return
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
|
||||
for _, k := range sortedKeys(m) {
|
||||
fmt.Fprintf(w, "%s\t%s\n", k, fmtCell(m[k]))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// printTable renders a list of rows. headers select & order the columns.
|
||||
// When outputFormat=="json" the raw slice is dumped as-is.
|
||||
func printTable(headers []string, rows []map[string]any) {
|
||||
if outputFormat == "json" {
|
||||
printJSON(rows)
|
||||
return
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
fmt.Println("(empty)")
|
||||
return
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
|
||||
fmt.Fprintln(w, strings.Join(headers, "\t"))
|
||||
for _, row := range rows {
|
||||
cells := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
cells[i] = fmtCell(row[h])
|
||||
}
|
||||
fmt.Fprintln(w, strings.Join(cells, "\t"))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// fmtCell turns an arbitrary value into a compact table cell.
|
||||
func fmtCell(v any) string {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return truncate(x, 80)
|
||||
case bool:
|
||||
if x {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
if x == float64(int64(x)) {
|
||||
return strconv.FormatInt(int64(x), 10)
|
||||
}
|
||||
return strconv.FormatFloat(x, 'f', -1, 64)
|
||||
case int, int32, int64:
|
||||
return fmt.Sprintf("%d", x)
|
||||
default:
|
||||
b, _ := json.Marshal(x)
|
||||
return truncate(string(b), 80)
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
if len(s) > n {
|
||||
return s[:n-1] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]any) []string {
|
||||
ks := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
// Deterministic ordering; small map so O(n log n) is fine.
|
||||
for i := 1; i < len(ks); i++ {
|
||||
for j := i; j > 0 && ks[j-1] > ks[j]; j-- {
|
||||
ks[j-1], ks[j] = ks[j], ks[j-1]
|
||||
}
|
||||
}
|
||||
return ks
|
||||
}
|
||||
|
||||
// flexIDFloat coerces OnlyOffice numeric id fields surfaced as float64 / int
|
||||
// / string into a float64. CLI-only; the library's own flexInt is unexported.
|
||||
// / string into a float64.
|
||||
func flexIDFloat(v any) float64 {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
@@ -71,3 +157,21 @@ func flexIDFloat(v any) float64 {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// idString returns the id field as a string (works for int/float/string).
|
||||
func idString(m map[string]any, key string) string {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(x), 10)
|
||||
case int:
|
||||
return strconv.Itoa(x)
|
||||
default:
|
||||
return fmt.Sprint(x)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Contacts cover both persons and companies; OnlyOffice exposes them through
|
||||
// the same `crm/contact/*` endpoint family. `oo persons` and `oo companies`
|
||||
// are filtered views on top of `oo contacts list`, plus dedicated create
|
||||
// commands that pick the right library call.
|
||||
|
||||
var contactsCmd = &cobra.Command{
|
||||
Use: "contacts",
|
||||
Aliases: []string{"contact"},
|
||||
Short: "CRM contacts (persons + companies)",
|
||||
}
|
||||
|
||||
var personsCmd = &cobra.Command{
|
||||
Use: "persons",
|
||||
Aliases: []string{"person"},
|
||||
Short: "CRM persons (contacts with isCompany=false)",
|
||||
}
|
||||
|
||||
var companiesCmd = &cobra.Command{
|
||||
Use: "companies",
|
||||
Aliases: []string{"company"},
|
||||
Short: "CRM companies (contacts with isCompany=true)",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(contactsCmd)
|
||||
rootCmd.AddCommand(personsCmd)
|
||||
rootCmd.AddCommand(companiesCmd)
|
||||
|
||||
contactsCmd.AddCommand(contactsListCmd(nil))
|
||||
contactsCmd.AddCommand(contactsGetCmd())
|
||||
contactsCmd.AddCommand(contactsDeleteCmd())
|
||||
contactsCmd.AddCommand(contactsInfoAddCmd())
|
||||
|
||||
only := true
|
||||
personsCmd.AddCommand(contactsListCmd(&only)) // persons only
|
||||
personsCmd.AddCommand(personsCreateCmd())
|
||||
personsCmd.AddCommand(contactsDeleteCmd())
|
||||
|
||||
onlyCo := false
|
||||
companiesCmd.AddCommand(contactsListCmd(&onlyCo)) // companies only
|
||||
companiesCmd.AddCommand(companiesCreateCmd())
|
||||
companiesCmd.AddCommand(contactsDeleteCmd())
|
||||
}
|
||||
|
||||
// contactsListCmd returns a `list` subcommand.
|
||||
//
|
||||
// - personsOnly == nil → list all contacts
|
||||
// - personsOnly == &true → keep only persons (isCompany=false)
|
||||
// - personsOnly == &false → keep only companies (isCompany=true)
|
||||
func contactsListCmd(personsOnly *bool) *cobra.Command {
|
||||
var search string
|
||||
var count, offset int
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List contacts",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, total, err := c.ListContacts(cmd.Context(), count, offset, search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filtered := list
|
||||
if personsOnly != nil {
|
||||
filtered = filtered[:0]
|
||||
for _, row := range list {
|
||||
isCo, _ := row["isCompany"].(bool)
|
||||
if *personsOnly && isCo {
|
||||
continue
|
||||
}
|
||||
if !*personsOnly && !isCo {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, row)
|
||||
}
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Printf("total: %d (shown: %d)\n", total, len(filtered))
|
||||
}
|
||||
printTable([]string{"id", "displayName", "isCompany", "email", "companyName"}, filtered)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&search, "search", "", "search filter")
|
||||
cmd.Flags().IntVar(&count, "count", 50, "")
|
||||
cmd.Flags().IntVar(&offset, "offset", 0, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func contactsGetCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get CONTACT_ID",
|
||||
Short: "Show a contact by id",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.GetContact(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func contactsDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete CONTACT_ID [CONTACT_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more contacts",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range args {
|
||||
out, err := c.DeleteContact(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func contactsInfoAddCmd() *cobra.Command {
|
||||
var infoType, value, category string
|
||||
var isPrimary bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "info-add CONTACT_ID",
|
||||
Short: "Add a contact info entry (email, phone, website, linkedin, …)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if infoType == "" || value == "" {
|
||||
return fmt.Errorf("--type and --value are required")
|
||||
}
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.AddContactInfo(cmd.Context(), args[0], infoType, value, category, isPrimary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&infoType, "type", "", "Email|Phone|Website|LinkedIn|…")
|
||||
cmd.Flags().StringVar(&value, "value", "", "value")
|
||||
cmd.Flags().StringVar(&category, "category", "Work", "Work|Home|Other")
|
||||
cmd.Flags().BoolVar(&isPrimary, "primary", false, "mark as primary")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func personsCreateCmd() *cobra.Command {
|
||||
var first, last, email, linkedin string
|
||||
var companyID int
|
||||
var jobTitle, about string
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create a person",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if first == "" || last == "" {
|
||||
return fmt.Errorf("--first and --last are required")
|
||||
}
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.CreatePerson(cmd.Context(), first, last, companyID, jobTitle, about)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pid := strconv.Itoa(int(flexIDFloat(out["id"])))
|
||||
if email != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), pid, "Email", email, "Work", true)
|
||||
}
|
||||
if linkedin != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), pid, "LinkedIn", linkedin, "Work", false)
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&first, "first", "", "first name")
|
||||
cmd.Flags().StringVar(&last, "last", "", "last name")
|
||||
cmd.Flags().IntVar(&companyID, "company-id", 0, "employer company id")
|
||||
cmd.Flags().StringVar(&jobTitle, "job-title", "", "")
|
||||
cmd.Flags().StringVar(&about, "about", "", "about / bio")
|
||||
cmd.Flags().StringVar(&email, "email", "", "primary email (adds ContactInfo)")
|
||||
cmd.Flags().StringVar(&linkedin, "linkedin", "", "linkedin url (adds ContactInfo)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func companiesCreateCmd() *cobra.Command {
|
||||
var name, email, website string
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create a company",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("--name is required")
|
||||
}
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.CreateCompany(cmd.Context(), name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cid := strconv.Itoa(int(flexIDFloat(out["id"])))
|
||||
if email != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), cid, "Email", email, "Work", true)
|
||||
}
|
||||
if website != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), cid, "Website", website, "Work", false)
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&name, "name", "", "company name")
|
||||
cmd.Flags().StringVar(&email, "email", "", "primary email (adds ContactInfo)")
|
||||
cmd.Flags().StringVar(&website, "website", "", "website url (adds ContactInfo)")
|
||||
return cmd
|
||||
}
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func cmdCRMContacts() *cobra.Command {
|
||||
var companies, persons bool
|
||||
var search string
|
||||
cmd := &cobra.Command{
|
||||
Use: "crm-contacts",
|
||||
Short: "List CRM contacts",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, _, err := c.ListContacts(cmd.Context(), 50, 0, search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range list {
|
||||
isCo, _ := row["isCompany"].(bool)
|
||||
if companies && !isCo {
|
||||
continue
|
||||
}
|
||||
if persons && isCo {
|
||||
continue
|
||||
}
|
||||
printJSON(row)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&companies, "companies", false, "")
|
||||
cmd.Flags().BoolVar(&persons, "persons", false, "")
|
||||
cmd.Flags().StringVar(&search, "search", "", "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCRMAddContact() *cobra.Command {
|
||||
var company, email, website, linkedin string
|
||||
var personFirst, personLast string
|
||||
var companyID int
|
||||
cmd := &cobra.Command{
|
||||
Use: "crm-add-contact",
|
||||
Short: "Add company or person",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if company != "" {
|
||||
out, err := c.CreateCompany(cmd.Context(), company)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cid := fmt.Sprint(int(flexIDFloat(out["id"])))
|
||||
if email != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), cid, "Email", email, "Work", true)
|
||||
}
|
||||
if website != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), cid, "Website", website, "Work", false)
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
}
|
||||
if personFirst != "" && personLast != "" {
|
||||
out, err := c.CreatePerson(cmd.Context(), personFirst, personLast, companyID, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pid := fmt.Sprint(int(flexIDFloat(out["id"])))
|
||||
if email != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), pid, "Email", email, "Work", true)
|
||||
}
|
||||
if linkedin != "" {
|
||||
_, _ = c.AddContactInfo(cmd.Context(), pid, "LinkedIn", linkedin, "Work", false)
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("set --company or --person-first and --person-last")
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&company, "company", "", "")
|
||||
cmd.Flags().StringVar(&personFirst, "person-first", "", "")
|
||||
cmd.Flags().StringVar(&personLast, "person-last", "", "")
|
||||
cmd.Flags().IntVar(&companyID, "company-id", 0, "")
|
||||
cmd.Flags().StringVar(&email, "email", "", "")
|
||||
cmd.Flags().StringVar(&website, "website", "", "")
|
||||
cmd.Flags().StringVar(&linkedin, "linkedin", "", "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCRMDeals() *cobra.Command {
|
||||
var stages bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "crm-deals",
|
||||
Short: "List opportunities / stages",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stages {
|
||||
out, err := c.ListDealStages(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
}
|
||||
list, total, err := c.ListOpportunities(cmd.Context(), 100, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("total:", total)
|
||||
printJSON(list)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&stages, "stages", false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCRMAddDeal() *cobra.Command {
|
||||
var stage int
|
||||
var bid float64
|
||||
var contacts []string
|
||||
cmd := &cobra.Command{
|
||||
Use: "crm-add-deal TITLE",
|
||||
Short: "Create opportunity",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stage == 0 {
|
||||
stage = 1
|
||||
}
|
||||
out, err := c.CreateOpportunity(cmd.Context(), args[0], stage, "", "EUR", "", bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
did := fmt.Sprint(int(flexIDFloat(out["id"])))
|
||||
for _, cid := range contacts {
|
||||
_, _ = c.AddOpportunityMember(cmd.Context(), did, cid)
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVar(&stage, "stage", 1, "")
|
||||
cmd.Flags().Float64Var(&bid, "bid", 0, "")
|
||||
cmd.Flags().StringSliceVar(&contacts, "contact", nil, "contact id (repeatable)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdCRMCases() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "crm-cases",
|
||||
Short: "List CRM cases",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, total, err := c.ListCases(cmd.Context(), 50, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("total:", total)
|
||||
printJSON(list)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var crmTasksCmd = &cobra.Command{
|
||||
Use: "crm-tasks",
|
||||
Aliases: []string{"crm-task"},
|
||||
Short: "CRM tasks (standalone, not project tasks)",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(crmTasksCmd)
|
||||
crmTasksCmd.AddCommand(crmTasksListCmd())
|
||||
crmTasksCmd.AddCommand(crmTasksCreateCmd())
|
||||
crmTasksCmd.AddCommand(crmTasksDeleteCmd())
|
||||
crmTasksCmd.AddCommand(crmTasksCategoriesCmd())
|
||||
}
|
||||
|
||||
func crmTasksListCmd() *cobra.Command {
|
||||
var count, offset int
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List CRM tasks",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, total, err := c.ListCRMTasks(cmd.Context(), count, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Printf("total: %d (shown: %d)\n", total, len(list))
|
||||
}
|
||||
printTable([]string{"id", "title", "deadline", "isClosed", "categoryID"}, list)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVar(&count, "count", 50, "")
|
||||
cmd.Flags().IntVar(&offset, "offset", 0, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func crmTasksCreateCmd() *cobra.Command {
|
||||
var deadline, desc, entityType string
|
||||
var categoryID, contactID, entityID int
|
||||
cmd := &cobra.Command{
|
||||
Use: "create TITLE",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create a CRM task",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.CreateCRMTask(cmd.Context(), args[0], deadline, categoryID, contactID, entityType, entityID, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline (YYYY-MM-DD or ISO8601)")
|
||||
cmd.Flags().IntVar(&categoryID, "category", 0, "category id (see `oo crm-tasks categories`)")
|
||||
cmd.Flags().IntVar(&contactID, "contact", 0, "contact id")
|
||||
cmd.Flags().StringVar(&entityType, "entity-type", "", "opportunity|case|contact")
|
||||
cmd.Flags().IntVar(&entityID, "entity-id", 0, "parent entity id")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "description")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func crmTasksDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete TASK_ID [TASK_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete CRM tasks",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range args {
|
||||
if _, err := strconv.Atoi(id); err != nil {
|
||||
return fmt.Errorf("task id %q must be integer: %w", id, err)
|
||||
}
|
||||
out, err := c.DeleteCRMTask(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func crmTasksCategoriesCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "categories",
|
||||
Short: "List CRM task categories",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.ListTaskCategories(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printTable([]string{"id", "title", "sortOrder", "imagePath"}, out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+16
-6
@@ -1,14 +1,24 @@
|
||||
// Command oo is a thin CLI over the github.com/eslider/go-onlyoffice library.
|
||||
// It exposes calendar, CRM, tasks, subtasks, and application-sync commands.
|
||||
//
|
||||
// Command tree is subject-based (mirrors the library split and the `tea` CLI):
|
||||
//
|
||||
// oo calendar list | events | add | delete
|
||||
// oo projects list | get | milestones | create | update | delete
|
||||
// oo tasks list | get | create | update | delete | subtask add
|
||||
// oo users list | self (alias: oo whoami)
|
||||
// oo contacts list | get | delete | info-add
|
||||
// oo persons list | create | delete
|
||||
// oo companies list | create | delete
|
||||
// oo opportunities list | get | create | delete | stages | member-add
|
||||
// oo cases list | create | delete | member-add
|
||||
// oo crm-tasks list | create | delete | categories
|
||||
// oo applications sync
|
||||
//
|
||||
// Every list supports `--output/-o json|table` (table is the default).
|
||||
//
|
||||
// Build & install:
|
||||
//
|
||||
// go install github.com/eslider/go-onlyoffice/cmd/oo@latest
|
||||
//
|
||||
// Rationale: previous versions shipped as `oo-cli` with the cobra tree living
|
||||
// in `internal/cli`. Since the CLI is a consumer of the library — not a part
|
||||
// of its public surface — cobra lives right here in package main, split by
|
||||
// domain (calendar.go, crm.go, tasks.go, apps.go).
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var opportunitiesCmd = &cobra.Command{
|
||||
Use: "opportunities",
|
||||
Aliases: []string{"deals", "deal"},
|
||||
Short: "CRM opportunities (a.k.a. deals)",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(opportunitiesCmd)
|
||||
opportunitiesCmd.AddCommand(oppListCmd())
|
||||
opportunitiesCmd.AddCommand(oppGetCmd())
|
||||
opportunitiesCmd.AddCommand(oppCreateCmd())
|
||||
opportunitiesCmd.AddCommand(oppDeleteCmd())
|
||||
opportunitiesCmd.AddCommand(oppStagesCmd())
|
||||
opportunitiesCmd.AddCommand(oppMemberAddCmd())
|
||||
}
|
||||
|
||||
func oppListCmd() *cobra.Command {
|
||||
var count, offset int
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List opportunities",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, total, err := c.ListOpportunities(cmd.Context(), count, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outputFormat == "table" {
|
||||
fmt.Printf("total: %d (shown: %d)\n", total, len(list))
|
||||
// Flatten nested bidCurrency {abbreviation, ...} for readability.
|
||||
for _, row := range list {
|
||||
if cur, ok := row["bidCurrency"].(map[string]any); ok {
|
||||
row["bidCurrency"] = cur["abbreviation"]
|
||||
}
|
||||
}
|
||||
}
|
||||
printTable([]string{"id", "title", "bidValue", "bidCurrency", "stageName"}, list)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVar(&count, "count", 100, "")
|
||||
cmd.Flags().IntVar(&offset, "offset", 0, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func oppGetCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get OPPORTUNITY_ID",
|
||||
Short: "Show an opportunity by id",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.GetOpportunity(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func oppCreateCmd() *cobra.Command {
|
||||
var stage int
|
||||
var bid float64
|
||||
var currency, responsible, desc string
|
||||
var contacts []string
|
||||
cmd := &cobra.Command{
|
||||
Use: "create TITLE",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create an opportunity",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stage == 0 {
|
||||
stage = 1
|
||||
}
|
||||
out, err := c.CreateOpportunity(cmd.Context(), args[0], stage, responsible, currency, desc, bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
did := strconv.Itoa(int(flexIDFloat(out["id"])))
|
||||
for _, cid := range contacts {
|
||||
_, _ = c.AddOpportunityMember(cmd.Context(), did, cid)
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVar(&stage, "stage", 1, "pipeline stage id")
|
||||
cmd.Flags().Float64Var(&bid, "bid", 0, "bid value")
|
||||
cmd.Flags().StringVar(¤cy, "currency", "EUR", "bid currency")
|
||||
cmd.Flags().StringVar(&responsible, "responsible", "", "responsible user id")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "description")
|
||||
cmd.Flags().StringSliceVar(&contacts, "contact", nil, "contact id to add as member (repeatable)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func oppDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete OPPORTUNITY_ID [OPPORTUNITY_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more opportunities",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range args {
|
||||
out, err := c.DeleteOpportunity(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func oppStagesCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stages",
|
||||
Short: "List pipeline stages",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.ListDealStages(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printTable([]string{"id", "title", "sortOrder", "color"}, out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func oppMemberAddCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "member-add OPPORTUNITY_ID CONTACT_ID",
|
||||
Short: "Attach a contact to an opportunity",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.AddOpportunityMember(cmd.Context(), args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
onlyoffice "github.com/eslider/go-onlyoffice"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var projectsCmd = &cobra.Command{
|
||||
Use: "projects",
|
||||
Aliases: []string{"prj"},
|
||||
Short: "OnlyOffice Projects",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(projectsCmd)
|
||||
projectsCmd.AddCommand(prjListCmd())
|
||||
projectsCmd.AddCommand(prjGetCmd())
|
||||
projectsCmd.AddCommand(prjMilestonesCmd())
|
||||
projectsCmd.AddCommand(prjCreateCmd())
|
||||
projectsCmd.AddCommand(prjUpdateCmd())
|
||||
projectsCmd.AddCommand(prjDeleteCmd())
|
||||
}
|
||||
|
||||
func prjListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all projects",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := c.GetProjects()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(list))
|
||||
for _, p := range list {
|
||||
row := map[string]any{
|
||||
"id": derefInt(p.ID),
|
||||
"title": p.String(),
|
||||
"status": derefInt(p.Status),
|
||||
}
|
||||
if p.TaskCount != nil {
|
||||
row["tasks"] = *p.TaskCount
|
||||
}
|
||||
if p.IsPrivate != nil {
|
||||
row["private"] = *p.IsPrivate
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
printTable([]string{"id", "title", "status", "tasks", "private"}, rows)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func prjGetCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get [PROJECT_ID]",
|
||||
Short: "Show a single project (default: $OO_PROJECT_ID)",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := ""
|
||||
if len(args) == 1 {
|
||||
id = args[0]
|
||||
}
|
||||
out, err := c.GetProjectByID(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func prjMilestonesCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "milestones PROJECT_ID",
|
||||
Short: "List milestones of a project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pid, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("project id must be integer: %w", err)
|
||||
}
|
||||
ms, err := c.GetProjectMilestones(&onlyoffice.Project{ID: &pid})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(ms))
|
||||
for _, m := range ms {
|
||||
rows = append(rows, map[string]any{
|
||||
"id": derefInt64(m.ID),
|
||||
"title": derefString(m.Title),
|
||||
"status": derefInt64(m.Status),
|
||||
})
|
||||
}
|
||||
printTable([]string{"id", "title", "status"}, rows)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func prjCreateCmd() *cobra.Command {
|
||||
var desc, resp string
|
||||
cmd := &cobra.Command{
|
||||
Use: "create TITLE",
|
||||
Short: "Create a new project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := c.CreateProject(onlyoffice.NewProjectRequest{
|
||||
Title: args[0],
|
||||
Description: desc,
|
||||
ResponsibleID: resp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{
|
||||
"id": derefInt(p.ID),
|
||||
"title": p.String(),
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&desc, "description", "", "project description")
|
||||
cmd.Flags().StringVar(&resp, "responsible", "", "responsible user id (default: self)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func prjUpdateCmd() *cobra.Command {
|
||||
var title, desc, resp string
|
||||
cmd := &cobra.Command{
|
||||
Use: "update PROJECT_ID",
|
||||
Short: "Update project fields (only non-empty)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("project id must be integer: %w", err)
|
||||
}
|
||||
p, err := c.UpdateProject(onlyoffice.ProjectUpdateRequest{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Description: desc,
|
||||
ResponsibleID: resp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{
|
||||
"id": derefInt(p.ID),
|
||||
"title": p.String(),
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&title, "title", "", "new title")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "new description")
|
||||
cmd.Flags().StringVar(&resp, "responsible", "", "new responsible user id")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func prjDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete PROJECT_ID [PROJECT_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more projects",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, raw := range args {
|
||||
id, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("project id %q must be integer: %w", raw, err)
|
||||
}
|
||||
p, err := c.DeleteProject(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": derefInt(p.ID), "title": p.String()})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func derefString(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func derefInt(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func derefInt64(p *int64) int64 {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
+120
-65
@@ -6,14 +6,30 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func cmdTaskList() *cobra.Command {
|
||||
var tasksCmd = &cobra.Command{
|
||||
Use: "tasks",
|
||||
Aliases: []string{"task"},
|
||||
Short: "Project tasks and subtasks",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(tasksCmd)
|
||||
tasksCmd.AddCommand(taskListCmd())
|
||||
tasksCmd.AddCommand(taskGetCmd())
|
||||
tasksCmd.AddCommand(taskCreateCmd())
|
||||
tasksCmd.AddCommand(taskUpdateCmd())
|
||||
tasksCmd.AddCommand(taskDeleteCmd())
|
||||
tasksCmd.AddCommand(subtaskCmd())
|
||||
}
|
||||
|
||||
func taskListCmd() *cobra.Command {
|
||||
var project, status string
|
||||
var all, verbose bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "task-list",
|
||||
Use: "list",
|
||||
Short: "List project tasks",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -26,36 +42,54 @@ func cmdTaskList() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tasks {
|
||||
fmt.Printf("[%v] %v status=%v\n", t["id"], t["title"], t["status"])
|
||||
if verbose {
|
||||
d, _ := c.GetTaskByID(cmd.Context(), fmt.Sprint(t["id"]))
|
||||
subs, _ := d["subtasks"].([]any)
|
||||
for _, s := range subs {
|
||||
sm, _ := s.(map[string]any)
|
||||
fmt.Printf(" └─ [%v] %v\n", sm["id"], sm["title"])
|
||||
if verbose {
|
||||
for i, t := range tasks {
|
||||
d, err := c.GetTaskByID(cmd.Context(), idString(t, "id"))
|
||||
if err == nil {
|
||||
tasks[i] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
printTable([]string{"id", "title", "status", "priority", "responsible"}, tasks)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&project, "project", "p", "", "project id")
|
||||
cmd.Flags().StringVarP(&project, "project", "p", "", "project id (default $OO_PROJECT_ID)")
|
||||
cmd.Flags().StringVarP(&status, "status", "s", "", "open|closed")
|
||||
cmd.Flags().BoolVarP(&all, "all", "a", false, "all projects (@self)")
|
||||
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "")
|
||||
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "include subtasks & full details")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdTaskAdd() *cobra.Command {
|
||||
var project, desc, deadline string
|
||||
var prio string
|
||||
cmd := &cobra.Command{
|
||||
Use: "task-add TITLE",
|
||||
Short: "Add project task",
|
||||
func taskGetCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get TASK_ID",
|
||||
Short: "Show a single task (incl. subtasks)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.GetTaskByID(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func taskCreateCmd() *cobra.Command {
|
||||
var project, desc, deadline, prio string
|
||||
cmd := &cobra.Command{
|
||||
Use: "create TITLE",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Create a project task",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,24 +104,79 @@ func cmdTaskAdd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&project, "project", "", "")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "")
|
||||
cmd.Flags().StringVar(&deadline, "deadline", "", "")
|
||||
cmd.Flags().StringVarP(&project, "project", "p", "", "project id (default $OO_PROJECT_ID)")
|
||||
cmd.Flags().StringVar(&desc, "description", "", "description")
|
||||
cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD")
|
||||
cmd.Flags().StringVar(&prio, "priority", "normal", "high|normal|low")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func cmdSubtaskAdd() *cobra.Command {
|
||||
func taskUpdateCmd() *cobra.Command {
|
||||
var status string
|
||||
cmd := &cobra.Command{
|
||||
Use: "update TASK_ID",
|
||||
Short: "Update task status",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if status == "" {
|
||||
return fmt.Errorf("--status is required (open|closed)")
|
||||
}
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := c.UpdateTaskStatus(cmd.Context(), args[0], status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&status, "status", "s", "", "new status: open|closed")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func taskDeleteCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "subtask-add PARENT_TASK_ID TITLE [TITLE...]",
|
||||
Short: "Add subtask(s)",
|
||||
Use: "delete TASK_ID [TASK_ID...]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Delete one or more tasks",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range args {
|
||||
out, err := c.DeleteTask(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// subtaskCmd exposes `oo tasks subtask {add}` — keeps things tidy without a
|
||||
// dedicated subtasks top-level command.
|
||||
func subtaskCmd() *cobra.Command {
|
||||
sub := &cobra.Command{
|
||||
Use: "subtask",
|
||||
Short: "Subtasks of a parent task",
|
||||
}
|
||||
sub.AddCommand(&cobra.Command{
|
||||
Use: "add PARENT_TASK_ID TITLE [TITLE...]",
|
||||
Short: "Add one or more subtasks",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,44 +186,10 @@ func cmdSubtaskAdd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
printObject(out)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cmdTaskUpdate() *cobra.Command {
|
||||
var del bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "task-update TASK_ID open|closed",
|
||||
Short: "Update task status or delete",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := args[0]
|
||||
if del {
|
||||
out, err := c.DeleteTask(cmd.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
}
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("need status or --delete")
|
||||
}
|
||||
out, err := c.UpdateTaskStatus(cmd.Context(), id, args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printJSON(out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&del, "delete", false, "delete task")
|
||||
return cmd
|
||||
})
|
||||
return sub
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var usersCmd = &cobra.Command{
|
||||
Use: "users",
|
||||
Short: "Portal / workspace users",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
usersCmd.AddCommand(usersListCmd())
|
||||
usersCmd.AddCommand(usersSelfCmd())
|
||||
rootCmd.AddCommand(whoamiCmd())
|
||||
}
|
||||
|
||||
func usersListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List portal users",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users, err := c.GetUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(users))
|
||||
for _, u := range users {
|
||||
rows = append(rows, map[string]any{
|
||||
"id": derefString(u.ID),
|
||||
"userName": derefString(u.UserName),
|
||||
"displayName": derefString(u.DisplayName),
|
||||
"email": derefString(u.Email),
|
||||
"isAdmin": derefBool(u.IsAdmin),
|
||||
"status": derefInt(u.Status),
|
||||
})
|
||||
}
|
||||
printTable([]string{"id", "userName", "displayName", "email", "isAdmin", "status"}, rows)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func usersSelfCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "self",
|
||||
Short: "Show current user id (people/@self)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := c.SelfUserID(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": id})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// whoamiCmd is a convenience shortcut at the root level.
|
||||
func whoamiCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "whoami",
|
||||
Short: "Alias for `oo users self`",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newOO(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := c.SelfUserID(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(map[string]any{"id": id})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func derefBool(p *bool) bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
return *p
|
||||
}
|
||||
Reference in New Issue
Block a user