Commit Graph

10 Commits

Author SHA1 Message Date
nullkey e623e8208f refactor(cli): delete envelope infrastructure, errors to stderr
Removes the entire envelope machinery now that every success path
emits bare JSON:

- cli/internal/format/envelope.go (Envelope, Success, Failure,
  SuccessWithRisk, WriteEnvelope, Meta, Notice, UpdateNotice,
  VersionSkewNotice, Risk, RiskLevel, ErrorBody) + tests.
- cli/internal/format/filter.go envelope-specific helpers
  (WriteEnvelopeFiltered, marshalEnvelope, applyFieldFilter,
  filterDataPayload, filterObjectData); the reusable
  filterArrayItems / filterObjectKeys / writeJQ stay for bare.go.
- cli/internal/cmdutil/exporter.go + tests (envelope-only).
- cli/internal/cmdutil/PrintErrorEnvelope + ToErrorBody +
  operationRiskOf + Error.OperationRisk field + OperationRisk struct.

Error path: all errors now go to stderr via cmdutil.PrintError in
`code: message\nhint: ...` form, regardless of --json. Stdout stays
empty (or holds the partial-success the command already wrote) so
downstream `--json | jq` pipelines never have to filter error shapes
out of the success stream. Typed exit codes (3 auth.* / 4
resource.not_found / 5 input.* / 6 server.rate_limited / 7 server.*
+ network.* / 10 input.confirmation_required) carry the failure
class for agents that branch on it.

Acceptance contract:
- envelope_test.go → wire_test.go (TestEnvelopeGolden → TestWireGolden).
- testdata/envelopes/ → testdata/wire/.
- Error-path cases assert the typed code substring on stderr.
- Orphan whoami.*.json goldens deleted.

AGENTS.md + README.md rewritten for the bare-data contract:
- Drop envelope schema section + dry-run rule.
- Document bare JSON on stdout + `code: msg\nhint: …` on stderr.
- ADR-3 reframed around bare data and why error separation matters
  for `--json | jq` pipelines.

WriteJSONFiltered short-circuits to WriteJSON when both filters are
empty (skip the marshal-buffer round-trip for the common case).

Final review pass:
- Fix wire-contract bug: `--json id,name` (space form) is broken by
  pflag's NoOptDefVal; AGENTS.md / README.md / SetAgentHelp + the
  field-discovery help text all switched to `--json=id,name`.
- Fix `weknora api --jq` silently ignored: api.go now routes through
  WriteJSONFiltered with jopts.JQ.
- AGENTS.md: drop the false claim that `auth logout` honors `-y`
  (logout is local-only with no ConfirmDestructive guard); list the
  actual destructive commands instead.
- Rewrite cli/acceptance/e2e/e2e_test.go for the bare-data wire shape
  (was still parsing `out["data"]` / `env["ok"]`).
- Add `JSONOptions.Emit(w, v)` helper; collapse ~33 repeated
  `format.WriteJSONFiltered(iostreams.IO.Out, X, jopts.Fields,
  jopts.JQ)` sites to `jopts.Emit(iostreams.IO.Out, X)` — drops the
  format import from 22 cmd/* files.
- Delete single-caller `cmdutil.MustRequireFlag`; inline as
  `_ = cmd.MarkFlagRequired(...)` everywhere.
- Add `_ = cmd.MarkFlagRequired("name")` to `kb create`; it was the
  only write command relying on runtime --name validation while
  `context add` already used the cobra-level mark.
- `context use`: register `--json` / `--jq` (was always emitting JSON
  unconditionally with no human path and no flag — diverged from
  every other write command); human mode now prints
  `✓ Switched context to X (was Y)`.
- Replace per-package `confirmPrompter` / `scriptedConfirm` /
  `errPrompter` test doubles with `testutil.ConfirmPrompter`.
- Rename `chatService` → `ChatService` (export to match siblings
  `ListService` / `ViewService`); rename `printUploadSuccess` →
  `renderUploadSuccess` (siblings use `render*`).
- `defaultHint(CodeResourceNotFound)`: drop the hardcoded
  "list available with `weknora kb list`" — misleading on agent /
  doc / session 404. Replaced with "verify the resource ID and try
  again".
- Strip stale `v0.2/v0.3` / "envelope" / "v0.0/v0.1 supports only"
  historical tags from production comments and a few test
  descriptions.
2026-05-15 12:03:56 +08:00
nullkey cc8254f862 refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):

1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
   (gh, aws, stripe, lark): none expose --dry-run. The mainstream that
   does (kubectl/git/helm/ansible) operates on declarative manifests
   or local state where the preview is materially different from the
   executed action. WeKnora's CLI just echoed the same parameters
   that would have gone on the wire — the preview added no real
   signal over `--help` + reading the call site. Removes:
   - root --dry-run persistent flag + cmdutil/dryrun.go
   - DryRun fields + EmitDryRun calls in 12 write commands
   - format.Envelope.DryRun field
   - 8 corresponding *_test.go cases
   - --dry-run mention from README.md and CHANGELOG.md
   - "dry_run":false from 16 golden envelopes

2. Migrate every --json output to bare data:
   - New format.WriteJSON / WriteJSONFiltered helpers
     (cli/internal/format/bare.go) share filterArrayItems /
     filterObjectKeys / writeJQ with the (still-live for now) envelope
     filter helpers.
   - Read commands (kb/doc/session list+view, search chunks/docs/
     sessions/kb, auth list/status, agent list/view, context list,
     doctor) emit bare arrays / objects on stdout.
   - Write commands (kb create/edit/delete/pin/empty, doc upload/
     upload_recursive/delete, session delete, auth login/logout/
     refresh/token, link/unlink, context add/use/remove, agent
     invoke, chat, api, version) emit bare result objects. Risk
     classification dropped — the resource + exit code already
     convey the action.

Per-command shape changes:
   list / search       → []T   (was {ok, data:{items:[…]}})
   view                → T     (was {ok, data:T, _meta:…})
   create / edit       → T
   delete / pin / etc. → {id, …action result…}
   doctor              → {summary, checks}
   api                 → {status, headers, body}

_meta dropped on the read path:
   pagination (page/page_size/total/has_more) — agents iterate with
   --all-pages or accept --limit (gh CLI parity);
   kb_id / context echo — caller already knows what it asked for.

Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
2026-05-15 12:03:56 +08:00
nullkey bdc589e1c0 refactor(cli): --limit/--all-pages, Go 1.26, internal/agent → aiclient
Cross-cutting cleanup that lands alongside the new feature surface:

- `--limit / -L` and `--all-pages` on every list command. Default
  --limit 30 (gh-parity); --all-pages drains every server page
  client-side, capped by --limit. Closes the audit finding that the
  old "1000 max per call" implicit cap was undiscoverable.
- `auth token` emits a TTY-only stderr advisory when stdout is a
  terminal (the credential just got displayed in scrollback) plus an
  api-key-mode rotation hint.
- Comment + doc discipline pass: drop external project name
  references from in-code comments (we reference them in design notes,
  not inline).
- Bump `go` directive to 1.26.0 and CI matrix to 1.26.x to align with
  the main module's go.mod.
- Rename `cli/internal/agent` → `cli/internal/aiclient` to
  disambiguate from the new `cli/cmd/agent` resource subtree. The
  package handles AI coding-agent env detection + per-command --help
  annotations; the new name reflects that more precisely.
2026-05-15 12:03:56 +08:00
nullkey 9bb83b47fd feat(cli): mcp serve curated stdio MCP server
`weknora mcp serve` — long-lived stdio MCP (Model Context Protocol)
transport that exposes a fixed, curated tool surface to MCP-aware
agents (Claude Desktop, Claude Code, custom MCP clients).

Curated tool set (readonly by default):
- whoami — active context + tenant
- search (hybrid retrieval against a KB)
- kb list / view
- doc list / view
- agent list / view / invoke
- session list / view

The list is intentionally narrow to the read + agent-invoke surface;
destructive verbs (`delete` / `empty` / `upload`) are gated behind
`--write`. Schema is built from each leaf cobra command's flags so
adding a new tool is a single registry entry plus a Service interface.

Includes the simplify post-review polish + a second simplify pass to
fold the resulting feedback (typed schemas, agent_help wording, unify
chat / agent invoke option names).
2026-05-15 12:03:56 +08:00
nullkey 1b20b06f5e feat(cli): --json field-select, --jq, auth token, doc --from-url
Output ergonomics:
- `--json` accepts a comma-separated field list (gh-parity); selects
  named keys from the per-command payload. Bare `--json` keeps the
  full shape.
- `--jq <expr>` evaluates a gojq expression over the JSON; pairs with
  `--json field-list` so projection runs before jq.
- `--version` is a global cobra flag in addition to the `version`
  subcommand; both render the same line.
- Per-command `--help` now renders the available JSON field list under
  "JSON fields available via `--json id,name,...`" (field-discovery
  parity with gh / kubectl `-o jsonpath`).

New commands:
- `auth token` — print the active context's credential to stdout for
  shell command substitution (`WEKNORA_TOKEN=$(weknora auth token)`).
  Default: raw secret, no trailing newline. `--json` emits
  `{token, mode, context}`.
- `doc upload --from-url <URL>` — ingest a remote URL via the SDK
  `CreateKnowledgeFromURL`. `--name` forwarded as `FileName` so the
  server's known-extension heuristic upgrades crawl-mode to
  file-download-mode where appropriate.

Includes the simplify post-review polish pass (field-filter unit
tests, --json/--jq compose check, agent_help copy fixes).
2026-05-15 12:03:56 +08:00
nullkey 5adcedf170 refactor(cli): v0.3 cross-cutting cleanup
Cross-cutting findings surfaced by the branch-completion review.

Perf bug:
- Factory.Client closure was not memoized. Factory.ResolveKB internally
  calls f.Client() to resolve --kb name → id, then the command's RunE
  calls f.Client() again. Two SDK clients, two keyring reads, two
  AuthRetryTransport allocations per name-resolved invocation, with
  *independent* token state (a refresh in one was invisible to the
  other). Switched to sync.Once like Secrets already does.

Silent bug bait:
- cmdutil.NormalizeHost docstring claimed CodeInputMissingFlag for the
  empty case; code returned CodeInputInvalidArgument. Aligned doc to
  code (present-but-empty is a bad value, not a missing flag).

Agent contract gaps:
- Five user-facing subcommands lacked SetAgentHelp: auth login /
  logout / list / status and chat. Added concise strings with error-
  code call-outs so agents can branch without parsing human strings.

Helper extraction (≥3 callers):
- text.KnowledgeDisplayName(fileName, title, id) — byte-identical
  formatter that was in both cmd/doc/list.go and cmd/search/docs.go.
  Takes fields directly so internal/text stays SDK-free.
- cmdutil.WrapHTTP(cause, fmt, args...) *Error — replaces the
  `Wrapf(ClassifyHTTPError(err), err, ...)` pattern across 24 SDK
  call sites. Sed-driven migration; off-pattern shapes in chat.go
  (used streamErr) and cmdutil/kb.go (in-package) hand-edited.
  Contract test gains a comment update: post-migration the dominant
  pattern is WrapHTTP which the AST scanner skips entirely (only
  NewError/Wrapf selectors inspected); ClassifyHTTPErrorOutputs()
  bridge still covers the dynamic codes those paths can yield.

UX consistency:
- cmd/doc/list.go --page-size help now reads "Items per page
  (1..1000)" matching cmd/session/list.go. The bounds validation
  already enforced 1..1000; the help text was the last drift.

Comment-discipline sweep:
- Deleted the WHAT-only "*Options captures `weknora ...` flag state"
  docstring across 23 files (context, kb, auth, doc, session, search,
  chat, doctor, link). Where the line carried a real WHY clause
  (kb/delete, doc/delete, session/delete, kb/edit), kept the WHY and
  dropped only the leading WHAT phrase.
- Stripped third-party project-name attribution from inline comments
  and one user-visible flag-help string across ~40 files in cli/cmd
  and cli/internal (plus 4 test-file comments). Removed phrases like
  "Mirrors `gh X`", "borrowed from lark-cli", "kubectl-style",
  "gcloud `--project`", "Stripe pattern", and the embedded GitHub
  URLs pointing at those projects. Behavioral descriptions and the
  WHY behind each comment are preserved; only the upstream-name
  attribution is gone. Inspiration / north-star references belong in
  cli/AGENTS.md (the design doc) and commit messages, not scattered
  through every file.

  Triggered by an audit round that surfaced several false / fragile
  parity claims (e.g. "Mirrors `gh repo edit`" — gh repo edit has no
  --name flag; "matches gcloud `--project` id-or-name" — gcloud's
  --project accepts ID only). Rather than fix them one by one, the
  whole category of in-comment external-project references was
  stripped uniformly.
2026-05-14 10:57:17 +08:00
nullkey e236be1ced fix(cli): correct KB id detection, SSE terminal-frame, and CI test isolation
Three defects surfaced during end-to-end RAG verification — the first two
block real chat usage, the third makes Linux CI flaky:

1. KB id detection — `IsKBID` was checking
   `strings.HasPrefix(s, "kb_")`, but WeKnora generates KB ids as bare
   UUIDs (internal/types/knowledge_base.go: `uuid.New().String()` stored
   in a `varchar(36)` column). Real ids therefore fell through to the
   name-resolution path:

     $ weknora chat ... --kb a32a63ff-fb36-4874-bcaa-30f48570a694
     Error: knowledge base not found: a32a63ff-...

   Switched the discriminator to a UUID regex
   (`^[0-9a-fA-F]{8}-…-[0-9a-fA-F]{12}$`). KB names are arbitrary
   user-supplied strings, so the canonical 8-4-4-4-12 form is an
   unambiguous signal. Mirrors gcloud `--project`'s id-vs-name detection.

2. SSE terminal-frame — the accumulator's `Append` was gating
   finalization on `r.Done`, but the server's KnowledgeQAStream protocol
   emits a leading `agent_query` frame with `done=true` to deliver
   session + message metadata *before* the answer fragments arrive:

     event: message
     data: {"response_type":"agent_query","content":"","done":true,…}

     event: message
     data: {"response_type":"answer","content":"你好","done":false}
     …
     event: message
     data: {"response_type":"complete","content":"","done":true}

   The accumulator therefore flipped to `finished=true` on frame #1 and
   discarded every subsequent answer fragment — `weknora chat … --json`
   returned `answer: ""` even though the LLM reported completion_tokens
   > 0. Fixed: terminate only on `response_type == complete`.
   References still captured opportunistically (they may arrive on a
   dedicated `references` event before the terminator).

3. doctor credential_storage CI isolation — the check probes the real
   OS keyring via `secrets.NewBestEffortStore()`: present on macOS dev
   machines → StatusOK; absent on Linux CI runners without libsecret /
   Gnome-Keyring → StatusWarn ("falling back to file store"). That
   host-dependence was leaking into two test classes that assumed
   StatusOK:

     * cmd/doctor/doctor_test.go: TestDoctor_AllOK and
       TestDoctor_NoConfig_StillRunsCredentialStorage already had a
       withCredStoreFactory seam but didn't use it. Added the pin.

     * acceptance/contract/envelope_test.go: doctor.success_offline
       and doctor.error_network golden cases. The contract test runs
       through the cobra tree in-process and shares cmd/doctor's
       package-level credStoreFactory var — but couldn't reach it
       because the existing seam was unexported.

   Fix: export `doctor.SetCredStoreFactoryForTest(fn) (restore func())`
   for out-of-package tests; acceptance/contract/helpers_test.go adds
   a TestMain that pins the factory to a MemStore-returning closure
   for the whole suite (MemStore is neither *FileStore nor a real
   keyring, so doctor's type-switch hits StatusOK). Production stays
   at secrets.NewBestEffortStore — only the test hook is now reachable
   from across packages.

Test fixtures and goldens that used the old `kb_xxx` literals or
`Done: true` terminators were rewritten to use real UUIDs and
`ResponseType: ResponseTypeComplete` respectively. Per-command --help
text and Long descriptions / Examples now show a UUID rather than
`kb_…` so users see the correct shape from the start. New
TestAccumulator_IgnoresAgentQueryDone pins the SSE terminator bug so
it can't regress.

Tests: 24 cli packages green on macOS dev + Linux/macOS/Windows CI
matrix. Verified end-to-end against a live WeKnora server: `weknora
chat "..." --kb <UUID> --no-stream --json` returns the full LLM answer
in the envelope, live token streaming in TTY mode works, and the
credential_storage check renders deterministic envelopes across hosts.
2026-05-12 13:20:42 +08:00
nullkey 8bcbf5a154 refactor(cli): align command surface with mainstream conventions
Empirical mainstream-CLI surveys (gh / kubectl / aws / gcloud / stripe /
flyctl / terraform / vercel / netlify / lark) drove five alignment
fixes — each replaces a weknora-only design choice that mainstream CLIs
do not share. No backwards-compat shims; the CLI has no v0.1 users yet.

1. Single --kb flag (was --kb-id + --kb mutually exclusive)

   Survey: 0/7 mainstream CLIs use two parallel flags for "by id" vs
   "by name". Single flag (gh -R, gcloud --project) or positional
   (kubectl, stripe, terraform). Closest analog — gcloud --project —
   collapses identifier types onto one flag.

   Now: every command exposes one --kb flag; client-side prefix
   detection (cmdutil.IsKBID looks for "kb_") routes id-form values
   through directly and name-form values through ListKnowledgeBases.
   Mirrors gcloud --project's id-or-name auto-detection.

   Touched: search, chat, doc list / upload / delete, link.
   Factory.ResolveKB chain trimmed from 5 levels to 4.

2. link supersedes init

   Survey: only vercel and netlify ship both `init` AND `link` as
   siblings, and they keep them semantically distinct. weknora's pair
   wrote the same .weknora/project.yaml file with the same meaning,
   differentiated only by interactivity — that's a flag concern, not
   a command concern.

   Now: cmd/init/ deleted. cmd/link absorbs the interactive flow:
     - link --kb <id-or-name>  → non-interactive write
     - link on a TTY            → interactive prompt (lists KBs)
     - link non-TTY without --kb → CodeKBIDRequired
   Always overwrites silently (matches vercel link / netlify link /
   kubectl apply rather than git init's refuse-if-exists).

   Dead code purged: --force flag, CodeProjectAlreadyLinked error code.

3. whoami dropped

   Survey: 7/7 mainstream CLIs ship exactly one identity command —
   never both a status and a whoami. gh / gcloud / stripe pick status
   (config + live API); aws / kubectl / flyctl pick whoami (live API).

   weknora's auth status was already a superset of whoami (host +
   context + user + email + tenant_id + tenant_name vs user_id +
   tenant_id), so dropping whoami preserves all functionality and
   aligns with the gh / gcloud / stripe form.

4. kb get alias dropped

   `view` was already primary (gh repo view / gh pr view convention);
   `get` was kept as a cobra alias for v0.0/v0.1 callers. With no
   v0.0/v0.1 users to break, the alias is just noise on the command
   surface. Acceptance contract envelope cases renamed kb_get.* →
   kb_view.*; goldens renamed in lockstep.

5. api refactored to gh shape (-X/--method, default GET, auto-POST)

   gh CLI's signature is `gh api <endpoint> [--method M]` — single
   positional path, method as a flag, default GET, auto-promoted to
   POST when a body is supplied. weknora's previous `api <method>
   <path>` inverted this and forced the method to be passed even for
   GET — a needless deviation from our declared north star.

   Now: `api <path> [-X METHOD] [--data ...]`. Exit-10 protocol
   on the DELETE escape-hatch is preserved; -X DELETE still hits
   ConfirmDestructive when -y absent.

Plus: AGENTS.md gains an explicit note that `doctor` is a deliberate
divergence from gh / lark — borrowed from `flutter doctor` / `brew
doctor` because RAG deployments routinely break on misconfigured
embeddings / storage / credentials and a 4-status structured envelope
is the cleanest surface for it.

Tests: 24 cli packages green (was 26 in PR-14; init + whoami packages
removed). Acceptance contract envelope cases for whoami removed,
kb_get → kb_view renamed, search args / mock path updated for the
kb_<id> form. e2e harness flag args updated. Factory.ResolveKB tests
rewritten for the single-flag shape. api_test driver updated for the
positional-path / -X-method shape.
2026-05-12 13:20:42 +08:00
nullkey f7d7c8054d chore(cli): remove unused v0.0 scaffolding
Foundation PR-1 reserved several internal packages and helpers as
scaffolding for follow-up PRs that ended up taking different routes.
Audit confirms zero production references; this commit removes them so
the cli/ tree reflects what's actually shipped.

Removed (148 LOC):

  cli/internal/safepaths/                 — `Validate` / `WithinRoot` /
                                            three sentinel errors. Reserved
                                            for `weknora doc upload`'s path
                                            scrubbing; that command landed
                                            in PR-10 using its own
                                            `validateUploadPath` (os.Stat +
                                            regular-file check) — sufficient
                                            for the actual threat model
                                            (local CLI invocations).

  cli/internal/cmdutil/json_flags.go      — `AddJSONFlags` helper +
                                            unused --jq / --template flag
                                            registration. Reserved for PR-3
                                            "lipgloss tables / jq evaluator"
                                            which never materialized; every
                                            command directly registers
                                            BoolVar(&JSONOut, "json", ...)
                                            since v0.0 ship time.

  cmdutil.NewTableExporter                — empty alias for jsonExporter,
                                            reserved for the same PR-3
                                            renderer. Removed; jsonExporter
                                            stays under NewJSONExporter.

  cmdutil.Options marker interface        — empty interface{} reserved as a
                                            convention; no command embeds
                                            or asserts against it.

Stale comments fixed:

  - cmd/root.go: package comment updated kb (list+get) → kb
    (list+view+create+delete) and noted the `get` cobra alias.
  - cmd/root.go: dropped --no-version-check forward-reference (no such flag).
  - cmd/root.go: removed "(PR-7)" attribution from NewRootCmd doc comment.
  - cmd/kb/kb.go: same package-comment update.
  - cmd/chat/chat.go: replaced "PR-7" mention in --help example with a
    generic placeholder so cobra-rendered help is review-clean.
  - cmd/search/search.go: removed "Lipgloss tables arrive in PR-3"
    forward-reference; the inline indent helper is the shipped form.
  - internal/agent/annotations.go: ShouldUseAgentMode → DetectAIAgent
    (removed in PR-12).

AGENTS.md "Known limitations" section added:
  Documents that chat / search / doc upload currently surface server-side
  precondition misses (LLM / vector store / storage engine not configured)
  as `network.error` with `context deadline exceeded`. A planned future
  release will introduce a `precondition.*` typed error namespace
  (server returns HTTP 412 before opening the SSE / streaming response).
  This documents the limitation honestly for reviewers and integrators
  rather than claiming a behavior we don't yet have.

Tests: 27 cli packages pass (safepaths_test was the 28th — gone with the
package). go vet clean.
2026-05-12 13:20:42 +08:00
nullkey 3fb3583a92 feat(cli): add api passthrough, chat streaming, doctor warn status
Close the v0.2 RAG demo loop and ship the validation infrastructure:

  - weknora api <method> <path> [--data X | --data-file F]
      Raw passthrough wrapping client.Raw, gh-style. JSON envelope mode
      surfaces status / headers / parsed body. Non-2xx routes through
      cmdutil.ClassifyHTTPStatus (factored out of ClassifyHTTPError so
      both SDK-error and direct-status paths stay aligned — reuse review).

  - weknora chat <text> [--session-id S] [--no-stream]
      KnowledgeQAStream consumer with two output modes:
        - TTY default: token streaming + references footer
        - --json / --no-stream / non-TTY: buffered single envelope
      Auto-creates a session when --session-id is omitted; the id prints
      to stderr at start AND on stream failure (^C scrolls past the
      first announcement, so the recovery hint is re-surfaced when the
      user is most likely to need it).

  - cli/internal/sse/Accumulator
      buffers Content / References / SessionID across SDK callbacks.
      Idempotent post-Done so misbehaving servers don't corrupt state.

  - doctor: ok → ok / warn / fail / skip
      warn marks soft issues that don't block: server within compat range
      but >=1 minor behind CLI; credential storage falling back to file
      because keyring is unavailable. Envelope.ok stays true on warn,
      flips false on fail (exit 1). doctor.error_network golden updated.

  - cli/acceptance/e2e/    real-server RAG full loop
      Build-tagged //go:build acceptance_e2e — kept out of the default
      `go test ./...`. Exercises kb create → doc upload → poll ready →
      search → chat → cleanup against a server pointed at by
      WEKNORA_E2E_HOST / _TOKEN.

  - .github/workflows/cli-e2e.yml
      manual workflow_dispatch + label-gated PR trigger
      ("acceptance-e2e"). No-ops gracefully when the secrets aren't set
      so cross-fork PRs can't accidentally fail the suite.
2026-05-12 13:20:42 +08:00