@kirrosh/zond 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +110 -3
  2. package/README.md +26 -15
  3. package/package.json +10 -6
  4. package/src/cli/commands/ci-init.ts +12 -6
  5. package/src/cli/commands/completions.ts +176 -0
  6. package/src/cli/commands/db.ts +2 -1
  7. package/src/cli/commands/generate.ts +0 -1
  8. package/src/cli/commands/init/agents-md.ts +61 -0
  9. package/src/cli/commands/init/bootstrap.ts +79 -0
  10. package/src/cli/commands/init/skills.ts +45 -0
  11. package/src/cli/commands/init/templates/agents.md +73 -0
  12. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  13. package/src/cli/commands/init/templates/skills/scenarios.md +97 -0
  14. package/src/cli/commands/init/templates/skills/zond.md +184 -0
  15. package/src/cli/commands/init/templates/zond-config.yml +15 -0
  16. package/src/cli/commands/init.ts +124 -31
  17. package/src/cli/commands/probe-methods.ts +108 -0
  18. package/src/cli/commands/probe-validation.ts +124 -0
  19. package/src/cli/commands/run.ts +99 -10
  20. package/src/cli/commands/serve.ts +52 -19
  21. package/src/cli/commands/sync.ts +0 -1
  22. package/src/cli/commands/update.ts +1 -1
  23. package/src/cli/commands/use.ts +57 -0
  24. package/src/cli/index.ts +21 -609
  25. package/src/cli/program.ts +655 -0
  26. package/src/cli/version.ts +3 -0
  27. package/src/core/context/current.ts +35 -0
  28. package/src/core/diagnostics/db-analysis.ts +11 -2
  29. package/src/core/diagnostics/render-md.ts +112 -0
  30. package/src/core/generator/chunker.ts +14 -2
  31. package/src/core/generator/data-factory.ts +50 -19
  32. package/src/core/generator/guide-builder.ts +1 -1
  33. package/src/core/generator/openapi-reader.ts +18 -0
  34. package/src/core/generator/serializer.ts +11 -2
  35. package/src/core/generator/suite-generator.ts +106 -7
  36. package/src/core/meta/types.ts +0 -2
  37. package/src/core/parser/schema.ts +3 -1
  38. package/src/core/parser/types.ts +10 -1
  39. package/src/core/parser/variables.ts +90 -2
  40. package/src/core/parser/yaml-parser.ts +50 -1
  41. package/src/core/probe/method-probe.ts +197 -0
  42. package/src/core/probe/negative-probe.ts +657 -0
  43. package/src/core/reporter/console.ts +29 -3
  44. package/src/core/reporter/index.ts +2 -2
  45. package/src/core/reporter/json.ts +5 -2
  46. package/src/core/runner/assertions.ts +4 -1
  47. package/src/core/runner/executor.ts +132 -37
  48. package/src/core/runner/http-client.ts +40 -5
  49. package/src/core/runner/rate-limiter.ts +131 -0
  50. package/src/core/setup-api.ts +4 -1
  51. package/src/core/workspace/root.ts +94 -0
  52. package/src/db/schema.ts +4 -1
package/CHANGELOG.md CHANGED
@@ -2,12 +2,66 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [Unreleased] — fix/generator-quality-improvements
5
+ ## [0.22.0] — 2026-04-29
6
+
7
+ ### Round-2 papercuts (TASK-68 → TASK-86)
8
+
9
+ - **TASK-68: `zond run --safe` (no path) no longer crashes with `paths[0] must be of type string, got boolean`.**
10
+ Commander's auto-negation `--no-db` defaulted `opts.db` to `true`; the boolean leaked into `path.resolve()` via a lazy
11
+ cast. dbPath is now normalised the same way as elsewhere; the no-path / no-`.zond-current` error is explicit and
12
+ mentions both `zond use <api>` and `--api`.
13
+
14
+ - **TASK-69: `zond db diagnose` no longer hides 5xx failures behind cluster summaries.**
15
+ `groupFailures` previously kept only the first item per group plus 2 examples — for `assertion_failed` clusters that's
16
+ fine, but for `api_error` (5xx) it silently dropped backend-bug evidence. 5xx groups are now always preserved in full
17
+ in `data.failures` and `examples`; assertion/network groups continue to fold.
18
+
19
+ - **TASK-71: YAML parse errors now report `file:line:col` plus a snippet with a column pointer.**
20
+ `Bun.YAML.parse` exposes JS-stack coordinates, not YAML positions — on failure we re-parse with `yaml` (eemeli) just
21
+ for diagnostics and surface `linePos` in the error. Pre-checks for embedded NUL bytes and points at the
22
+ `{{$nullByte}}` generator. Adds `yaml@2.8.3` dependency.
23
+
24
+ - **TASK-77: suite-level `parameterize: { key: [val, …] }` cross-product.**
25
+ Replaces copy-pasting one test across N endpoints. Multiple keys produce the cross-product. Captures and
26
+ tainted/missing-capture state are reset between iterations so values from one binding never leak into the next; step
27
+ names are interpolated through `{{var}}` so reporters and `db diagnose` can tell iterations apart.
28
+
29
+ - **TASK-79: `probe-validation` now pairs every mutating probe with a cleanup-DELETE.**
30
+ When a probe accidentally returns 2xx (the bug class probe-validation hunts for), the new follow-up `DELETE` step
31
+ (`always: true`) consumes a `leaked_id_<i>` capture and removes the resource. When the probe correctly gets 4xx, no id
32
+ is captured and the cleanup is skipped automatically. If the spec defines no DELETE counterpart, the generator emits a
33
+ warning instead. New `--no-cleanup` flag opts out for namespace-isolated test envs.
34
+
35
+ - **TASK-81: `--rate-limit auto` reads `RateLimit-*` response headers and adapts.**
36
+ Implements RFC `draft-ietf-httpapi-ratelimit-headers` plus the GitHub/Stripe `X-RateLimit-*` aliases. When `remaining`
37
+ drops to ≤5, subsequent requests pause until reset (relative-seconds vs Unix-timestamp distinguished by magnitude).
38
+ Static `--rate-limit N` benefits from the same hook — the cap is a floor, headers can push pauses out further.
39
+
40
+ - **TASK-86: `zond generate` honours `format` even when `type` is absent or array (OpenAPI 3.1 nullable).**
41
+ `format: email` on a schema with no `type` (or `type: ["string", "null"]`) used to fall through to the default branch
42
+ and produce `{{$randomString}}`. Format-to-placeholder mapping is now dispatched before the type switch.
6
43
 
7
44
  ### Breaking changes
8
45
 
9
- - **MCP layer removed** `zond mcp` command and `@modelcontextprotocol/sdk` dependency deleted.
10
- The agent interface is now exclusively the CLI + skills in `skills/`. No migration path needed.
46
+ - **MCP layer removed** (see [decision-2](backlog/decisions/decision-2%20-%20Drop-MCP-server-—-keep-CLI-agent-skills-as-the-only-integration-surface.md))
47
+ CLI is the only integration surface; agent skills in `skills/*/SKILL.md`
48
+ are read directly. Specifically:
49
+ - `zond mcp start` removed.
50
+ - `zond install --claude/--cursor` removed (was only used to write
51
+ `~/.claude/mcp.json` / `~/.cursor/mcp.json` for the MCP transport).
52
+ - `--integration mcp` flag of `zond init` removed; default integration
53
+ is now `cli` (writes a self-contained `AGENTS.md` with full workflow
54
+ inline). `--integration skip` still works.
55
+ - `@modelcontextprotocol/sdk` runtime dependency dropped.
56
+ - `src/mcp/` deleted entirely (~817 LOC).
57
+ - `src/cli/commands/install.ts` and `src/cli/commands/mcp.ts` deleted.
58
+ - `tests/integration/mcp*.test.ts` removed.
59
+ - All MCP references purged from README, ZOND.md, docs/, skills/,
60
+ AGENTS.md, CLAUDE.md.
61
+ - Migration: existing `~/.claude/mcp.json` / `~/.cursor/mcp.json` keep
62
+ referencing a `zond` server that no longer responds; remove the
63
+ `zond` entry from your client config. New flow — see updated
64
+ `AGENTS.md`: agents call `zond` commands directly.
11
65
 
12
66
  - **`zond migrate` removed** — the migration system was added and then removed in the same branch.
13
67
  Format changes in zond are backward-compatible or require a clean `zond generate`.
@@ -100,6 +154,59 @@ All notable changes to this project will be documented in this file.
100
154
  of the expected `404` (after a DELETE), the diagnostic now surfaces a "likely soft delete" hint
101
155
  with a concrete suggestion to assert the status field value.
102
156
 
157
+ - **5xx response highlighting** — console reporter now flags failed steps with HTTP 5xx
158
+ responses with a yellow `[5xx <status>]` tag, and the suite/grand-total lines show a
159
+ separate `<N> 5xx` count. The `--json` envelope adds `http_status` and `is_5xx` per
160
+ failure plus a top-level `summary.fiveXx` count, so probe-validation runs surface
161
+ bug candidates at a glance.
162
+
163
+ - **`--report-out <file>`** on `zond run` — writes the JSON or JUnit report directly to a
164
+ file (with `mkdir -p`) instead of to stdout, logging `zond: <FORMAT> report written to
165
+ <path>` on stderr. Decouples the report from any wrapper banner that prefixes stdout
166
+ (notably `bun run zond -- run …`), so downstream JSON parsers don't break.
167
+
168
+ #### Bug-hunting probes
169
+
170
+ - **`zond probe-validation <spec>`** — generates deterministic negative-input probe
171
+ suites that catch the 5xx-on-bad-input class of bugs (the contract: any malformed
172
+ client input must produce a 4xx, never a 5xx). Per endpoint emits probes for: invalid
173
+ path UUIDs, empty body, missing required fields, type confusion, invalid format
174
+ (`email`/`uri`/`date-time`/`uuid`), boundary strings (empty, 10000-char,
175
+ unicode/emoji/RTL), invalid enum values and array-of-string-enum (catches the
176
+ webhooks-events bug shape). `--max-per-endpoint` caps probe count, `--tag` filters
177
+ endpoints. Generated suites embed suite-level `base_url`/auth and are runnable as-is.
178
+
179
+ - **`zond probe-methods <spec>`** — HTTP method completeness sweep. For every path,
180
+ emits one probe per `{GET, POST, PUT, PATCH, DELETE}` method that is *not* declared
181
+ in the spec, expecting a 4xx (`401/403/404/405`). Path placeholders are substituted
182
+ with valid-shape sentinels so the request reaches the router. Catches "PUT on a
183
+ POST-only endpoint returns 500" bugs.
184
+
185
+ - **`probe-validation --list-tags`** — lists all tags from the OpenAPI spec without
186
+ generating anything. `--tag X` is now case-insensitive and trims whitespace; matching
187
+ zero endpoints exits 2 with a clear error and the available-tags list.
188
+
189
+ #### Runner
190
+
191
+ - **`zond run --sequential`** — opt-out of parallel suite execution. Forces
192
+ sequential runs of all suites (useful when a setup token must propagate or when
193
+ rate-limits make parallel suites trigger 429s).
194
+
195
+ - **Auto-load `./.env.yaml`** — `zond run` now also tries `$PWD/.env.yaml` when
196
+ `--env` is not given and neither searchDir nor its parent has one. Logs
197
+ `zond: using ./.env.yaml (cwd fallback)` on stderr. Unblocks running absolute test
198
+ paths from a collection cwd.
199
+
200
+ #### Reporter / DB
201
+
202
+ - **Cascade-skip reason inline** — console reporter now prints
203
+ `(skipped: <error>)` instead of just `(skipped)`, surfacing the underlying
204
+ capture/auth failure on the very same line.
205
+
206
+ - **Run classification** — `zond db runs` now classifies a run with `total > 0`,
207
+ `passed == 0`, and many errors as **FAIL** instead of PASS. Prevents a probe run
208
+ with all 5xx responses from looking green in the runs listing.
209
+
103
210
  ---
104
211
 
105
212
  ### Fixes
package/README.md CHANGED
@@ -4,35 +4,37 @@ AI-powered API testing for Claude Code, Cursor, and CI/CD.
4
4
 
5
5
  Say "test my API" — get working tests, coverage dashboard, and CI config in minutes.
6
6
 
7
- <!-- TODO: add demo GIF (15 sec: plugin install "cover openapi.json with tests" 42/47 endpoints covered dashboard) -->
8
-
9
- Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: structured tools, safety guardrails, coverage tracking, and run history. You don't need to learn anything new — just describe what you want and the agent handles the rest.
7
+ Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: a focused CLI, safety guardrails, coverage tracking, and run history. You don't need to learn anything new just describe what you want and the agent runs `zond` commands.
10
8
 
11
9
  ## Quick Start
12
10
 
11
+ Install the binary (no Node.js required):
12
+
13
+ ```bash
14
+ curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
15
+ iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
13
16
  ```
14
- /plugin marketplace add kirrosh/zond
15
- /plugin install zond@zond-marketplace
17
+
18
+ Bootstrap a workspace and register your first API:
19
+
20
+ ```bash
21
+ zond init --workspace --with-spec ./openapi.json
16
22
  ```
17
23
 
18
- Then say: _"Safely cover the API from openapi.json with tests"_
24
+ `zond init` writes a self-contained [`AGENTS.md`](AGENTS.md) agents read it
25
+ and use the CLI directly (`zond run`, `zond probe-validation`,
26
+ `zond db diagnose`, …). No daemon, no transport, no extra configuration.
19
27
 
20
- You get auto-validation hooks and CLI tools all in one package.
28
+ Then say to your agent: _"Safely cover the API from openapi.json with tests."_
21
29
 
22
30
  <details>
23
- <summary>Other installation methods (CLI, binary)</summary>
24
-
25
- ### CLI / Binary
31
+ <summary>Other installation methods (npx)</summary>
26
32
 
27
33
  ```bash
28
34
  npx -y @kirrosh/zond --version
29
-
30
- # Standalone binary (no Node.js required)
31
- curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
32
- iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
33
35
  ```
34
36
 
35
- See [ZOND.md](ZOND.md) for full CLI reference.
37
+ See [ZOND.md](ZOND.md) for the full CLI reference.
36
38
 
37
39
  </details>
38
40
 
@@ -67,11 +69,20 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
67
69
  "Set up CI for API tests"
68
70
  ```
69
71
 
72
+ ## Shell completions
73
+
74
+ ```bash
75
+ zond completions bash > ~/.local/share/bash-completion/completions/zond
76
+ zond completions zsh > ~/.zsh/completions/_zond # then `compinit`
77
+ zond completions fish > ~/.config/fish/completions/zond.fish
78
+ ```
79
+
70
80
  ## Documentation
71
81
 
72
82
  - [ZOND.md](ZOND.md) — full CLI reference
73
83
  - [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart (RU)
74
84
  - [docs/ci.md](docs/ci.md) — CI/CD integration
85
+ - [backlog/](backlog/) — project tasks (powered by [Backlog.md](https://backlog.md), see [docs/backlog.md](docs/backlog.md))
75
86
 
76
87
  ## License
77
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
@@ -25,17 +25,20 @@
25
25
  },
26
26
  "scripts": {
27
27
  "zond": "bun run src/cli/index.ts",
28
+ "backlog": "bunx backlog",
29
+ "board": "bunx backlog board",
28
30
  "test": "bun run test:unit && bun run test:mocked",
29
- "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/diagnostics/ tests/cli/args.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/cli/json-envelope.test.ts tests/cli/describe.test.ts tests/cli/catalog.test.ts tests/cli/db.test.ts tests/cli/request.test.ts tests/cli/init.test.ts tests/cli/guide.test.ts tests/integration/ tests/web/ tests/reporter/ tests/version-sync.test.ts",
31
+ "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/core/meta/ tests/diagnostics/ tests/cli/program.test.ts tests/cli/cli-smoke.test.ts tests/cli/completions.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/cli/json-envelope.test.ts tests/cli/describe.test.ts tests/cli/catalog.test.ts tests/cli/db.test.ts tests/cli/request.test.ts tests/cli/init.test.ts tests/cli/init/agents-md.test.ts tests/cli/init/bootstrap.test.ts tests/cli/use.test.ts tests/cli/guide.test.ts tests/cli/update.test.ts tests/cli/serve.test.ts tests/integration/ tests/web/ tests/reporter/",
30
32
  "test:mocked": "bun run scripts/run-mocked-tests.ts",
31
33
  "check": "tsc --noEmit --project tsconfig.json",
34
+ "lint:dead": "knip --reporter compact",
32
35
  "build": "bun build --compile src/cli/index.ts --outfile zond",
33
- "version:sync": "bun run scripts/sync-version.ts",
34
- "postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json",
35
36
  "bench:api": "bun benchmarks/api/server.ts"
36
37
  },
37
38
  "devDependencies": {
38
- "@types/bun": "latest"
39
+ "@types/bun": "latest",
40
+ "backlog.md": "^1.44.0",
41
+ "knip": "^6.7.0"
39
42
  },
40
43
  "engines": {
41
44
  "bun": ">=1.1.0"
@@ -44,11 +47,12 @@
44
47
  "typescript": "^5"
45
48
  },
46
49
  "dependencies": {
47
- "@humanwhocodes/momoa": "^2.0.3",
48
50
  "@hono/zod-openapi": "^1.2.2",
49
51
  "@readme/openapi-parser": "^5.5.0",
52
+ "commander": "^14.0.0",
50
53
  "hono": "^4.12.2",
51
54
  "openapi-types": "^12.1.3",
55
+ "yaml": "^2.8.3",
52
56
  "zod": "^4.3.6"
53
57
  }
54
58
  }
@@ -42,13 +42,16 @@ jobs:
42
42
  - name: Run smoke tests (read-only, safe for production)
43
43
  run: |
44
44
  mkdir -p test-results
45
- zond run apis/ --tag smoke --safe --report junit --no-db > test-results/smoke.xml
45
+ # --exclude-tag needs-id skips positive smoke that needs real IDs from .env.yaml
46
+ zond run apis/ --tag smoke --exclude-tag needs-id --safe --report junit --no-db > test-results/smoke.xml
46
47
  # Use --env-var "API_KEY=\${{ secrets.API_KEY }}" to inject secrets without writing to disk
47
48
  continue-on-error: true
48
49
 
49
- - name: Run CRUD tests (staging only)
50
+ - name: Run CRUD tests (staging only — ephemeral suites only)
50
51
  run: |
51
- zond run apis/ --tag crud --env staging --report junit --no-db > test-results/crud.xml
52
+ # --exclude-tag persistent-write keeps only ephemeral CRUD (suites that DELETE what they create).
53
+ # Drop --exclude-tag persistent-write to opt into write suites that leave residual data.
54
+ zond run apis/ --tag crud --exclude-tag persistent-write --env staging --report junit --no-db > test-results/crud.xml
52
55
  # Add --env-var "BASE_URL=\${{ secrets.STAGING_URL }}" for staging URL
53
56
  continue-on-error: true
54
57
 
@@ -86,8 +89,9 @@ api-smoke:
86
89
  - curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
87
90
  script:
88
91
  - mkdir -p test-results
89
- # Use --env-var to inject secrets without writing to disk
90
- - zond run apis/ --tag smoke --safe --report junit --no-db --env-var "API_KEY=$API_KEY" > test-results/smoke.xml
92
+ # Use --env-var to inject secrets without writing to disk.
93
+ # --exclude-tag needs-id skips positive smoke that needs real IDs from .env.yaml.
94
+ - zond run apis/ --tag smoke --exclude-tag needs-id --safe --report junit --no-db --env-var "API_KEY=$API_KEY" > test-results/smoke.xml
91
95
  allow_failure:
92
96
  exit_codes: 1
93
97
  artifacts:
@@ -102,7 +106,9 @@ api-crud:
102
106
  - curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
103
107
  script:
104
108
  - mkdir -p test-results
105
- - zond run apis/ --tag crud --env staging --report junit --no-db > test-results/crud.xml
109
+ # --exclude-tag persistent-write keeps only ephemeral CRUD (suites that DELETE what they create).
110
+ # Drop --exclude-tag persistent-write to opt into write suites that leave residual data.
111
+ - zond run apis/ --tag crud --exclude-tag persistent-write --env staging --report junit --no-db > test-results/crud.xml
106
112
  allow_failure:
107
113
  exit_codes: 1
108
114
  artifacts:
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Shell completion script generator.
3
+ *
4
+ * Builds a static completion script from a commander program tree. No external
5
+ * completion library — bash/zsh/fish templates are inlined.
6
+ *
7
+ * Install:
8
+ * bash: zond completions bash > ~/.local/share/bash-completion/completions/zond
9
+ * zsh: zond completions zsh > ~/.zsh/completions/_zond (then `compinit`)
10
+ * fish: zond completions fish > ~/.config/fish/completions/zond.fish
11
+ */
12
+
13
+ import type { Command } from "commander";
14
+
15
+ export const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const;
16
+ export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
17
+
18
+ export interface CompletionsOptions {
19
+ shell: CompletionShell;
20
+ program: Command;
21
+ }
22
+
23
+ interface CommandSpec {
24
+ name: string;
25
+ description: string;
26
+ options: string[]; // long flags only, e.g. ["--tag", "--safe"]
27
+ subcommands: CommandSpec[];
28
+ }
29
+
30
+ function extractSpec(cmd: Command): CommandSpec {
31
+ // Option's internal shape exposes `long`; commander v14 keeps this stable.
32
+ const rawOpts = cmd.options as unknown as Array<{ long?: string }>;
33
+ const opts: string[] = rawOpts
34
+ .map((o) => o.long)
35
+ .filter((s): s is string => typeof s === "string");
36
+
37
+ return {
38
+ name: cmd.name(),
39
+ description: cmd.description(),
40
+ options: opts,
41
+ // Drop commander's auto `help` subcommand to keep templates lean.
42
+ subcommands: cmd.commands
43
+ .filter((c) => c.name() !== "help")
44
+ .map(extractSpec),
45
+ };
46
+ }
47
+
48
+ // ── bash ──
49
+
50
+ function renderBash(root: CommandSpec): string {
51
+ const topLevel = root.subcommands.map((s) => s.name).join(" ");
52
+
53
+ const perCmd = root.subcommands
54
+ .map((sub) => {
55
+ const flags = sub.options.join(" ");
56
+ const subs = sub.subcommands.map((s) => s.name).join(" ");
57
+ return ` ${sub.name})
58
+ COMPREPLY=( $(compgen -W "${[flags, subs].filter(Boolean).join(" ")}" -- "$cur") )
59
+ return 0
60
+ ;;`;
61
+ })
62
+ .join("\n");
63
+
64
+ return `# bash completion for zond — generated by 'zond completions bash'
65
+ _zond_completion() {
66
+ local cur prev
67
+ cur="\${COMP_WORDS[COMP_CWORD]}"
68
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
69
+
70
+ if [ "\$COMP_CWORD" -eq 1 ]; then
71
+ COMPREPLY=( $(compgen -W "${topLevel} --help --version" -- "$cur") )
72
+ return 0
73
+ fi
74
+
75
+ case "\${COMP_WORDS[1]}" in
76
+ ${perCmd}
77
+ esac
78
+ }
79
+ complete -F _zond_completion zond
80
+ `;
81
+ }
82
+
83
+ // ── zsh ──
84
+
85
+ function renderZsh(root: CommandSpec): string {
86
+ const cmdLines = root.subcommands
87
+ .map((s) => ` '${s.name}:${s.description.replace(/'/g, "'\\''")}'`)
88
+ .join(" \\\n");
89
+
90
+ const perCmd = root.subcommands
91
+ .map((sub) => {
92
+ const flagSpecs = sub.options
93
+ .map((f) => ` '${f}[${f.slice(2)}]'`)
94
+ .join(" \\\n");
95
+ const subCmds = sub.subcommands.length > 0
96
+ ? `\n _values 'subcommand' \\\n` +
97
+ sub.subcommands.map((s) => ` '${s.name}[${s.description.replace(/'/g, "'\\''")}]'`).join(" \\\n")
98
+ : "";
99
+ return ` ${sub.name})
100
+ _arguments \\
101
+ ${flagSpecs || " ':path:_files'"}${subCmds}
102
+ ;;`;
103
+ })
104
+ .join("\n");
105
+
106
+ return `#compdef zond
107
+ # zsh completion for zond — generated by 'zond completions zsh'
108
+
109
+ _zond() {
110
+ local context state state_descr line
111
+ local -A opt_args
112
+
113
+ _arguments -C \\
114
+ '1: :->command' \\
115
+ '*::arg:->args'
116
+
117
+ case $state in
118
+ command)
119
+ _values 'zond command' \\
120
+ ${cmdLines}
121
+ ;;
122
+ args)
123
+ case $line[1] in
124
+ ${perCmd}
125
+ esac
126
+ ;;
127
+ esac
128
+ }
129
+
130
+ _zond "$@"
131
+ `;
132
+ }
133
+
134
+ // ── fish ──
135
+
136
+ function renderFish(root: CommandSpec): string {
137
+ const lines: string[] = [
138
+ "# fish completion for zond — generated by 'zond completions fish'",
139
+ ];
140
+
141
+ for (const sub of root.subcommands) {
142
+ const desc = sub.description.replace(/'/g, "\\'");
143
+ lines.push(`complete -c zond -n '__fish_use_subcommand' -a '${sub.name}' -d '${desc}'`);
144
+ }
145
+ lines.push(`complete -c zond -n '__fish_use_subcommand' -l help -d 'Show help'`);
146
+ lines.push(`complete -c zond -n '__fish_use_subcommand' -l version -d 'Show version'`);
147
+
148
+ for (const sub of root.subcommands) {
149
+ const condition = `__fish_seen_subcommand_from ${sub.name}`;
150
+ for (const opt of sub.options) {
151
+ lines.push(`complete -c zond -n '${condition}' -l ${opt.slice(2)}`);
152
+ }
153
+ for (const nested of sub.subcommands) {
154
+ const desc = nested.description.replace(/'/g, "\\'");
155
+ lines.push(`complete -c zond -n '${condition}' -a '${nested.name}' -d '${desc}'`);
156
+ }
157
+ }
158
+
159
+ return lines.join("\n") + "\n";
160
+ }
161
+
162
+ // ── Public entry ──
163
+
164
+ export function completionsCommand(options: CompletionsOptions): number {
165
+ const spec = extractSpec(options.program);
166
+
167
+ let script: string;
168
+ switch (options.shell) {
169
+ case "bash": script = renderBash(spec); break;
170
+ case "zsh": script = renderZsh(spec); break;
171
+ case "fish": script = renderFish(spec); break;
172
+ }
173
+
174
+ process.stdout.write(script);
175
+ return 0;
176
+ }
@@ -46,7 +46,8 @@ export async function dbCommand(options: DbOptions): Promise<number> {
46
46
  } else {
47
47
  for (const r of runs) {
48
48
  const run = r as any;
49
- const status = run.failed > 0 ? "FAIL" : "PASS";
49
+ const isFail = run.failed > 0 || (run.total > 0 && run.passed === 0);
50
+ const status = isFail ? "FAIL" : "PASS";
50
51
  console.log(`#${run.id} ${status} ${run.passed}/${run.total} passed (${run.started_at})`);
51
52
  }
52
53
  }
@@ -92,7 +92,6 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
92
92
  await writeMeta(options.output, {
93
93
  zondVersion: ZOND_VERSION,
94
94
  lastSyncedAt: new Date().toISOString(),
95
- specUrl: options.specPath,
96
95
  specHash: hashSpec(specContent),
97
96
  files: { ...(existingMeta?.files ?? {}), ...metaFiles },
98
97
  });
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import agentsTemplate from "./templates/agents.md" with { type: "text" };
5
+
6
+ export const START_MARKER = "<!-- zond:start -->";
7
+ export const END_MARKER = "<!-- zond:end -->";
8
+
9
+ export interface AgentsBlockResult {
10
+ path: string;
11
+ action: "created" | "updated" | "noop";
12
+ }
13
+
14
+ function blockBody(): string {
15
+ return agentsTemplate.trim();
16
+ }
17
+
18
+ function wrap(body: string): string {
19
+ return `${START_MARKER}\n${body}\n${END_MARKER}`;
20
+ }
21
+
22
+ const BLOCK_RE = new RegExp(
23
+ `${escapeRe(START_MARKER)}[\\s\\S]*?${escapeRe(END_MARKER)}`,
24
+ );
25
+
26
+ function escapeRe(s: string): string {
27
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+
30
+ /**
31
+ * Idempotently inserts (or updates) the zond instruction block in `<cwd>/AGENTS.md`.
32
+ *
33
+ * - Missing file → create with just the block.
34
+ * - File without markers → append block at the end (preceded by `\n\n---\n\n`).
35
+ * - File with existing markers → replace the body between them.
36
+ * - File whose existing block already matches → noop.
37
+ */
38
+ export function upsertAgentsBlock(cwd: string): AgentsBlockResult {
39
+ const path = join(cwd, "AGENTS.md");
40
+ const next = wrap(blockBody());
41
+
42
+ if (!existsSync(path)) {
43
+ writeFileSync(path, next + "\n", "utf-8");
44
+ return { path, action: "created" };
45
+ }
46
+
47
+ const current = readFileSync(path, "utf-8");
48
+
49
+ if (BLOCK_RE.test(current)) {
50
+ const updated = current.replace(BLOCK_RE, next);
51
+ if (updated === current) return { path, action: "noop" };
52
+ writeFileSync(path, updated, "utf-8");
53
+ return { path, action: "updated" };
54
+ }
55
+
56
+ // Append with separator
57
+ const sep = current.endsWith("\n") ? "\n" : "\n\n";
58
+ const updated = current + sep + "---\n\n" + next + "\n";
59
+ writeFileSync(path, updated, "utf-8");
60
+ return { path, action: "updated" };
61
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ import { upsertAgentsBlock, type AgentsBlockResult } from "./agents-md.ts";
5
+ import { upsertSkills, type SkillResult } from "./skills.ts";
6
+ import zondConfigTemplate from "./templates/zond-config.yml" with { type: "text" };
7
+
8
+ export interface BootstrapOptions {
9
+ cwd?: string;
10
+ /** Whether to write/upsert AGENTS.md. Defaults to true. */
11
+ writeAgents?: boolean;
12
+ /** Whether to write Claude Code skills under .claude/skills/. Defaults to true. */
13
+ writeSkills?: boolean;
14
+ /** Override $HOME — used by tests and intentional overrides. */
15
+ home?: string;
16
+ dryRun?: boolean;
17
+ }
18
+
19
+ export interface BootstrapResult {
20
+ cwd: string;
21
+ configPath: string;
22
+ configAction: "created" | "noop";
23
+ apisDir: string;
24
+ apisAction: "created" | "noop";
25
+ agents: AgentsBlockResult | null;
26
+ skills: SkillResult[];
27
+ warnings: string[];
28
+ }
29
+
30
+ /**
31
+ * Idempotent workspace bootstrap. Creates `zond.config.yml`, `apis/`, and
32
+ * (unless `writeAgents` is false) `AGENTS.md`.
33
+ */
34
+ export function bootstrapWorkspace(opts: BootstrapOptions = {}): BootstrapResult {
35
+ const cwd = resolve(opts.cwd ?? process.cwd());
36
+ const warnings: string[] = [];
37
+ const writeAgents = opts.writeAgents ?? true;
38
+ const writeSkills = opts.writeSkills ?? true;
39
+
40
+ // 1. zond.config.yml
41
+ const configPath = join(cwd, "zond.config.yml");
42
+ let configAction: "created" | "noop" = "noop";
43
+ if (!existsSync(configPath)) {
44
+ if (!opts.dryRun) writeFileSync(configPath, zondConfigTemplate, "utf-8");
45
+ configAction = "created";
46
+ }
47
+
48
+ // 2. apis/
49
+ const apisDir = join(cwd, "apis");
50
+ let apisAction: "created" | "noop" = "noop";
51
+ if (!existsSync(apisDir)) {
52
+ if (!opts.dryRun) mkdirSync(apisDir, { recursive: true });
53
+ apisAction = "created";
54
+ }
55
+
56
+ // 3. AGENTS.md
57
+ let agents: AgentsBlockResult | null = null;
58
+ if (writeAgents) {
59
+ if (!opts.dryRun) {
60
+ agents = upsertAgentsBlock(cwd);
61
+ } else {
62
+ agents = { path: join(cwd, "AGENTS.md"), action: existsSync(join(cwd, "AGENTS.md")) ? "updated" : "created" };
63
+ }
64
+ }
65
+
66
+ // 4. .claude/skills/zond-*/SKILL.md
67
+ const skills: SkillResult[] = writeSkills ? upsertSkills(cwd, { dryRun: opts.dryRun }) : [];
68
+
69
+ return {
70
+ cwd,
71
+ configPath,
72
+ configAction,
73
+ apisDir,
74
+ apisAction,
75
+ agents,
76
+ skills,
77
+ warnings,
78
+ };
79
+ }