diff --git a/aidd_docs/memory/coding-assertions.md b/aidd_docs/memory/coding-assertions.md index f68a25294..03baec882 100644 --- a/aidd_docs/memory/coding-assertions.md +++ b/aidd_docs/memory/coding-assertions.md @@ -51,6 +51,8 @@ Each runs through `scripts/gate-witness.js`, which skips a gate this exact tree - Any pull request fires `ci.yml` and `cli-ci.yml`, whatever its base. - Only a pull request targeting `main` or `next` replays the pre-commit, and `validate.yml` drops `cli-biome`, `cli-architecture` and `cli-typecheck` from it. +Deleting a remote branch is the one push to send with it. `git push --delete` carries no commit, and the gates still run: measured, lefthook forwards git's pre-push stdin to no command, the hook environment is byte-identical to a normal push, and a `glob` filter matches anyway. Nothing inside the hook can tell the two apart, so the full `cli` suite runs over a push that has nothing to validate. + ## Behavior Done means every gate green. On failure, one agent per failing assertion — typecheck, tests, rules — not one agent for all. diff --git a/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/backlog-link.json b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/backlog-link.json new file mode 100644 index 000000000..fd6267b4e --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/backlog-link.json @@ -0,0 +1,5 @@ +{ + "backlog": "ai-driven-dev/framework#891", + "written_at": "2026-10-06T15:05:28Z", + "written_by": "aidd-dev:01-plan" +} diff --git a/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/challenge.md b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/challenge.md new file mode 100644 index 000000000..fd3740648 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/challenge.md @@ -0,0 +1,14 @@ +My confidence level of correctness now: 95% + +# Correctness (100%) +- The delivered change matches the agreed issue #891 contract: unsupported user-scope plugin selections retain the existing cause/remedies and exit 1 through the presentation error handler. `setup.ts:156-195` guards constructor validation without moving policy out of `SetupFlow`. +- Independent focused verification against commit `65d1917a`: 62 tests passed, including seven hermetic built-binary tests. Exact stderr assertions exclude stack traces and bundle dumps; supported user setup still leaves the project unchanged. Full suite and required repository gates are evidenced in the review report. +- The README distinguishes user registration, native activation, plugin file installation and project hooks. Support derives from present profiles; no global installer or configuration rewriting was introduced. Help enumerates supported tools from the existing predicate rather than duplicating policy. +- Edge cases considered: all/recommended/named plugin modes; empty or omitted AI selection; IDE selection; unsupported AI tools; project-scope behavior; supported user setup; missing native binaries. Broader setup construction errors also reach the same handler. Invalid parser inputs keep their existing handling. +- Trust answers are all yes: the implementation is appropriately small, consequential choices preserve existing policy, and the user's end-to-end refusal and discoverability needs are met. No issue-contract gap or delivery blocker found. + +# Deal breakers +- None. + +# Suggestions (enhancements only) +- Clarify the plan's “dependencies are not created” wording to identify the setup action. Existing `cli.ts:55-61` can construct dependencies in the unchanged startup update-check hook; command tests and hermetic e2e prove the intended command-local boundary. The caller has confirmed this scope and owns the wording correction. No code expansion is warranted for issue #891. diff --git a/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/phase-1.md b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/phase-1.md new file mode 100644 index 000000000..fcbfbdb75 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/phase-1.md @@ -0,0 +1,68 @@ +--- +status: reviewed +--- + +# Instruction: Guard setup validation + +## Architecture projection +```txt +cli/ + src/presentation/commands/setup.ts modify: guard constructor and clarify help + tests/presentation/commands/setup-wiring.integration.test.ts modify: prove constructor failures render before dependency creation + tests/e2e/setup-scope-user.e2e.test.ts modify: prove clean refusal in the real binary + tests/golden/snapshots/help/surface.json modify: record the intended setup help change + README.md modify: document current scope support and restrictions +``` +No source files are created or deleted. Domain validation and tool activation remain unchanged. + +## User Journey +```mermaid +flowchart TD + A[Run setup with user scope] --> B{Supported options?} + B -->|No| C[Read validation message and remedies; exit 1] + B -->|Yes| D[Register shared source and supported tool; exit 0] + E[Read setup help and README] --> F[Discover scope limits before installation] +``` + +## Test Scope +```mermaid +journey + section Setup + Create isolated project and user directories => no real profile touched: 5: system + section Happy path + Run supported user-scope setup with plugins none => exit 0 and project unchanged: 5: cli + Read setup help => restrictions discoverable: 5: cli + section Edge case - unsupported plugin mode + User scope with all or recommended or named plugins => setup => message and remedies with exit 1 and no stack or bundle dump: 1: cli + section Edge case - other constructor validation + User scope with no AI tools or an IDE or an unsupported AI tool => setup => clean validation error before dependencies: 1: cli + section Teardown + Remove isolated directories => baseline restored: 5: system +``` + +## Wireframe +Not applicable: command-line validation only. + +## Tasks to do +### 1) Reproduce and guard constructor errors +1. Add regression tests and demonstrate failure against the current command. +2. Move SetupFlow construction and dependent banner into the existing try block before dependency creation. +3. Preserve the existing error messages, exit status, domain policy, and supported setup behavior. + +### 2) Explain shipped scope support +1. Extend the existing README scope documentation with support grounded in current profiles and registry. +2. Clarify setup help: explicit supported AI tools, no IDE tools, no plugin enabling at user scope; native activation depends on available host CLI. +3. Distinguish setup registration from plugin installation and avoid promising global configuration rewriting. +4. Regenerate the existing help golden snapshot and confirm only setup help changes. + +### 3) Validate +1. Run relevant command, domain, and user-scope e2e tests, typecheck, architecture suite, and lint on changed files. +2. Reproduce the issue through the hermetic built-binary tests after other checks. +3. Report exact commands and results, including limitations. + +## Test acceptance criteria +| Task | Acceptance criteria | +| --- | --- | +| 1 | Unsupported plugin modes print the existing cause and remedies, exit 1, and emit no exception name, stack frame, or minified bundle; the setup action does not create dependencies. Other constructor validation errors use the same boundary. | +| 2 | Help and README accurately explain current user-scope setup and plugin installation limitations from source; no installation capability is added. | +| 3 | Relevant tests, typecheck, architecture checks, and changed-file lint pass; built-binary reproduction runs with isolated user directories. | diff --git a/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/plan.md b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/plan.md new file mode 100644 index 000000000..b5e4eb359 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/plan.md @@ -0,0 +1,36 @@ +--- +objective: "Setup renders constructor validation failures as clean CLI errors and documents the current user-scope support limits." +status: reviewed +--- + +# Plan: Render setup validation errors + +## Overview +| Field | Value | +| --- | --- | +| **Goal** | Refuse unsupported setup options with a useful message and exit 1, without a stack trace or bundle dump. | +| **Source** | https://github.com/ai-driven-dev/framework/issues/891 and its follow-up comment | + +## Phases +| # | Phase | File | +| --- | --- | --- | +| 1 | Guard setup construction and explain user scope | [phase-1.md](./phase-1.md) | + +## Resources +| Source | Verified | +| --- | --- | +| `cli/src/presentation/commands/setup.ts` | SetupFlow is constructed before the existing error boundary. | +| `cli/src/contexts/framework/domain/setup-flow.ts` | Constructor validation intentionally refuses plugins and other unsupported user-scope options. | +| `cli/src/presentation/error-handler.ts` | Existing handler prints the message and exits 1. | +| `cli/src/contexts/tools/domain/registry.ts` and profiles | User-scope setup requires native activation or a user install directory; plugin installation scope is a separate declaration. | +| `cli/tests/e2e/helpers.ts` | Built-binary runs isolate user directories and remove host tools from PATH. | +| `cli/aidd_docs/memory/architecture.md` and `testing.md` | Preserve context boundaries; regression evidence must exercise the built binary. | + +## Decisions +| Decision | Why | +| --- | --- | +| Move construction into the existing presentation error boundary. | Covers all current and future constructor validation without duplicating domain rules. | +| Reuse README scope documentation and clarify setup help. | Make shipped restrictions discoverable without inventing broader global installation support. | +| Add focused command and built-binary regression coverage. | Domain tests alone cannot detect an escaped command exception. | +| Run typecheck, relevant tests, architecture checks, lint, and hermetic CLI journey. | Validate behavior and responsibility placement; no browser surface exists. | +| Preserve pre-existing `.gitignore` and `.hermes.md` changes outside task commits. | They are unrelated workspace changes. | diff --git a/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/review.md b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/review.md new file mode 100644 index 000000000..e39ed6d08 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_06_setup-validation/review.md @@ -0,0 +1,36 @@ +# Review: Render setup validation errors +- **Verdict**: approve +- **Diff**: `156f5432...65d1917a` +- **Axes run**: code, functional, relevancy +- **Date**: 2026_10_06 +- **Findings**: 0 critical, 0 warning, 0 minor +- **Score**: 100% of acceptance criteria and baseline checklist fulfilled; no severity adjustment. + +## Phases +### Phase 1 — Guard setup construction and explain user scope +- [x] Unsupported plugin modes retain the cause and remedies, exit 1, and emit no exception name, stack frame, or bundle dump; the setup action creates no dependencies. Other constructor validation uses the same boundary. — `cli/src/presentation/commands/setup.ts:156`, `cli/src/presentation/commands/setup.ts:194`, `cli/tests/presentation/commands/setup-wiring.integration.test.ts:261`, `cli/tests/e2e/setup-scope-user.e2e.test.ts:28`. +- [x] Help and README describe shipped user-scope setup and plugin installation limits from current profiles; no installation capability is added. — `cli/src/presentation/commands/setup.ts:122`, `cli/README.md:71`, `cli/src/contexts/tools/domain/registry.ts:172`, `cli/src/contexts/tools/domain/profiles/cursor/profile.ts:120`, `cli/src/contexts/tools/domain/profiles/claude/profile.ts:125`, `cli/src/contexts/tools/domain/profiles/codex/profile.ts:194`, `cli/src/contexts/tools/domain/profiles/copilot/profile.ts:302`; golden diff changes only setup help. +- [x] Relevant tests, typecheck, architecture checks and lint pass; built-binary reproduction isolates user directories and host binaries. — checker rerun: 62 passed across domain, command and user-scope e2e; `cli/tests/e2e/helpers.ts:66`, `cli/tests/e2e/helpers.ts:161`, `cli/tests/e2e/helpers.ts:196`, `cli/tests/e2e/global-setup.ts:30`; `/tmp/aidd-891-commit.log:55`, `/tmp/aidd-891-commit.log:727`, `/tmp/aidd-891-push.log:828`. + +## Findings +| Sev | Kind | Phase | Location | Issue | Fix | +| --- | --- | --- | --- | --- | --- | +| - | - | - | - | None. | - | + +## Verification +| Metric | Value | +| --- | --- | +| Verified | 100% (3/3 acceptance criteria) | +| Files checked | All eight changed files: `plan.md`, `phase-1.md`, `backlog-link.json`, `cli/README.md`, `cli/src/presentation/commands/setup.ts`, `cli/tests/e2e/setup-scope-user.e2e.test.ts`, `cli/tests/golden/snapshots/help/surface.json`, `cli/tests/presentation/commands/setup-wiring.integration.test.ts`; canonical domain flow, error handler, registry, profiles, machine-scope use case, activation flow, binary helpers, CLI startup and project rules also inspected. | +| Unchecked | none | +| Unplanned | none; task metadata links the issue and records the agreed plan. | +| No information duplication | Fulfilled: constructor policy remains solely in `SetupFlow`; error rendering reuses `ErrorHandler`; help derives supported tools from the same registry predicate; README is the single detailed support matrix. Exact error assertions and the required golden snapshot verify their respective public contracts. | +| No incoherence or contradiction | Fulfilled: source profiles support the documented matrix, missing-host warning, native scopes and Cursor project-hook caveat. `setup --help` golden change is isolated to setup. | +| No over-engineering | Fulfilled: one existing error boundary extended to cover construction, no new abstraction, installer, dependency or configuration writer. | +| No dead code or debug leftovers | Fulfilled: every added import is used; changed lines contain no debug logs, commented blocks or silent TODOs; knip green at `/tmp/aidd-891-push.log:836`. | +| Architecture and declared rules | Fulfilled: command layer catches and exits; domain invariant remains in its constructor; presentation reads public registry capability declarations; dependencies remain in runtime wiring. `cli/.claude/rules/00-architecture/0-error-handling.md:8`, `0-hexagonal.md:11`, `0-contexts.md:13`; 140 architecture assertions, lint, typecheck and type honesty green in commit log. | +| Independent focused command | `PATH=/opt/homebrew/Cellar/node/26.8.2/bin:$PATH pnpm --dir cli exec vitest run --project=unit --project=integration --project=e2e tests/presentation/commands/setup-wiring.integration.test.ts tests/contexts/framework/domain/setup-flow.unit.test.ts tests/e2e/setup-scope-user.e2e.test.ts` => 3 files passed, 62 tests passed (23 domain, 32 command, 7 e2e), exit 0. | +| Full-suite and repository evidence | Inspected `/tmp/aidd-891-full-test.log:853`: 529 files passed, 6805 tests passed, one opt-in Kilo test skipped; repeated full-suite result and knip green in push log. Commit log proves 549 repository tests passed, zero failed, plus documentation checks and CLI gates. Full suite not repeated without a new concern. | +| Diff hygiene | `git diff --check 156f5432..65d1917a` => exit 0, no output. HEAD independently verified as `65d1917a4b7f668ca5bd804fe05fc142a179099d`. | +| Acceptance wording ambiguity | The original phrase “dependencies are not created” is broader than its command-local evidence. Caller explicitly confirmed it means the setup action's `createDeps` call (`setup.ts:172`). Existing `cli.ts:55-61` preAction can initialize dependencies for update checking first; hermetic tests disable it with `AIDD_SKIP_UPDATE_CHECK=1`. This startup behavior is unchanged and outside issue #891; caller owns clarification of the plan wording. | +| Actual end-to-end need | Fulfilled: real binary refuses all, recommended and named plugin selections with exact one-line stderr and exit 1; supported user setup remains green and leaves the project untouched. Current constructor failures are rendered before the welcome banner and command dependency graph. No contract gap found. | diff --git a/cli/README.md b/cli/README.md index 0013c0390..eca3691d9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -32,7 +32,7 @@ aidd update ``` The first command bootstraps the project: manifest, default marketplace, tool configs, plugins. -`--scope user` on `setup` registers the framework source and native activation machine-wide instead, and writes nothing under the project. +`--scope user` on `setup` registers the framework source and supported tools machine-wide instead, and writes nothing under the project. See the scope support below. ## Authentication @@ -68,6 +68,22 @@ Run `aidd --help`, then a group's own `--help`, for flags this page does not rep `setup`, `doctor`, `sync` and `clean` accept `--scope `. Project scope is the default and acts on this project alone. +`setup --scope user` requires an explicit supported `--ai` list, no `--ide`, and `--plugins none` (or omit `--plugins` in a scripted run). It records the source and tools in the user registry without installing tool configuration files or enabling plugins. `--ai all` includes unsupported tools and is refused. + +| Tool | `setup --scope user` | +| --- | --- | +| `claude`, `codex`, `copilot` | Registers the source and drives the tool's own CLI | +| `cursor` | Records the source and tool only; no native activation | +| `opencode`, `kilo` | Unsupported; use project scope | + +Native activation requires the corresponding `claude`, `codex`, or `copilot` executable on `PATH`. When it is missing, registration succeeds with a warning and activation remains unrun; install the host CLI, then run `aidd sync --scope user`. + +```sh +aidd setup --scope user --ai claude,codex,copilot --plugins none --yes +``` + +`aidd setup --help` prints the same support list, generated from the registry. + ### Framework | Command | Does | diff --git a/cli/src/presentation/commands/setup.ts b/cli/src/presentation/commands/setup.ts index 3fdf558b4..b789ffe52 100644 --- a/cli/src/presentation/commands/setup.ts +++ b/cli/src/presentation/commands/setup.ts @@ -3,7 +3,11 @@ import type { Command } from "commander"; import { MarketplaceSourceMode } from "../../contexts/distribution/domain/marketplace-source-mode.js"; import { SetupUseCase } from "../../contexts/framework/application/setup-use-case.js"; import { SetupFlow } from "../../contexts/framework/domain/setup-flow.js"; -import { assertToolIdsMatchCategory } from "../../contexts/tools/domain/registry.js"; +import { + assertToolIdsMatchCategory, + getAllRegisteredTools, + supportsUserScopeActivation, +} from "../../contexts/tools/domain/registry.js"; import type { ToolId } from "../../kernel/tool.js"; import { AI_TOOL_IDS, IDE_TOOL_IDS } from "../../kernel/tool.js"; import { createDeps } from "../../runtime/wiring/framework.js"; @@ -113,7 +117,16 @@ export function registerSetupCommand(program: Command): void { .option( "--scope ", "project (default) installs into this project alone; user registers the shared " + - "framework source and native activation machine-wide, writing nothing under this project" + "framework source and supported tools machine-wide, writing nothing under this project; " + + "see user-scope requirements below" + ) + .addHelpText( + "after", + () => + "\nUser scope:\n" + + ` Pass --ai ${[...getAllRegisteredTools().keys()].filter(supportsUserScopeActivation).sort().join(",")} (choose one or more), --plugins none, and no --ide.\n` + + " Native activation requires the corresponding host CLI on PATH.\n" + + " Tools without native activation are registered only; setup installs no plugins or tool configuration files." ) .action(async (cmdOptions: SetupCmdOptions) => { const { verbose, output, projectRoot } = parseGlobalOptions(program); @@ -140,22 +153,22 @@ export function registerSetupCommand(program: Command): void { ); const registerDefaultMarketplace = cmdOptions.defaultMarketplace !== false; - const flow = new SetupFlow({ - projectRoot, - source, - aiTools: toolIds.aiTools, - ideTools: toolIds.ideTools, - pluginMode, - pluginNames, - interactive, - force: false, - registerDefaultMarketplace, - scope, - }); + try { + const flow = new SetupFlow({ + projectRoot, + source, + aiTools: toolIds.aiTools, + ideTools: toolIds.ideTools, + pluginMode, + pluginNames, + interactive, + force: false, + registerDefaultMarketplace, + scope, + }); - if (interactive) printWelcomeBanner(output); + if (interactive) printWelcomeBanner(output); - try { const deps = await createDeps(projectRoot, { verbose }, output); const result = await new SetupUseCase( diff --git a/cli/tests/e2e/setup-scope-user.e2e.test.ts b/cli/tests/e2e/setup-scope-user.e2e.test.ts index 2f446caf6..a246cc633 100644 --- a/cli/tests/e2e/setup-scope-user.e2e.test.ts +++ b/cli/tests/e2e/setup-scope-user.e2e.test.ts @@ -25,6 +25,46 @@ async function readJson(path: string): Promise> { } describe("E2E: setup --scope user writes nothing under the project", () => { + it.each(["all", "recommended", "aidd-dev"])( + "prints a clean refusal for user-scope plugin mode %s", + async (plugins) => { + const { projectDir, fakeHome, cleanup } = await createTestEnv("scope-user-plugins"); + try { + const result = await runCli( + ["setup", "--scope", "user", "--ai", "claude", "--plugins", plugins, "--yes"], + projectDir, + fakeHome + ); + + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe( + "Error: --scope user has no manifest entry a plugin can be recorded against yet, so " + + "--plugins has nothing to enable. Drop --plugins, or run `aidd plugin install` " + + "separately at project scope.\n" + ); + } finally { + await cleanup(); + } + } + ); + + it("explains user-scope restrictions in setup help", async () => { + const { projectDir, fakeHome, cleanup } = await createTestEnv("scope-user-help"); + try { + const result = await runCli(["setup", "--help"], projectDir, fakeHome); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("--ai claude,codex,copilot,cursor"); + expect(result.stdout).toContain("--plugins none"); + expect(result.stdout).toContain("no --ide"); + expect(result.stdout).toContain("host CLI on PATH"); + } finally { + await cleanup(); + } + }); + it("leaves the git-tracked project untouched and writes the user manifest under fakeHome", async () => { const { projectDir, fakeHome, cleanup } = await createTestEnv("scope-user-setup"); try { diff --git a/cli/tests/golden/snapshots/help/surface.json b/cli/tests/golden/snapshots/help/surface.json index c69798490..4d8f88536 100644 --- a/cli/tests/golden/snapshots/help/surface.json +++ b/cli/tests/golden/snapshots/help/surface.json @@ -122,7 +122,7 @@ { "invocation": "aidd setup", "exitCode": 0, - "help": "Usage: aidd setup [options]\n\nSet up or update the project to a correct state — bootstraps the whole project\n(marketplace, framework, tools, plugins); see `framework install`, which acts on\nthe framework alone\n\nOptions:\n --source Framework source: remote or local\n --path Absolute path to local framework (required with\n --source local)\n --release Marketplace release tag to fetch (e.g., v1.2.3)\n --ai Comma-separated AI tool IDs, or 'all' (e.g.,\n claude,cursor or all)\n --ide Comma-separated IDE tool IDs, or 'all' (e.g., vscode\n or all)\n --plugins Plugin install mode: none | all | recommended |\n comma-separated names\n --no-default-marketplace Skip auto-registering aidd-framework (no source\n prompt, no plugin install)\n --yes Accept defaults without prompting\n --scope project (default) installs into this project alone;\n user registers the shared framework source and\n native activation machine-wide, writing nothing\n under this project\n -h, --help display help for command" + "help": "Usage: aidd setup [options]\n\nSet up or update the project to a correct state — bootstraps the whole project\n(marketplace, framework, tools, plugins); see `framework install`, which acts on\nthe framework alone\n\nOptions:\n --source Framework source: remote or local\n --path Absolute path to local framework (required with\n --source local)\n --release Marketplace release tag to fetch (e.g., v1.2.3)\n --ai Comma-separated AI tool IDs, or 'all' (e.g.,\n claude,cursor or all)\n --ide Comma-separated IDE tool IDs, or 'all' (e.g., vscode\n or all)\n --plugins Plugin install mode: none | all | recommended |\n comma-separated names\n --no-default-marketplace Skip auto-registering aidd-framework (no source\n prompt, no plugin install)\n --yes Accept defaults without prompting\n --scope project (default) installs into this project alone;\n user registers the shared framework source and\n supported tools machine-wide, writing nothing under\n this project; see user-scope requirements below\n -h, --help display help for command\n\nUser scope:\n Pass --ai claude,codex,copilot,cursor (choose one or more), --plugins none, and no --ide.\n Native activation requires the corresponding host CLI on PATH.\n Tools without native activation are registered only; setup installs no plugins or tool configuration files." }, { "invocation": "aidd sync", diff --git a/cli/tests/presentation/commands/setup-wiring.integration.test.ts b/cli/tests/presentation/commands/setup-wiring.integration.test.ts index d8a348990..ea31fe91e 100644 --- a/cli/tests/presentation/commands/setup-wiring.integration.test.ts +++ b/cli/tests/presentation/commands/setup-wiring.integration.test.ts @@ -258,6 +258,55 @@ describe("aidd setup — every flag that makes a run a scripted one", () => { }); describe("aidd setup — what it refuses and what it reports", () => { + it.each(["all", "recommended", "aidd-dev"])( + "refuses user-scope plugin mode %s before building dependencies", + async (plugins) => { + const exit = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("exited"); + }); + + await expect(run("--scope", "user", "--ai", "claude", "--plugins", plugins)).rejects.toThrow( + "exited" + ); + + expect(errors.join("")).toBe( + "Error: --scope user has no manifest entry a plugin can be recorded against yet, so " + + "--plugins has nothing to enable. Drop --plugins, or run `aidd plugin install` " + + "separately at project scope.\n" + ); + expect(exit).toHaveBeenCalledWith(1); + expect(vi.mocked(createDeps)).not.toHaveBeenCalled(); + expect(written).toEqual([]); + } + ); + + it.each([ + { args: [], cause: "with no --ai", remedy: "Pass `--ai `" }, + { + args: ["--ai", "claude", "--ide", "vscode"], + cause: "an IDE tool (vscode)", + remedy: "Drop --ide", + }, + { + args: ["--ai", "opencode"], + cause: "opencode declares no user-scope settings", + remedy: "Drop it from --ai", + }, + ])("renders user-scope validation for $cause before building dependencies", async (test) => { + const exit = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("exited"); + }); + + await expect(run("--scope", "user", ...test.args)).rejects.toThrow("exited"); + + expect(errors.join("")).toContain(test.cause); + expect(errors.join("")).toContain(test.remedy); + expect(errors.join("")).not.toMatch(/UserScope\w+Error|\n\s+at /); + expect(exit).toHaveBeenCalledWith(1); + expect(vi.mocked(createDeps)).not.toHaveBeenCalled(); + expect(written).toEqual([]); + }); + it("refuses a local source with nowhere to read it from", async () => { vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exited"); @@ -362,7 +411,8 @@ describe("aidd setup — the help surface", () => { [ "--scope ", "project (default) installs into this project alone; user registers the shared " + - "framework source and native activation machine-wide, writing nothing under this project", + "framework source and supported tools machine-wide, writing nothing under this project; " + + "see user-scope requirements below", ], ]); }); diff --git a/scripts/__tests__/check-referenced-paths.test.js b/scripts/__tests__/check-referenced-paths.test.js index 07cc65ca6..7dc00079c 100644 --- a/scripts/__tests__/check-referenced-paths.test.js +++ b/scripts/__tests__/check-referenced-paths.test.js @@ -38,6 +38,15 @@ test("referencedPaths ignores what only looks like a path", () => { assert.deepEqual(referencedPaths(content), []); }); +test("referencedPaths skips a path the reader creates, never this repository's own content", () => { + // `.aidd/config.json` is the telemetry opt-in switch a repository commits to turn measurement + // on. This repository has not, so its absence is the state the prose describes — and the + // anchor exists as soon as a contributor runs the CLI here, which is what made this a refusal. + const content = "Opt in by committing `.aidd/config.json` with `telemetry.enabled: true`."; + + assert.deepEqual(referencedPaths(content, new Set([".aidd"])), []); +}); + test("deadReferences names the file, the line and the path", () => { const tempDir = fs.mkdtempSync(path.join(root, "scripts/__tests__/.tmp-check-referenced-paths-")); diff --git a/scripts/check-referenced-paths.js b/scripts/check-referenced-paths.js index 035e32e12..4511453bc 100644 --- a/scripts/check-referenced-paths.js +++ b/scripts/check-referenced-paths.js @@ -26,6 +26,14 @@ function topLevelEntries() { return new Set(fs.readdirSync(ROOT).filter((entry) => entry !== ".git")); } +/** + * Paths this repository's prose names as something the reader creates, never as its own content. + * `.aidd/config.json` is the telemetry opt-in switch a repository commits to turn measurement on; + * this one has not, so that absence is the state the prose describes. The anchor appears as soon + * as a contributor runs the CLI in this checkout, which is what turned a page into a refusal. + */ +const READER_CREATED = new Set([".aidd/config.json"]); + const BACKTICKED = /`([^`\n]+)`/gu; function referencedPaths(content, entries = topLevelEntries()) { @@ -43,6 +51,8 @@ function referencedPaths(content, entries = topLevelEntries()) { // directories drift far less than the files inside them. if (!path.extname(token)) continue; + if (READER_CREATED.has(token)) continue; + const head = token.split("/")[0]; if (!entries.has(head)) continue;