refactor!: rename oo-cli → oo, split library by domain, relocate applications

BREAKING:
- Binary renamed oo-cli → oo; install path is now
  github.com/eslider/go-onlyoffice/cmd/oo.
- Former internal/cli tree removed; cobra commands live in cmd/oo/ as
  package main, split by domain (calendar.go, crm.go, tasks.go, apps.go,
  common.go, main.go).
- internal/applications moved to cmd/oo/applications/ (CV-specific workflow;
  not a library feature).
- examples/applications removed (it depended on an internal package).

Library split (mechanical, zero API surface change):
- client.go  — Client, Credentials, Defaults, env helpers, NewClient.
- request.go — Request, Query, Time, Token, MetaResponse, Permissions.
- auth.go    — Authenticate/AuthenticateContext/InvalidateToken.
- http.go    — transport + ResponseArray/ResponseObject/postFormObject/
               putFormObject/deleteObject/unmarshalResponseObject (renamed
               from httpx.go).
- projects.go, tasks.go, users.go, calendar.go, crm.go, files.go — typed
  / untyped domain methods. tasks_extra.go merged into tasks.go.
- onlyoffice.go deleted (content redistributed).

AGENTS.md, CHANGELOG.md, README.md updated accordingly.

Made-with: Cursor
This commit is contained in:
2026-04-24 14:55:21 +01:00
parent e2d481e7b8
commit 0973975471
27 changed files with 1704 additions and 2315 deletions
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"time"
"github.com/spf13/cobra"
)
func cmdCalList() *cobra.Command {
return &cobra.Command{
Use: "cal-list",
Short: "List calendars (default date span)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
out, err := c.ListCalendars(cmd.Context(), "", "")
if err != nil {
return err
}
printJSON(out)
return nil
},
}
}
func cmdCalEvents() *cobra.Command {
var start, end string
cmd := &cobra.Command{
Use: "cal-events",
Short: "List calendar data for period (default: next 7 days)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
if start == "" || end == "" {
start = time.Now().Format("2006-01-02")
end = time.Now().AddDate(0, 0, 7).Format("2006-01-02")
}
out, err := c.ListEvents(cmd.Context(), start, end)
if err != nil {
return err
}
printJSON(out)
return nil
},
}
cmd.Flags().StringVar(&start, "start", "", "start date YYYY-MM-DD")
cmd.Flags().StringVar(&end, "end", "", "end date YYYY-MM-DD")
return cmd
}
func cmdCalAdd() *cobra.Command {
var cal, desc string
var allDay bool
cmd := &cobra.Command{
Use: "cal-add TITLE START END",
Short: "Add calendar event",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
ev, err := c.AddEvent(cmd.Context(), cal, args[0], args[1], args[2], desc, allDay)
if err != nil {
return err
}
printJSON(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, "")
return cmd
}
func cmdCalDel() *cobra.Command {
return &cobra.Command{
Use: "cal-delete EVENT_ID [EVENT_ID...]",
Short: "Delete calendar events",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
for _, id := range args {
out, err := c.DeleteEvent(cmd.Context(), id)
if err != nil {
return err
}
printJSON(out)
}
return nil
},
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strconv"
onlyoffice "github.com/eslider/go-onlyoffice"
"github.com/joho/godotenv"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "oo",
Short: "OnlyOffice Workspace CLI (Go port of cv/bin/office)",
}
// 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) {
_ = godotenv.Load()
creds := onlyoffice.GetEnvironmentCredentials()
if creds.Url == "" || creds.User == "" || creds.Password == "" {
return nil, fmt.Errorf("need ONLYOFFICE_URL (or ONLYOFFICE_HOST), user (ONLYOFFICE_USER or ONLYOFFICE_NAME), password (ONLYOFFICE_PASS or ONLYOFFICE_PASSWORD)")
}
c := onlyoffice.NewClient(creds)
c.SetDefaults(onlyoffice.GetEnvironmentDefaults())
if err := c.Authenticate(); err != nil {
return nil, err
}
return c, nil
}
func printJSON(v any) {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
// flexIDFloat coerces OnlyOffice numeric id fields surfaced as float64 / int
// / string into a float64. CLI-only; the library's own flexInt is unexported.
func flexIDFloat(v any) float64 {
switch x := v.(type) {
case float64:
return x
case int:
return float64(x)
default:
f, _ := strconv.ParseFloat(fmt.Sprint(x), 64)
return f
}
}
+181
View File
@@ -0,0 +1,181 @@
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
},
}
}
+24
View File
@@ -0,0 +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.
//
// 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 (
"fmt"
"os"
)
func main() {
if err := execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func cmdTaskList() *cobra.Command {
var project, status string
var all, verbose bool
cmd := &cobra.Command{
Use: "task-list",
Short: "List project tasks",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
var tasks []map[string]any
if all {
tasks, err = c.ListAllTasks(cmd.Context(), status)
} else {
tasks, err = c.ListTasks(cmd.Context(), project, status)
}
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"])
}
}
}
return nil
},
}
cmd.Flags().StringVarP(&project, "project", "p", "", "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, "")
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",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
p := 0
switch prio {
case "high":
p = 1
case "low":
p = -1
}
out, err := c.AddTask(cmd.Context(), project, args[0], desc, p, deadline)
if err != nil {
return err
}
printJSON(out)
return nil
},
}
cmd.Flags().StringVar(&project, "project", "", "")
cmd.Flags().StringVar(&desc, "description", "", "")
cmd.Flags().StringVar(&deadline, "deadline", "", "")
cmd.Flags().StringVar(&prio, "priority", "normal", "high|normal|low")
return cmd
}
func cmdSubtaskAdd() *cobra.Command {
return &cobra.Command{
Use: "subtask-add PARENT_TASK_ID TITLE [TITLE...]",
Short: "Add subtask(s)",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newOO()
if err != nil {
return err
}
parent := args[0]
for _, title := range args[1:] {
out, err := c.AddSubtask(cmd.Context(), parent, title)
if err != nil {
return err
}
printJSON(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
}