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.
Files changed (235) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +508 -101
  11. package/packages/cli/dist/index.js +658 -47
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +45 -12
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +4 -2
  44. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  45. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  46. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  47. package/templates/agent-os/universal/.claude/rules/workflow.md +59 -22
  48. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  49. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  50. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  51. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  52. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  53. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  54. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  55. package/templates/agent-os/universal/.claude/settings.json +0 -8
  56. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +45 -12
  57. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  58. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  59. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  60. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  61. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  62. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  63. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  64. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  65. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  66. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  67. package/templates/agent-os/universal/AGENTS.md +208 -107
  68. package/templates/agent-os/universal/CLAUDE.md +15 -162
  69. package/templates/agent-os/universal/PLAN.md +17 -10
  70. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  71. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  72. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  73. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  74. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  75. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  76. package/templates/agent-os/universal/layers.json +25 -30
  77. package/templates/hash-history.json +112 -40
  78. package/templates/release-ledger.json +3 -1
  79. package/packages/cli/dist/lib/composition.js +0 -20
  80. package/packages/cli/dist/lib/targets.js +0 -28
  81. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  82. package/packages/cli/dist/policy/core/adapter.js +0 -18
  83. package/packages/cli/dist/policy/core/coverage.js +0 -253
  84. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  85. package/packages/cli/dist/policy/core/declaration.js +0 -127
  86. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  87. package/packages/cli/dist/policy/core/probe.js +0 -442
  88. package/packages/cli/dist/policy/core/registry.js +0 -115
  89. package/packages/cli/dist/policy/core/validation.js +0 -275
  90. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  91. package/packages/cli/dist/policy/harness/claude.js +0 -47
  92. package/packages/cli/dist/policy/harness/codex.js +0 -87
  93. package/packages/cli/dist/policy/harness/index.js +0 -15
  94. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  95. package/packages/cli/dist/policy/index.js +0 -17
  96. package/templates/agent-os/init/AGENTS.md +0 -201
  97. package/templates/agent-os/init/CLAUDE.md +0 -201
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  99. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  100. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  101. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  103. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  104. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  105. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  106. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  107. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  108. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  109. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  110. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  111. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  112. package/templates/skeleton/aws-serverless/README.md +0 -181
  113. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  114. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  116. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  118. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  119. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  120. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  121. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  122. package/templates/skeleton/aws-serverless/gitignore +0 -56
  123. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  124. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  125. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  126. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  127. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  128. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  129. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  130. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  131. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  132. package/templates/skeleton/aws-serverless/package.json +0 -30
  133. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  134. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  135. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  136. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  137. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  138. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  139. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  140. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  141. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  142. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  143. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  144. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  145. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  146. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  147. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  148. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  149. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  150. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  151. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  152. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  153. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  154. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  155. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  157. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  158. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  159. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  161. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  163. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  164. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  165. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  166. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  167. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  168. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  169. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  170. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  171. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  172. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  173. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  174. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  175. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  176. package/templates/skeleton/node-service/README.md +0 -104
  177. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  178. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  179. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  180. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  181. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  182. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  183. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  184. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  185. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  186. package/templates/skeleton/node-service/gitignore +0 -54
  187. package/templates/skeleton/node-service/package.json +0 -30
  188. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  189. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  190. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  191. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  192. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  193. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  194. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  195. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  196. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  197. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  198. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  199. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  200. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  201. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  202. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  203. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  204. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  205. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  206. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  207. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  208. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  209. package/templates/skeleton/node-service/services/api/package.json +0 -17
  210. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  211. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  212. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  213. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  214. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  215. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  216. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  217. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  218. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  219. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  220. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  221. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  222. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  223. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  224. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  225. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  226. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  227. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  228. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  229. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  230. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  231. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  232. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  233. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  234. package/templates/skeleton/node-service/tsconfig.json +0 -13
  235. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -1,13 +0,0 @@
1
- packages:
2
- - packages/*
3
- - services/*
4
- - apps/*
5
-
6
- # esbuild arrives via tsx. Its postinstall is intentionally NOT approved:
7
- # skipping it keeps the JS launcher (which resolves the platform binary
8
- # itself); approving it swaps in a raw binary that pnpm's bin shim can't run.
9
- # sharp (via next) only matters for the image optimizer, which a static
10
- # export never runs — its native build stays off.
11
- allowBuilds:
12
- esbuild: false
13
- sharp: false
@@ -1,34 +0,0 @@
1
- // Builds the deployable artifact: a bundled server plus the web public dir,
2
- // into dist/. It produces a real, runnable thing — it does not ship it
3
- // anywhere. Run it: STATIC_DIR=dist/public node dist/server.mjs
4
- import { build } from 'esbuild';
5
- import { cp, mkdir, rm } from 'node:fs/promises';
6
- import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
- const dist = path.join(root, 'dist');
11
-
12
- await rm(dist, { recursive: true, force: true });
13
- await mkdir(dist, { recursive: true });
14
-
15
- await build({
16
- entryPoints: [path.join(root, 'services', 'api', 'src', 'main.ts')],
17
- outfile: path.join(dist, 'server.mjs'),
18
- bundle: true,
19
- platform: 'node',
20
- format: 'esm',
21
- target: 'node22',
22
- // ESM-on-node output may reference require() from a dependency — shim it.
23
- banner: {
24
- js: "import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);",
25
- },
26
- });
27
-
28
- // The built web bundle, if present (pnpm build:web). Served via STATIC_DIR.
29
- await cp(path.join(root, 'apps', 'web', 'out'), path.join(dist, 'public'), {
30
- recursive: true,
31
- }).catch(() => {});
32
-
33
- console.log('artifact built: dist/server.mjs (+ dist/public if the web was built).');
34
- console.log('run it: STATIC_DIR=dist/public node dist/server.mjs');
@@ -1,17 +0,0 @@
1
- {
2
- "name": "@app/api",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "main": "src/main.ts",
7
- "types": "src/main.ts",
8
- "scripts": {
9
- "start": "tsx src/main.ts"
10
- },
11
- "dependencies": {
12
- "@app/core": "workspace:*",
13
- "@app/db": "workspace:*",
14
- "@app/shared": "workspace:*",
15
- "zod": "^4.4.3"
16
- }
17
- }
@@ -1,23 +0,0 @@
1
- // The queue between api and worker is a spool directory: one JSON file per
2
- // event. Same DLQ discipline as any queue — the worker moves poison files to
3
- // the DLQ directory after its retry budget.
4
- import { mkdir, rename, writeFile } from 'node:fs/promises';
5
- import path from 'node:path';
6
- import type { NoteCreatedEvent } from '@app/core';
7
- import type { EventPublisher } from '../usecases/create-note.js';
8
-
9
- export class SpoolEventPublisher implements EventPublisher {
10
- constructor(
11
- private readonly queueDir: string,
12
- /** Unique, sortable message name — injected so tests stay deterministic. */
13
- private readonly nextName: () => string,
14
- ) {}
15
-
16
- async publish(event: NoteCreatedEvent): Promise<void> {
17
- await mkdir(this.queueDir, { recursive: true });
18
- const file = path.join(this.queueDir, `${this.nextName()}.json`);
19
- // Write-then-rename so the worker never picks up a half-written message.
20
- await writeFile(`${file}.tmp`, JSON.stringify(event));
21
- await rename(`${file}.tmp`, file);
22
- }
23
- }
@@ -1,40 +0,0 @@
1
- // The handler translates transport ⇄ domain and nothing else: decode the
2
- // payload, call the usecase, map the result (or the typed error) to HTTP.
3
- import { InvalidNoteError } from '@app/core';
4
- import { AppError } from '@app/shared';
5
- import { createNoteUsecase, type CreateNoteDeps } from '../usecases/create-note.js';
6
-
7
- export interface HttpResponse {
8
- statusCode: number;
9
- body: string;
10
- }
11
-
12
- const json = (statusCode: number, body: unknown): HttpResponse => ({
13
- statusCode,
14
- body: JSON.stringify(body),
15
- });
16
-
17
- export function makeCreateNoteHandler(deps: CreateNoteDeps) {
18
- return async (rawBody: string): Promise<HttpResponse> => {
19
- let payload: unknown;
20
- try {
21
- payload = JSON.parse(rawBody);
22
- } catch {
23
- return json(400, { error: 'body must be valid JSON' });
24
- }
25
-
26
- try {
27
- const note = await createNoteUsecase(payload, deps);
28
- return json(201, { note });
29
- } catch (error) {
30
- if (error instanceof InvalidNoteError) {
31
- return json(400, { error: 'invalid note', issues: error.issues });
32
- }
33
- if (error instanceof AppError) {
34
- return json(error.statusCode, { error: error.message, code: error.code });
35
- }
36
- deps.log.error('unhandled error in create-note', { error: String(error) });
37
- return json(500, { error: 'internal error' });
38
- }
39
- };
40
- }
@@ -1,23 +0,0 @@
1
- import { AppError } from '@app/shared';
2
- import type { Logger } from '@app/shared';
3
- import { listNotesUsecase, type ListNotesDeps } from '../usecases/list-notes.js';
4
- import type { HttpResponse } from './create-note.js';
5
-
6
- const json = (statusCode: number, body: unknown): HttpResponse => ({
7
- statusCode,
8
- body: JSON.stringify(body),
9
- });
10
-
11
- export function makeListNotesHandler(deps: ListNotesDeps & { log: Logger }) {
12
- return async (): Promise<HttpResponse> => {
13
- try {
14
- return json(200, { notes: await listNotesUsecase(deps) });
15
- } catch (error) {
16
- if (error instanceof AppError) {
17
- return json(error.statusCode, { error: error.message, code: error.code });
18
- }
19
- deps.log.error('unhandled error in list-notes', { error: String(error) });
20
- return json(500, { error: 'internal error' });
21
- }
22
- };
23
- }
@@ -1,40 +0,0 @@
1
- // Composition root — the ONLY file with real wiring. Tests target the
2
- // factories with stub deps and never import this module.
3
- import { randomUUID } from 'node:crypto';
4
- import path from 'node:path';
5
- import { z } from 'zod';
6
- import { JsonFileNoteStore } from '@app/db';
7
- import { createLogger, loadEnv } from '@app/shared';
8
- import { SpoolEventPublisher } from './adapters/spool-publisher.js';
9
- import { makeServer } from './server.js';
10
- import { defaultStaticDirFor } from './static-dir.js';
11
-
12
- // The built web bundle (pnpm build:web) lives here; served when present.
13
- const defaultStaticDir = defaultStaticDirFor(import.meta.url);
14
-
15
- const env = loadEnv(
16
- z.object({
17
- PORT: z.coerce.number().int().positive().default(3000),
18
- DATA_DIR: z.string().min(1).default('var/data'),
19
- QUEUE_DIR: z.string().min(1).default('var/queue'),
20
- STATIC_DIR: z.string().min(1).default(defaultStaticDir),
21
- }),
22
- process.env,
23
- );
24
-
25
- const log = createLogger({ service: 'api' });
26
-
27
- const server = makeServer(
28
- {
29
- notes: new JsonFileNoteStore(path.join(env.DATA_DIR, 'notes.json')),
30
- events: new SpoolEventPublisher(env.QUEUE_DIR, () => `${Date.now()}-${randomUUID()}`),
31
- newId: () => randomUUID(),
32
- now: () => new Date().toISOString(),
33
- log,
34
- },
35
- { staticDir: env.STATIC_DIR },
36
- );
37
-
38
- server.listen(env.PORT, () => {
39
- log.info('api listening', { port: env.PORT });
40
- });
@@ -1,172 +0,0 @@
1
- // Thin HTTP shell around the handlers: routing, body collection, and (when
2
- // configured) serving the built web bundle. No second runtime for the
3
- // frontend — the same process serves the static export.
4
- import { createReadStream } from 'node:fs';
5
- import { realpath, stat } from 'node:fs/promises';
6
- import { createServer, type Server } from 'node:http';
7
- import path from 'node:path';
8
- import type { CreateNoteDeps } from './usecases/create-note.js';
9
- import type { ListNotesDeps } from './usecases/list-notes.js';
10
- import { makeCreateNoteHandler } from './handlers/create-note.js';
11
- import { makeListNotesHandler } from './handlers/list-notes.js';
12
- import type { HttpResponse } from './handlers/create-note.js';
13
-
14
- export type ServerDeps = CreateNoteDeps & ListNotesDeps;
15
-
16
- export interface ServerOptions {
17
- /** Directory of the built web bundle (apps/web/out). Unset = API only. */
18
- staticDir?: string;
19
- }
20
-
21
- /**
22
- * Largest request body `POST /notes` will buffer — that route and no other:
23
- * the cap is applied where the body is read, so a second route that reads one
24
- * brings its own. A note is a title and some tags; anything past this is a
25
- * mistake or an attack, and either way the process must not hold it in memory.
26
- */
27
- const MAX_BODY_BYTES = 1024 * 1024;
28
-
29
- const CONTENT_TYPES: Record<string, string> = {
30
- '.html': 'text/html; charset=utf-8',
31
- '.css': 'text/css',
32
- '.js': 'text/javascript',
33
- '.json': 'application/json',
34
- '.svg': 'image/svg+xml',
35
- '.png': 'image/png',
36
- '.ico': 'image/x-icon',
37
- '.txt': 'text/plain; charset=utf-8',
38
- };
39
-
40
- export function makeServer(deps: ServerDeps, options: ServerOptions = {}): Server {
41
- const createNote = makeCreateNoteHandler(deps);
42
- const listNotes = makeListNotesHandler(deps);
43
-
44
- return createServer((request, response) => {
45
- const respond = (result: HttpResponse) => {
46
- response.writeHead(result.statusCode, { 'content-type': 'application/json' });
47
- response.end(result.body);
48
- };
49
- const notFound = () => respond({ statusCode: 404, body: JSON.stringify({ error: 'not found' }) });
50
- const badRequest = () =>
51
- respond({ statusCode: 400, body: JSON.stringify({ error: 'bad request' }) });
52
-
53
- if (request.method === 'POST' && request.url === '/notes') {
54
- const chunks: Buffer[] = [];
55
- let size = 0;
56
- let refused = false;
57
- // A socket that dies mid-upload emits 'error'; unhandled, it takes the
58
- // process with it.
59
- request.on('error', () => {
60
- refused = true;
61
- });
62
- request.on('data', (chunk: Buffer) => {
63
- if (refused) return;
64
- size += chunk.length;
65
- if (size > MAX_BODY_BYTES) {
66
- refused = true;
67
- chunks.length = 0;
68
- respond({ statusCode: 413, body: JSON.stringify({ error: 'payload too large' }) });
69
- // Keep draining rather than destroying the socket: a client that is
70
- // still uploading has to stay connected to read the refusal.
71
- request.resume();
72
- return;
73
- }
74
- chunks.push(chunk);
75
- });
76
- request.on('end', () => {
77
- if (refused) return;
78
- void createNote(Buffer.concat(chunks).toString('utf8')).then(respond);
79
- });
80
- return;
81
- }
82
-
83
- if (request.method === 'GET' && request.url === '/notes') {
84
- void listNotes().then(respond);
85
- return;
86
- }
87
-
88
- if (request.method === 'GET' && options.staticDir) {
89
- // The rejection handler is the point: without it a throw in here is an
90
- // unhandled rejection, which ends the process on one malformed request.
91
- void serveStatic(options.staticDir, request.url ?? '/', response, notFound, badRequest).catch(
92
- (error: unknown) => {
93
- // Say nothing to the client beyond the code, but never swallow it —
94
- // a static path that fails silently is a blind spot in every
95
- // generated project.
96
- deps.log.error('static serve failed', { url: request.url, error: String(error) });
97
- if (response.headersSent) {
98
- response.destroy();
99
- return;
100
- }
101
- respond({ statusCode: 500, body: JSON.stringify({ error: 'internal error' }) });
102
- },
103
- );
104
- return;
105
- }
106
-
107
- notFound();
108
- });
109
- }
110
-
111
- async function serveStatic(
112
- staticDir: string,
113
- rawUrl: string,
114
- response: import('node:http').ServerResponse,
115
- notFound: () => void,
116
- badRequest: () => void,
117
- ): Promise<void> {
118
- let pathname: string;
119
- try {
120
- pathname = decodeURIComponent(new URL(rawUrl, 'http://local').pathname);
121
- } catch {
122
- // A malformed escape (`GET /%`) is the client's error, not ours.
123
- return badRequest();
124
- }
125
- const relative = pathname.endsWith('/') ? `${pathname}index.html` : pathname;
126
- // Resolve the root first: a configured dir with a trailing separator would
127
- // otherwise build a `//` prefix that no resolved path can start with, and
128
- // every request would 404.
129
- const root = path.resolve(staticDir);
130
- const resolved = path.resolve(root, `.${path.sep}${relative}`);
131
- // Path-traversal guard: whatever the URL said, we never leave staticDir.
132
- // The string test is only half of it — it says where the path points, not
133
- // what the filesystem will open, and stat/open both follow symlinks.
134
- if (!within(root, resolved)) {
135
- return notFound();
136
- }
137
- let file: string;
138
- try {
139
- // …so ask the filesystem too, and test the answer the same way. A missing
140
- // file throws here, which is the 404 we would give it anyway.
141
- file = await realpath(resolved);
142
- if (!within(await realpath(root), file)) return notFound();
143
- const stats = await stat(file);
144
- if (!stats.isFile()) return notFound();
145
- } catch {
146
- return notFound();
147
- }
148
- // Open before the headers go out: a file that passes stat can still fail to
149
- // open (EACCES, or it is gone by now), and after writeHead(200) there is no
150
- // way left to say so — which is one GET away from an unhandled stream error.
151
- const stream = createReadStream(file);
152
- await new Promise<void>((resolve, reject) => {
153
- stream.once('error', reject);
154
- stream.once('open', () => {
155
- stream.removeListener('error', reject);
156
- response.writeHead(200, {
157
- 'content-type': CONTENT_TYPES[path.extname(file)] ?? 'application/octet-stream',
158
- });
159
- // Past the headers the only honest answer to a read failure is to cut
160
- // the response short, so the client sees a truncated body rather than a
161
- // complete one.
162
- stream.once('error', () => response.destroy());
163
- stream.pipe(response);
164
- resolve();
165
- });
166
- });
167
- }
168
-
169
- /** Whether `candidate` is `root` itself or sits underneath it. */
170
- function within(root: string, candidate: string): boolean {
171
- return candidate === root || candidate.startsWith(root + path.sep);
172
- }
@@ -1,20 +0,0 @@
1
- // Where the built web bundle lives, resolved relative to the composition root.
2
- // A pure function rather than an inline expression in main.ts: main.ts is wiring
3
- // that tests never import, and this resolution has a bug worth pinning —
4
- // `new URL(url).pathname` leaves percent-escapes in place (a checkout under
5
- // "/Users/a b/" resolves to "/Users/a%20b/") and prefixes a slash on Windows.
6
- import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- /** `apps/web/out`, three levels up from a module in `services/api/src`. */
10
- export function defaultStaticDirFor(moduleUrl: string): string {
11
- return path.resolve(
12
- path.dirname(fileURLToPath(moduleUrl)),
13
- '..',
14
- '..',
15
- '..',
16
- 'apps',
17
- 'web',
18
- 'out',
19
- );
20
- }
@@ -1,30 +0,0 @@
1
- // The usecase layer is mandatory: every business operation lives here, and
2
- // handlers call nothing below it. Dependencies are consumer-owned interfaces —
3
- // the usecase says what it needs, adapters satisfy it.
4
- import { createNote, makeNoteCreatedEvent, type Note, type NoteCreatedEvent } from '@app/core';
5
- import type { Logger } from '@app/shared';
6
-
7
- export interface NoteStore {
8
- put(note: Note): Promise<void>;
9
- }
10
-
11
- export interface EventPublisher {
12
- publish(event: NoteCreatedEvent): Promise<void>;
13
- }
14
-
15
- export interface CreateNoteDeps {
16
- notes: NoteStore;
17
- events: EventPublisher;
18
- /** Id generation and the clock are decided here, never inside the core. */
19
- newId(): string;
20
- now(): string;
21
- log: Logger;
22
- }
23
-
24
- export async function createNoteUsecase(input: unknown, deps: CreateNoteDeps): Promise<Note> {
25
- const note = createNote(input, { id: deps.newId(), createdAt: deps.now() });
26
- await deps.notes.put(note);
27
- await deps.events.publish(makeNoteCreatedEvent(note));
28
- deps.log.info('note created', { noteId: note.id, slug: note.slug });
29
- return note;
30
- }
@@ -1,14 +0,0 @@
1
- // Even a trivial read goes through a usecase — the uniformity is the rule.
2
- import type { Note } from '@app/core';
3
-
4
- export interface NoteLister {
5
- list(): Promise<Note[]>;
6
- }
7
-
8
- export interface ListNotesDeps {
9
- notes: NoteLister;
10
- }
11
-
12
- export async function listNotesUsecase(deps: ListNotesDeps): Promise<Note[]> {
13
- return deps.notes.list();
14
- }
@@ -1,92 +0,0 @@
1
- // CD brief §2: node-service's deployable artifact must actually build and run.
2
- // This bundles the server, boots the bundle over a real socket, and closes the
3
- // path — proving the artifact is genuine, not a stub.
4
- import { spawn } from 'node:child_process';
5
- import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
6
- import { tmpdir } from 'node:os';
7
- import type { AddressInfo } from 'node:net';
8
- import { createServer } from 'node:net';
9
- import path from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
- import { afterAll, beforeAll, describe, expect, it } from 'vitest';
12
- import { runPackageManager } from './package-manager.js';
13
-
14
- const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
15
- const dist = path.join(projectRoot, 'dist');
16
-
17
- function freePort(): Promise<number> {
18
- return new Promise((resolve, reject) => {
19
- const srv = createServer();
20
- srv.listen(0, () => {
21
- const { port } = srv.address() as AddressInfo;
22
- srv.close(() => resolve(port));
23
- });
24
- srv.on('error', reject);
25
- });
26
- }
27
-
28
- describe('deployable artifact (dist/)', () => {
29
- let child: ReturnType<typeof spawn> | undefined;
30
- let work: string;
31
-
32
- beforeAll(async () => {
33
- work = await mkdtemp(path.join(tmpdir(), 'artifact-'));
34
- // `pnpm check` builds the artifact before tests; build it here only if a
35
- // bare `pnpm test` run left no dist to boot.
36
- const built = await stat(path.join(dist, 'server.mjs')).then(
37
- () => true,
38
- () => false,
39
- );
40
- if (!built) await runPackageManager(['build:artifact'], { cwd: projectRoot });
41
- }, 180_000);
42
-
43
- afterAll(async () => {
44
- child?.kill();
45
- await rm(work, { recursive: true, force: true });
46
- });
47
-
48
- it('produces a bundled server and the web public dir', async () => {
49
- expect((await stat(path.join(dist, 'server.mjs'))).size).toBeGreaterThan(1000);
50
- await expect(readFile(path.join(dist, 'public', 'index.html'), 'utf8')).resolves.toContain(
51
- '<',
52
- );
53
- });
54
-
55
- it('the bundle boots and serves the full request path', async () => {
56
- const port = await freePort();
57
- child = spawn(process.execPath, [path.join(dist, 'server.mjs')], {
58
- env: {
59
- ...process.env,
60
- PORT: String(port),
61
- DATA_DIR: path.join(work, 'data'),
62
- QUEUE_DIR: path.join(work, 'queue'),
63
- STATIC_DIR: path.join(dist, 'public'),
64
- },
65
- stdio: 'ignore',
66
- });
67
-
68
- const base = `http://127.0.0.1:${port}`;
69
- // wait for readiness
70
- for (let i = 0; i < 50; i++) {
71
- try {
72
- await fetch(base + '/notes');
73
- break;
74
- } catch {
75
- await new Promise((r) => setTimeout(r, 100));
76
- }
77
- }
78
-
79
- const created = await fetch(base + '/notes', {
80
- method: 'POST',
81
- headers: { 'content-type': 'application/json' },
82
- body: JSON.stringify({ title: 'artifact proof' }),
83
- });
84
- expect(created.status).toBe(201);
85
- const listed = (await (await fetch(base + '/notes')).json()) as { notes: unknown[] };
86
- expect(listed.notes).toHaveLength(1);
87
- // the bundle also serves the web shell
88
- const root = await fetch(base + '/');
89
- expect(root.status).toBe(200);
90
- expect(root.headers.get('content-type')).toContain('text/html');
91
- }, 30_000);
92
- });
@@ -1,64 +0,0 @@
1
- import { AppError, createLogger } from '@app/shared';
2
- import { describe, expect, it } from 'vitest';
3
- import { makeCreateNoteHandler } from '../src/handlers/create-note.js';
4
- import type { CreateNoteDeps } from '../src/usecases/create-note.js';
5
-
6
- function stubDeps(overrides: Partial<CreateNoteDeps> = {}) {
7
- const deps: CreateNoteDeps = {
8
- notes: { put: async () => {} },
9
- events: { publish: async () => {} },
10
- newId: () => 'fixed-id',
11
- now: () => '2024-01-01T00:00:00.000Z',
12
- log: createLogger({}, () => {}),
13
- ...overrides,
14
- };
15
- return deps;
16
- }
17
-
18
- describe('POST /notes handler', () => {
19
- it('returns 201 with the created note', async () => {
20
- const handler = makeCreateNoteHandler(stubDeps());
21
- const result = await handler(JSON.stringify({ title: 'Hello' }));
22
- expect(result.statusCode).toBe(201);
23
- expect(JSON.parse(result.body)).toMatchObject({
24
- note: { id: 'fixed-id', slug: 'hello', createdAt: '2024-01-01T00:00:00.000Z' },
25
- });
26
- });
27
-
28
- it('returns 400 for malformed JSON', async () => {
29
- const result = await makeCreateNoteHandler(stubDeps())('{not json');
30
- expect(result.statusCode).toBe(400);
31
- });
32
-
33
- it('returns 400 with issues for invalid input', async () => {
34
- const result = await makeCreateNoteHandler(stubDeps())(JSON.stringify({ title: '' }));
35
- expect(result.statusCode).toBe(400);
36
- expect(JSON.parse(result.body).issues.join(' ')).toMatch(/title/);
37
- });
38
-
39
- it('maps typed errors to their status code', async () => {
40
- const handler = makeCreateNoteHandler(
41
- stubDeps({
42
- notes: {
43
- put: () => Promise.reject(new AppError('conflict', { statusCode: 409, code: 'DUP' })),
44
- },
45
- }),
46
- );
47
- const result = await handler(JSON.stringify({ title: 'T' }));
48
- expect(result.statusCode).toBe(409);
49
- });
50
-
51
- it('hides unknown errors behind a 500 and logs them', async () => {
52
- const logs: string[] = [];
53
- const handler = makeCreateNoteHandler(
54
- stubDeps({
55
- notes: { put: () => Promise.reject(new Error('secret detail')) },
56
- log: createLogger({}, (line) => logs.push(line)),
57
- }),
58
- );
59
- const result = await handler(JSON.stringify({ title: 'T' }));
60
- expect(result.statusCode).toBe(500);
61
- expect(result.body).not.toContain('secret detail');
62
- expect(logs.join('\n')).toContain('secret detail');
63
- });
64
- });
@@ -1,43 +0,0 @@
1
- import { InvalidNoteError, type Note, type NoteCreatedEvent } from '@app/core';
2
- import { createLogger } from '@app/shared';
3
- import { describe, expect, it } from 'vitest';
4
- import { createNoteUsecase, type CreateNoteDeps } from '../src/usecases/create-note.js';
5
-
6
- function stubDeps() {
7
- const stored: Note[] = [];
8
- const published: NoteCreatedEvent[] = [];
9
- const logs: string[] = [];
10
- const deps: CreateNoteDeps = {
11
- notes: { put: async (note) => void stored.push(note) },
12
- events: { publish: async (event) => void published.push(event) },
13
- newId: () => 'fixed-id',
14
- now: () => '2024-01-01T00:00:00.000Z',
15
- log: createLogger({}, (line) => logs.push(line)),
16
- };
17
- return { deps, stored, published, logs };
18
- }
19
-
20
- describe('createNoteUsecase', () => {
21
- it('creates, stores, publishes, and logs', async () => {
22
- const { deps, stored, published, logs } = stubDeps();
23
- const note = await createNoteUsecase({ title: 'Hello World' }, deps);
24
- expect(note).toMatchObject({ id: 'fixed-id', slug: 'hello-world' });
25
- expect(stored).toEqual([note]);
26
- expect(published).toEqual([{ type: 'note.created', note }]);
27
- expect(logs.join('\n')).toContain('fixed-id');
28
- });
29
-
30
- it('rejects invalid input before any side effect', async () => {
31
- const { deps, stored, published } = stubDeps();
32
- await expect(createNoteUsecase({ title: '' }, deps)).rejects.toThrow(InvalidNoteError);
33
- expect(stored).toEqual([]);
34
- expect(published).toEqual([]);
35
- });
36
-
37
- it('propagates store failures and does not publish', async () => {
38
- const { deps, published } = stubDeps();
39
- deps.notes = { put: () => Promise.reject(new Error('disk full')) };
40
- await expect(createNoteUsecase({ title: 'T' }, deps)).rejects.toThrow('disk full');
41
- expect(published).toEqual([]);
42
- });
43
- });