apcore-cli 0.6.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,193 @@ All notable changes to apcore-cli (TypeScript SDK) will be documented in this fi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.0] - 2026-05-08
9
+
10
+ ### Security
11
+
12
+ - **D11-008 — `AuditLogger.getUser` now includes `LOGNAME` in the env-var fallback chain** (`src/security/audit.ts:138`). The canonical user-resolution chain per spec `security.md` is `getlogin → pwd.getpwuid(getuid).pw_name → USER → LOGNAME → USERNAME → unknown`. The TS port collapsed the first two POSIX steps via `os.userInfo()` (correct) but then jumped from `USER` straight to `USERNAME`, dropping `LOGNAME`. On systems where only `LOGNAME` is set (some CI runners), audit entries fell through to `USERNAME` or `"unknown"` while Python/Rust/Go correctly resolved to `LOGNAME` — a cross-SDK divergence affecting the audit trail's user attribution. Fix inserts `process.env.LOGNAME` between `USER` and `USERNAME`.
13
+
14
+ ### Removed (BREAKING)
15
+
16
+ - **D9-002 — root-level deprecation shims removed (FE-13 §11.3).** Pre-v0.8
17
+ `createCli()` registered 13 hidden root-level commands (`list`, `describe`,
18
+ `exec`, `init`, `validate`, `health`, `usage`, `enable`, `disable`,
19
+ `reload`, `config`, `completion`, `describe-pipeline`) that printed a
20
+ `WARNING: '<name>' as a root-level command is deprecated. ... Will be
21
+ removed in v0.8` line on stderr and forwarded to `apcli <name>`. Per
22
+ PROTOCOL_SPEC FE-13 §11.3 these shims are removed in v0.8 — the `apcli`
23
+ sub-group (or the renamed `builtinGroupName`) is now the only path to
24
+ built-in commands. Internal `_DEPRECATED_ROOT_COMMANDS`,
25
+ `_registerDeprecationShims`, `_collectShimForwardArgs`, and the
26
+ `__isDeprecationShim` collision-detection branch in `createCli`'s
27
+ `extraCommands` handler are deleted.
28
+ - **D9-W1 — raw mutable bindings `verboseHelp` / `docsUrl` no longer exported** from `src/main.ts:50,58`. The setter pair `setVerboseHelp` / `setDocsUrl` is the sanctioned API and remains exported. Mutable `let` exports are brittle as a public surface (live-binding semantics depend on the importer's bundler) and no embedder imports the raw bindings; the public surface now only includes the setters.
29
+ - **D9-W2 — `emitErrorJson` / `emitErrorTty` no longer exported** from `src/main.ts:158,180` and dropped from the `src/index.ts` public re-export list. Both are only consumed inside `main.ts` (`buildModuleCommand` action handler). Now annotated `@internal`.
30
+
31
+ ### Added
32
+
33
+ - **`builtinGroupName?: string` option on `createCli`** — downstream branded CLIs that embed apcore-cli can now expose the built-in commands under a custom namespace (e.g. `mycorp-cli admin health` instead of `mycorp-cli apcli health`). `ApcliGroup` gains a `name` getter and the constructor option is threaded through `fromCliConfig` / `fromYaml` / `tryFromYaml` / `_build`. Default `"apcli"` is unchanged. Validated against `/^[a-z][a-z0-9_-]*$/`; invalid values exit 2. Two new module-level accessors `getReservedGroupNames()` / `setReservedGroupNames()` expose the live reserved-set so `cli.ts`'s `assertNotReserved` and `listCommands` honour the renamed group. Env var `APCORE_CLI_APCLI` and config keys `apcli.*` deliberately do NOT rename — they are apcore-cli-internal toggles, not user-facing. Cross-SDK parity with Python `create_cli(builtin_group_name=...)`. New `DEFAULT_BUILTIN_GROUP_NAME` constant exported from `./builtin-group.js`.
34
+ - **Client-side approval gate for `apcli enable / disable / reload / config set`** — new `requireApprovalForSystemCommand(moduleId, autoApprove)` helper in `src/system-cmd.ts` synthesises a minimal `ModuleDescriptor` with `annotations.requires_approval = true` and invokes `checkApproval(...)` before dispatching the executor call. `ApprovalDeniedError` / `ApprovalTimeoutError` propagate to `emitErrorAndExit` which maps them to exit 46 via `exitCodeForError`. `apcli config set` gains a `-y, --yes` flag for parity with the other three. Mirrors Python `_check_system_approval` and Rust `require_approval_for_system_command`. Audit D11-B-001 (see Fixed).
35
+ - **14 new tests in `tests/builtin-group.test.ts`** — `ApcliGroup builtin-group rename` describe block covers default name, custom name via both factories, validation of 6 invalid + 5 valid name shapes.
36
+ - **5 new tests in `tests/system-cmd.test.ts`** — `client-side approval gate (D11-B-001)` describe block covers `enable / disable / reload / config set` deny path on non-TTY without `--yes` (exit 46 + executor never called) and the `--yes` bypass.
37
+ - **D1-info-1 — `ApcliGroupError` typed exception** (`src/builtin-group.ts:101`, re-exported from `src/index.ts`) for cross-SDK error-class parity with Rust (`apcore_cli::ApcliGroupError`, re-exported at `lib.rs:183`). The previous plain `Error` throw on invalid `builtinGroupName` gave consumers no programmatic way to discriminate apcli config errors from generic ones. Existing `catch (e) { if (e instanceof Error) ... }` blocks continue to work; new code can use `instanceof ApcliGroupError`. The neighbouring throw at `builtin-group.ts:464` (caller-bug guard for `isSubcommandIncluded` invoked under wrong mode) stays as plain `Error` — out of scope for apcli config validation.
38
+ - **D1-004 — `Sandbox` builder methods `withExtensionsRoot` and `withMaxOutputBytes`** (`src/security/sandbox.ts`). Cross-SDK parity with Python `with_extensions_root` / `with_max_output_bytes` and the new Rust builders. Both fields drive runtime behaviour: `extensions_root` takes precedence over inherited `APCORE_EXTENSIONS_ROOT`, `max_output_bytes` replaces the per-stream output cap. The "future builder (Python parity)" comment at `sandbox.ts:77` is removed.
39
+ - **D1-006 — `allowedPrefixes?: string[]` option on `CreateCliOptions`** plumbed through `createCli → applyToolkitIntegration → loadBindingDisplayOverlay` so non-allowlisted `target:` entries are dropped before they pollute the display map. Mirrors Python `factory.py:78 allowed_prefixes` safety knob. New `ApplyToolkitIntegrationOptions` type exported.
40
+ - **D1-007 — `formatModuleList`, `formatModuleDetail`, `resolveFormat` re-exported from `src/index.ts`** alongside `formatExecResult`. The output-formatter feature spec declares contracts for all four; embedders building custom output paths can now consume the canonical formatters from the package root.
41
+ - **D5-003 — dedicated `tests/system-usage.test.ts`** covering the new `src/system-usage.ts` aggregator (period filtering, per-module aggregates, missing-audit-log fallback) — previously coverage for the aggregator was only incidental via the discovery-layer integration tests.
42
+
43
+ ### Fixed
44
+
45
+ - **D11-B-001 — `system-cmd.ts` skipped the client-side approval gate entirely**. Each of the four mutating subcommands (`enable`, `disable`, `reload`, `config set`) declared `--yes` but never read it; no `checkApproval(...)` call existed in the file. Operators on Python or Rust SDKs got an interactive 60s confirmation prompt; TS users got nothing — server-side enforcement was the only gate, and the `--yes` flag was completely dead. Fix wires `requireApprovalForSystemCommand(moduleId, opts.yes)` into all four action handlers (see Added). Description text updated from "Signal explicit intent (forwarded to server-side approval gate)" to "Skip approval prompt" — the original copy was misleading because nothing was actually forwarded.
46
+ - **D11-NEW-005 — TS RESERVED_NAMES exit code was 2, not 48**. `schema-parser.ts:101` previously called `process.exit(EXIT_CODES.INVALID_CLI_INPUT)` (=2) when a schema property collided with a reserved CLI option name. Spec mandates exit 48 (cross-SDK parity with Python `sys.exit(48)` and Rust `CliError::SchemaParserFailure → EXIT_SCHEMA_CIRCULAR_REF`). Fix changes to `EXIT_CODES.SCHEMA_CIRCULAR_REF`. The neighbour flag-collision branch already exited 48; both schema-author errors are now consistent. 6 existing tests updated from "exits 2" assertions to "exits 48".
47
+ - **D11-NEW-001 — `resolveRefs` dropped parent `required` when resolving `anyOf` / `oneOf` branches**. A schema like `{required: ["x"], anyOf: [{required: ["a"]}, {required: ["a"]}]}` resolved to `required: ["a"]` in TS — silently losing `"x"`. Per JSON Schema semantics, `parent.required` applies in addition to the branch intersection. Branch handling now preserves the parent node's `required` field (deduplicated, sibling-first ordering). Aligned with Python `ref_resolver.py:100-118`; Rust got the same fix in a sibling commit. 4 new ref-resolver regression tests.
48
+ - **D11-NEW-003 — `max_depth` over-counted plain nested-object recursion**. Previously a schema with >32 levels of nested object `properties` (no `$refs`) was rejected; the depth budget should only count `$ref` hops + composition-branch descents, not pure structural recursion. Aligned with Rust's interpretation of the spec ("Maximum $ref resolution recursion depth").
49
+ - **D11-NEW-004 — `schema_to_cli_options` flag-collision sites exited 2, not 48**. The two flag-collision sites in `src/schema-parser.ts` (regular flag collision, no-flag collision) previously exited 2 (`INVALID_CLI_INPUT`) instead of 48 (`SCHEMA_CIRCULAR_REF`) — observable: a CI / embedder checking `$? == 48` to detect schema-related failures missed the TS path. Fix matches Python `sys.exit(48)`. 2 updated schema-parser collision tests.
50
+ - **D11-W4 — `schema_to_cli_options` did not warn on `required` entries missing from `properties`** (`src/schema-parser.ts:81`). Python (`schema_parser.py:93-98`) and Rust (`schema_parser.rs:220-228`) iterate the schema `required` list and warn for entries not present in `properties`. The TS SDK skipped that loop, so schemas like `{required: ["foo"]}` with no matching property parsed silently — schema authors lost the cross-SDK guard rail. Now emits the same warning text as Python: `Required property '%s' not found in properties, skipping.` 3 new regression tests cover missing-only, all-present, and multiple-missing cases.
51
+ - **D10-info-1 — `APCORE_CLI_APCLI` env-var parser did not trim** (`src/builtin-group.ts:488`). Per spec invariant 2 in `features/builtin-group.md`, the parser is case-insensitive AND trim-on-read. The TS port lowercased but skipped the trim, so values like `" show "` or `"\thide\n"` silently fell through to the unknown-value warning branch — diverging from Python and Rust. Fix adds `.trim()` before `.toLowerCase()` and short-circuits to `null` when the trimmed value is empty (all-whitespace env is now treated as unset without emitting a warning, matching empty-string handling).
52
+
53
+ ### Changed
54
+
55
+ - **`vitest.config.ts` `coverage.thresholds`** added — `lines / functions / statements: 85`, `branches: 75`. Cross-SDK CI parity with Python `pyproject.toml [tool.coverage.report] fail_under = 85` and Rust `make coverage --fail-under-lines 85`. Audit D5-004.
56
+
57
+ - **D8-W2 — `validateModuleId` extracted to dedicated `src/validate.ts`** for parallel-layout parity with Python (`apcore_cli/validate.py`) and Rust (`src/validate.rs`). Embedders reading multiple SDKs side-by-side previously had to grep for the helper inside `main.ts`. The existing import path is preserved via re-export from `main.ts`, so `discovery.ts` and tests need no changes. Adds an exported `MAX_MODULE_ID_LENGTH` constant for spec parity.
58
+
59
+ - **D1-W4 — `formatPreflightResult` / `firstFailedExitCode` annotated `@internal`** (`src/output.ts:406`). Both stay `export`ed because `main.ts` and `discovery.ts` import them across module boundaries, but they are not re-exported from `index.ts` and have no documented embedder use case. The `@internal` JSDoc tag now matches the import-graph reality so doc generators and human readers see they are not part of the package's stable API.
60
+
61
+ - **D6-TS-info — `@sinclair/typebox` ^0.32 → ^0.34** (`package.json:41`, resolves to 0.34.48). Cross-SDK dependency hygiene: Rust and Python pulled their matching schema-validation deps to current minors months ago; TS was the laggard. The only usage is the stable `Type.*` builder facade in `src/init-cmd.ts` (`Type.Object` / `Type.Array` / `Type.Optional` / `Type.String` / `Type.Number` / `Type.Boolean`), all of which are identical between 0.32 and 0.34 — no source changes required.
62
+
63
+ - **D6-001 — `apcore-toolkit` devDependency switched from local `link:` path to registry version `^0.6.0`**. The previous entry pointed at a developer's absolute local clone, which broke `pnpm install` for any other contributor. Clean clones now resolve `apcore-toolkit` identically to the peer-dependency declaration (`>=0.6.0`).
64
+
65
+ - **`apcli list` and `apcli describe` `--format` choices** are now validated
66
+ via Commander's `Option.choices(...)` against the canonical set
67
+ `[table, json, csv, yaml, jsonl, markdown, skill]`. Unknown values exit
68
+ with code 2 instead of silently no-op'ing. Issue
69
+ [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20).
70
+ - **Dependency bump**: peer-dep `apcore-js >= 0.21.0` (was `>= 0.19.0`) and the
71
+ optional `apcore-toolkit >= 0.6.0` (was `>= 0.5.0`). Aligns with upstream
72
+ `apcore 0.21.0` (Module.preview / PreflightResult.predicted_changes) and
73
+ `apcore-toolkit 0.6.0` (surface-aware formatters).
74
+ - **Issue #19 — drop "apcore" branding from embedded-mode `--help`**: top-level
75
+ CLI description now resolves from a new `description?: string` field on
76
+ `CreateCliOptions` (defaults to `${progName} CLI`); the `apcli` subgroup
77
+ description is now `Built-in commands` instead of `apcore-cli built-in
78
+ commands`; `--verbose` option text and the help footer drop the trailing
79
+ `apcore` from `(including built-in apcore options)`. Standalone bin entry
80
+ (`bin/apcore-cli.ts → main()`) passes `description="<prog> — execute apcore
81
+ modules from the command line"` explicitly so the standalone surface is
82
+ unchanged.
83
+ - **Conformance fixtures (`aiperceivable/apcore-cli/conformance/fixtures/apcli-visibility/`)**
84
+ refreshed to match the new debranded help output and to forward `version` /
85
+ `description` from the fixture inputs through `captureHelp()`.
86
+
87
+ ### Added
88
+
89
+ - **`--format markdown` and `--format skill`** for `apcli list` and `apcli describe`
90
+ (issue [aiperceivable/apcore-cli#20](https://github.com/aiperceivable/apcore-cli/issues/20)).
91
+ Both delegate to `apcore-toolkit` (`formatModule` / `formatModules`, peer dep
92
+ ≥0.6) so the output is byte-identical to the same toolkit call in the Python
93
+ and Rust SDKs. `--format skill` emits vendor-neutral SKILL.md content
94
+ directly loadable by Claude Code (`.claude/skills/<id>/SKILL.md`) and
95
+ Gemini CLI (`.gemini/skills/<id>/SKILL.md`):
96
+
97
+ ```bash
98
+ apcore-cli apcli describe users.create --format skill > .claude/skills/users.create/SKILL.md
99
+ ```
100
+
101
+ A new internal adapter `descriptorToScanned()` maps `ModuleDescriptor`
102
+ to the toolkit's `ScannedModule`. The `formatModuleList` and
103
+ `formatModuleDetail` functions are now `async` to support the dynamic
104
+ toolkit import (the existing five-format paths remain effectively
105
+ synchronous and complete before the returned promise resolves).
106
+ - **Issue #18 — host-app `--version` opt-in**: new `version?: string` field on
107
+ `CreateCliOptions`. When supplied, registers `-V/--version` with the host's
108
+ version string. **When omitted, the `--version` flag is no longer registered**
109
+ — embedded CLIs that do not opt in stop leaking the SDK's own version. The
110
+ standalone bin entry passes `version: VERSION` (the SDK package version)
111
+ explicitly so the `apcore-cli` binary's behaviour is preserved. The
112
+ `configureManHelp(...)` man-page generator falls back to the SDK version
113
+ when the host does not supply one, so manpages always carry a version stamp.
114
+ - **Issue #19 — `description?: string`** on `CreateCliOptions`.
115
+ - **Issue #17 — `system.usage` aggregator + `list --sort calls|errors|latency`**:
116
+ new module `src/system-usage.ts` reads `~/.apcore-cli/audit.jsonl`, filters
117
+ by period (default 24h), and returns per-module aggregates (`calls`,
118
+ `errors`, `avg latency_ms`). `list --sort {calls,errors,latency}` now
119
+ consults the aggregator instead of falling back to id-sort with a buried
120
+ `process.stderr.write("Warning: ...")`. When the audit log has no entries
121
+ in the period window the discovery layer prints a user-visible note to
122
+ stderr (`note: no usage data available for --sort <field>; sorted by id.
123
+ ...`) and falls back to id-sort. Module-protocol registration of
124
+ `system.usage.summary` / `system.usage.module` as registry-callable
125
+ built-ins is tracked as a follow-up — today the readers are invoked
126
+ directly by the discovery layer.
127
+ - New file: `src/system-usage.ts`.
128
+
129
+ ---
130
+
131
+ ## [0.7.0] - 2026-04-25
132
+
133
+ ### Added
134
+
135
+ - **Canonical clap v4 / GNU-style help formatter** (`src/canonical-help.ts`) overriding Commander's default `formatHelp` so `--help` output is byte-stable across SDK implementations. Disables terminal-width wrapping, uppercases `<PLACEHOLDER>`s, enforces `Commands:` before `Options:`, and renders `-h, --help` / `-V, --version` last with `Print help` / `Print version` descriptions.
136
+ - **Cross-language conformance test harness** (`tests/conformance/apcli-visibility.test.ts`) now consumes the shared fixtures from the `aiperceivable/apcore-cli` spec repo (`conformance/fixtures/apcli-visibility/`). Dynamically discovers scenarios and byte-matches `--help` output against each `expected_help.txt`. Set `APCORE_CLI_SPEC_REPO` to point at a non-sibling checkout; defaults to `../apcore-cli/`.
137
+ - **CI — spec-repo checkout**: `.github/workflows/ci.yml` now checks out `aiperceivable/apcore-cli` into `.apcore-cli-spec/` and exposes it to `pnpm test` via `APCORE_CLI_SPEC_REPO`.
138
+ - **FE-13: Built-in command group (`apcli`)** — consolidates the 13 canonical built-in commands (`list`, `describe`, `exec`, `validate`, `init`, `health`, `usage`, `enable`, `disable`, `reload`, `config`, `completion`, `describe-pipeline`) under a single `apcli` sub-group. Invocation shifts from `<cli> list` to `<cli> apcli list`.
139
+ - `ApcliGroup` class + `ApcliConfig` / `ApcliMode` types, exported from `src/index.ts`.
140
+ - `RESERVED_GROUP_NAMES = new Set(["apcli"])` as the enforced collision surface (replaces the retired per-command `BUILTIN_COMMANDS` constant).
141
+ - New env var `APCORE_CLI_APCLI` — accepts `show`, `hide`, `1`, `0`, `true`, `false` (case-insensitive).
142
+ - New config keys (snake_case DEFAULTS): `apcli.mode`, `apcli.include`, `apcli.exclude`, `apcli.disable_env`.
143
+ - `ConfigResolver.resolveObject(key)` — non-leaf accessor that returns object-shaped config values without flattening.
144
+ - `createCli({ apcli })` option — accepts `boolean | object | ApcliGroup` to configure the built-in group surface.
145
+ - See [migration guide](../apcore-cli/docs/features/builtin-group.md#11-migration) for the full v0.7 → v0.8 timeline.
146
+ - **New error-code → exit-code mappings** in `src/errors.ts` and `src/main.ts`: `DEPENDENCY_NOT_FOUND` and `DEPENDENCY_VERSION_MISMATCH` both map to exit code 44. Preserves the pre-0.19.0 exit code (`MODULE_LOAD_ERROR` = 44) for missing / version-mismatched module dependencies, now that apcore-js surfaces these through dedicated error types per PROTOCOL_SPEC §5.15.2.
147
+ - **Binding-overlay tests** in `tests/display-helpers.test.ts`: a tmp binding YAML is written, `applyToolkitIntegration` is called, and `getDisplay()` is verified to return the overlay for a descriptor that has no baked-in `metadata.display`.
148
+ - **`createCli({ app })` — `APCore` unified client**: `CreateCliOptions` now accepts an `app?: APCore` field. When provided, `app.registry` and `app.executor` are extracted and used in place of explicit `registry`/`executor` fields. Passing `app` together with `registry` or `executor` throws `"app is mutually exclusive with registry/executor"`.
149
+ - `APCore` interface exported from package index. `StrategyInfo` and `StrategyStep` interfaces exported from package index.
150
+ - `Executor` interface extended with optional `describePipeline(strategyName?: string): StrategyInfo` and `strategy?: { steps: StrategyStep[] }` fields.
151
+ - **FE-12: Module Exposure Filtering** — Declarative control over which discovered modules are exposed as CLI commands.
152
+ - `ExposureFilter` class in `exposure.ts` with `isExposed(moduleId)` and `filterModules(ids)` methods.
153
+ - Three modes: `all` (default), `include` (whitelist), `exclude` (blacklist) with glob-pattern matching.
154
+ - `ExposureFilter.fromConfig(obj)` static method for loading from `apcore.yaml` `expose` section.
155
+ - `CreateCliOptions.expose` field accepting object or `ExposureFilter` instance.
156
+ - `list --exposure {exposed,hidden,all}` filter flag in discovery commands.
157
+ - `GroupedModuleGroup` integration: applies exposure filter during command registration.
158
+ - `ConfigResolver` gains `expose.*` config keys.
159
+ - 4-tier config precedence: `CreateCliOptions.expose` > `--expose-mode` CLI flag > env var > `apcore.yaml`.
160
+ - Hidden modules remain invocable via `exec <module_id>`.
161
+ - New file: `exposure.ts`.
162
+
163
+ ### Changed
164
+
165
+ - Built-in commands now live under the `apcli` sub-group. Pre-v0.7 invocations (`<cli> list`, `<cli> describe`, etc.) still work in **standalone mode** via deprecation shims that print a `WARNING` to stderr and forward to `apcli <name>`. Shims are not installed in embedded mode.
166
+ - Discovery flags (`--extensions-dir`, `--commands-dir`, `--binding`) are now gated on standalone mode — they are only registered when no `registry` is injected.
167
+ - Shell-completion generators (bash/zsh/fish) enumerate registered Commander subcommands dynamically; hardcoded command lists are gone.
168
+ - **Dependency bump**: requires `apcore-js >= 0.19.0` (was `>= 0.18.0`) and `apcore-toolkit >= 0.5.0` (was `>= 0.4.0`). Aligns with upstream releases `apcore-js 0.19.0` (dependency graph errors, async `buildStrategyFromConfig`, auto-schema adapter chain, `BindingSchemaMissingError` rename) and `apcore-toolkit 0.5.0` (`BindingLoader`, `ScannedModule.display`, `apcore-toolkit/browser` subpath).
169
+ - **Placeholder types in `src/cli.ts` realigned with real apcore-js shapes.** `PipelineTrace` / `StepTrace` / `PreflightResult` / `StrategyStep` now use camelCase (`strategyName`, `totalDurationMs`, `durationMs`, `skipReason`, `requiresApproval`, `timeoutMs`) matching the apcore-js runtime object shape. `Executor.describePipeline` is typed as `(): StrategyInfo` (zero arguments — the previous `describePipeline?(strategyName?: string)` signature declared an argument that the real apcore-js method ignores). `Executor.strategy` renamed to `Executor.currentStrategy` to match the upstream getter.
170
+ - **`--trace` output now reads the correct runtime fields.** `main.ts` previously read `trace.strategy_name` / `trace.total_duration_ms` / `s.duration_ms` / `s.skip_reason` (snake_case) from the camelCase `PipelineTrace` returned by apcore-js, so those values surfaced as `undefined` at runtime. Now reads `strategyName` / `totalDurationMs` / `durationMs` / `skipReason` correctly. JSON output keys remain snake_case to preserve the cross-language CLI output contract.
171
+ - **`formatPreflightResult` now reads `result.requiresApproval`** (was `result.requires_approval`). The JSON output key remains `requires_approval`.
172
+ - **`MAX_MODULE_ID_LENGTH` 128 → 192**: `validateModuleId()` now enforces a 192-character limit for module IDs, up from 128, to accommodate Java/.NET deep-namespace FQN-derived IDs (PROTOCOL_SPEC §2.7 spec 1.6.0-draft).
173
+ - **`Executor.describePipeline()` returns `StrategyInfo`**: `describe-pipeline` command in `strategy.ts` now calls `executor.describePipeline(strategyName)` and consumes the returned `StrategyInfo` object (`name`, `stepCount`, `stepNames`, `description`). Pipeline header format updated to `Pipeline: ${info.name} (${info.stepCount} steps)`. Step metadata (Pure/Removable/Timeout columns) sourced from `executor.strategy.steps` (`pure: boolean`, `removable: boolean`, `timeoutMs: number`). Falls back to static preset table when `describePipeline` is not available.
174
+
175
+ ### Deprecated
176
+
177
+ - Root-level v0.6 built-in commands continue to work in standalone mode but emit a `WARNING` and forward to `apcli <name>`. **Scheduled for removal in v0.8.**
178
+
179
+ ### Removed
180
+
181
+ - The per-command `BUILTIN_COMMANDS` constant and its re-export from `src/index.ts`. Replaced by `RESERVED_GROUP_NAMES`.
182
+ - Monolithic registrars `registerDiscoveryCommands`, `registerSystemCommands`, `registerShellCommands` — replaced by per-subcommand exports invoked through `ApcliGroup`.
183
+
184
+ ### Fixed
185
+
186
+ - **`describe-pipeline --strategy <name>` now works for non-current strategies.** Previously the command called `executor.describePipeline(strategyName)` — the real apcore-js signature takes no arguments and always returns info for the executor's *current* strategy, so all `--strategy` values produced identical output. `src/strategy.ts` now uses a two-step lookup: if the requested name matches the current strategy, use `describePipeline()`; otherwise fall back to the static `Executor.listStrategies()` (reached via `executor.constructor.listStrategies`) to introspect other registered strategies.
187
+ - **`--binding <path>` flag now actually applies display overlay.** `applyToolkitIntegration` previously instantiated a `DisplayResolver` and discarded it. The implementation now uses apcore-toolkit 0.5.0's `BindingLoader` + `DisplayResolver` pipeline to parse the binding YAML, resolve the sparse overlay, and populate a module-level binding display map. `display-helpers.ts#getDisplay` consults the map as a fallback when the descriptor itself has no `metadata.display`, so `cli.alias` / `cli.description` / tags from `.binding.yaml` are now honored by `list`, `describe`, and command help output. New exports: `lookupBindingDisplay(moduleId)` and `clearBindingDisplayMap()` from `src/main.ts`.
188
+
189
+ ### Breaking
190
+
191
+ - Reserved-name enforcement is now a **hard exit 2** when a module's explicit group, auto-group prefix, or top-level name/alias equals `apcli`. Previously this was warn-and-drop.
192
+
193
+ ---
194
+
8
195
  ## [0.6.0] - 2026-04-06
9
196
 
10
197
  ### Changed
package/LICENSE CHANGED
@@ -1,21 +1,17 @@
1
- MIT License
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
2
4
 
3
- Copyright (c) 2026 AI Perceivable
5
+ Copyright 2024 aiperceivable <tercel.yi@gmail.com>
4
6
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
11
10
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
11
+ http://www.apache.org/licenses/LICENSE-2.0
14
12
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Node](https://img.shields.io/badge/node-18%2B-blue.svg)](https://nodejs.org)
11
- [![Tests](https://img.shields.io/badge/tests-183%20passed-brightgreen.svg)]()
11
+ [![Tests](https://img.shields.io/badge/tests-275%2B%20passed-brightgreen.svg)]()
12
12
 
13
13
  | | |
14
14
  |---|---|
@@ -49,7 +49,14 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
49
49
  pnpm add apcore-cli apcore-js
50
50
  ```
51
51
 
52
- Requires Node.js 18+ and `apcore-js >= 0.14.0`.
52
+ Requires Node.js 18+ and `apcore-js >= 0.21.0`.
53
+
54
+ **Optional:** install `apcore-toolkit` (>=0.6.0) to enable display overlay and registry writer integration via `applyToolkitIntegration`, `DisplayResolver`, and `RegistryWriter`.
55
+
56
+ ```bash
57
+ pnpm add apcore-cli apcore-js
58
+ pnpm add -D apcore-toolkit # optional, for display overlay / registry writer
59
+ ```
53
60
 
54
61
  ## Quick Start
55
62
 
@@ -95,21 +102,34 @@ const cli = createCli({
95
102
  cli.parse(process.argv);
96
103
  ```
97
104
 
98
- Or use the `LazyModuleGroup` directly with Commander:
105
+ Or wire the `createCli` options-object form directly with a runtime-supplied registry/executor:
99
106
 
100
107
  ```typescript
101
- import { LazyModuleGroup, buildModuleCommand } from "apcore-cli";
102
- import { Registry, Executor } from "apcore-js";
103
-
104
- const registry = new Registry("./extensions");
105
- registry.discover();
106
- const executor = new Executor(registry);
108
+ import { createCli } from "apcore-cli";
107
109
 
108
- const group = new LazyModuleGroup(registry, executor);
109
- const cmd = group.getCommand("math.add");
110
- cmd?.parse(process.argv);
110
+ async function main() {
111
+ // Obtain registry/executor from your apcore-js setup
112
+ // (e.g., via ExtensionsLoader or your framework's module discovery).
113
+ // See apcore-js docs for the exact bootstrap API.
114
+ const { registry, executor } = await bootstrapApcoreRuntime("./extensions");
115
+
116
+ const cli = createCli({
117
+ registry,
118
+ executor,
119
+ progName: "myapp",
120
+ // expose: { mode: "include", include: ["admin.*"] },
121
+ // apcli: { mode: "exclude", exclude: ["init"] }, // hide init from the apcli group
122
+ // apcli: false, // or hide the entire apcli group
123
+ // extraCommands: [customCmd1, customCmd2],
124
+ });
125
+ cli.parse(process.argv);
126
+ }
127
+
128
+ main();
111
129
  ```
112
130
 
131
+ > **Known gap:** The `Registry`, `Executor`, and `ModuleDescriptor` types re-exported by `apcore-cli` are currently **local placeholder interfaces** pending upstream export from `apcore-js`. Direct construction (`new Registry(...)` / `new Executor(registry)`) is **not supported** at this version. Structural typing allows runtime apcore-js objects to satisfy these interfaces, so `createCli({ registry, executor })` works when you pass in objects produced by your apcore-js runtime.
132
+
113
133
  ## Integration with Existing Projects
114
134
 
115
135
  ### Typical apcore project structure
@@ -168,15 +188,66 @@ apcore-cli [OPTIONS] COMMAND [ARGS]
168
188
  | `--verbose` | | Show all options in help (including built-in apcore options) |
169
189
  | `--man` | | Output man page in roff format (use with `--help`) |
170
190
 
171
- ### Built-in Commands
191
+ ### Built-in Commands (the `apcli` group)
192
+
193
+ Starting in **v0.7.0**, all built-in commands live under an `apcli` sub-group
194
+ (see the `RESERVED_GROUP_NAMES` collision surface in `src/builtin-group.ts`).
195
+ Invocation:
196
+
197
+ ```bash
198
+ apcore-cli apcli list
199
+ apcore-cli apcli describe math.add
200
+ apcore-cli apcli exec math.add --a 5 --b 10
201
+ ```
202
+
203
+ > **Migration note (v0.8):** Root-level built-ins (`apcore-cli list`, `apcore-cli describe`, …) were removed in v0.8 — use `<cli> apcli <subcommand>`.
204
+ > Only the `apcli` group is supported; legacy root-level shims have been retired.
205
+ > Embedded integrations (host CLIs that inject a `registry`) never had root-level
206
+ > built-ins in the first place. See the full migration timeline in the spec repo:
207
+ > [`docs/features/builtin-group.md §11 Migration`](../apcore-cli/docs/features/builtin-group.md#11-migration).
208
+
209
+ The canonical 13 `apcli` subcommands:
210
+
211
+ **Module invocation & discovery**
212
+
213
+ | Command | Description |
214
+ |---------|-------------|
215
+ | `apcli list` | List available modules with search, status, tag/annotation filters, sort, and dependency inspection (see `registerListCommand` in `src/discovery.ts`) |
216
+ | `apcli describe <module_id>` | Show full module metadata, schemas, and annotations (see `registerDescribeCommand` in `src/discovery.ts`) |
217
+ | `apcli describe-pipeline <module_id>` | Inspect the execution pipeline for a module (strategies, hooks, middleware; see `registerPipelineCommand` in `src/strategy.ts`) |
218
+ | `apcli exec <module_id>` | Internal routing alias for module execution (see `registerExecCommand` in `src/discovery.ts`) |
219
+ | `apcli usage <module_id>` | Show usage examples and flag hints for a module (see `registerUsageCommand` in `src/system-cmd.ts`) |
220
+
221
+ **System management**
172
222
 
173
223
  | Command | Description |
174
224
  |---------|-------------|
175
- | `list` | List available modules with optional tag filtering |
176
- | `describe <module_id>` | Show full module metadata and schemas |
177
- | `exec <module_id>` | Internal routing alias for module execution |
178
- | `completion <shell>` | Generate shell completion script (bash/zsh/fish) |
179
- | `man <command>` | Generate man page in roff format |
225
+ | `apcli config` | Inspect effective configuration and precedence (see `registerConfigCommand` in `src/system-cmd.ts`) |
226
+ | `apcli health` | Run health checks on registry, executor, config, and auth (see `registerHealthCommand` in `src/system-cmd.ts`) |
227
+ | `apcli reload` | Reload registry / rediscover extensions (see `registerReloadCommand` in `src/system-cmd.ts`) |
228
+ | `apcli enable <module_id>` | Enable a disabled module (see `registerEnableCommand` in `src/system-cmd.ts`) |
229
+ | `apcli disable <module_id>` | Disable a module without removing it (see `registerDisableCommand` in `src/system-cmd.ts`) |
230
+
231
+ **Workflow**
232
+
233
+ | Command | Description |
234
+ |---------|-------------|
235
+ | `apcli init module <id>` | Scaffold a new module (TS/JS/YAML binding) into the extensions or commands directory (see `registerInitCommand` in `src/init-cmd.ts`) |
236
+ | `apcli validate` | Validate modules and configuration against JSON Schema (see `registerValidateCommand` in `src/discovery.ts`) |
237
+
238
+ **Shell integration**
239
+
240
+ | Command | Description |
241
+ |---------|-------------|
242
+ | `apcli completion <shell>` | Generate shell completion script for bash / zsh / fish (see `registerCompletionCommand` in `src/shell.ts`) |
243
+ | `man [command]` (root) | Generate a man page in roff format for a single command or the whole program (see `configureManHelp` in `src/shell.ts`). Stays at the root (meta-command). |
244
+
245
+ #### Standalone vs. embedded surfaces
246
+
247
+ | Mode | Invocation | Notes |
248
+ |------|------------|-------|
249
+ | **Standalone** (`apcore-cli`) | `apcore-cli apcli <subcommand>` | Discovery flags (`--extensions-dir`, `--commands-dir`, `--binding`) are registered. Only the `apcli` group is supported (legacy root-level shims were removed in v0.8). |
250
+ | **Embedded** (`createCli({ registry, … })`) | `<host-cli> apcli <subcommand>` | Discovery flags are gated off (injected registry already supplies modules). Only the `apcli` group is supported. |
180
251
 
181
252
  ### Module Execution Options
182
253
 
@@ -187,25 +258,48 @@ When executing a module (e.g. `apcore-cli math.add`), these built-in options are
187
258
  | `--input -` | Read JSON input from STDIN |
188
259
  | `--yes` / `-y` | Bypass approval prompts |
189
260
  | `--large-input` | Allow STDIN input larger than 10MB |
190
- | `--format` | Output format: `json` or `table` |
191
- | `--sandbox` | Run module in subprocess sandbox (not yet implemented always hidden) |
261
+ | `--format <fmt>` | Output format: `json`, `table`, `csv`, `yaml`, or `jsonl` |
262
+ | `--sandbox` | Run module in a subprocess sandbox (re-exec with stripped env; 64MiB stdout/stderr cap; 300s default timeout). Hidden by default — set `APCORE_CLI_SANDBOX=1` to enable globally. |
263
+ | `--dry-run` | Run preflight checks (schema, ACL, approval) without executing (FE-11) |
264
+ | `--trace` | Emit execution pipeline trace (strategy, hooks, middleware timings) |
265
+ | `--stream` | Stream results line-by-line for stream-capable modules |
266
+ | `--strategy <name>` | Override execution strategy: `standard`, `internal`, `testing`, `performance`, or `minimal` |
267
+ | `--fields <csv>` | Select output fields via dot-path notation (e.g. `result.sum,meta.duration`) |
268
+ | `--approval-timeout <seconds>` | Override approval timeout (default `60`) |
269
+ | `--approval-token <token>` | Provide a pre-obtained approval token (bypasses interactive prompt) |
192
270
 
193
271
  Schema-generated flags (e.g. `--a`, `--b`) are added automatically from the module's `input_schema`.
194
272
 
273
+ #### `list` command flags (v0.6.0)
274
+
275
+ The `list` command supports enhanced filtering and inspection flags:
276
+
277
+ | Option | Description |
278
+ |--------|-------------|
279
+ | `--search <query>` | Fuzzy search across module IDs, descriptions, and annotations |
280
+ | `--status <state>` | Filter by status (e.g. `enabled`, `disabled`, `deprecated`) |
281
+ | `--annotation <key=value>` | Filter by an annotation key/value pair |
282
+ | `--sort <field>` | Sort by `name`, `status`, or other indexed fields |
283
+ | `--reverse` | Reverse sort order |
284
+ | `--deprecated` | Include deprecated modules in the output |
285
+ | `--deps` | Show dependency graph for each module |
286
+
195
287
  ### Exit Codes
196
288
 
197
- | Code | Meaning |
198
- |------|---------|
199
- | `0` | Success |
200
- | `1` | Module execution error |
201
- | `2` | Invalid CLI input |
202
- | `44` | Module not found / disabled / load error |
203
- | `45` | Schema validation error |
204
- | `46` | Approval denied or timed out |
205
- | `47` | Configuration error |
206
- | `48` | Schema circular reference |
207
- | `77` | ACL denied |
208
- | `130` | Execution cancelled (Ctrl+C) |
289
+ The exit codes below are also exported as the `EXIT_CODES` const-object from `apcore-cli` (canonical source: `src/errors.ts`) and are mapped from thrown errors via `exitCodeForError()`.
290
+
291
+ | Code | `EXIT_CODES` key | Meaning |
292
+ |------|------------------|---------|
293
+ | `0` | `SUCCESS` | Success |
294
+ | `1` | `MODULE_EXECUTE_ERROR` / `MODULE_TIMEOUT` | Module execution error |
295
+ | `2` | `INVALID_CLI_INPUT` | Invalid CLI input |
296
+ | `44` | `MODULE_NOT_FOUND` / `MODULE_LOAD_ERROR` / `MODULE_DISABLED` / `DEPENDENCY_NOT_FOUND` / `DEPENDENCY_VERSION_MISMATCH` | Module not found / disabled / load error |
297
+ | `45` | `SCHEMA_VALIDATION_ERROR` | Schema validation error |
298
+ | `46` | `APPROVAL_DENIED` / `APPROVAL_TIMEOUT` | Approval denied or timed out |
299
+ | `47` | `CONFIG_NOT_FOUND` / `CONFIG_INVALID` | Configuration error |
300
+ | `48` | `SCHEMA_CIRCULAR_REF` | Schema circular reference |
301
+ | `77` | `ACL_DENIED` | ACL denied |
302
+ | `130` | *(no key — set by signal handler)* | Execution cancelled (Ctrl+C) |
209
303
 
210
304
  ## Configuration
211
305
 
@@ -227,6 +321,10 @@ apcore-cli uses a 4-tier configuration precedence:
227
321
  | `APCORE_AUTH_API_KEY` | API key for remote registry authentication | *(unset)* |
228
322
  | `APCORE_CLI_SANDBOX` | Set to `1` to enable subprocess sandboxing | *(unset)* |
229
323
  | `APCORE_CLI_HELP_TEXT_MAX_LENGTH` | Maximum characters for CLI option help text before truncation | `1000` |
324
+ | `APCORE_CLI_APPROVAL_TIMEOUT` | Default approval prompt timeout in seconds | `60` |
325
+ | `APCORE_CLI_STRATEGY` | Default execution strategy (`standard`, `internal`, `testing`, `performance`, `minimal`) | `standard` |
326
+ | `APCORE_CLI_GROUP_DEPTH` | Maximum nesting depth when rendering grouped module command trees | `2` |
327
+ | `APCORE_CLI_APCLI` | Override apcli group visibility (Tier 2). Accepts `show`/`1`/`true` → all, `hide`/`0`/`false` → none. Sealed by `apcli.disable_env: true`. | *(unset)* |
230
328
 
231
329
  ### Config File (`apcore.yaml`)
232
330
 
@@ -239,6 +337,14 @@ sandbox:
239
337
  enabled: false
240
338
  cli:
241
339
  help_text_max_length: 1000
340
+ approval_timeout: 60 # seconds
341
+ strategy: standard # standard | internal | testing | performance | minimal
342
+ group_depth: 2 # grouped-module command-tree nesting depth
343
+ apcli: # built-in command group visibility (FE-13)
344
+ mode: all # all | none | include | exclude
345
+ include: [] # subcommand allowlist when mode=include
346
+ exclude: [] # subcommand denylist when mode=exclude
347
+ disable_env: false # set true to seal Tier 2 (APCORE_CLI_APCLI env var)
242
348
  ```
243
349
 
244
350
  ## Features
@@ -251,7 +357,7 @@ cli:
251
357
  - **TTY-adaptive output** -- rich tables for terminals, JSON for pipes (configurable via `--format`)
252
358
  - **Approval gate** -- TTY-aware HITL prompts for modules with `requires_approval: true`, with `--yes` bypass and 60s timeout
253
359
  - **Schema validation** -- inputs validated against JSON Schema before execution, with `$ref`/`allOf`/`anyOf`/`oneOf` resolution
254
- - **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing
360
+ - **Security** -- API key auth (keyring + AES-256-GCM), append-only audit logging, subprocess sandboxing (re-exec model, env stripping, 64MiB output cap)
255
361
  - **Shell completions** -- `apcore-cli completion bash|zsh|fish` generates completion scripts with dynamic module ID completion
256
362
  - **Man pages** -- `apcore-cli man <command>` for single commands, or `--help --man` for a complete program man page. `configureManHelp()` provides one-line integration for downstream projects
257
363
  - **Documentation URL** -- `setDocsUrl()` adds doc links to help footers and man pages
@@ -284,7 +390,7 @@ apcore-cli (the adapter)
284
390
  +-- approval TTY-aware HITL approval
285
391
  +-- output TTY-adaptive JSON/table output
286
392
  +-- AuditLogger JSON Lines execution logging
287
- +-- Sandbox Subprocess isolation
393
+ +-- Sandbox Subprocess isolation (re-exec, env stripped, output capped)
288
394
  |
289
395
  v
290
396
  apcore Registry + Executor (your modules, unchanged)
@@ -292,24 +398,40 @@ apcore Registry + Executor (your modules, unchanged)
292
398
 
293
399
  ## API Overview
294
400
 
295
- **Classes:** `LazyModuleGroup`, `ConfigResolver`, `AuthProvider`, `ConfigEncryptor`, `AuditLogger`, `Sandbox`
401
+ **Classes:** `LazyModuleGroup`, `GroupedModuleGroup`, `ApcliGroup`, `ExposureFilter`, `CliApprovalHandler`, `ConfigResolver`, `AuthProvider`, `ConfigEncryptor`, `AuditLogger`, `Sandbox`
296
402
 
297
- **Interfaces:** `CreateCliOptions`, `Registry`, `Executor`, `ModuleDescriptor`
403
+ **Interfaces:** `CreateCliOptions`, `Registry`, `Executor`, `ModuleDescriptor`, `APCore`, `ApcliConfig`, `ApcliMode`, `StrategyInfo`, `StrategyStep`
298
404
 
299
- **Functions:** `createCli`, `main`, `buildModuleCommand`, `validateModuleId`, `collectInput`, `schemaToCliOptions`, `reconvertEnumValues`, `resolveRefs`, `checkApproval`, `resolveFormat`, `formatModuleList`, `formatModuleDetail`, `formatExecResult`, `registerDiscoveryCommands`, `registerShellCommands`, `setAuditLogger`, `getAuditLogger`, `setVerboseHelp`, `setDocsUrl`, `buildProgramManPage`, `configureManHelp`, `exitCodeForError`, `mapType`, `extractHelp`, `truncate`
405
+ **Functions:** `createCli`, `main`, `buildModuleCommand`, `validateModuleId`, `collectInput`, `schemaToCliOptions`, `reconvertEnumValues`, `resolveRefs`, `checkApproval`, `formatExecResult`, `registerListCommand`, `registerDescribeCommand`, `registerExecCommand`, `registerValidateCommand`, `registerHealthCommand`, `registerUsageCommand`, `registerEnableCommand`, `registerDisableCommand`, `registerReloadCommand`, `registerConfigCommand`, `registerCompletionCommand`, `registerPipelineCommand`, `registerInitCommand`, `setAuditLogger`, `getAuditLogger`, `setVerboseHelp`, `setDocsUrl`, `configureManHelp`, `exitCodeForError`
300
406
 
301
407
  **Errors:** `ApprovalTimeoutError`, `ApprovalDeniedError`, `AuthenticationError`, `ConfigDecryptionError`, `ModuleExecutionError`, `ModuleNotFoundError`, `SchemaValidationError`
302
408
 
303
409
  ## Development
304
410
 
411
+ The conformance suite under `tests/conformance/` reads shared fixtures from
412
+ the **spec repo** (`aiperceivable/apcore-cli`). Clone it as a sibling of
413
+ this repo, or point `APCORE_CLI_SPEC_REPO` at an existing checkout:
414
+
305
415
  ```bash
416
+ # One-time: clone both repos side by side
417
+ git clone https://github.com/aiperceivable/apcore-cli.git
306
418
  git clone https://github.com/aiperceivable/apcore-cli-typescript.git
419
+
307
420
  cd apcore-cli-typescript
308
421
  pnpm install
309
- pnpm test # 183 tests
422
+ pnpm test # reads fixtures from ../apcore-cli/conformance/
310
423
  pnpm build # compile TypeScript
311
424
  ```
312
425
 
426
+ Alternative layout (spec repo checked out elsewhere):
427
+
428
+ ```bash
429
+ export APCORE_CLI_SPEC_REPO=/path/to/apcore-cli
430
+ pnpm test
431
+ ```
432
+
433
+ CI does this automatically — see `.github/workflows/ci.yml`.
434
+
313
435
  ## License
314
436
 
315
437
  Apache-2.0