create-agent-rig 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/package.json +54 -0
  4. package/packages/cli/dist/commands/create.js +109 -0
  5. package/packages/cli/dist/index.js +102 -0
  6. package/packages/cli/dist/lib/colors.js +14 -0
  7. package/packages/cli/dist/lib/composition.js +20 -0
  8. package/packages/cli/dist/lib/copy-tree.js +91 -0
  9. package/packages/cli/dist/lib/prompts.js +24 -0
  10. package/packages/cli/dist/lib/substitute.js +22 -0
  11. package/packages/cli/dist/lib/summary.js +41 -0
  12. package/packages/cli/dist/lib/targets.js +14 -0
  13. package/packages/cli/dist/templates.js +21 -0
  14. package/scripts/prepare.mjs +29 -0
  15. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +49 -0
  16. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +59 -0
  17. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +51 -0
  18. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +39 -0
  19. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +36 -0
  20. package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
  21. package/templates/agent-os/universal/.claude/agents/test-writer.md +36 -0
  22. package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +43 -0
  23. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +79 -0
  24. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +53 -0
  25. package/templates/agent-os/universal/.claude/rules/architecture.md +74 -0
  26. package/templates/agent-os/universal/.claude/rules/autonomy.md +81 -0
  27. package/templates/agent-os/universal/.claude/rules/workflow.md +62 -0
  28. package/templates/agent-os/universal/.claude/settings.json +28 -0
  29. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +53 -0
  30. package/templates/agent-os/universal/CLAUDE.md +62 -0
  31. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +23 -0
  32. package/templates/skeleton/aws-serverless/README.md +78 -0
  33. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +17 -0
  34. package/templates/skeleton/aws-serverless/apps/web/package.json +19 -0
  35. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +17 -0
  36. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +96 -0
  37. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +29 -0
  38. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +23 -0
  39. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +38 -0
  40. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +14 -0
  41. package/templates/skeleton/aws-serverless/eslint.config.mjs +20 -0
  42. package/templates/skeleton/aws-serverless/gitignore +9 -0
  43. package/templates/skeleton/aws-serverless/infra/bin/app.ts +19 -0
  44. package/templates/skeleton/aws-serverless/infra/cdk.json +3 -0
  45. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +116 -0
  46. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +32 -0
  47. package/templates/skeleton/aws-serverless/infra/package.json +18 -0
  48. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +104 -0
  49. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +41 -0
  50. package/templates/skeleton/aws-serverless/package.json +30 -0
  51. package/templates/skeleton/aws-serverless/packages/core/package.json +11 -0
  52. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +14 -0
  53. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +15 -0
  54. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +69 -0
  55. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +23 -0
  56. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +101 -0
  57. package/templates/skeleton/aws-serverless/packages/db/package.json +14 -0
  58. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +17 -0
  59. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +2 -0
  60. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +52 -0
  61. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +91 -0
  62. package/templates/skeleton/aws-serverless/packages/shared/package.json +11 -0
  63. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +17 -0
  64. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +33 -0
  65. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +3 -0
  66. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +20 -0
  67. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +26 -0
  68. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +28 -0
  69. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +19 -0
  70. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +2855 -0
  71. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +14 -0
  72. package/templates/skeleton/aws-serverless/services/api/package.json +15 -0
  73. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +26 -0
  74. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +42 -0
  75. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +24 -0
  76. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +12 -0
  77. package/templates/skeleton/aws-serverless/services/api/src/main.ts +21 -0
  78. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +30 -0
  79. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +14 -0
  80. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +92 -0
  81. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +45 -0
  82. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +51 -0
  83. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +22 -0
  84. package/templates/skeleton/aws-serverless/services/worker/package.json +12 -0
  85. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +15 -0
  86. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +7 -0
  87. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +37 -0
  88. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +61 -0
  89. package/templates/skeleton/aws-serverless/tsconfig.base.json +15 -0
  90. package/templates/skeleton/aws-serverless/tsconfig.json +16 -0
  91. package/templates/skeleton/aws-serverless/vitest.config.ts +14 -0
  92. package/templates/skeleton/node-service/.github/workflows/ci.yml +22 -0
  93. package/templates/skeleton/node-service/README.md +74 -0
  94. package/templates/skeleton/node-service/apps/web/next.config.mjs +17 -0
  95. package/templates/skeleton/node-service/apps/web/package.json +19 -0
  96. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +17 -0
  97. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +96 -0
  98. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +29 -0
  99. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +23 -0
  100. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +38 -0
  101. package/templates/skeleton/node-service/apps/web/tsconfig.json +14 -0
  102. package/templates/skeleton/node-service/eslint.config.mjs +20 -0
  103. package/templates/skeleton/node-service/gitignore +9 -0
  104. package/templates/skeleton/node-service/package.json +28 -0
  105. package/templates/skeleton/node-service/packages/core/package.json +11 -0
  106. package/templates/skeleton/node-service/packages/core/src/events.ts +14 -0
  107. package/templates/skeleton/node-service/packages/core/src/index.ts +15 -0
  108. package/templates/skeleton/node-service/packages/core/src/note.ts +69 -0
  109. package/templates/skeleton/node-service/packages/core/test/events.test.ts +23 -0
  110. package/templates/skeleton/node-service/packages/core/test/note.test.ts +101 -0
  111. package/templates/skeleton/node-service/packages/db/package.json +12 -0
  112. package/templates/skeleton/node-service/packages/db/src/index.ts +1 -0
  113. package/templates/skeleton/node-service/packages/db/src/note-store.ts +63 -0
  114. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +80 -0
  115. package/templates/skeleton/node-service/packages/shared/package.json +11 -0
  116. package/templates/skeleton/node-service/packages/shared/src/env.ts +17 -0
  117. package/templates/skeleton/node-service/packages/shared/src/errors.ts +33 -0
  118. package/templates/skeleton/node-service/packages/shared/src/index.ts +3 -0
  119. package/templates/skeleton/node-service/packages/shared/src/logger.ts +20 -0
  120. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +26 -0
  121. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +28 -0
  122. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +19 -0
  123. package/templates/skeleton/node-service/pnpm-lock.yaml +2399 -0
  124. package/templates/skeleton/node-service/pnpm-workspace.yaml +13 -0
  125. package/templates/skeleton/node-service/services/api/package.json +17 -0
  126. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +23 -0
  127. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +40 -0
  128. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +23 -0
  129. package/templates/skeleton/node-service/services/api/src/main.ts +47 -0
  130. package/templates/skeleton/node-service/services/api/src/server.ts +89 -0
  131. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +30 -0
  132. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +14 -0
  133. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +64 -0
  134. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +43 -0
  135. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +48 -0
  136. package/templates/skeleton/node-service/services/api/test/server.test.ts +123 -0
  137. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +32 -0
  138. package/templates/skeleton/node-service/services/worker/package.json +16 -0
  139. package/templates/skeleton/node-service/services/worker/src/main.ts +28 -0
  140. package/templates/skeleton/node-service/services/worker/src/spool.ts +60 -0
  141. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +38 -0
  142. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +34 -0
  143. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +76 -0
  144. package/templates/skeleton/node-service/tsconfig.base.json +15 -0
  145. package/templates/skeleton/node-service/tsconfig.json +13 -0
  146. package/templates/skeleton/node-service/vitest.config.ts +12 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Serhii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # create-agent-rig
2
+
3
+ Scaffold a project that ships with an **agent operating system** — rules,
4
+ gates, and hooks that hold the architecture mechanically, not by prose.
5
+
6
+ ```sh
7
+ npx create-agent-rig my-app
8
+ ```
9
+
10
+ ## What you get
11
+
12
+ **A system of boundaries, each held by tooling.** An agent (or a human using
13
+ one) cannot talk its way past them:
14
+
15
+ - **`guard-core-purity`** — refuses any edit that puts I/O, clock, randomness,
16
+ or environment access into the pure domain core;
17
+ - **`guard-web-boundary`** — refuses `db`/service imports from the frontend;
18
+ the web talks to the backend over HTTP only;
19
+ - **`block-no-verify`** — refuses bypassing pre-commit checks (and knows the
20
+ difference between using the flag and merely mentioning it in a message).
21
+
22
+ Around the hooks, the operating system: **autonomy tiers** (what an agent does
23
+ alone / after review / never), **stop rules** (three strikes, flaky ≠ retry,
24
+ session staleness), **subagent gates** (`test-writer`, `code-reviewer`,
25
+ `security-scanner`, `cdk-diff-reviewer`), **skills** (`pr-ship` pre-merge
26
+ gate; `post-deploy-verify` with its binary HEALTHY/REGRESSION verdict), and a
27
+ one-page `CLAUDE.md` map a fresh session orients by.
28
+
29
+ The skeleton around it is real and runnable — pure core shared by server _and_
30
+ browser (one schema validates on both sides of the wire), a mandatory usecase
31
+ layer, a queue with DLQ discipline, tests at every layer.
32
+
33
+ ## Targets
34
+
35
+ | Target | One line |
36
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `aws-serverless` | DynamoDB single-table, SQS + DLQ + alarm, three Lambdas behind an HTTP API, static web via S3 + CloudFront, CDK with least-privilege IAM |
38
+ | `node-service` | `node:http` server that also serves the web bundle, JSON-file store behind the same model boundary, spool-directory queue, worker process |
39
+
40
+ Coherent alternatives, not a parameterized abstraction. Flexibility is
41
+ **subtraction**: the generated project is yours — delete what you don't need.
42
+
43
+ ## What it deliberately does not do
44
+
45
+ No authentication. No design system or UI kit. No state manager. No i18n,
46
+ analytics, or error tracking. No third cloud. No component-testing apparatus.
47
+
48
+ Each of these is application surface, not an architecture proof — and every
49
+ addition is permanent maintenance in every target. The frontend is plain on
50
+ purpose: scaffolding gets replaced without friction; a finished-looking UI
51
+ gets fought. If you need one of these, add it — the project is yours.
52
+
53
+ ## The 2-minute demo
54
+
55
+ ```sh
56
+ ./demo.sh # from a clone of this repo
57
+ ```
58
+
59
+ generate → the generated project's own gates pass → **an attempted core-purity
60
+ violation is refused live by the hook** → the service runs, a smoke request
61
+ travels every layer, the worker drains the queue, the DLQ stays empty:
62
+
63
+ ```
64
+ == 3/4 an agent tries to put I/O and clock access into the pure core… ==
65
+ BLOCKED — packages/core is a pure module and this change breaks its purity:
66
+ - imports "node:fs/promises" — the core may import only its own modules and: zod
67
+ - reads the clock — take a timestamp as an argument
68
+ Move the impure part behind the usecase layer or into an adapter.
69
+ …and the guard-core-purity hook REFUSED the edit at the tool layer (exit 2). ✔
70
+ ```
71
+
72
+ ## Requirements
73
+
74
+ - Node ≥ 20 (pnpm recommended for the generated workspace)
75
+
76
+ ## How it stays honest
77
+
78
+ Every template is a real project tested in place on every push; every e2e run
79
+ generates a project cold and runs the generated project's own full checks
80
+ (install → lint → typecheck → test → build → synth); a grep-test keeps the
81
+ universal rules free of any provider mention; the hook-blocking behavior
82
+ itself is under test; and a weekly lockfile-free run catches upstream breakage
83
+ early. This repo dogfoods its own rulebook — `CLAUDE.md` and `.claude/` are
84
+ composed from the templates, and drift fails the suite.
85
+
86
+ Development: `pnpm test` (full), `pnpm test:unit` (fast loop),
87
+ `pnpm template:check` (templates in place). The plan of record is `PLAN.md`.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "create-agent-rig",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new project with an agent operating system (rules, gates, hooks) and a runnable code skeleton",
5
+ "keywords": [
6
+ "create",
7
+ "scaffold",
8
+ "generator",
9
+ "starter",
10
+ "template",
11
+ "agent",
12
+ "claude-code",
13
+ "agent-os",
14
+ "aws-serverless",
15
+ "node-service"
16
+ ],
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "bin": {
20
+ "create-agent-rig": "packages/cli/dist/index.js"
21
+ },
22
+ "files": [
23
+ "packages/cli/dist",
24
+ "templates",
25
+ "scripts/prepare.mjs"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "packageManager": "pnpm@11.16.0",
31
+ "scripts": {
32
+ "prepare": "node scripts/prepare.mjs",
33
+ "build": "tsc -p packages/cli/tsconfig.build.json",
34
+ "lint": "eslint . && prettier --check .",
35
+ "format": "prettier --write .",
36
+ "typecheck": "tsc -p packages/cli/tsconfig.json && tsc -p tsconfig.json",
37
+ "test": "pnpm build && vitest run",
38
+ "test:unit": "vitest run --project unit --project template",
39
+ "test:e2e": "pnpm build && vitest run --project e2e",
40
+ "template:install": "pnpm --dir templates/skeleton/aws-serverless install",
41
+ "template:check": "pnpm --dir templates/skeleton/aws-serverless run check"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@types/node": "^26.1.1",
46
+ "eslint": "^10.7.0",
47
+ "eslint-config-prettier": "^10.1.8",
48
+ "globals": "^17.7.0",
49
+ "prettier": "^3.9.6",
50
+ "typescript": "~6.0.3",
51
+ "typescript-eslint": "^8.65.0",
52
+ "vitest": "^4.1.10"
53
+ }
54
+ }
@@ -0,0 +1,109 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, readdir, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { copyTree, listTree } from '../lib/copy-tree.js';
6
+ import { ALLOWED_OVERWRITES, detectCollisions } from '../lib/composition.js';
7
+ import { substituteContent, substituteFileName } from '../lib/substitute.js';
8
+ import { DEFAULT_TARGET, TARGETS, TARGET_NAMES } from '../lib/targets.js';
9
+ import { agentOsStackDir, agentOsUniversalDir, skeletonDir } from '../templates.js';
10
+ /** A user-facing failure: message is printed as-is, no stack trace. */
11
+ export class CreateError extends Error {
12
+ }
13
+ /** Valid npm package name (unscoped part) — also used as the npm scope. */
14
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
15
+ export async function createProject(dirArg, options) {
16
+ const projectDir = path.resolve(options.cwd, dirArg);
17
+ const projectName = path.basename(projectDir);
18
+ if (!NAME_PATTERN.test(projectName)) {
19
+ throw new CreateError(`Invalid project name "${projectName}": use lowercase letters, digits, ".", "_" and "-" ` +
20
+ '(it becomes the npm package name and scope).');
21
+ }
22
+ await ensureEmptyOrAbsent(projectDir);
23
+ const targetName = options.target ?? DEFAULT_TARGET;
24
+ const target = TARGETS[targetName];
25
+ if (!target) {
26
+ throw new CreateError(`Unknown target "${targetName}". Known targets: ${TARGET_NAMES.join(', ')}.`);
27
+ }
28
+ const ctx = {
29
+ projectName,
30
+ projectScope: projectName,
31
+ region: target.defaultRegion ?? '',
32
+ };
33
+ const transforms = {
34
+ transformContent: (content) => substituteContent(content, ctx),
35
+ transformName: (name) => substituteFileName(name, ctx),
36
+ };
37
+ // Layer 2 (the skeleton) + layer 1 (agent-os: universal + stack overlays).
38
+ const layers = [
39
+ { name: `skeleton/${target.skeletonDir}`, dir: skeletonDir(target.skeletonDir) },
40
+ { name: 'agent-os/universal', dir: agentOsUniversalDir() },
41
+ ...target.stacks.map((stack) => ({
42
+ name: `agent-os/stack/${stack}`,
43
+ dir: agentOsStackDir(stack),
44
+ })),
45
+ ];
46
+ // Composition safety: layers must claim disjoint paths. Checked before any
47
+ // copy — a collision is a template bug and must never be resolved by order.
48
+ const claimed = [];
49
+ for (const layer of layers) {
50
+ claimed.push({ name: layer.name, files: await listTree(layer.dir, transforms) });
51
+ }
52
+ const collisions = detectCollisions(claimed, ALLOWED_OVERWRITES);
53
+ if (collisions.length > 0) {
54
+ const detail = collisions
55
+ .map((c) => ` ${c.path} — claimed by ${c.layers.join(' and ')}`)
56
+ .join('\n');
57
+ throw new CreateError(`Template layers collide (fix the templates, not the order):\n${detail}`);
58
+ }
59
+ await mkdir(projectDir, { recursive: true });
60
+ for (const layer of layers) {
61
+ await copyTree(layer.dir, projectDir, transforms);
62
+ }
63
+ if (options.git !== false) {
64
+ await initGitBaseline(projectDir);
65
+ }
66
+ return { projectDir, projectName };
67
+ }
68
+ const run = promisify(execFile);
69
+ async function initGitBaseline(projectDir) {
70
+ try {
71
+ await run('git', ['init', '--quiet'], { cwd: projectDir });
72
+ await run('git', ['add', '-A'], { cwd: projectDir });
73
+ // Explicit identity: the baseline must commit even where git has no
74
+ // global user configured (fresh machines, CI). --no-verify here shields
75
+ // the baseline from the USER'S global hooks only — the generated
76
+ // project's own gates do not exist yet, so nothing is being bypassed.
77
+ await run('git', [
78
+ '-c',
79
+ 'user.name=create-agent-rig',
80
+ '-c',
81
+ 'user.email=create-agent-rig@localhost',
82
+ 'commit',
83
+ '--quiet',
84
+ '--no-verify',
85
+ '-m',
86
+ 'Pristine template (create-agent-rig)',
87
+ ], { cwd: projectDir });
88
+ }
89
+ catch {
90
+ // git missing or unusable — generation never fails on this.
91
+ }
92
+ }
93
+ async function ensureEmptyOrAbsent(dir) {
94
+ let stats;
95
+ try {
96
+ stats = await stat(dir);
97
+ }
98
+ catch {
99
+ return; // does not exist — fine
100
+ }
101
+ if (!stats.isDirectory()) {
102
+ throw new CreateError(`Target "${dir}" exists and is not a directory.`);
103
+ }
104
+ const entries = await readdir(dir);
105
+ if (entries.length > 0) {
106
+ throw new CreateError(`Target directory "${dir}" is not empty (${entries.length} entries). ` +
107
+ 'Choose a new directory — the generator never overwrites existing files.');
108
+ }
109
+ }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseArgs } from 'node:util';
6
+ import { CreateError, createProject } from './commands/create.js';
7
+ import { makePalette } from './lib/colors.js';
8
+ import { promptTarget } from './lib/prompts.js';
9
+ import { collectGovernance, renderSummary } from './lib/summary.js';
10
+ import { DEFAULT_TARGET, TARGET_NAMES } from './lib/targets.js';
11
+ const USAGE = `Usage: create-agent-rig <dir> [options]
12
+
13
+ Scaffolds a new project into <dir>: agent operating system (.claude/, CLAUDE.md)
14
+ plus a runnable code skeleton. Refuses to write into a non-empty directory.
15
+
16
+ Options
17
+ --target <name> ${TARGET_NAMES.join(' | ')}
18
+ (interactive selection when omitted on a terminal;
19
+ required when not a terminal — default: ${DEFAULT_TARGET})
20
+ --no-git skip git init + the pristine-template baseline commit
21
+ --no-color plain output (NO_COLOR is respected too)
22
+ --version print the version
23
+ -h, --help this text`;
24
+ async function packageVersion() {
25
+ // dist/index.js lives three levels under the package root — same walk as
26
+ // the templates resolver, valid in the repo and in the published package.
27
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json');
28
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
29
+ return pkg.version;
30
+ }
31
+ async function main() {
32
+ let positionals;
33
+ let values;
34
+ try {
35
+ ({ positionals, values } = parseArgs({
36
+ args: process.argv.slice(2),
37
+ options: {
38
+ help: { type: 'boolean', short: 'h' },
39
+ target: { type: 'string' },
40
+ version: { type: 'boolean' },
41
+ 'no-git': { type: 'boolean' },
42
+ 'no-color': { type: 'boolean' },
43
+ },
44
+ allowPositionals: true,
45
+ }));
46
+ }
47
+ catch (error) {
48
+ process.stderr.write(`${error.message}\n\n${USAGE}\n`);
49
+ return 1;
50
+ }
51
+ if (values.version) {
52
+ process.stdout.write(`${await packageVersion()}\n`);
53
+ return 0;
54
+ }
55
+ if (values.help) {
56
+ process.stdout.write(`${USAGE}\n`);
57
+ return 0;
58
+ }
59
+ const dirArg = positionals[0];
60
+ if (!dirArg || positionals.length > 1) {
61
+ process.stderr.write(`${USAGE}\n`);
62
+ return 1;
63
+ }
64
+ // Non-TTY correctness (polish brief §5): never prompt into a pipe — a
65
+ // prompt would hang CI. Non-interactive runs must state the target.
66
+ const isInteractive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
67
+ let target = values.target;
68
+ if (!target) {
69
+ if (!isInteractive) {
70
+ process.stderr.write(`Missing --target in a non-interactive run. ` +
71
+ `Pass --target <${TARGET_NAMES.join('|')}>.\n`);
72
+ return 1;
73
+ }
74
+ target = await promptTarget(TARGET_NAMES, DEFAULT_TARGET, {
75
+ input: process.stdin,
76
+ output: process.stderr,
77
+ isInteractive,
78
+ });
79
+ }
80
+ const { projectDir, projectName } = await createProject(dirArg, {
81
+ cwd: process.cwd(),
82
+ target,
83
+ git: values['no-git'] !== true,
84
+ });
85
+ const palette = makePalette(Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && values['no-color'] !== true);
86
+ const summary = await collectGovernance(projectDir);
87
+ process.stdout.write('\n' + renderSummary(projectName, target, dirArg, summary, palette));
88
+ return 0;
89
+ }
90
+ main()
91
+ .then((code) => {
92
+ process.exitCode = code;
93
+ })
94
+ .catch((error) => {
95
+ if (error instanceof CreateError) {
96
+ process.stderr.write(`${error.message}\n`);
97
+ }
98
+ else {
99
+ console.error(error); // unexpected: the trace is the diagnostic
100
+ }
101
+ process.exitCode = 1;
102
+ });
@@ -0,0 +1,14 @@
1
+ const wrap = (open, close) => (text) => `[${open}m${text}[${close}m`;
2
+ const COLORED = {
3
+ accent: wrap('36', '39'), // cyan
4
+ dim: wrap('2', '22'),
5
+ red: wrap('31', '39'),
6
+ };
7
+ const PLAIN = {
8
+ accent: (text) => text,
9
+ dim: (text) => text,
10
+ red: (text) => text,
11
+ };
12
+ export function makePalette(enabled) {
13
+ return enabled ? COLORED : PLAIN;
14
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Layer-composition policy (PLAN phase 9): layers must claim disjoint paths.
3
+ * A collision is refused, never resolved silently by copy order. Intended
4
+ * overwrites — if one ever becomes necessary — are declared here, visibly,
5
+ * not implied by ordering.
6
+ */
7
+ export const ALLOWED_OVERWRITES = new Set([]);
8
+ export function detectCollisions(layers, allowed) {
9
+ const claims = new Map();
10
+ for (const layer of layers) {
11
+ for (const file of layer.files) {
12
+ const owners = claims.get(file) ?? [];
13
+ owners.push(layer.name);
14
+ claims.set(file, owners);
15
+ }
16
+ }
17
+ return [...claims]
18
+ .filter(([file, owners]) => owners.length > 1 && !allowed.has(file))
19
+ .map(([file, owners]) => ({ path: file, layers: owners }));
20
+ }
@@ -0,0 +1,91 @@
1
+ import { chmod, copyFile, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /** Entry names never copied out of a template (local artifacts, never payload). */
4
+ export const DEFAULT_IGNORE = [
5
+ '.git',
6
+ 'node_modules',
7
+ 'dist',
8
+ 'coverage',
9
+ 'cdk.out',
10
+ '.turbo',
11
+ '.DS_Store',
12
+ // packaging metadata of the template itself, meaningless in a generated project
13
+ '.npmignore',
14
+ 'var',
15
+ // frontend build artifacts of in-place template runs
16
+ '.next',
17
+ 'out',
18
+ 'next-env.d.ts',
19
+ ];
20
+ /** A file is treated as binary if its first bytes contain a NUL byte. */
21
+ function isBinary(buffer) {
22
+ return buffer.subarray(0, 8192).includes(0);
23
+ }
24
+ export async function copyTree(srcDir, destDir, options = {}) {
25
+ const ignore = new Set(options.ignore ?? DEFAULT_IGNORE);
26
+ await mkdir(destDir, { recursive: true });
27
+ await copyDir(srcDir, destDir, '', { ...options, ignore });
28
+ }
29
+ /**
30
+ * The destination-relative file paths {@link copyTree} would produce — same
31
+ * ignore list, same name transform, no writes. Used to check layer
32
+ * composition for collisions before anything is copied.
33
+ */
34
+ export async function listTree(srcDir, options = {}) {
35
+ const ignore = new Set(options.ignore ?? DEFAULT_IGNORE);
36
+ const files = [];
37
+ const walk = async (dir, relDir) => {
38
+ const entries = await readdir(dir, { withFileTypes: true });
39
+ for (const entry of entries) {
40
+ if (ignore.has(entry.name))
41
+ continue;
42
+ const destName = options.transformName ? options.transformName(entry.name) : entry.name;
43
+ const relPath = relDir === '' ? destName : `${relDir}/${destName}`;
44
+ if (entry.isDirectory()) {
45
+ await walk(path.join(dir, entry.name), relPath);
46
+ }
47
+ else if (entry.isFile()) {
48
+ files.push(relPath);
49
+ }
50
+ }
51
+ };
52
+ await walk(srcDir, '');
53
+ return files;
54
+ }
55
+ async function copyDir(srcDir, destDir, relDir, options) {
56
+ const entries = await readdir(srcDir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (options.ignore.has(entry.name))
59
+ continue;
60
+ const srcPath = path.join(srcDir, entry.name);
61
+ const destName = options.transformName ? options.transformName(entry.name) : entry.name;
62
+ const destPath = path.join(destDir, destName);
63
+ const relPath = path.join(relDir, entry.name);
64
+ if (entry.isDirectory()) {
65
+ await mkdir(destPath, { recursive: true });
66
+ await copyDir(srcPath, destPath, relPath, options);
67
+ }
68
+ else if (entry.isFile()) {
69
+ await copyFileEntry(srcPath, destPath, relPath, options);
70
+ }
71
+ // Symlinks and other special entries are intentionally not copied:
72
+ // templates are plain trees.
73
+ }
74
+ }
75
+ async function copyFileEntry(srcPath, destPath, relPath, options) {
76
+ if (!options.transformContent) {
77
+ await copyFile(srcPath, destPath); // copyFile preserves the mode by itself
78
+ return;
79
+ }
80
+ const buffer = await readFile(srcPath);
81
+ if (isBinary(buffer)) {
82
+ await writeFile(destPath, buffer);
83
+ }
84
+ else {
85
+ await writeFile(destPath, options.transformContent(buffer.toString('utf8'), relPath));
86
+ }
87
+ // writeFile does NOT preserve permissions — restore them (chmod ignores umask),
88
+ // otherwise executable template files (scripts, hooks) arrive non-executable.
89
+ const { mode } = await stat(srcPath);
90
+ await chmod(destPath, mode & 0o777);
91
+ }
@@ -0,0 +1,24 @@
1
+ import { createInterface } from 'node:readline';
2
+ /**
3
+ * Pick a target interactively: by number, by name, or Enter for the default.
4
+ * Anything unrecognised falls back to the default — generation should never
5
+ * dead-end on a typo.
6
+ */
7
+ export function promptTarget(targets, defaultTarget, streams) {
8
+ if (!streams.isInteractive) {
9
+ return Promise.resolve(defaultTarget);
10
+ }
11
+ const menu = targets
12
+ .map((name, index) => ` ${index + 1}. ${name}${name === defaultTarget ? ' (default)' : ''}`)
13
+ .join('\n');
14
+ const rl = createInterface({ input: streams.input, output: streams.output });
15
+ return new Promise((resolve) => {
16
+ rl.question(`Target:\n${menu}\nChoose [1-${targets.length}]: `, (answer) => {
17
+ rl.close();
18
+ const trimmed = answer.trim();
19
+ const byNumber = targets[Number.parseInt(trimmed, 10) - 1];
20
+ const byName = targets.find((name) => name === trimmed);
21
+ resolve(byName ?? byNumber ?? defaultTarget);
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,22 @@
1
+ export function substituteContent(content, ctx) {
2
+ return content
3
+ .replaceAll('__PROJECT_NAME__', ctx.projectName)
4
+ .replaceAll('__PROJECT_SCOPE__', ctx.projectScope)
5
+ .replaceAll('__REGION__', ctx.region)
6
+ .replaceAll('@app/', `@${ctx.projectScope}/`);
7
+ }
8
+ /**
9
+ * Files that must exist in the generated project under a dotted name, but are
10
+ * stored un-dotted in the template because `npm publish` strips the dotted
11
+ * original from tarballs (the create-react-app `gitignore` trick).
12
+ */
13
+ const UNDOTTED_NAMES = {
14
+ gitignore: '.gitignore',
15
+ };
16
+ export function substituteFileName(name, ctx) {
17
+ const substituted = name
18
+ .replaceAll('__PROJECT_NAME__', ctx.projectName)
19
+ .replaceAll('__PROJECT_SCOPE__', ctx.projectScope)
20
+ .replaceAll('__REGION__', ctx.region);
21
+ return UNDOTTED_NAMES[substituted] ?? substituted;
22
+ }
@@ -0,0 +1,41 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const names = async (dir, strip) => {
4
+ try {
5
+ return (await readdir(dir)).map((entry) => entry.replace(strip, '')).sort();
6
+ }
7
+ catch {
8
+ return [];
9
+ }
10
+ };
11
+ export async function collectGovernance(projectDir) {
12
+ const claude = path.join(projectDir, '.claude');
13
+ return {
14
+ rules: await names(path.join(claude, 'rules'), /\.md$/),
15
+ agents: await names(path.join(claude, 'agents'), /\.md$/),
16
+ hooks: (await names(path.join(claude, 'hooks'), /\.mjs$/))
17
+ // guard-core-purity → "core purity": the mechanism, not the filename
18
+ .map((hook) => hook.replace(/^(guard|block)-/, '').replaceAll('-', ' ')),
19
+ skills: await names(path.join(claude, 'skills'), /$^/),
20
+ };
21
+ }
22
+ export function renderSummary(projectName, target, dirArg, summary, p) {
23
+ const row = (label, count, detail, note = '') => {
24
+ const tag = note ? ` ${note}` : '';
25
+ return ` ${label.padEnd(8)}${String(count).padEnd(3)}${tag.padEnd(note ? 11 : 0)} ${p.dim(detail)}`;
26
+ };
27
+ return [
28
+ `Created ${p.accent(projectName)} ${p.dim('·')} ${target}`,
29
+ '',
30
+ row('Rules', summary.rules.length, '.claude/rules/'),
31
+ row('Agents', summary.agents.length, summary.agents.join(', ')),
32
+ row('Hooks', summary.hooks.length, summary.hooks.join(', '), 'enforced'),
33
+ row('Skills', summary.skills.length, summary.skills.join(', ')),
34
+ '',
35
+ 'Next',
36
+ p.dim(` cd ${dirArg}`),
37
+ p.dim(' pnpm install'),
38
+ p.dim(' pnpm check'),
39
+ '',
40
+ ].join('\n');
41
+ }
@@ -0,0 +1,14 @@
1
+ export const TARGETS = {
2
+ 'aws-serverless': {
3
+ skeletonDir: 'aws-serverless',
4
+ stacks: ['node-ts', 'aws-cdk'],
5
+ defaultRegion: 'eu-central-1',
6
+ },
7
+ 'node-service': {
8
+ skeletonDir: 'node-service',
9
+ stacks: ['node-ts'],
10
+ },
11
+ };
12
+ export const TARGET_NAMES = Object.keys(TARGETS);
13
+ /** Zero options at the personal stage: one implicit target (PLAN.md §6). */
14
+ export const DEFAULT_TARGET = 'aws-serverless';
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ /**
4
+ * Resolve the repo/package `templates/` directory.
5
+ *
6
+ * This file lives at `packages/cli/src/templates.ts` in the repo and at
7
+ * `packages/cli/dist/templates.js` in the published package — three levels below
8
+ * the root in both cases, so one relative walk serves dev, tarball and git installs.
9
+ */
10
+ export function templatesRoot() {
11
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'templates');
12
+ }
13
+ export function skeletonDir(skeleton) {
14
+ return path.join(templatesRoot(), 'skeleton', skeleton);
15
+ }
16
+ export function agentOsUniversalDir() {
17
+ return path.join(templatesRoot(), 'agent-os', 'universal');
18
+ }
19
+ export function agentOsStackDir(stack) {
20
+ return path.join(templatesRoot(), 'agent-os', 'stack', stack);
21
+ }
@@ -0,0 +1,29 @@
1
+ // Runs on `pnpm install` locally AND when npm installs this package from git
2
+ // (`npx github:<user>/create-agent-rig`). It must therefore work with only
3
+ // the root devDependencies present and no pnpm available.
4
+ import { spawnSync } from 'node:child_process';
5
+ import { existsSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const root = path.dirname(fileURLToPath(import.meta.url)) + '/..';
11
+
12
+ // 1. Wire up the pre-commit hook when working inside the git checkout.
13
+ if (existsSync(path.join(root, '.git'))) {
14
+ spawnSync('git', ['config', 'core.hooksPath', '.husky'], { cwd: root, stdio: 'inherit' });
15
+ }
16
+
17
+ // 2. Build the CLI so the `bin` entry exists (required for git/tarball installs).
18
+ const require = createRequire(import.meta.url);
19
+ const tscPath = path.join(
20
+ path.dirname(require.resolve('typescript/package.json')),
21
+ 'lib',
22
+ 'tsc.js',
23
+ );
24
+ const result = spawnSync(
25
+ process.execPath,
26
+ [tscPath, '-p', path.join(root, 'packages/cli/tsconfig.build.json')],
27
+ { cwd: root, stdio: 'inherit' },
28
+ );
29
+ process.exit(result.status ?? 1);