From 28d2ed235f44b3ec7eb7c7f64ac1963d4c858e90 Mon Sep 17 00:00:00 2001 From: vmsharoshkin Date: Tue, 29 Sep 2026 10:00:12 +0300 Subject: [PATCH] simple printer --- CLAUDE.md | 22 ++--- CONTRIBUTING.md | 4 +- README.md | 7 +- cmd/app.go | 10 +- cmd/config.go | 13 ++- cmd/helpers.go | 12 +-- cmd/job.go | 7 +- cmd/repo.go | 57 +---------- cmd/root.go | 2 + cmd/run.go | 7 +- go.mod | 1 + go.sum | 2 + internal/output/output.go | 193 +++++--------------------------------- internal/output/tty.go | 12 --- skills/eds/SKILL.md | 32 +++---- 15 files changed, 84 insertions(+), 297 deletions(-) delete mode 100644 internal/output/tty.go diff --git a/CLAUDE.md b/CLAUDE.md index 7ca3c35..7d90901 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,8 @@ Both products are authenticated with the same API key (`--api-key`/`EDS_API_KEY` sent as `X-API-KEY`. `--project`/`EDS_PROJECT_ID` is the only other platform-level (shared, unprefixed) setting. -The CLI is explicitly designed to be **agent-friendly**: every command has -stable `--json` output, config comes from env vars, and `skills/eds/SKILL.md` +The CLI is explicitly designed to be **agent-friendly**: every command outputs +stable JSON by default, config comes from env vars, and `skills/eds/SKILL.md` documents the CLI's contract for AI agents driving it. The primary agent-facing scenario is *ship a vibe-coded app*: `eds repo create` + `git push` gets code hosted, `eds wf app create` publishes it (auto-deploy @@ -67,7 +67,8 @@ Cobra-based CLI, one command per file under `cmd/`, thin API client under - `cmd/root.go` — builds the root `eds` command, registers global persistent flags. Product-scoped: `--repo-api-url`, `--repo-git-host` (Repo product); `--wf-api-url` (Workflow Studio). Platform-level (shared, - unprefixed): `--project`, `--api-key`. Plus `--json`, `--quiet`. + unprefixed): `--project`, `--api-key`. Plus `--yaml`, `--quiet` + (`--json` is a backward-compatible no-op, since JSON is the default). the API client and printer. - `cmd/helpers.go` — `resolveContext(cmd)` is the entry point every subcommand calls first. It loads config, layers flag overrides on top (flags > env > @@ -111,11 +112,10 @@ the API client and printer. their pre-rename spelling — only the user-facing CLI flags/env vars/command names follow the `eds`/`repo`/`wf` product split; this is a deliberate minimal-diff choice, not an oversight. -- `internal/output/output.go` — `Printer` with `FormatAuto` (TTY -> table, - pipe -> JSON), `FormatTable`, `FormatJSON`. All list/show commands render - through `Printer.Table` / `Printer.KeyValue` / `Printer.PrintJSON` so both - output modes stay in sync from one call site. Also has `HumanTime` / - `HumanSize` formatters. +- `internal/output/output.go` — `Printer` outputs JSON by default or YAML + when `--yaml` is passed. `--quiet` suppresses all printer output. All + commands render through `Printer.Print(v)` so output stays consistent. + The `--json` flag is retained as a backward-compatible no-op. - `internal/repoapi/client.go` — generic authenticated HTTP client (`Do`), sends `X-API-KEY`, decodes JSON, maps non-2xx responses to `*APIError` (tries `error`, `message`, and per-field `errors` response @@ -159,9 +159,9 @@ the API client and printer. If it hits a Repo-product API, call `ctx.requireAPIKey()`. If it hits a Workflow Studio API, call `ctx.ensureWorkflowAuth(cmd.Context())` — both validate that `EDS_API_KEY` is set. -- Repo subcommands that return structured data support both table (default - on TTY) and `--json` output via `ctx.Printer`. Workflow Studio subcommands - currently output JSON only. +- Repo subcommands and Workflow Studio subcommands both output JSON by + default via `ctx.Printer`. Use `--yaml` for YAML output. `--quiet` + suppresses all printer output. - `eds repo` commands (and `eds wf app create --repository`) accept either a repository UUID or a name (`resolveRepoID` / `looksLikeUUID`); don't require callers to look up IDs first. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f630574..c1e088b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,8 +42,8 @@ starting point for understanding how a change should fit in. In short: - One command per file under `cmd/`, thin HTTP clients under `internal/`. -- Every subcommand that returns structured data supports both a table - (default on TTY) and `--json` output via `ctx.Printer`. +- Every subcommand that returns structured data outputs JSON by default + (or YAML when `--yaml`) via `ctx.Printer`. `--quiet` suppresses all output. - New flags/env vars follow the existing product-scoping convention (`--repo-*`/`EDS_REPO_*`, `--wf-*`/`EDS_WF_*`; only genuinely cross-product settings stay unprefixed). diff --git a/README.md b/README.md index e6a8fcf..0db7d0c 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ developer tools products. It currently covers two products: Repo and Workflow Studio are independent products, but both are authenticated with the same API key (`EDS_API_KEY`) via `X-API-KEY`. `eds` is just the platform CLI they're both driven through. Every command is designed to be safely -driven by automation and AI agents: stable `--json` output, environment variables +driven by automation and AI agents: JSON output by default, environment variables for secrets. - Single static binary (Go, no runtime dependencies). - Reads the API key from `EDS_API_KEY` or from `~/.config/eds/config.json`. -- Outputs JSON when piped, pretty tables on a TTY (`--json` to force). +- Outputs JSON by default. Use `--yaml` for YAML output. `--quiet` suppresses + all output (useful for fire-and-forget calls from agents). - Repo uses the local `git` CLI for clone. Both products authenticate with `X-API-KEY`. `eds repo clone` embeds the API key as HTTP Basic Auth credentials directly into the smart-HTTP URL it passes to `git clone`, so @@ -307,7 +308,7 @@ cmd/ job.go # `eds wf job logs` internal/ config/ # disk config + env overrides - output/ # JSON / table formatting + output/ # JSON / YAML formatting repoapi/ # thin HTTP client for the Repo product API workflow_client/ # generated OpenAPI client for Workflow Studio scripts/ diff --git a/cmd/app.go b/cmd/app.go index 9e3d41f..ce603ad 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -89,7 +89,7 @@ accepts either its id or its name) or an external git URL (--repository-url).`, return fmt.Errorf("workflow client application post: %w", err) } - return ctx.Printer.PrintJSON(app) + return ctx.Printer.Print(app) }, } @@ -132,7 +132,7 @@ func newAppListCmd() *cobra.Command { return fmt.Errorf("workflow client application list: %w", err) } - return ctx.Printer.PrintJSON(resp) + return ctx.Printer.Print(resp) }, } @@ -163,7 +163,7 @@ func newAppShowCmd() *cobra.Command { return fmt.Errorf("workflow client get app: %w", err) } - return ctx.Printer.PrintJSON(app) + return ctx.Printer.Print(app) }, } return cmd @@ -282,7 +282,7 @@ follow progress, and check the "url" field once it succeeds.`, return fmt.Errorf("workflow client deploy application: %w", err) } - return ctx.Printer.PrintJSON(dep) + return ctx.Printer.Print(dep) }, } return cmd @@ -321,7 +321,7 @@ func newAppDeploymentsCmd() *cobra.Command { return fmt.Errorf("workflow client list application: %w", err) } - return ctx.Printer.PrintJSON(resp) + return ctx.Printer.Print(resp) }, } diff --git a/cmd/config.go b/cmd/config.go index 874bb4d..9600ec0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -18,14 +18,13 @@ variables. Useful to debug "why is my CLI pointing at the wrong host?".`, return err } - ctx.Printer.KeyValue([][2]string{ - {"project_id", ctx.Cfg.ProjectID}, - {"repo_api_url", ctx.Cfg.APIURL}, - {"api_key", maskKey(ctx.Cfg.APIKey)}, - {"repo_git_host", ctx.Cfg.GitHost}, - {"wf_api_url", ctx.Cfg.WorkflowAPIURL}, + return ctx.Printer.Print(map[string]any{ + "project_id": ctx.Cfg.ProjectID, + "repo_api_url": ctx.Cfg.APIURL, + "api_key": maskKey(ctx.Cfg.APIKey), + "repo_git_host": ctx.Cfg.GitHost, + "wf_api_url": ctx.Cfg.WorkflowAPIURL, }) - return nil }, } return cmd diff --git a/cmd/helpers.go b/cmd/helpers.go index de0307f..60968b8 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -18,7 +18,6 @@ type runtimeContext struct { Cfg *config.Config API *repoapi.Client Printer *output.Printer - Quiet bool ProjectID string WorkflowClient *workflowclient.APIClient } @@ -48,12 +47,12 @@ func resolveContext(cmd *cobra.Command) (*runtimeContext, error) { cfg.WorkflowAPIURL = v } - wantJSON, _ := cmd.Flags().GetBool("json") + wantYaml, _ := cmd.Flags().GetBool("yaml") quiet, _ := cmd.Flags().GetBool("quiet") - format := output.FormatAuto - if wantJSON { - format = output.FormatJSON + format := output.FormatJSON + if wantYaml { + format = output.FormatYAML } workflowClientCfg := workflowclient.NewConfiguration() @@ -63,8 +62,7 @@ func resolveContext(cmd *cobra.Command) (*runtimeContext, error) { rt := &runtimeContext{ Cfg: cfg, API: repoapi.New(cfg.APIURL, cfg.APIKey, cfg.ProjectID), - Printer: output.New(format), - Quiet: quiet, + Printer: output.New(format, quiet), ProjectID: cfg.ProjectID, WorkflowClient: workflowclient.NewAPIClient(workflowClientCfg), } diff --git a/cmd/job.go b/cmd/job.go index 2d76e5a..c475eee 100644 --- a/cmd/job.go +++ b/cmd/job.go @@ -51,12 +51,7 @@ func newJobLogsCmd() *cobra.Command { return fmt.Errorf("workflow client logs: %w", err) } - err = ctx.Printer.PrintJSON(resp) - if err != nil { - return fmt.Errorf("print json: %w", err) - } - - return nil + return ctx.Printer.Print(resp) }, } diff --git a/cmd/repo.go b/cmd/repo.go index a5c161c..9034fa1 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -12,7 +12,6 @@ import ( "github.com/google/uuid" "github.com/spf13/cobra" - "github.com/cloud-ru/evolution-devservices-cli/internal/output" "github.com/cloud-ru/evolution-devservices-cli/internal/repoapi" ) @@ -73,24 +72,7 @@ func newRepoListCmd() *cobra.Command { return fmt.Errorf("api list repositories: %w", err) } - if ctx.Printer.Format == output.FormatJSON { - return ctx.Printer.PrintJSON(resp) - } - - headers := []string{"ID", "NAME", "TYPE", "VISIBILITY", "UPDATED"} - rows := make([][]string, 0, len(resp.Repositories)) - for _, r := range resp.Repositories { - rows = append(rows, []string{ - r.ID, - r.Name, - r.Type, - r.Visibility, - output.HumanTime(r.UpdatedAt), - }) - } - ctx.Printer.Table(headers, rows) - fmt.Fprintf(cmd.OutOrStdout(), "Showing %d of %d\n", len(resp.Repositories), resp.Total) - return nil + return ctx.Printer.Print(resp) }, } @@ -135,11 +117,7 @@ func newRepoCreateCmd() *cobra.Command { return fmt.Errorf("api create repository: %w", err) } - if !ctx.Quiet { - fmt.Fprintf(cmd.OutOrStdout(), - "Created repository %q (id=%s)\n", r.Name, r.ID) - } - return ctx.Printer.PrintJSON(r) + return ctx.Printer.Print(r) }, } @@ -175,24 +153,7 @@ func newRepoShowCmd() *cobra.Command { return fmt.Errorf("api get repository: %w", err) } - if ctx.Printer.Format == output.FormatJSON { - return ctx.Printer.PrintJSON(info) - } - ctx.Printer.KeyValue([][2]string{ - {"id", info.ID}, - {"name", info.Name}, - {"description", info.Description}, - {"type", info.Type}, - {"default_branch", info.DefaultBranch}, - {"branches", fmt.Sprintf("%d", info.BranchesCount)}, - {"commits", fmt.Sprintf("%d", info.CommitsCount)}, - {"size", output.HumanSize(info.Size)}, - {"created_at", output.HumanTime(info.CreatedAt)}, - {"updated_at", output.HumanTime(info.UpdatedAt)}, - {"clone_https", info.Clone.HTTPS}, - {"clone_ssh", info.Clone.SSH}, - }) - return nil + return ctx.Printer.Print(info) }, } return cmd @@ -295,7 +256,6 @@ the clone, as you would with any other git server.`, // the URL so clone/push work standalone, without relying on a // git credential helper or ~/.netrc being pre-configured in the // environment (agents/CI have neither). - displayCloneURL := cloneURL if !useSSH { cloneURL = withBasicAuth(cloneURL, ctx.Cfg.APIKey) } @@ -306,14 +266,8 @@ the clone, as you would with any other git server.`, } gitArgs := []string{"clone", cloneURL} - displayArgs := []string{"clone", displayCloneURL} if dst != "" { gitArgs = append(gitArgs, dst) - displayArgs = append(displayArgs, dst) - } - - if !ctx.Quiet { - fmt.Fprintf(cmd.OutOrStdout(), "Running: git %s\n", strings.Join(displayArgs, " ")) } c := exec.CommandContext(cmd.Context(), "git", gitArgs...) @@ -378,15 +332,10 @@ made the remote). If you don't have a local checkout yet, use return fmt.Errorf("repository has no clone url") } - displayURL := remoteURL if !useSSH { remoteURL = withBasicAuth(remoteURL, ctx.Cfg.APIKey) } - if !ctx.Quiet { - fmt.Fprintf(cmd.OutOrStdout(), "Running: git remote add %s %s\n", remoteName, displayURL) - } - c := exec.CommandContext(cmd.Context(), "git", "remote", "add", remoteName, remoteURL) c.Stdout = cmd.OutOrStdout() c.Stderr = cmd.ErrOrStderr() diff --git a/cmd/root.go b/cmd/root.go index f62fec2..3c34dbc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,6 +40,8 @@ It is designed to be safely driven by automation and AI agents via the root.PersistentFlags().Bool("json", false, "force JSON output") + root.PersistentFlags().Bool("yaml", false, + "force YAML output") root.PersistentFlags().BoolP("quiet", "q", false, "suppress non-essential output") diff --git a/cmd/run.go b/cmd/run.go index 3c1c54a..0fabe77 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -41,12 +41,7 @@ func newRunShowCmd() *cobra.Command { return fmt.Errorf("workflow client get run: %w", err) } - err = ctx.Printer.PrintJSON(run) - if err != nil { - return fmt.Errorf("print json run: %w", err) - } - - return nil + return ctx.Printer.Print(run) }, } return cmd diff --git a/go.mod b/go.mod index 37918cb..b702175 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.6 require ( github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 + go.yaml.in/yaml/v3 v3.0.4 ) require ( diff --git a/go.sum b/go.sum index fd37884..8709702 100644 --- a/go.sum +++ b/go.sum @@ -8,5 +8,7 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/output/output.go b/internal/output/output.go index 0061c34..118710e 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -1,4 +1,3 @@ -// Package output handles printing human-readable tables and JSON output. package output import ( @@ -6,192 +5,50 @@ import ( "fmt" "io" "os" - "strings" - "time" + + "go.yaml.in/yaml/v3" ) -// Format selects the output format. type Format int const ( - // FormatAuto picks a format based on whether the output is a TTY. - // When non-TTY (e.g. pipe to another command), JSON is used. - FormatAuto Format = iota - // FormatTable forces a human-readable table. - FormatTable - // FormatJSON forces JSON output. - FormatJSON + FormatJSON Format = iota + FormatYAML ) -// Printer renders data to w. type Printer struct { - W io.Writer - Format Format + writer io.Writer + format Format + quiet bool } -// New creates a Printer using stdout and the requested format. -func New(format Format) *Printer { - if format == FormatAuto { - if isTerminal(os.Stdout) { - format = FormatTable - } else { - format = FormatJSON - } - } - return &Printer{W: os.Stdout, Format: format} +func New(format Format, quiet bool) *Printer { + return &Printer{writer: os.Stdout, format: format, quiet: quiet} } -// PrintJSON marshals v as indented JSON to the writer. -func (p *Printer) PrintJSON(v any) error { - enc := json.NewEncoder(p.W) - enc.SetIndent("", " ") - - err := enc.Encode(v) - if err != nil { - return fmt.Errorf("json encode %w", err) - } - - return nil -} - -// Table renders a human-readable table. Headers is the header row, -// rows are arbitrary string cells. A nil or empty rows slice prints -// a friendly placeholder. -func (p *Printer) Table(headers []string, rows [][]string) { - if p.Format == FormatJSON { - out := make([]map[string]string, 0, len(rows)) - for _, r := range rows { - row := map[string]string{} - for i, h := range headers { - if i < len(r) { - row[h] = r[i] - } - } - out = append(out, row) - } - _ = p.PrintJSON(out) - return - } - - if len(rows) == 0 { - fmt.Fprintln(p.W, "No results.") - return +func (p *Printer) Print(v any) error { + if p.quiet { + return nil } - widths := make([]int, len(headers)) - for i, h := range headers { - widths[i] = len(h) - } - for _, r := range rows { - for i, cell := range r { - if i >= len(widths) { - continue - } - if w := displayWidth(cell); w > widths[i] { - widths[i] = w - } - } - } + if p.format == FormatJSON { + enc := json.NewEncoder(p.writer) + enc.SetIndent("", " ") - fmt.Fprintln(p.W, formatRow(headers, widths)) - fmt.Fprintln(p.W, formatSeparator(widths)) - for _, r := range rows { - fmt.Fprintln(p.W, formatRow(r, widths)) - } -} - -// KeyValue renders a small two-column key/value listing. -// Useful for `eds repo show` and `eds config`. -func (p *Printer) KeyValue(pairs [][2]string) { - if p.Format == FormatJSON { - obj := make(map[string]string, len(pairs)) - for _, kv := range pairs { - obj[kv[0]] = kv[1] + err := enc.Encode(v) + if err != nil { + return fmt.Errorf("json encode %w", err) } - _ = p.PrintJSON(obj) - return - } - - if len(pairs) == 0 { - fmt.Fprintln(p.W, "(empty)") - return } + if p.format == FormatYAML { + enc := yaml.NewEncoder(p.writer) + defer enc.Close() - maxLen := 0 - for _, kv := range pairs { - if len(kv[0]) > maxLen { - maxLen = len(kv[0]) + err := enc.Encode(v) + if err != nil { + return fmt.Errorf("yaml encode: %w", err) } } - for _, kv := range pairs { - fmt.Fprintf(p.W, "%-*s %s\n", maxLen, kv[0]+":", kv[1]) - } -} -// HumanTime returns a friendly short-form representation. -// Falls back to the raw string when parsing fails. -func HumanTime(s string) string { - if s == "" { - return "-" - } - layouts := []string{ - time.RFC3339Nano, - time.RFC3339, - "2006-01-02T15:04:05Z", - "2006-01-02 15:04:05", - } - for _, layout := range layouts { - if t, err := time.Parse(layout, s); err == nil { - return t.UTC().Format("2006-01-02 15:04 UTC") - } - } - return s -} - -// HumanSize returns a human-readable byte count. -func HumanSize(n int64) string { - const unit = 1024 - if n < unit { - return fmt.Sprintf("%d B", n) - } - div, exp := int64(unit), 0 - for n2 := n / unit; n2 >= unit; n2 /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp]) -} - -func formatRow(cells []string, widths []int) string { - parts := make([]string, len(cells)) - for i, c := range cells { - w := 0 - if i < len(widths) { - w = widths[i] - } - parts[i] = padRight(c, w) - } - return strings.Join(parts, " ") -} - -func formatSeparator(widths []int) string { - parts := make([]string, len(widths)) - for i, w := range widths { - parts[i] = strings.Repeat("-", w) - } - return strings.Join(parts, " ") -} - -func padRight(s string, width int) string { - w := displayWidth(s) - if w >= width { - return s - } - return s + strings.Repeat(" ", width-w) -} - -// displayWidth approximates the visible width of s. It treats runes -// as single-width which is sufficient for typical repo names. -func displayWidth(s string) int { - return len([]rune(s)) + return nil } diff --git a/internal/output/tty.go b/internal/output/tty.go deleted file mode 100644 index 9714f61..0000000 --- a/internal/output/tty.go +++ /dev/null @@ -1,12 +0,0 @@ -package output - -import "os" - -// isTerminal reports whether f refers to a character device. -func isTerminal(f *os.File) bool { - fi, err := f.Stat() - if err != nil { - return false - } - return (fi.Mode() & os.ModeCharDevice) != 0 -} diff --git a/skills/eds/SKILL.md b/skills/eds/SKILL.md index 8a6aa03..14386e2 100644 --- a/skills/eds/SKILL.md +++ b/skills/eds/SKILL.md @@ -46,28 +46,28 @@ datasets, merge requests). Those are out of scope for this CLI. ## Tool contract -Every command supports `--json` for machine-readable output. When the -output is piped to another command, JSON is selected automatically. +Every command outputs JSON by default. Use `--yaml` for YAML output. +`--quiet` suppresses all output (useful for fire-and-forget calls from agents). Errors go to stderr and the process exits non-zero. | Command | Purpose | |-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| | `eds version` | Print the installed CLI version | | `eds config` | Print effective configuration | -| `eds repo list [--search S] [--sort name_asc\|name_desc\|updated_at_asc\|updated_at_desc] [--limit N] [--offset N] [--json]` | List repositories in the configured project | -| `eds repo create [--description "..."] [--visibility private\|shadow] [--json]` | Create a new git repository | -| `eds repo show [--json]` | Show details: id, default_branch, size, clone URLs | -| `eds repo delete [--force] [--json]` | Delete (irreversible; requires confirmation unless `--force`) | +| `eds repo list [--search S] [--sort name_asc\|name_desc\|updated_at_asc\|updated_at_desc] [--limit N] [--offset N]` | List repositories in the configured project | +| `eds repo create [--description "..."] [--visibility private\|shadow]` | Create a new git repository | +| `eds repo show ` | Show details: id, default_branch, size, clone URLs | +| `eds repo delete [--force]` | Delete (irreversible; requires confirmation unless `--force`) | | `eds repo clone [dir] [--ssh] [--target DIR]` | Clone via local `git` CLI | | `eds repo remote-add [--name N] [--ssh]` | Wire an existing local checkout to it (`git remote add`, authenticated) | -| `eds wf app create --repository R\|--repository-url URL --branch B [--json]` | Create a Workflow Studio application from a repo + branch (auto-triggers first deploy) | -| `eds wf app list [--search S] [--sort created_at_asc\|created_at_desc] [--json]` | List applications | -| `eds wf app show [--json]` | Show application details (status, run_id, pipeline_id, ...) | -| `eds wf app update --branch B [--name N] [--json]` | Update an application's name/branch | -| `eds wf app delete [--force] [--json]` | Delete an application and its deployments (irreversible) | -| `eds wf app deploy [--json]` | Run the pipeline and publish (the "deploy" action) | -| `eds wf app deployments [--json]` | List publish history for an application | -| `eds wf run show [--json]` / `eds wf run stop ` | Inspect/control a pipeline run | +| `eds wf app create --repository R\|--repository-url URL --branch B` | Create a Workflow Studio application from a repo + branch (auto-triggers first deploy) | +| `eds wf app list [--search S] [--sort created_at_asc\|created_at_desc]` | List applications | +| `eds wf app show ` | Show application details (status, run_id, pipeline_id, ...) | +| `eds wf app update --branch B [--name N]` | Update an application's name/branch | +| `eds wf app delete [--force]` | Delete an application and its deployments (irreversible) | +| `eds wf app deploy ` | Run the pipeline and publish (the "deploy" action) | +| `eds wf app deployments ` | List publish history for an application | +| `eds wf run show ` / `eds wf run stop ` | Inspect/control a pipeline run | | `eds wf job logs ` | Stream job logs | | `eds login --api-key --project [--repo-api-url URL]` | Persist credentials (one-time setup) | @@ -205,7 +205,7 @@ echo "my-old-repo" | eds repo delete my-old-repo --force --json ### Inspect the active configuration ```bash -eds config --json +eds config # { "project_id": "...", # "repo_api_url": "...", "api_key": "abcd…wxyz", "repo_git_host": "...", # "wf_api_url": "..." } @@ -287,7 +287,7 @@ eds wf app deploy "$APP_ID" --json | jq -r '.deployment.run_id' The CLI prints a single-line error to stderr and exits with a non-zero status on failure. Suggested agent policy: -- **2xx**: parse stdout as JSON (when `--json`) or treat stdout as human-readable text. +- **2xx**: parse stdout as JSON (or YAML when `--yaml`). - **non-zero**: read stderr, retry only on transient errors (timeouts, 5xx). Do **not** retry on 4xx — they indicate an agent bug (wrong id, missing key, etc.).