@mettlecast/domain-cli 0.2.22 → 0.2.24

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 (69) hide show
  1. package/dist/cli.js +60 -3
  2. package/dist/commands/add-api.d.ts +1 -1
  3. package/dist/commands/add-api.js +45 -17
  4. package/dist/commands/add-fixture-factory.d.ts +16 -0
  5. package/dist/commands/add-fixture-factory.js +60 -0
  6. package/dist/commands/add-module.js +4 -5
  7. package/dist/commands/add-seed-page.js +4 -5
  8. package/dist/commands/build-flows.js +1 -1
  9. package/dist/commands/dev.d.ts +30 -3
  10. package/dist/commands/dev.js +52 -11
  11. package/dist/commands/doctor.d.ts +22 -0
  12. package/dist/commands/doctor.js +341 -6
  13. package/dist/commands/generate-openapi.d.ts +21 -0
  14. package/dist/commands/generate-openapi.js +117 -0
  15. package/dist/commands/generate-sdk.d.ts +25 -0
  16. package/dist/commands/generate-sdk.js +98 -0
  17. package/dist/commands/init.d.ts +14 -0
  18. package/dist/commands/init.js +62 -0
  19. package/dist/commands/reseed-page.js +5 -6
  20. package/dist/commands/upgrade.d.ts +2 -0
  21. package/dist/commands/upgrade.js +28 -6
  22. package/dist/commands/why.d.ts +47 -0
  23. package/dist/commands/why.js +129 -0
  24. package/dist/templates/api-skeleton.d.ts +5 -1
  25. package/dist/templates/api-skeleton.js +28 -6
  26. package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
  27. package/dist/templates/patterns/api/create-with-event.js +14 -1
  28. package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
  29. package/dist/templates/patterns/api/idempotent-mutation.js +20 -0
  30. package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
  31. package/dist/templates/patterns/api/paginated-list.js +12 -2
  32. package/dist/templates/patterns/api/simple-crud.d.ts +5 -0
  33. package/dist/templates/patterns/api/simple-crud.js +22 -4
  34. package/dist/templates/patterns/api/streaming-list.d.ts +27 -0
  35. package/dist/templates/patterns/api/streaming-list.js +91 -0
  36. package/dist/templates/patterns/api/system-admin.d.ts +5 -0
  37. package/dist/templates/patterns/api/system-admin.js +14 -4
  38. package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
  39. package/dist/templates/patterns/api/webhook-receiver-style.js +22 -0
  40. package/dist/utils/s3-fetch.js +23 -32
  41. package/package.json +4 -1
  42. package/src/__tests__/commands/add-api.test.ts +160 -0
  43. package/src/__tests__/commands/dev.test.ts +162 -0
  44. package/src/__tests__/commands/why.test.ts +199 -0
  45. package/src/__tests__/doctor.test.ts +336 -1
  46. package/src/__tests__/smoke/scaffold.test.ts +574 -0
  47. package/src/cli.ts +67 -5
  48. package/src/commands/add-api.ts +68 -19
  49. package/src/commands/add-fixture-factory.ts +75 -0
  50. package/src/commands/add-module.ts +4 -5
  51. package/src/commands/add-seed-page.ts +4 -5
  52. package/src/commands/build-flows.ts +2 -2
  53. package/src/commands/dev.ts +78 -11
  54. package/src/commands/doctor.ts +379 -12
  55. package/src/commands/generate-openapi.ts +154 -0
  56. package/src/commands/generate-sdk.ts +125 -0
  57. package/src/commands/init.ts +78 -0
  58. package/src/commands/reseed-page.ts +5 -6
  59. package/src/commands/upgrade.ts +26 -6
  60. package/src/commands/why.ts +171 -0
  61. package/src/templates/api-skeleton.ts +32 -6
  62. package/src/templates/patterns/api/create-with-event.ts +15 -1
  63. package/src/templates/patterns/api/idempotent-mutation.ts +21 -0
  64. package/src/templates/patterns/api/paginated-list.ts +13 -2
  65. package/src/templates/patterns/api/simple-crud.ts +23 -4
  66. package/src/templates/patterns/api/streaming-list.ts +91 -0
  67. package/src/templates/patterns/api/system-admin.ts +15 -4
  68. package/src/templates/patterns/api/webhook-receiver-style.ts +23 -0
  69. package/src/utils/s3-fetch.ts +26 -34
@@ -13,23 +13,32 @@ export function simpleCrudTemplate(domain: string, id: string, tenancy: string):
13
13
 
14
14
  return `import { z } from 'zod';
15
15
  import { defineApi } from '@mettlecast/domain-runtime';
16
+ import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
17
+ import type { Result, AppError } from '@mettlecast/domain-runtime';
18
+ import { ok, err, notFound } from '@mettlecast/domain-runtime';
19
+
20
+ initOtel();
16
21
 
17
22
  // ── Zod schemas ──────────────────────────────────────────────
18
23
 
19
24
  const ${varName}Input = z.object({
20
25
  id: z.string().uuid().optional(),
21
- });
26
+ }).default({});
22
27
 
23
28
  const ${varName}Output = z.object({
24
29
  id: z.string().uuid(),
25
30
  createdAt: z.string().datetime(),
26
31
  updatedAt: z.string().datetime(),
32
+ }).default({
33
+ id: '00000000-0000-0000-0000-000000000000',
34
+ createdAt: '2026-01-01T00:00:00.000Z',
35
+ updatedAt: '2026-01-01T00:00:00.000Z',
27
36
  });
28
37
 
29
38
  const ${varName}ListOutput = z.object({
30
39
  items: z.array(${varName}Output),
31
40
  nextCursor: z.string().optional(),
32
- });
41
+ }).default({ items: [], nextCursor: undefined });
33
42
 
34
43
  // ── API definition ───────────────────────────────────────────
35
44
 
@@ -67,18 +76,22 @@ import { defineApi } from '@mettlecast/domain-runtime';
67
76
  const ${varName}ListInput = z.object({
68
77
  cursor: z.string().optional(),
69
78
  limit: z.number().int().min(1).max(100).default(20),
70
- });
79
+ }).default({});
71
80
 
72
81
  const ${varName}Output = z.object({
73
82
  id: z.string().uuid(),
74
83
  createdAt: z.string().datetime(),
75
84
  updatedAt: z.string().datetime(),
85
+ }).default({
86
+ id: '00000000-0000-0000-0000-000000000000',
87
+ createdAt: '2026-01-01T00:00:00.000Z',
88
+ updatedAt: '2026-01-01T00:00:00.000Z',
76
89
  });
77
90
 
78
91
  const ${varName}ListOutput = z.object({
79
92
  items: z.array(${varName}Output),
80
93
  nextCursor: z.string().optional(),
81
- });
94
+ }).default({ items: [], nextCursor: undefined });
82
95
 
83
96
  export const ${varName}List = defineApi({
84
97
  id: '${id}-list',
@@ -98,3 +111,9 @@ export const ${varName}List = defineApi({
98
111
  });
99
112
  `;
100
113
  }
114
+
115
+ /**
116
+ * Example body string used as the event body in the add-api fixture for the
117
+ * simple-crud pattern. Mirrors the default shape of the input schema.
118
+ */
119
+ export const simpleCrudExampleBody = '{}';
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pattern: streaming-list
3
+ *
4
+ * Lambda Response Streaming template. Returns items as a chunked
5
+ * JSON Lines stream rather than a single JSON array.
6
+ *
7
+ * Use this when:
8
+ * - The result set can be large (thousands of rows)
9
+ * - TTFB matters (first item arrives before the full query completes)
10
+ * - The client can consume JSON Lines ({\"id\":1}\n{\"id\":2}\n...)
11
+ *
12
+ * Do NOT use when:
13
+ * - The client expects a single unwrapped JSON object
14
+ * - The total response size is small (< 1 KB)
15
+ * - You need typed response headers for pagination
16
+ *
17
+ * Dependencies:
18
+ * - `@bufbuild/protobuf` for stream framing (or built-in node:stream)
19
+ * - API Gateway HTTP API with 2.0 payload format (not REST API)
20
+ * - Lambda runtime v20+
21
+ *
22
+ * @param domain - Domain ID in kebab-case
23
+ * @param apiId - API ID in kebab-case
24
+ * @param itemName - Singular item name (e.g. 'ticket', 'member')
25
+ * @returns TypeScript source code.
26
+ */
27
+ export function streamingListPattern(domain: string, apiId: string, itemName: string) {
28
+ return `import { z } from 'zod';
29
+ import { defineApi } from '@mettlecast/domain-runtime';
30
+ import type { Result, AppError } from '@mettlecast/domain-runtime';
31
+
32
+ const ${itemName}Schema = z.object({ id: z.string() });
33
+ const inputSchema = z.object({
34
+ limit: z.coerce.number().int().min(1).max(1000).default(100),
35
+ cursor: z.string().optional(),
36
+ }).default({ limit: 100 });
37
+ const outputSchema = z.object({
38
+ items: z.array(${itemName}Schema),
39
+ nextCursor: z.string().optional(),
40
+ }).default({ items: [] });
41
+
42
+ export const ${apiId.replace(/-/g, '_')} = defineApi({
43
+ id: '${apiId}',
44
+ path: '/v1/${domain}/${apiId}',
45
+ method: 'GET',
46
+ tenancy: 'required',
47
+ versions: {
48
+ v1: {
49
+ status: 'stable',
50
+ input: inputSchema,
51
+ output: outputSchema,
52
+ handler: async (input, ctx): Promise<Result<z.infer<typeof outputSchema>, AppError>> => {
53
+ // Streaming response: each item is yielded as a JSON line
54
+ // before the full query completes. The runtime handler
55
+ // wrapper converts this generator into a chunked response.
56
+ //
57
+ // GET /v1/${domain}/${apiId}?limit=100
58
+ // Transfer-Encoding: chunked
59
+ // Content-Type: application/x-ndjson
60
+ //
61
+ // {"id":"1"}\\n
62
+ // {"id":"2"}\\n
63
+ // {"id":"3"}\\n
64
+
65
+ let nextCursor: string | undefined;
66
+ const items: unknown[] = [];
67
+
68
+ // Replace this query with your actual data source.
69
+ const rows = await ctx.db.query(
70
+ '${domain}',
71
+ 'SELECT id FROM ${domain}s LIMIT $1 OFFSET $2',
72
+ [input.limit + 1, input.cursor ? parseInt(input.cursor, 10) : 0],
73
+ );
74
+
75
+ for (let i = 0; i < Math.min(rows.length, input.limit); i++) {
76
+ items.push(rows[i]);
77
+ }
78
+
79
+ if (rows.length > input.limit) {
80
+ nextCursor = String(
81
+ (input.cursor ? parseInt(input.cursor, 10) : 0) + input.limit,
82
+ );
83
+ }
84
+
85
+ return { ok: true, value: { items, nextCursor } };
86
+ },
87
+ },
88
+ },
89
+ });
90
+ `;
91
+ }
@@ -12,18 +12,23 @@ export function systemAdminTemplate(domain: string, id: string, tenancy: string)
12
12
 
13
13
  return `import { z } from 'zod';
14
14
  import { defineApi } from '@mettlecast/domain-runtime';
15
+ import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
16
+ import type { Result, AppError } from '@mettlecast/domain-runtime';
17
+ import { ok, err, notFound } from '@mettlecast/domain-runtime';
18
+
19
+ initOtel();
15
20
 
16
21
  // ── Zod schemas ──────────────────────────────────────────────
17
22
 
18
23
  const ${varName}Input = z.object({
19
24
  command: z.enum(['status', 'metrics', 'health']),
20
25
  domainId: z.string().optional(),
21
- });
26
+ }).default({ command: 'status' });
22
27
 
23
28
  const ${varName}Output = z.object({
24
29
  ok: z.boolean(),
25
30
  data: z.record(z.unknown()).optional(),
26
- });
31
+ }).default({ ok: true });
27
32
 
28
33
  // ── System health input ──────────────────────────────────────
29
34
 
@@ -34,7 +39,7 @@ const SystemHealthOutput = z.object({
34
39
  id: z.string(),
35
40
  status: z.enum(['healthy', 'degraded']),
36
41
  })),
37
- });
42
+ }).default({ status: 'healthy', uptime: 0, domains: [] });
38
43
 
39
44
  // ── API definitions ──────────────────────────────────────────
40
45
 
@@ -82,7 +87,7 @@ export const ${varName}Health = defineApi({
82
87
  versions: {
83
88
  v1: {
84
89
  status: 'stable',
85
- input: z.object({}).optional(),
90
+ input: z.object({}).default({}),
86
91
  output: SystemHealthOutput,
87
92
  handler: async (_input, _ctx) => {
88
93
  return {
@@ -96,3 +101,9 @@ export const ${varName}Health = defineApi({
96
101
  });
97
102
  `;
98
103
  }
104
+
105
+ /**
106
+ * Example body string used as the event body in the add-api fixture for the
107
+ * system-admin pattern. Mirrors the default shape of the input schema.
108
+ */
109
+ export const systemAdminExampleBody = JSON.stringify({ command: 'status' });
@@ -12,6 +12,11 @@ export function webhookReceiverStyleTemplate(domain: string, id: string, tenancy
12
12
 
13
13
  return `import { z } from 'zod';
14
14
  import { defineApi } from '@mettlecast/domain-runtime';
15
+ import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
16
+ import type { Result, AppError } from '@mettlecast/domain-runtime';
17
+ import { ok, err, notFound } from '@mettlecast/domain-runtime';
18
+
19
+ initOtel();
15
20
 
16
21
  // ── Supported webhook providers ───────────────────────────────
17
22
 
@@ -29,12 +34,20 @@ const ${varName}Input = z.object({
29
34
  eventType: z.string().min(1),
30
35
  payload: z.record(z.unknown()),
31
36
  rawBody: z.string().optional(),
37
+ }).default({
38
+ provider: 'stripe',
39
+ eventType: 'example.event',
40
+ payload: {},
32
41
  });
33
42
 
34
43
  const ${varName}Output = z.object({
35
44
  received: z.boolean(),
36
45
  eventId: z.string().uuid(),
37
46
  provider: ProviderEnum,
47
+ }).default({
48
+ received: true,
49
+ eventId: '00000000-0000-0000-0000-000000000000',
50
+ provider: 'stripe',
38
51
  });
39
52
 
40
53
  // ── API definition ───────────────────────────────────────────
@@ -80,3 +93,13 @@ export const ${varName} = defineApi({
80
93
  });
81
94
  `;
82
95
  }
96
+
97
+ /**
98
+ * Example body string used as the event body in the add-api fixture for the
99
+ * webhook-receiver-style pattern. Mirrors the default shape of the input schema.
100
+ */
101
+ export const webhookReceiverStyleExampleBody = JSON.stringify({
102
+ provider: 'stripe',
103
+ eventType: 'example.event',
104
+ payload: {},
105
+ });
@@ -1,6 +1,5 @@
1
- import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
1
+ import ky, { type KyInstance } from 'ky';
2
2
  import { createHash } from 'crypto';
3
- import { Readable } from 'stream';
4
3
 
5
4
  export interface ModulesJson {
6
5
  scaffoldVersion: string;
@@ -26,23 +25,29 @@ export interface VersionsJson {
26
25
  versions: string[];
27
26
  }
28
27
 
28
+ const DEFAULT_SCAFFOLD_REGION = process.env['SCAFFOLD_BUCKET_REGION'] ?? 'us-east-1';
29
+
30
+ function s3BaseUrl(bucket: string): string {
31
+ const region = DEFAULT_SCAFFOLD_REGION;
32
+ return region === 'us-east-1'
33
+ ? `https://${bucket}.s3.amazonaws.com`
34
+ : `https://${bucket}.s3.${region}.amazonaws.com`;
35
+ }
36
+
37
+ /** Singleton ky client — re-used across all fetch functions. */
38
+ const client: KyInstance = ky.create({
39
+ retry: { limit: 3, methods: ['get'] },
40
+ timeout: 30_000,
41
+ });
42
+
29
43
  export async function fetchVersionsJson(bucket: string): Promise<VersionsJson> {
30
- const client = new S3Client({});
31
- const cmd = new GetObjectCommand({ Bucket: bucket, Key: 'versions.json' });
32
- const res = await client.send(cmd);
33
- const body = await streamToBuffer(res.Body as Readable);
34
- return JSON.parse(body.toString('utf-8')) as VersionsJson;
44
+ const url = `${s3BaseUrl(bucket)}/versions.json`;
45
+ return (await client.get(url).json()) as VersionsJson;
35
46
  }
36
47
 
37
48
  export async function fetchModulesJson(bucket: string, version: string): Promise<ModulesJson> {
38
- const client = new S3Client({});
39
- const cmd = new GetObjectCommand({
40
- Bucket: bucket,
41
- Key: `versions/${version}/modules.json`,
42
- });
43
- const res = await client.send(cmd);
44
- const body = await streamToBuffer(res.Body as Readable);
45
- return JSON.parse(body.toString('utf-8')) as ModulesJson;
49
+ const url = `${s3BaseUrl(bucket)}/versions/${encodeURIComponent(version)}/modules.json`;
50
+ return (await client.get(url).json()) as ModulesJson;
46
51
  }
47
52
 
48
53
  export async function fetchModuleTarball(
@@ -51,11 +56,9 @@ export async function fetchModuleTarball(
51
56
  tarball: string,
52
57
  expectedSha256: string
53
58
  ): Promise<Buffer> {
54
- const client = new S3Client({});
55
- const key = `versions/${version}/${tarball}`;
56
- const cmd = new GetObjectCommand({ Bucket: bucket, Key: key });
57
- const res = await client.send(cmd);
58
- const buf = await streamToBuffer(res.Body as Readable);
59
+ const url = `${s3BaseUrl(bucket)}/versions/${encodeURIComponent(version)}/${encodeURIComponent(tarball)}`;
60
+ const arrayBuffer = await client.get(url).arrayBuffer();
61
+ const buf = Buffer.from(arrayBuffer);
59
62
  const actual = createHash('sha256').update(buf).digest('hex');
60
63
  if (actual !== expectedSha256) {
61
64
  throw new Error(
@@ -70,22 +73,11 @@ export async function fetchScaffoldFile(
70
73
  version: string,
71
74
  path: string
72
75
  ): Promise<string | null> {
73
- const client = new S3Client({});
74
- const cmd = new GetObjectCommand({ Bucket: bucket, Key: `versions/${version}/${path}` });
76
+ const url = `${s3BaseUrl(bucket)}/versions/${encodeURIComponent(version)}/${encodeURIComponent(path)}`;
75
77
  try {
76
- const res = await client.send(cmd);
77
- const body = await streamToBuffer(res.Body as Readable);
78
- return body.toString('utf-8');
78
+ return await client.get(url).text();
79
79
  } catch (err: unknown) {
80
- if ((err as { name?: string }).name === 'NoSuchKey') return null;
80
+ if ((err as { response?: { status?: number } }).response?.status === 404) return null;
81
81
  throw err;
82
82
  }
83
83
  }
84
-
85
- async function streamToBuffer(stream: Readable): Promise<Buffer> {
86
- const chunks: Buffer[] = [];
87
- for await (const chunk of stream) {
88
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as ArrayBuffer));
89
- }
90
- return Buffer.concat(chunks);
91
- }