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
@@ -0,0 +1,63 @@
1
+ // The model boundary, same contract as any other target — only the medium
2
+ // differs: one JSON file. This is the ONLY module that knows how notes are
3
+ // persisted; nothing else composes a path into the data file.
4
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { NoteSchema, type Note } from '@app/core';
7
+ import { AppError, NotFoundError } from '@app/shared';
8
+
9
+ type NoteTable = Record<string, unknown>;
10
+
11
+ export class JsonFileNoteStore {
12
+ constructor(private readonly file: string) {}
13
+
14
+ async put(note: Note): Promise<void> {
15
+ const table = await this.load();
16
+ if (note.id in table) {
17
+ throw new AppError(`note ${note.id} already exists`, { code: 'CONFLICT', statusCode: 409 });
18
+ }
19
+ table[note.id] = note;
20
+ await this.save(table);
21
+ }
22
+
23
+ async get(id: string): Promise<Note> {
24
+ const table = await this.load();
25
+ const raw = table[id];
26
+ if (raw === undefined) {
27
+ throw new NotFoundError(`note ${id} not found`);
28
+ }
29
+ // Validate on the way out: the file is an external system.
30
+ return NoteSchema.parse(raw);
31
+ }
32
+
33
+ async list(): Promise<Note[]> {
34
+ const table = await this.load();
35
+ // Validate every entry — silently skipping corruption would hide data loss.
36
+ return Object.values(table)
37
+ .map((raw) => NoteSchema.parse(raw))
38
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
39
+ }
40
+
41
+ private async load(): Promise<NoteTable> {
42
+ let content: string;
43
+ try {
44
+ content = await readFile(this.file, 'utf8');
45
+ } catch (error) {
46
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return {};
47
+ throw error;
48
+ }
49
+ try {
50
+ return JSON.parse(content) as NoteTable;
51
+ } catch (cause) {
52
+ throw new AppError(`data file ${this.file} is corrupt`, { code: 'DATA_CORRUPT', cause });
53
+ }
54
+ }
55
+
56
+ private async save(table: NoteTable): Promise<void> {
57
+ await mkdir(path.dirname(this.file), { recursive: true });
58
+ // Write-then-rename keeps readers from ever seeing a half-written file.
59
+ const tmp = `${this.file}.tmp`;
60
+ await writeFile(tmp, JSON.stringify(table, null, 2));
61
+ await rename(tmp, this.file);
62
+ }
63
+ }
@@ -0,0 +1,80 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { AppError, NotFoundError } from '@app/shared';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { JsonFileNoteStore } from '../src/note-store.js';
7
+
8
+ const note = {
9
+ id: 'n1',
10
+ title: 'Hello',
11
+ slug: 'hello',
12
+ tags: ['x'],
13
+ createdAt: '2024-01-01T00:00:00.000Z',
14
+ };
15
+
16
+ let dir: string;
17
+ let file: string;
18
+
19
+ beforeEach(async () => {
20
+ dir = await mkdtemp(path.join(tmpdir(), 'note-store-'));
21
+ file = path.join(dir, 'data', 'notes.json');
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await rm(dir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe('JsonFileNoteStore', () => {
29
+ it('round-trips a note (creating the data dir on first write)', async () => {
30
+ const store = new JsonFileNoteStore(file);
31
+ await store.put(note);
32
+ expect(await store.get('n1')).toEqual(note);
33
+ expect(JSON.parse(await readFile(file, 'utf8'))).toEqual({ n1: note });
34
+ });
35
+
36
+ it('refuses to overwrite an existing id', async () => {
37
+ const store = new JsonFileNoteStore(file);
38
+ await store.put(note);
39
+ await expect(store.put(note)).rejects.toThrow(AppError);
40
+ await expect(store.put(note)).rejects.toMatchObject({ statusCode: 409 });
41
+ });
42
+
43
+ it('throws NotFoundError on a miss', async () => {
44
+ await expect(new JsonFileNoteStore(file).get('nope')).rejects.toThrow(NotFoundError);
45
+ });
46
+
47
+ it('refuses corrupt stored entries instead of returning them', async () => {
48
+ const store = new JsonFileNoteStore(file);
49
+ await store.put(note);
50
+ const table = JSON.parse(await readFile(file, 'utf8'));
51
+ table.n1 = { id: 'n1' };
52
+ await writeFile(file, JSON.stringify(table));
53
+ await expect(store.get('n1')).rejects.toThrow();
54
+ });
55
+
56
+ it('reports a corrupt data file clearly', async () => {
57
+ const store = new JsonFileNoteStore(file);
58
+ await store.put(note);
59
+ await writeFile(file, '{not json');
60
+ await expect(store.get('n1')).rejects.toMatchObject({ code: 'DATA_CORRUPT' });
61
+ });
62
+
63
+ it('lists notes newest-first, and an empty store as []', async () => {
64
+ const store = new JsonFileNoteStore(file);
65
+ expect(await store.list()).toEqual([]);
66
+ await store.put({ ...note, id: 'a', createdAt: '2024-01-01T00:00:00.000Z' });
67
+ await store.put({ ...note, id: 'b', createdAt: '2024-02-01T00:00:00.000Z' });
68
+ const listed = await store.list();
69
+ expect(listed.map((n) => n.id)).toEqual(['b', 'a']);
70
+ });
71
+
72
+ it('refuses to list corrupt entries instead of skipping them silently', async () => {
73
+ const store = new JsonFileNoteStore(file);
74
+ await store.put(note);
75
+ const table = JSON.parse(await readFile(file, 'utf8'));
76
+ table.bad = { id: 'bad' };
77
+ await writeFile(file, JSON.stringify(table));
78
+ await expect(store.list()).rejects.toThrow();
79
+ });
80
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@app/shared",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "dependencies": {
9
+ "zod": "^4.4.3"
10
+ }
11
+ }
@@ -0,0 +1,17 @@
1
+ import type { ZodType } from 'zod';
2
+ import { AppError } from './errors.js';
3
+
4
+ /**
5
+ * Parse the environment through a zod schema, once, at startup. Configuration
6
+ * is invalid → the process fails loudly at boot, not deep inside a request.
7
+ */
8
+ export function loadEnv<T>(schema: ZodType<T>, source: Record<string, string | undefined>): T {
9
+ const parsed = schema.safeParse(source);
10
+ if (!parsed.success) {
11
+ const details = parsed.error.issues
12
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
13
+ .join('; ');
14
+ throw new AppError(`invalid environment: ${details}`, { code: 'ENV_INVALID' });
15
+ }
16
+ return parsed.data;
17
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed errors. Every error that crosses a layer boundary is one of these —
3
+ * handlers map them to transport codes without inspecting messages.
4
+ */
5
+ export class AppError extends Error {
6
+ readonly code: string;
7
+ readonly statusCode: number;
8
+
9
+ constructor(
10
+ message: string,
11
+ options: { code?: string; statusCode?: number; cause?: unknown } = {},
12
+ ) {
13
+ super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
14
+ this.name = new.target.name;
15
+ this.code = options.code ?? 'INTERNAL';
16
+ this.statusCode = options.statusCode ?? 500;
17
+ }
18
+ }
19
+
20
+ export class ValidationError extends AppError {
21
+ readonly issues: readonly string[];
22
+
23
+ constructor(message: string, issues: readonly string[] = []) {
24
+ super(message, { code: 'VALIDATION', statusCode: 400 });
25
+ this.issues = issues;
26
+ }
27
+ }
28
+
29
+ export class NotFoundError extends AppError {
30
+ constructor(message: string) {
31
+ super(message, { code: 'NOT_FOUND', statusCode: 404 });
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export { AppError, NotFoundError, ValidationError } from './errors.js';
2
+ export { createLogger, type Logger } from './logger.js';
3
+ export { loadEnv } from './env.js';
@@ -0,0 +1,20 @@
1
+ /** Minimal structured logger: one JSON object per line on stdout. */
2
+ export interface Logger {
3
+ info(message: string, fields?: Record<string, unknown>): void;
4
+ warn(message: string, fields?: Record<string, unknown>): void;
5
+ error(message: string, fields?: Record<string, unknown>): void;
6
+ }
7
+
8
+ export function createLogger(
9
+ base: Record<string, unknown> = {},
10
+ write: (line: string) => void = (line) => process.stdout.write(line + '\n'),
11
+ ): Logger {
12
+ const emit = (level: string, message: string, fields?: Record<string, unknown>) => {
13
+ write(JSON.stringify({ level, message, ...base, ...fields }));
14
+ };
15
+ return {
16
+ info: (message, fields) => emit('info', message, fields),
17
+ warn: (message, fields) => emit('warn', message, fields),
18
+ error: (message, fields) => emit('error', message, fields),
19
+ };
20
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { AppError } from '../src/errors.js';
4
+ import { loadEnv } from '../src/env.js';
5
+
6
+ const schema = z.object({
7
+ TABLE_NAME: z.string().min(1),
8
+ QUEUE_URL: z.string().min(1),
9
+ });
10
+
11
+ describe('loadEnv', () => {
12
+ it('returns the typed subset of the environment', () => {
13
+ const env = loadEnv(schema, { TABLE_NAME: 't', QUEUE_URL: 'q', UNRELATED: 'x' });
14
+ expect(env).toEqual({ TABLE_NAME: 't', QUEUE_URL: 'q' });
15
+ });
16
+
17
+ it('fails loudly, naming every missing key', () => {
18
+ expect(() => loadEnv(schema, {})).toThrow(AppError);
19
+ expect(() => loadEnv(schema, {})).toThrow(/TABLE_NAME/);
20
+ expect(() => loadEnv(schema, {})).toThrow(/QUEUE_URL/);
21
+ });
22
+
23
+ it('rejects empty values', () => {
24
+ expect(() => loadEnv(schema, { TABLE_NAME: '', QUEUE_URL: 'q' })).toThrow(/TABLE_NAME/);
25
+ });
26
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AppError, NotFoundError, ValidationError } from '../src/errors.js';
3
+
4
+ describe('typed errors', () => {
5
+ it('AppError defaults to INTERNAL/500', () => {
6
+ const error = new AppError('boom');
7
+ expect(error.code).toBe('INTERNAL');
8
+ expect(error.statusCode).toBe(500);
9
+ expect(error.name).toBe('AppError');
10
+ });
11
+
12
+ it('preserves an explicit cause', () => {
13
+ const cause = new Error('root');
14
+ expect(new AppError('boom', { cause }).cause).toBe(cause);
15
+ });
16
+
17
+ it('ValidationError is 400 and carries issues', () => {
18
+ const error = new ValidationError('bad input', ['title: required']);
19
+ expect(error.statusCode).toBe(400);
20
+ expect(error.code).toBe('VALIDATION');
21
+ expect(error.issues).toEqual(['title: required']);
22
+ expect(error).toBeInstanceOf(AppError);
23
+ });
24
+
25
+ it('NotFoundError is 404', () => {
26
+ expect(new NotFoundError('nope').statusCode).toBe(404);
27
+ });
28
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createLogger } from '../src/logger.js';
3
+
4
+ describe('createLogger', () => {
5
+ it('emits one JSON line per call with level, message, base and call fields', () => {
6
+ const lines: string[] = [];
7
+ const log = createLogger({ service: 'api' }, (line) => lines.push(line));
8
+ log.info('started', { port: 3000 });
9
+ log.error('failed');
10
+ expect(lines).toHaveLength(2);
11
+ expect(JSON.parse(lines[0]!)).toEqual({
12
+ level: 'info',
13
+ message: 'started',
14
+ service: 'api',
15
+ port: 3000,
16
+ });
17
+ expect(JSON.parse(lines[1]!)).toMatchObject({ level: 'error', message: 'failed' });
18
+ });
19
+ });