create-agent-rig 0.9.1 → 0.10.1
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 +462 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +508 -101
- package/packages/cli/dist/index.js +658 -47
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +4 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +59 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +112 -40
- package/templates/release-ledger.json +3 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -1,36 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { parseArgs } from 'node:util';
|
|
3
5
|
import { CreateError, createProject } from './commands/create.js';
|
|
4
6
|
import { InitError, initFileContents, initProject, planInit } from './commands/init.js';
|
|
7
|
+
import { INTEGRATIONS_VERBS, runIntegrationsCommand } from './commands/integrations.js';
|
|
8
|
+
import { runSetupWizard } from './commands/setup-wizard.js';
|
|
5
9
|
import { execFileRunner, setupSubsystems } from './commands/setup.js';
|
|
6
|
-
import { UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
|
|
10
|
+
import { AGENTS_MD_RESCUE, UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
|
|
11
|
+
import { CHANGED_SINCE_PLANNING_REASON, UninstallError, applyUninstall, isUnverifiedReason, planUninstall, protectedFileReason, } from './commands/uninstall.js';
|
|
7
12
|
import { makePalette } from './lib/colors.js';
|
|
8
|
-
import { readManifest } from './lib/manifest.js';
|
|
13
|
+
import { readManifest, sha256 } from './lib/manifest.js';
|
|
9
14
|
import { SubsystemsError, refreshSubsystems, subsystemsManifestPath } from './lib/subsystems.js';
|
|
10
|
-
import { promptConfirm
|
|
15
|
+
import { promptConfirm } from './lib/prompts.js';
|
|
11
16
|
import { collectGovernance, renderSummary } from './lib/summary.js';
|
|
12
|
-
import { DEFAULT_TARGET, TARGET_NAMES } from './lib/targets.js';
|
|
13
17
|
import { packageVersion, rigHandshake } from './lib/version.js';
|
|
14
18
|
import { runMemory } from './commands/memory.js';
|
|
19
|
+
import { runDoctor } from './commands/doctor.js';
|
|
15
20
|
const USAGE = `Usage: create-agent-rig <dir> [options]
|
|
16
21
|
|
|
17
|
-
Scaffolds a new
|
|
18
|
-
|
|
22
|
+
Scaffolds a new directory into <dir>: \`mkdir\` + \`git init\` + the same install
|
|
23
|
+
\`init\` runs into an existing repo — a Claude Code + Codex agent operating
|
|
24
|
+
system, no application scaffolding. Refuses to write into a non-empty
|
|
25
|
+
directory.
|
|
19
26
|
|
|
20
27
|
Options
|
|
21
|
-
--target <name> ${TARGET_NAMES.join(' | ')}
|
|
22
|
-
(interactive selection when omitted on a terminal;
|
|
23
|
-
required when not a terminal — default: ${DEFAULT_TARGET})
|
|
24
28
|
--no-git skip git init + the pristine-template baseline commit
|
|
29
|
+
--layer workflow also install the experimental, opt-in workflow layer (the
|
|
30
|
+
queue adapter, the loop and pr-ship skills, run-state,
|
|
31
|
+
journal, revalidation, claim-records, and the PR-lifecycle
|
|
32
|
+
helpers) — see init below; default is the core layer only.
|
|
33
|
+
"workflow" is the only accepted name — process/Core
|
|
34
|
+
installs unconditionally and is never named. Repeatable;
|
|
35
|
+
repeating the same name is harmless.
|
|
25
36
|
--no-color plain output (NO_COLOR is respected too)
|
|
26
37
|
--version print the version (--version --json: the contract handshake,
|
|
27
38
|
one JSON object with the name, version and contract version)
|
|
28
39
|
-h, --help this text
|
|
29
40
|
|
|
30
|
-
Also: create-agent-rig init [--dry-run]
|
|
41
|
+
Also: create-agent-rig init [--dry-run] [--layer workflow]
|
|
31
42
|
Install the process layer (rules, gates, stop rules — no architecture
|
|
32
43
|
assumptions) into the CURRENT existing repo. Refuses to clobber CLAUDE.md
|
|
33
44
|
or AGENTS.md.
|
|
45
|
+
--layer workflow also installs the experimental workflow layer: an
|
|
46
|
+
autonomous, cooperative multi-session queue/loop/PR-lifecycle mechanism,
|
|
47
|
+
never required by Lean Core. Without it, only the core layer is installed.
|
|
48
|
+
A rig that already has the workflow layer keeps it on a plain re-run with
|
|
49
|
+
no flag — the flag only ever adds the layer, never drops one a previous
|
|
50
|
+
run already recorded.
|
|
34
51
|
--force is deprecated: it refuses and points at upgrade, which refreshes a
|
|
35
52
|
rig file by file. It is removed in 0.6.
|
|
36
53
|
|
|
@@ -45,6 +62,53 @@ Also: create-agent-rig setup --memory-root <checkout> [--memory-ref <sha>] [--dr
|
|
|
45
62
|
one declared root. Performs the --version --json handshake first and refuses
|
|
46
63
|
a foreign contract major with exit 4 before writing anything.
|
|
47
64
|
|
|
65
|
+
Also: create-agent-rig setup list [--json]
|
|
66
|
+
List the supported repository integrations.
|
|
67
|
+
|
|
68
|
+
Also: create-agent-rig doctor [--json]
|
|
69
|
+
Diagnose installed Rig files, guards, provider wiring and available runtimes.
|
|
70
|
+
Does not apply setup plans or observe connectivity, authorization or trust.
|
|
71
|
+
|
|
72
|
+
Also: create-agent-rig setup
|
|
73
|
+
Choose a provider and harness interactively, then approve its exact plan.
|
|
74
|
+
|
|
75
|
+
Also: create-agent-rig setup add <id> [--harness <name>] [--adopt] [--dry-run] [--yes] [--json]
|
|
76
|
+
Plan MCP wiring or the pinned official Spec Kit lifecycle. Record intent and
|
|
77
|
+
ownership in .rig/integrations.json. JSON never prompts; writes require --yes.
|
|
78
|
+
--adopt explicitly adopts an existing external Spec Kit installation.
|
|
79
|
+
|
|
80
|
+
Also: create-agent-rig setup apply [id] [--dry-run] [--yes] [--json]
|
|
81
|
+
Apply declared integration intent. Refuse if files changed after planning.
|
|
82
|
+
Claude project wiring does not prove authorization, connectivity or trust.
|
|
83
|
+
|
|
84
|
+
Also: create-agent-rig setup remove <id> [--dry-run] [--yes] [--json]
|
|
85
|
+
Remove proven Rig-owned wiring, or delegate Spec Kit removal to its official
|
|
86
|
+
CLI. Preserve foreign wiring and provider data; retire intent after success.
|
|
87
|
+
|
|
88
|
+
Also: create-agent-rig uninstall [dir] [--dry-run] [--yes] [--detach] [--json]
|
|
89
|
+
Remove what a rig installed from [dir] (default: the current directory) —
|
|
90
|
+
only files whose bytes on disk still match what the manifest recorded, are
|
|
91
|
+
still one of the exact paths this release installs, and still match right
|
|
92
|
+
up to the moment each one is removed; anything under .git is refused
|
|
93
|
+
outright, whatever hash a manifest pairs it with. Everything else (edited,
|
|
94
|
+
foreign, deleted already, kept by init, changed since the plan was shown, or
|
|
95
|
+
not a path this release owns) is left in place and reported. The manifest
|
|
96
|
+
is removed last, and only once every removal succeeded AND nothing was
|
|
97
|
+
preserved — a preserved path means the rig still owns bytes it did not
|
|
98
|
+
remove, so the evidence naming them stays; a failed run also keeps it, so a
|
|
99
|
+
re-run picks up where it stopped. --detach removes the manifest anyway,
|
|
100
|
+
after the same safe cleanup, leaving every preserved path for you and
|
|
101
|
+
printing the full handover list — it never forces away a conflicting or
|
|
102
|
+
modified file. --json's payload names which of three outcomes a run
|
|
103
|
+
reached: "uninstalled" (clean), "partial" (something kept, manifest stays),
|
|
104
|
+
"detached" (--detach: manifest gone, a handover list left behind). Prints
|
|
105
|
+
the plan, then asks before removing anything: --yes answers up front
|
|
106
|
+
(required off a terminal, and always required with --json, which never
|
|
107
|
+
prompts) — the same consent rule applies to --detach. --json prints one
|
|
108
|
+
JSON object and nothing else on stdout (see docs/command-contract.md);
|
|
109
|
+
without it, uninstall reports in prose like init and upgrade. Idempotent: a
|
|
110
|
+
repeat run finds no manifest and does nothing, exit 0.
|
|
111
|
+
|
|
48
112
|
Also: create-agent-rig memory <doctor|load> [args…]
|
|
49
113
|
Run a Memory verb through the registered executable: the --version --json
|
|
50
114
|
handshake first (a foreign contract major exits 4 and the verb never runs),
|
|
@@ -54,6 +118,54 @@ Also: create-agent-rig memory <doctor|load> [args…]
|
|
|
54
118
|
60 s bound). No manifest answers unsupported/absent (exit 0); an invalid
|
|
55
119
|
invocation exits 2.`;
|
|
56
120
|
async function runSetup(rawArgs) {
|
|
121
|
+
// Integration verbs and legacy machine-scoped Memory registration are
|
|
122
|
+
// separate dispatch paths.
|
|
123
|
+
const verb = rawArgs[0];
|
|
124
|
+
if (INTEGRATIONS_VERBS.includes(verb ?? '')) {
|
|
125
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
126
|
+
const result = await runIntegrationsCommand({
|
|
127
|
+
verb: verb,
|
|
128
|
+
args: rawArgs.slice(1),
|
|
129
|
+
cwd: process.cwd(),
|
|
130
|
+
isTTY: isInteractive,
|
|
131
|
+
confirm: async (plan) => {
|
|
132
|
+
process.stderr.write(`${plan}\n`);
|
|
133
|
+
return promptConfirm('Apply this integration plan?', {
|
|
134
|
+
input: process.stdin,
|
|
135
|
+
output: process.stderr,
|
|
136
|
+
isInteractive,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
process.stdout.write(result.stdout);
|
|
141
|
+
process.stderr.write(result.stderr);
|
|
142
|
+
return result.exitCode;
|
|
143
|
+
}
|
|
144
|
+
if (!rawArgs.some((arg) => arg === '--memory-root' || arg.startsWith('--memory-root=')) &&
|
|
145
|
+
(verb === undefined || verb.startsWith('-'))) {
|
|
146
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
147
|
+
const result = await runSetupWizard({
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
isTTY: isInteractive,
|
|
150
|
+
args: rawArgs,
|
|
151
|
+
confirm: async (plan) => {
|
|
152
|
+
process.stderr.write(`${plan}\n`);
|
|
153
|
+
return promptConfirm('Apply this integration plan?', {
|
|
154
|
+
input: process.stdin,
|
|
155
|
+
output: process.stderr,
|
|
156
|
+
isInteractive,
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
process.stdout.write(result.stdout);
|
|
161
|
+
process.stderr.write(result.stderr);
|
|
162
|
+
return result.exitCode;
|
|
163
|
+
}
|
|
164
|
+
// Do not echo an untrusted positional value into terminal diagnostics.
|
|
165
|
+
if (verb !== undefined && !verb.startsWith('-')) {
|
|
166
|
+
process.stderr.write(`Unknown setup verb: expected one of ${INTEGRATIONS_VERBS.join(', ')} (or --memory-root for the legacy path).\n`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
57
169
|
let values;
|
|
58
170
|
try {
|
|
59
171
|
({ values } = parseArgs({
|
|
@@ -100,6 +212,27 @@ async function runSetup(rawArgs) {
|
|
|
100
212
|
throw error;
|
|
101
213
|
}
|
|
102
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* `--layer <name>`, repeatable (RP-180 round 2: the owner's spelling is
|
|
217
|
+
* `--layer workflow`, never `--with-workflow`). `workflow` is the only name a
|
|
218
|
+
* user may opt into today — `process`/Core installs unconditionally and is
|
|
219
|
+
* never something a user names. Repeating the same name is harmless; naming
|
|
220
|
+
* anything else is a usage error the caller reports the same way it reports
|
|
221
|
+
* any other bad flag (message to stderr + USAGE, exit 1) — neither `init` nor
|
|
222
|
+
* the top-level `create` accepts `--json` today, so there is no JSON error
|
|
223
|
+
* shape to match here; this follows the one shape those two commands already
|
|
224
|
+
* use.
|
|
225
|
+
*/
|
|
226
|
+
function resolveLayerFlag(layer) {
|
|
227
|
+
const names = layer ?? [];
|
|
228
|
+
const unknown = names.find((name) => name !== 'workflow');
|
|
229
|
+
if (unknown !== undefined) {
|
|
230
|
+
return {
|
|
231
|
+
error: `Unknown --layer "${unknown}" — the only accepted layer name is "workflow" (process/Core installs unconditionally and is never named).`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return { withWorkflow: names.length > 0 };
|
|
235
|
+
}
|
|
103
236
|
async function runInit(rawArgs) {
|
|
104
237
|
let values;
|
|
105
238
|
try {
|
|
@@ -111,6 +244,7 @@ async function runInit(rawArgs) {
|
|
|
111
244
|
'dry-run': { type: 'boolean' },
|
|
112
245
|
force: { type: 'boolean' },
|
|
113
246
|
'no-color': { type: 'boolean' },
|
|
247
|
+
layer: { type: 'string', multiple: true },
|
|
114
248
|
},
|
|
115
249
|
allowPositionals: false,
|
|
116
250
|
}));
|
|
@@ -119,8 +253,14 @@ async function runInit(rawArgs) {
|
|
|
119
253
|
process.stderr.write(`${error.message}\n\n${USAGE}\n`);
|
|
120
254
|
return 1;
|
|
121
255
|
}
|
|
256
|
+
const layerResult = resolveLayerFlag(values.layer);
|
|
257
|
+
if ('error' in layerResult) {
|
|
258
|
+
process.stderr.write(`${layerResult.error}\n\n${USAGE}\n`);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
122
261
|
const cwd = process.cwd();
|
|
123
262
|
const dryRun = values['dry-run'] === true;
|
|
263
|
+
const { withWorkflow } = layerResult;
|
|
124
264
|
// `init` adopts a repo the rig knows nothing about. Run inside a rig `create`
|
|
125
265
|
// generated — reachable when its CLAUDE.md was deleted — it is the wrong
|
|
126
266
|
// command: it installs the process layer alone and never refreshes the stack
|
|
@@ -133,8 +273,8 @@ async function runInit(rawArgs) {
|
|
|
133
273
|
// same limit `recordInstall` carries, stated in both places because either
|
|
134
274
|
// one alone reads as wider.
|
|
135
275
|
const existing = await readManifest(cwd);
|
|
136
|
-
const plan = await planInit(cwd);
|
|
137
|
-
process.stdout.write(`agent-rig init — process layer into ${cwd}\n\n` +
|
|
276
|
+
const plan = await planInit(cwd, { withWorkflow });
|
|
277
|
+
process.stdout.write(`agent-rig init — process layer${withWorkflow ? ' + the opt-in workflow layer' : ''} into ${cwd}\n\n` +
|
|
138
278
|
plan.files.map((f) => ` + ${f.path}`).join('\n') +
|
|
139
279
|
'\n');
|
|
140
280
|
if (existing?.kind === 'create') {
|
|
@@ -145,7 +285,7 @@ async function runInit(rawArgs) {
|
|
|
145
285
|
plan.conflicts.map((c) => ` · ${c}`).join('\n') +
|
|
146
286
|
'\n');
|
|
147
287
|
}
|
|
148
|
-
const result = await initProject(cwd, { dryRun, force: values.force === true });
|
|
288
|
+
const result = await initProject(cwd, { dryRun, force: values.force === true, withWorkflow });
|
|
149
289
|
if (dryRun) {
|
|
150
290
|
process.stdout.write(`\nDry run — nothing written (${result.plannedCount} files planned).\n`);
|
|
151
291
|
return 0;
|
|
@@ -160,6 +300,17 @@ async function runInit(rawArgs) {
|
|
|
160
300
|
for (const wiringPath of ['.claude/settings.json', '.codex/hooks.json']) {
|
|
161
301
|
if (!result.skipped.includes(wiringPath))
|
|
162
302
|
continue;
|
|
303
|
+
const installedHash = existing?.files[wiringPath];
|
|
304
|
+
if (installedHash !== undefined) {
|
|
305
|
+
try {
|
|
306
|
+
if (sha256(await readFile(path.join(cwd, wiringPath))) === installedHash)
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// The write preflight already classified this path. If it changes
|
|
311
|
+
// before reporting, fall through to the conservative warning.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
163
314
|
const wiring = generated.get(wiringPath) ?? '';
|
|
164
315
|
process.stdout.write(`\n! ${wiringPath} already exists — it was kept, so the rig's hooks are NOT wired there.\n` +
|
|
165
316
|
` Until you merge these entries into it, nothing enforces the rules:\n\n` +
|
|
@@ -175,6 +326,7 @@ const MARK = {
|
|
|
175
326
|
deleted: '-',
|
|
176
327
|
wiring: '!',
|
|
177
328
|
unchanged: '·',
|
|
329
|
+
retired: 'x',
|
|
178
330
|
};
|
|
179
331
|
function renderUpgradePlan(repoDir, plan) {
|
|
180
332
|
const of = (verdict) => plan.actions.filter((a) => a.verdict === verdict);
|
|
@@ -184,9 +336,17 @@ function renderUpgradePlan(repoDir, plan) {
|
|
|
184
336
|
? ` no readable manifest here (deleted, never written, or unparseable) — matching files against released versions`
|
|
185
337
|
: ` installed by ${plan.fromVersion}`,
|
|
186
338
|
` upgrading to ${plan.toVersion}`,
|
|
187
|
-
'',
|
|
188
339
|
];
|
|
189
|
-
|
|
340
|
+
// RP-180 round 4, blocker A(4): when a layer's membership was inferred
|
|
341
|
+
// from disk rather than read from a manifest, say so — and how much
|
|
342
|
+
// evidence it rested on — instead of silently deciding.
|
|
343
|
+
for (const note of plan.layerInference ?? []) {
|
|
344
|
+
lines.push(note.adopted
|
|
345
|
+
? ` ${note.layer} layer inferred from ${note.present} of ${note.total} files on disk`
|
|
346
|
+
: ` ${note.present} of ${note.total} ${note.layer}-layer files found on disk — below quorum, left as your own (not adopted)`);
|
|
347
|
+
}
|
|
348
|
+
lines.push('');
|
|
349
|
+
for (const verdict of ['update', 'new', 'deleted', 'retired', 'conflict', 'wiring']) {
|
|
190
350
|
for (const action of of(verdict)) {
|
|
191
351
|
lines.push(` ${MARK[verdict]} ${action.rel}` + (action.reason ? ` — ${action.reason}` : ''));
|
|
192
352
|
// A conflict is only useful if the new version can be diffed by hand.
|
|
@@ -195,13 +355,14 @@ function renderUpgradePlan(repoDir, plan) {
|
|
|
195
355
|
}
|
|
196
356
|
}
|
|
197
357
|
}
|
|
198
|
-
// Every one of `UpgradeVerdict`'s
|
|
199
|
-
// `wiring` and `
|
|
200
|
-
// buckets, so a reader counted lines and was told a smaller
|
|
201
|
-
// (`unchanged` is counted and prints nothing — the sum is over
|
|
202
|
-
// over printed lines.) The
|
|
203
|
-
// without them renders exactly as it always has. Pinned
|
|
204
|
-
// "renders a plan with no wiring action exactly
|
|
358
|
+
// Every one of `UpgradeVerdict`'s seven members is accounted for here.
|
|
359
|
+
// `wiring`, `deleted` and `retired` each print their own line and were in
|
|
360
|
+
// none of the buckets, so a reader counted lines and was told a smaller
|
|
361
|
+
// number. (`unchanged` is counted and prints nothing — the sum is over
|
|
362
|
+
// actions, not over printed lines.) The three appear only when they
|
|
363
|
+
// occurred, so a plan without them renders exactly as it always has. Pinned
|
|
364
|
+
// by, in cli-report.test.ts, "renders a plan with no wiring action exactly
|
|
365
|
+
// as it does today".
|
|
205
366
|
// `deleted` before `wiring`, the relative order the plan prints them in.
|
|
206
367
|
// ⚠ Only their order relative to EACH OTHER matches: the plan prints
|
|
207
368
|
// `deleted` before `conflict` and the summary prints it after, so this is not
|
|
@@ -209,15 +370,85 @@ function renderUpgradePlan(repoDir, plan) {
|
|
|
209
370
|
// "lists the two occasional buckets in the order the plan prints them".
|
|
210
371
|
const occasional = [
|
|
211
372
|
['deleted', (n) => `${n} you removed (left removed)`],
|
|
373
|
+
['retired', (n) => `${n} no longer shipped (now yours)`],
|
|
212
374
|
['wiring', (n) => `${n} wiring handed over`],
|
|
213
375
|
];
|
|
214
376
|
const extra = occasional
|
|
215
377
|
.map(([verdict, phrase]) => [of(verdict).length, phrase])
|
|
216
378
|
.filter(([count]) => count > 0)
|
|
217
379
|
.map(([count, phrase]) => phrase(count));
|
|
380
|
+
// RP-186 round 3 advisory: a held-back CLAUDE.md is `conflict` in name only
|
|
381
|
+
// — it is not the user's bytes kept aside, it is THIS release's own old
|
|
382
|
+
// content re-vouched pending a fix elsewhere (see the coupling in
|
|
383
|
+
// `upgrade.ts`), and counting it under "yours (kept)" tells the reader the
|
|
384
|
+
// opposite of what happened. Round 4 advisory: read the action's own
|
|
385
|
+
// `heldBack` field, set by `upgrade.ts` at the one place that decides it —
|
|
386
|
+
// sniffing `reason` for a fixed prefix broke the moment that wording
|
|
387
|
+
// changed, which is exactly what happened between round 2 and round 3.
|
|
388
|
+
const heldBack = of('conflict').filter((a) => a.heldBack === true);
|
|
389
|
+
const keptConflicts = of('conflict').length - heldBack.length;
|
|
390
|
+
const heldBackPhrase = heldBack.length > 0 ? [`${heldBack.length} held back (see reason above)`] : [];
|
|
218
391
|
lines.push('', ` ${of('update').length} to replace, ${of('new').length} new, ` +
|
|
219
|
-
`${
|
|
220
|
-
[...extra, `${of('unchanged').length} already current`].join(', '));
|
|
392
|
+
`${keptConflicts} yours (kept), ` +
|
|
393
|
+
[...extra, ...heldBackPhrase, `${of('unchanged').length} already current`].join(', '));
|
|
394
|
+
return `${lines.join('\n')}\n`;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The short, delimited remedy for a genuinely HELD-BACK AGENTS.md — round 5
|
|
398
|
+
* (replacing round 4's verdict-only rule, which fired this same notice for
|
|
399
|
+
* every edited AGENTS.md, including a customised-but-readable one: the
|
|
400
|
+
* shipped rulebook's own designed steady state). Printed as the LAST thing a
|
|
401
|
+
* run prints (after the plan, after any wiring hand-over, after "Wrote N
|
|
402
|
+
* files.") so it is what stays on screen. Never printed at all when
|
|
403
|
+
* `status.holdBack` is `false` and there is no rescue-file housekeeping to
|
|
404
|
+
* report (`status: 'none'`) — round 5, blockers 1 and 2: an ordinary,
|
|
405
|
+
* readable, customised AGENTS.md conflict earns no mention here whatsoever,
|
|
406
|
+
* and neither does unrelated clutter (a stray directory or symlink) sitting
|
|
407
|
+
* at the rescue path on an otherwise healthy rig.
|
|
408
|
+
*
|
|
409
|
+
* `mv` is printed ONLY for `would-write` and `identical` — bytes this run
|
|
410
|
+
* either just wrote or independently verified byte-identical to its own
|
|
411
|
+
* rendering. For `differs`, the file is NOT this run's — the remedy is `rm`
|
|
412
|
+
* (or restoring AGENTS.md some other way), never `mv` (round 5, blocker 2:
|
|
413
|
+
* printing `mv` there would install a hostile or merely stale file as the
|
|
414
|
+
* live rulebook the moment a user followed the instruction literally).
|
|
415
|
+
*/
|
|
416
|
+
function renderAgentsRescueNotice(status, isDryRun) {
|
|
417
|
+
const lines = ['', `---- ${AGENTS_MD_RESCUE} ----`];
|
|
418
|
+
if (!status.holdBack) {
|
|
419
|
+
if (status.status === 'none')
|
|
420
|
+
return null;
|
|
421
|
+
// 'cleanup': a leftover regular file, byte-identical to this release's
|
|
422
|
+
// own rendering, with AGENTS.md itself already resolved — pure
|
|
423
|
+
// housekeeping, never "migration" wording.
|
|
424
|
+
lines.push(isDryRun
|
|
425
|
+
? `A real run will remove a leftover ${AGENTS_MD_RESCUE} — it matches what would be installed.`
|
|
426
|
+
: `Removed a leftover ${AGENTS_MD_RESCUE} — it matched what this run just installed.`);
|
|
427
|
+
return `${lines.join('\n')}\n`;
|
|
428
|
+
}
|
|
429
|
+
lines.push('AGENTS.md needs your attention (see its line above) — the migration is NOT finished.', '', 'CLAUDE.md is held back — kept as its old content, not the new shim — until AGENTS.md', 'resolves: CLAUDE.md still imports AGENTS.md (`@AGENTS.md`), so shimming it now would', 'leave no rulebook loaded at all.');
|
|
430
|
+
switch (status.status) {
|
|
431
|
+
case 'would-write':
|
|
432
|
+
lines.push(isDryRun
|
|
433
|
+
? `A real run (no --dry-run) will write this release's rendered AGENTS.md content to ${AGENTS_MD_RESCUE}.`
|
|
434
|
+
: `Wrote this release's rendered AGENTS.md content to ${AGENTS_MD_RESCUE}.`);
|
|
435
|
+
lines.push('', 'Review it, then:', ` mv ${AGENTS_MD_RESCUE} AGENTS.md`, ' create-agent-rig upgrade');
|
|
436
|
+
break;
|
|
437
|
+
case 'identical':
|
|
438
|
+
lines.push(`${AGENTS_MD_RESCUE} already holds this release's rendered content, unchanged.`);
|
|
439
|
+
lines.push('', 'Review it, then:', ` mv ${AGENTS_MD_RESCUE} AGENTS.md`, ' create-agent-rig upgrade');
|
|
440
|
+
break;
|
|
441
|
+
case 'differs':
|
|
442
|
+
lines.push(`${AGENTS_MD_RESCUE} already exists with content that is NOT this run's rendering —`, 'left untouched (it may be your own in-progress merge, or a leftover from another', 'release). Never `mv` this one over AGENTS.md — it is not bytes this run wrote or', 'verified.');
|
|
443
|
+
lines.push('', 'Resolve it, then re-run upgrade:', ` rm ${AGENTS_MD_RESCUE}`, ' # …or restore/repair AGENTS.md yourself, then:', ' create-agent-rig upgrade');
|
|
444
|
+
break;
|
|
445
|
+
case 'unsafe':
|
|
446
|
+
lines.push(`${AGENTS_MD_RESCUE} exists but is not a plain file (a symlink, a directory, or similar).`);
|
|
447
|
+
if (isDryRun)
|
|
448
|
+
lines.push('A real run refuses to touch it rather than write through it.');
|
|
449
|
+
lines.push('', 'Move or remove it by hand, then:', ' create-agent-rig upgrade');
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
221
452
|
return `${lines.join('\n')}\n`;
|
|
222
453
|
}
|
|
223
454
|
async function runUpgrade(rawArgs) {
|
|
@@ -259,14 +490,22 @@ async function runUpgrade(rawArgs) {
|
|
|
259
490
|
'\n');
|
|
260
491
|
}
|
|
261
492
|
}
|
|
493
|
+
// Round 5: `plan.agentsRescue` is the one, plan-time-decided status this
|
|
494
|
+
// whole closing section reads — the same value a dry run and a real run
|
|
495
|
+
// both see, so they can never disagree about what would happen.
|
|
496
|
+
const claudeAction = plan.actions.find((a) => a.rel === 'CLAUDE.md');
|
|
262
497
|
if (values['dry-run'] === true) {
|
|
263
498
|
process.stdout.write('\nDry run — nothing written.\n');
|
|
264
|
-
|
|
499
|
+
const notice = renderAgentsRescueNotice(plan.agentsRescue, true);
|
|
500
|
+
if (notice !== null)
|
|
501
|
+
process.stdout.write(notice);
|
|
502
|
+
// The same exit the real run gives: `applyUpgrade` refuses this status.
|
|
503
|
+
return plan.agentsRescue.holdBack && plan.agentsRescue.status === 'unsafe' ? 1 : 0;
|
|
265
504
|
}
|
|
266
505
|
// The plan above is the review step, so it has to be answered before
|
|
267
|
-
// anything is written. On a terminal that is a question; off one it is
|
|
268
|
-
//
|
|
269
|
-
//
|
|
506
|
+
// anything is written. On a terminal that is a question; off one it is a
|
|
507
|
+
// refusal — never guess for a run that cannot be asked, least of all when
|
|
508
|
+
// the answer rewrites its repository.
|
|
270
509
|
const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
271
510
|
if (values.yes !== true) {
|
|
272
511
|
if (!isInteractive) {
|
|
@@ -284,8 +523,18 @@ async function runUpgrade(rawArgs) {
|
|
|
284
523
|
return 0;
|
|
285
524
|
}
|
|
286
525
|
}
|
|
526
|
+
// Decided from the PLAN, before applying it: whether this run is the one
|
|
527
|
+
// that actually adopts the shim (round 5 advisory — a positive completion
|
|
528
|
+
// line, not only ever bad news at the end of a run). `applyUpgrade` writes
|
|
529
|
+
// every `update`/`new` action unconditionally, so a `claudeAction` verdict
|
|
530
|
+
// of `update` here means CLAUDE.md really will be (or already was) written
|
|
531
|
+
// as the shim by the time this function returns.
|
|
532
|
+
const adoptsShimThisRun = claudeAction?.verdict === 'update';
|
|
287
533
|
const result = await applyUpgrade(cwd, plan);
|
|
288
534
|
process.stdout.write(`\nWrote ${result.written.length} files.\n`);
|
|
535
|
+
if (adoptsShimThisRun) {
|
|
536
|
+
process.stdout.write('CLAUDE.md now imports AGENTS.md.\n');
|
|
537
|
+
}
|
|
289
538
|
// The subsystem manifest is machine-scoped and written by `setup`; an
|
|
290
539
|
// upgrade re-runs the same derivation so `installedVersion` follows the
|
|
291
540
|
// executable the root now holds. It is never part of the rig manifest or of
|
|
@@ -306,9 +555,362 @@ async function runUpgrade(rawArgs) {
|
|
|
306
555
|
throw error;
|
|
307
556
|
process.stdout.write(`Subsystem manifest: not refreshed — ${error.message} (${error.code})\n`);
|
|
308
557
|
}
|
|
558
|
+
// Printed LAST, deliberately — round 4, blocker 1's "the run must not end
|
|
559
|
+
// on a bare 'Wrote N files.' that reads as success while CLAUDE.md is held
|
|
560
|
+
// back" — so this is what a reader scrolling to the bottom of the run
|
|
561
|
+
// actually sees, whether the news is "not finished, here is exactly what
|
|
562
|
+
// to run next" or a small, ordinary cleanup note. `null` (round 5,
|
|
563
|
+
// blockers 1/2) means genuinely nothing to add: an ordinary, readable,
|
|
564
|
+
// customised AGENTS.md conflict, or unrelated clutter at the rescue path,
|
|
565
|
+
// both print nothing here at all.
|
|
566
|
+
// A `cleanup` the apply step did not carry out (the leftover changed or went
|
|
567
|
+
// away after the plan) reports nothing rather than a removal.
|
|
568
|
+
const rescue = plan.agentsRescue.status === 'cleanup' && !result.removedRescue
|
|
569
|
+
? { holdBack: false, status: 'none' }
|
|
570
|
+
: plan.agentsRescue;
|
|
571
|
+
const notice = renderAgentsRescueNotice(rescue, false);
|
|
572
|
+
if (notice !== null)
|
|
573
|
+
process.stdout.write(notice);
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
const UNINSTALL_MARK = {
|
|
577
|
+
remove: '-',
|
|
578
|
+
absent: '·',
|
|
579
|
+
preserved: '!',
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* `removed` is always what actually happened — `applyUninstall`'s own result
|
|
583
|
+
* — never a re-derivation of the plan: empty on a dry run, a consent refusal,
|
|
584
|
+
* or a plan that itself failed, and on a partial failure the SUBSET that
|
|
585
|
+
* finished, not every `remove`-verdict path the plan named. `planned` is the
|
|
586
|
+
* plan's own answer regardless of outcome, so a caller can tell "what would
|
|
587
|
+
* this have done" from "what did it do" even when they differ.
|
|
588
|
+
*
|
|
589
|
+
* `changedSincePlanning` and `protectedHooksAtApply` paths are both folded
|
|
590
|
+
* into `preserved` — the first with reason
|
|
591
|
+
* {@link CHANGED_SINCE_PLANNING_REASON}, the second worded by
|
|
592
|
+
* {@link protectedFileReason} — the SAME function `planUninstall` itself
|
|
593
|
+
* calls to word a hook it protects at PLAN time, so a reader cannot tell
|
|
594
|
+
* which pass discovered the protection from the wording alone, and picking
|
|
595
|
+
* the right one of the four underlying strings never happens twice: a
|
|
596
|
+
* directly-named hook's wording depends on `entry.wiringKind` (a `kept`
|
|
597
|
+
* wiring file's hook was never "preserved as edited", and reusing that
|
|
598
|
+
* wording told two contradictory stories about the same file), a
|
|
599
|
+
* transitively-imported one on `entry.importedBy`, and one protected only by
|
|
600
|
+
* the conservative superset sweep — never traced at all — on
|
|
601
|
+
* `entry.unverifiedBecause`, which names the file whose own unreadability
|
|
602
|
+
* triggered the sweep rather than claiming a connection this command never
|
|
603
|
+
* confirmed. Neither `changedSincePlanning` nor
|
|
604
|
+
* `protectedHooksAtApply` is in `plan.actions` (both were `remove` at plan
|
|
605
|
+
* time and only discovered otherwise at apply time), but both are exactly as
|
|
606
|
+
* un-removed as any other preserved path, and a caller reading `preserved`
|
|
607
|
+
* for "what did this run leave behind" must see them there too, not in a
|
|
608
|
+
* third and fourth, easy-to-miss list.
|
|
609
|
+
*
|
|
610
|
+
* `outcome` is passed straight through from `applyUninstall`'s own result:
|
|
611
|
+
* present on every completed, non-dry-run call, absent on `--dry-run` and on
|
|
612
|
+
* a hard failure alike — this function never invents or infers it.
|
|
613
|
+
*/
|
|
614
|
+
function uninstallPayload(dryRun, actions, removed, applied) {
|
|
615
|
+
const of = (verdict) => actions.filter((a) => a.verdict === verdict).map((a) => a.rel);
|
|
616
|
+
const payload = {
|
|
617
|
+
schemaVersion: 1,
|
|
618
|
+
command: 'uninstall',
|
|
619
|
+
dryRun,
|
|
620
|
+
planned: of('remove'),
|
|
621
|
+
removed: [...removed],
|
|
622
|
+
absent: of('absent'),
|
|
623
|
+
preserved: [
|
|
624
|
+
...actions
|
|
625
|
+
.filter((a) => a.verdict === 'preserved')
|
|
626
|
+
.map((a) => ({ path: a.rel, reason: a.reason ?? '' })),
|
|
627
|
+
...(applied?.changedSincePlanning ?? []).map((path) => ({
|
|
628
|
+
path,
|
|
629
|
+
reason: CHANGED_SINCE_PLANNING_REASON,
|
|
630
|
+
})),
|
|
631
|
+
...(applied?.protectedHooksAtApply ?? []).map(({ rel, wiringRel, wiringKind, importedBy, unverifiedBecause }) => ({
|
|
632
|
+
path: rel,
|
|
633
|
+
reason: protectedFileReason(wiringRel, wiringKind, importedBy, unverifiedBecause),
|
|
634
|
+
})),
|
|
635
|
+
],
|
|
636
|
+
notes: actions.filter((a) => a.note !== undefined).map((a) => ({ path: a.rel, note: a.note })),
|
|
637
|
+
manifestRemoved: applied?.manifestRemoved ?? false,
|
|
638
|
+
};
|
|
639
|
+
if (applied?.completed !== undefined)
|
|
640
|
+
payload.completed = applied.completed;
|
|
641
|
+
if (applied?.remaining !== undefined)
|
|
642
|
+
payload.remaining = applied.remaining;
|
|
643
|
+
if (applied?.error !== undefined)
|
|
644
|
+
payload.error = applied.error;
|
|
645
|
+
if (applied?.outcome !== undefined)
|
|
646
|
+
payload.outcome = applied.outcome;
|
|
647
|
+
return payload;
|
|
648
|
+
}
|
|
649
|
+
function renderUninstallPlan(repoDir, plan) {
|
|
650
|
+
const of = (verdict) => plan.actions.filter((a) => a.verdict === verdict);
|
|
651
|
+
const lines = [`agent-rig uninstall — ${repoDir}`, ''];
|
|
652
|
+
for (const verdict of ['remove', 'preserved', 'absent']) {
|
|
653
|
+
for (const action of of(verdict)) {
|
|
654
|
+
// `reason` (only ever on `preserved`) and `note` (round 4, blocker 2 —
|
|
655
|
+
// only ever on `remove`, for the CLAUDE.md/AGENTS.md pair disclosure)
|
|
656
|
+
// never both apply to the same action, so one disclosure slot renders
|
|
657
|
+
// either.
|
|
658
|
+
const disclosure = action.reason ?? action.note;
|
|
659
|
+
lines.push(` ${UNINSTALL_MARK[verdict]} ${action.rel}` + (disclosure ? ` — ${disclosure}` : ''));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
lines.push('', ` ${of('remove').length} to remove, ${of('preserved').length} preserved, ` +
|
|
663
|
+
`${of('absent').length} already gone`);
|
|
664
|
+
return `${lines.join('\n')}\n`;
|
|
665
|
+
}
|
|
666
|
+
async function runUninstall(rawArgs) {
|
|
667
|
+
let positionals;
|
|
668
|
+
let values;
|
|
669
|
+
try {
|
|
670
|
+
({ positionals, values } = parseArgs({
|
|
671
|
+
args: rawArgs,
|
|
672
|
+
options: {
|
|
673
|
+
'dry-run': { type: 'boolean' },
|
|
674
|
+
json: { type: 'boolean' },
|
|
675
|
+
yes: { type: 'boolean' },
|
|
676
|
+
detach: { type: 'boolean' },
|
|
677
|
+
// `--no-color` for the same reason it is accepted on `init` and
|
|
678
|
+
// `upgrade`: USAGE offers it without scoping it to one command. It
|
|
679
|
+
// has no observable effect here specifically — uninstall's report
|
|
680
|
+
// never uses the colour palette in the first place — accepted only
|
|
681
|
+
// so the flag never produces an "unknown option" error a reader of
|
|
682
|
+
// USAGE would not expect.
|
|
683
|
+
'no-color': { type: 'boolean' },
|
|
684
|
+
},
|
|
685
|
+
allowPositionals: true,
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
process.stderr.write(`${error.message}\n\n${USAGE}\n`);
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
if (positionals.length > 1) {
|
|
693
|
+
process.stderr.write(`${USAGE}\n`);
|
|
694
|
+
return 1;
|
|
695
|
+
}
|
|
696
|
+
const repoDir = path.resolve(process.cwd(), positionals[0] ?? '.');
|
|
697
|
+
const dryRun = values['dry-run'] === true;
|
|
698
|
+
const json = values.json === true;
|
|
699
|
+
const yes = values.yes === true;
|
|
700
|
+
const detach = values.detach === true;
|
|
701
|
+
let plan;
|
|
702
|
+
try {
|
|
703
|
+
plan = await planUninstall(repoDir);
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
// `UninstallError` is a message this command composed on purpose — the
|
|
707
|
+
// usual case. Anything else (EACCES, ENOTDIR, a permission the caller did
|
|
708
|
+
// not expect) is unplanned, but `--json` promises one JSON object and
|
|
709
|
+
// nothing else on stdout regardless of which kind it is: a stack trace on
|
|
710
|
+
// stderr with no payload at all breaks that promise for a caller who only
|
|
711
|
+
// ever reads stdout. Off `--json`, the trace is still the right
|
|
712
|
+
// diagnostic, so it is rethrown to `main()`'s own handler unchanged.
|
|
713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
714
|
+
if (json) {
|
|
715
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], { manifestRemoved: false, error: message }))}\n`);
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
if (error instanceof UninstallError) {
|
|
719
|
+
process.stderr.write(`${message}\n`);
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
if (plan.noManifest) {
|
|
725
|
+
if (json) {
|
|
726
|
+
// `outcome` names an END STATE a real run reached; `--dry-run` never
|
|
727
|
+
// reaches one, even here — "nothing installed" is an end state only
|
|
728
|
+
// once a real (non-dry) run has acted, or declined to act, on it.
|
|
729
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(dryRun, [], [], dryRun
|
|
730
|
+
? { manifestRemoved: false }
|
|
731
|
+
: { manifestRemoved: false, outcome: 'uninstalled' }))}\n`);
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
process.stdout.write(`No rig manifest found in ${repoDir} — nothing to uninstall.\n`);
|
|
735
|
+
}
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
// The plan is the review step, so it is shown before anything is decided —
|
|
739
|
+
// in both output modes, and before the consent question below, not after.
|
|
740
|
+
if (!json)
|
|
741
|
+
process.stdout.write(renderUninstallPlan(repoDir, plan));
|
|
742
|
+
if (dryRun) {
|
|
743
|
+
if (json) {
|
|
744
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(true, plan.actions, []))}\n`);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
process.stdout.write('\nDry run — nothing removed.\n');
|
|
748
|
+
}
|
|
749
|
+
return 0;
|
|
750
|
+
}
|
|
751
|
+
// Consent, never guessed, least of all when the answer deletes files — the
|
|
752
|
+
// same shape `upgrade` asks before it writes: `--yes` up front, a prompt on
|
|
753
|
+
// a terminal, and an outright refusal off one. `--json` stays
|
|
754
|
+
// non-interactive on principle, the same reason `--version --json` and
|
|
755
|
+
// every other JSON payload here never prompts: it is read by a script, and
|
|
756
|
+
// a script blocking on a TTY question is a hang, not a safeguard. So
|
|
757
|
+
// without `--yes` it gets the same refusal a non-interactive run gets,
|
|
758
|
+
// reported in its own shape instead of a stderr sentence.
|
|
759
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
760
|
+
if (!yes) {
|
|
761
|
+
if (json || !isInteractive) {
|
|
762
|
+
const message = 'Refusing to remove files without --yes in a non-interactive run.';
|
|
763
|
+
if (json) {
|
|
764
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
process.stderr.write(`${message} Re-run with --yes once the plan above is what you want ` +
|
|
768
|
+
'(or --dry-run to keep looking).\n');
|
|
769
|
+
}
|
|
770
|
+
return 1;
|
|
771
|
+
}
|
|
772
|
+
const confirmed = await promptConfirm('\nRemove these files?', {
|
|
773
|
+
input: process.stdin,
|
|
774
|
+
output: process.stderr,
|
|
775
|
+
isInteractive,
|
|
776
|
+
});
|
|
777
|
+
if (!confirmed) {
|
|
778
|
+
process.stdout.write('Nothing removed.\n');
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
let result;
|
|
783
|
+
try {
|
|
784
|
+
result = await applyUninstall(repoDir, plan, { detach });
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
// Mirrors the `planUninstall` try/catch above, for the same reason: the
|
|
788
|
+
// apply-time hook-protection re-check added in the same change as this
|
|
789
|
+
// comment reads the filesystem again (`regularFileStatus`, `readFile`)
|
|
790
|
+
// OUTSIDE of `applyUninstall`'s own per-file try/catch, so an EACCES or
|
|
791
|
+
// ENOTDIR surfacing from THAT read must not escape as a bare stack trace
|
|
792
|
+
// with no JSON on stdout — `--json` promises exactly one object there
|
|
793
|
+
// regardless of which kind of failure this is.
|
|
794
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
795
|
+
if (json) {
|
|
796
|
+
// `removed: []` here is not a guess: EVERY call inside `applyUninstall`
|
|
797
|
+
// that can throw uncaught (`rigOwnedPaths`, the apply-time
|
|
798
|
+
// `protectedHooksFor` pass) runs strictly before its own removal loop
|
|
799
|
+
// starts — that loop wraps every per-file removal in its OWN
|
|
800
|
+
// try/catch and always RETURNS a result (with the real `removed` so
|
|
801
|
+
// far) rather than throwing. So an exception reaching this `catch` can
|
|
802
|
+
// only mean nothing was removed yet. This is a structural property of
|
|
803
|
+
// `applyUninstall`'s own control flow (see the comment immediately
|
|
804
|
+
// above its removal loop), not backed by a test of the hypothetical
|
|
805
|
+
// case, which does not exist today — if a future edit adds a
|
|
806
|
+
// throwing call INSIDE or AFTER that loop, this array would start
|
|
807
|
+
// lying, silently, exactly here.
|
|
808
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, [], { manifestRemoved: false, error: message }))}\n`);
|
|
809
|
+
return 1;
|
|
810
|
+
}
|
|
811
|
+
if (error instanceof UninstallError) {
|
|
812
|
+
process.stderr.write(`${message}\n`);
|
|
813
|
+
return 1;
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
if (result.error !== undefined) {
|
|
818
|
+
if (json) {
|
|
819
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
|
|
820
|
+
manifestRemoved: false,
|
|
821
|
+
completed: result.completed,
|
|
822
|
+
remaining: result.remaining,
|
|
823
|
+
error: result.error,
|
|
824
|
+
changedSincePlanning: result.changedSincePlanning,
|
|
825
|
+
protectedHooksAtApply: result.protectedHooksAtApply,
|
|
826
|
+
}))}\n`);
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
process.stderr.write(`\nStopped after a failure: ${result.error}\n` +
|
|
830
|
+
` completed: ${(result.completed ?? []).join(', ') || '(none)'}\n` +
|
|
831
|
+
` remaining: ${(result.remaining ?? []).join(', ') || '(none)'}\n` +
|
|
832
|
+
`The manifest was kept — re-run to continue.\n`);
|
|
833
|
+
}
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
836
|
+
if (json) {
|
|
837
|
+
process.stdout.write(`${JSON.stringify(uninstallPayload(false, plan.actions, result.removed, {
|
|
838
|
+
manifestRemoved: result.manifestRemoved,
|
|
839
|
+
changedSincePlanning: result.changedSincePlanning,
|
|
840
|
+
protectedHooksAtApply: result.protectedHooksAtApply,
|
|
841
|
+
outcome: result.outcome,
|
|
842
|
+
}))}\n`);
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
// The three outcomes `--json` names structurally are said in prose here
|
|
846
|
+
// too, not only encoded in a field: `preserved` below is the IDENTICAL
|
|
847
|
+
// list `uninstallPayload` already built for `--json` above (not a second,
|
|
848
|
+
// separately-maintained computation of the same thing) — the plan's own
|
|
849
|
+
// `preserved` verdicts, any path caught changed only at apply time, and
|
|
850
|
+
// any hook a wiring file's OWN apply-time edit or symlink just protected,
|
|
851
|
+
// each with the reason that pass recorded. All three are equally "left
|
|
852
|
+
// behind", and a report naming only one kind — or naming a count instead
|
|
853
|
+
// of the paths themselves — would read as if the others never happened, or
|
|
854
|
+
// leave an operator with nothing to grep for once the count passes a
|
|
855
|
+
// handful.
|
|
856
|
+
const { preserved } = uninstallPayload(false, plan.actions, result.removed, {
|
|
857
|
+
manifestRemoved: result.manifestRemoved,
|
|
858
|
+
changedSincePlanning: result.changedSincePlanning,
|
|
859
|
+
protectedHooksAtApply: result.protectedHooksAtApply,
|
|
860
|
+
outcome: result.outcome,
|
|
861
|
+
});
|
|
862
|
+
const preservedList = () => preserved.map(({ path, reason }) => ` ! ${path}${reason ? ` — ${reason}` : ''}`).join('\n');
|
|
863
|
+
// A roll-up, on top of the per-line reasons, distinguishing what the
|
|
864
|
+
// command actually traced from what it kept only as a precaution — at
|
|
865
|
+
// the scale a symlinked, single-seeded hook dependency can now produce
|
|
866
|
+
// (dozens of paths swept in by caution alone), one line naming the split
|
|
867
|
+
// does more for an operator than reading every reason individually
|
|
868
|
+
// (UX-lens review, RP-181, carried since cycle 5 as the roll-up advisory).
|
|
869
|
+
const unverifiedCount = preserved.filter((p) => isUnverifiedReason(p.reason)).length;
|
|
870
|
+
const rollup = unverifiedCount > 0
|
|
871
|
+
? ` (${preserved.length - unverifiedCount} genuinely referenced or imported; ` +
|
|
872
|
+
`${unverifiedCount} kept only as a precaution — something needed to verify them ` +
|
|
873
|
+
`could not be read)\n`
|
|
874
|
+
: '';
|
|
875
|
+
if (result.outcome === 'detached') {
|
|
876
|
+
process.stdout.write(`\nDetached: removed ${result.removed.length} files and the manifest.\n` +
|
|
877
|
+
(preserved.length > 0
|
|
878
|
+
? `${preserved.length} file(s) left behind — they are yours now, uninstall no longer owns them:\n` +
|
|
879
|
+
rollup +
|
|
880
|
+
`${preservedList()}\n`
|
|
881
|
+
: ''));
|
|
882
|
+
}
|
|
883
|
+
else if (result.manifestRemoved) {
|
|
884
|
+
process.stdout.write(`\nRemoved ${result.removed.length} files and the manifest.\n`);
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
// Every removal that was planned succeeded, but something else was
|
|
888
|
+
// preserved (in the plan, or discovered changed at apply time) — the rig
|
|
889
|
+
// still owns bytes it did not remove, so the manifest naming them was
|
|
890
|
+
// kept on purpose, not left behind by a failure. Named, not only
|
|
891
|
+
// counted: a run with the now-larger preserved count a transitive-import
|
|
892
|
+
// walk can produce still needs to be actionable from this one line of
|
|
893
|
+
// output, without re-running `--json` just to learn what survived.
|
|
894
|
+
process.stdout.write(`\nRemoved ${result.removed.length} files. ${preserved.length} preserved — the manifest ` +
|
|
895
|
+
`was kept: the rig is still installed.\n${rollup}${preservedList()}\n`);
|
|
896
|
+
}
|
|
897
|
+
// A removal is a working-tree change, not a commit — uninstall never
|
|
898
|
+
// touches git history itself (docs/command-contract.md, "## uninstall
|
|
899
|
+
// (RP-181)"), so nothing here is recorded until a run stages and commits
|
|
900
|
+
// it. Said only when something was actually deleted; a preserved-only or
|
|
901
|
+
// no-op run leaves nothing to stage.
|
|
902
|
+
if (result.removed.length > 0) {
|
|
903
|
+
process.stdout.write('Run `git add -A` and commit to record the removal.\n');
|
|
904
|
+
}
|
|
309
905
|
return 0;
|
|
310
906
|
}
|
|
311
907
|
async function main() {
|
|
908
|
+
if (process.argv[2] === 'doctor') {
|
|
909
|
+
const result = await runDoctor({ cwd: process.cwd(), args: process.argv.slice(3) });
|
|
910
|
+
process.stdout.write(result.stdout);
|
|
911
|
+
process.stderr.write(result.stderr);
|
|
912
|
+
return result.exitCode;
|
|
913
|
+
}
|
|
312
914
|
if (process.argv[2] === 'init') {
|
|
313
915
|
return runInit(process.argv.slice(3));
|
|
314
916
|
}
|
|
@@ -318,6 +920,9 @@ async function main() {
|
|
|
318
920
|
if (process.argv[2] === 'upgrade') {
|
|
319
921
|
return runUpgrade(process.argv.slice(3));
|
|
320
922
|
}
|
|
923
|
+
if (process.argv[2] === 'uninstall') {
|
|
924
|
+
return runUninstall(process.argv.slice(3));
|
|
925
|
+
}
|
|
321
926
|
if (process.argv[2] === 'memory') {
|
|
322
927
|
// The consumer path of the RP-19 handshake: manifest → `--version --json`
|
|
323
928
|
// → exit 4 on a foreign major → doctor/load passed through (`load` gains a
|
|
@@ -335,13 +940,13 @@ async function main() {
|
|
|
335
940
|
args: process.argv.slice(2),
|
|
336
941
|
options: {
|
|
337
942
|
help: { type: 'boolean', short: 'h' },
|
|
338
|
-
target: { type: 'string' },
|
|
339
943
|
version: { type: 'boolean' },
|
|
340
944
|
// `--json` is read on `--version` alone: the handshake object of
|
|
341
945
|
// docs/command-contract.md, one JSON line and nothing else on stdout.
|
|
342
946
|
json: { type: 'boolean' },
|
|
343
947
|
'no-git': { type: 'boolean' },
|
|
344
948
|
'no-color': { type: 'boolean' },
|
|
949
|
+
layer: { type: 'string', multiple: true },
|
|
345
950
|
},
|
|
346
951
|
allowPositionals: true,
|
|
347
952
|
}));
|
|
@@ -367,32 +972,37 @@ async function main() {
|
|
|
367
972
|
process.stderr.write(`${USAGE}\n`);
|
|
368
973
|
return 1;
|
|
369
974
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (!target) {
|
|
375
|
-
if (!isInteractive) {
|
|
376
|
-
process.stderr.write(`Missing --target in a non-interactive run. ` +
|
|
377
|
-
`Pass --target <${TARGET_NAMES.join('|')}>.\n`);
|
|
378
|
-
return 1;
|
|
379
|
-
}
|
|
380
|
-
target = await promptTarget(TARGET_NAMES, DEFAULT_TARGET, {
|
|
381
|
-
input: process.stdin,
|
|
382
|
-
output: process.stderr,
|
|
383
|
-
isInteractive,
|
|
384
|
-
});
|
|
975
|
+
const layerResult = resolveLayerFlag(values.layer);
|
|
976
|
+
if ('error' in layerResult) {
|
|
977
|
+
process.stderr.write(`${layerResult.error}\n\n${USAGE}\n`);
|
|
978
|
+
return 1;
|
|
385
979
|
}
|
|
386
980
|
const { projectDir, projectName } = await createProject(dirArg, {
|
|
387
981
|
cwd: process.cwd(),
|
|
388
|
-
target,
|
|
389
982
|
git: values['no-git'] !== true,
|
|
983
|
+
withWorkflow: layerResult.withWorkflow,
|
|
390
984
|
});
|
|
391
985
|
const palette = makePalette(Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && values['no-color'] !== true);
|
|
392
986
|
const summary = await collectGovernance(projectDir);
|
|
393
|
-
process.stdout.write('\n' + renderSummary(projectName,
|
|
987
|
+
process.stdout.write('\n' + renderSummary(projectName, dirArg, summary, palette));
|
|
394
988
|
return 0;
|
|
395
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* A reader closing stdout/stderr must not produce an EPIPE stack trace or
|
|
992
|
+
* replace the command's exit code. Other stream errors set failure only when
|
|
993
|
+
* no failing verdict has already been assigned. Built-CLI pipe regressions
|
|
994
|
+
* live in packages/cli/test/integrations-cli.test.ts.
|
|
995
|
+
*/
|
|
996
|
+
function quietlyExitOnEpipe(stream) {
|
|
997
|
+
stream.on('error', (error) => {
|
|
998
|
+
if (error.code === 'EPIPE')
|
|
999
|
+
return;
|
|
1000
|
+
if (process.exitCode === undefined || process.exitCode === 0)
|
|
1001
|
+
process.exitCode = 1;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
quietlyExitOnEpipe(process.stdout);
|
|
1005
|
+
quietlyExitOnEpipe(process.stderr);
|
|
396
1006
|
main()
|
|
397
1007
|
.then((code) => {
|
|
398
1008
|
process.exitCode = code;
|
|
@@ -400,7 +1010,8 @@ main()
|
|
|
400
1010
|
.catch((error) => {
|
|
401
1011
|
if (error instanceof CreateError ||
|
|
402
1012
|
error instanceof InitError ||
|
|
403
|
-
error instanceof UpgradeError
|
|
1013
|
+
error instanceof UpgradeError ||
|
|
1014
|
+
error instanceof UninstallError) {
|
|
404
1015
|
process.stderr.write(`${error.message}\n`);
|
|
405
1016
|
}
|
|
406
1017
|
else {
|