Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 >
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
10 changes: 5 additions & 5 deletions cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
},
}

Expand Down
13 changes: 6 additions & 7 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ type runtimeContext struct {
Cfg *config.Config
API *repoapi.Client
Printer *output.Printer
Quiet bool
ProjectID string
WorkflowClient *workflowclient.APIClient
}
Expand Down Expand Up @@ -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()
Expand All @@ -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),
}
Expand Down
7 changes: 1 addition & 6 deletions cmd/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down
57 changes: 3 additions & 54 deletions cmd/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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)
},
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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...)
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
7 changes: 1 addition & 6 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Loading
Loading