@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
package/dist/cli.js CHANGED
@@ -31,6 +31,11 @@ import { runReseedPage } from './commands/reseed-page.js';
31
31
  import { runAddSeedPage } from './commands/add-seed-page.js';
32
32
  import { runShowDns } from './commands/show-dns.js';
33
33
  import { runBuildUi } from './commands/build-ui.js';
34
+ import { runWhyCli } from './commands/why.js';
35
+ import { runGenerateOpenapiCli } from './commands/generate-openapi.js';
36
+ import { runGenerateSdkCli } from './commands/generate-sdk.js';
37
+ import { runAddFixtureFactoryCli } from './commands/add-fixture-factory.js';
38
+ import { runInitCli } from './commands/init.js';
34
39
  /**
35
40
  * Root CLI program for the TIB Domain Module local developer toolchain.
36
41
  * Provides build, validate, test, dev, add-domain, add-api, and add-subscriber subcommands.
@@ -130,12 +135,19 @@ program
130
135
  .description('Run all structural checks for the current TIB project (strict by default — any FAIL causes exit code 1)')
131
136
  .option('--strict', 'DEPRECATED: Doctor is always strict now. This flag is a no-op.', false)
132
137
  .option('--json', 'Output JSON instead of pretty table', false)
138
+ .option('--fix', 'Attempt to automatically fix issues that have a safe automatic fix', false)
133
139
  .action(async (opts) => {
134
140
  if (opts.strict) {
135
141
  // eslint-disable-next-line no-console
136
142
  console.warn('⚠ --strict is deprecated: doctor is always strict now. This flag is a no-op.');
137
143
  }
138
- const report = await runDoctor({ projectRoot: process.cwd() });
144
+ // `fix` is part of DoctorOptions in Wave 1 (Task doctor-fix). Cast through
145
+ // `unknown` so this file type-checks today and after the doctor-fix worker
146
+ // adds the `fix?: boolean` field to DoctorOptions.
147
+ const report = await runDoctor({
148
+ projectRoot: process.cwd(),
149
+ fix: opts.fix,
150
+ });
139
151
  if (opts.json) {
140
152
  // eslint-disable-next-line no-console
141
153
  console.log(JSON.stringify(report, null, 2));
@@ -247,13 +259,14 @@ program
247
259
  });
248
260
  program
249
261
  .command('upgrade [version]')
250
- .description('Upgrade scaffold to a new version — diffs S3 tarballs, raises a GitHub PR with exact file changes')
262
+ .description('Upgrade, check for drift, or dry-run scaffold to a new version — diffs S3 tarballs and (when not --check or --dry-run) raises a GitHub PR with exact file changes')
251
263
  .option('--dry-run', 'Preview changes without writing files or creating PR', false)
264
+ .option('--check', 'Read-only CI gate: compute upgrade diff, exit 0 for no drift or 1 if drift detected. Writes nothing.', false)
252
265
  .option('--project-dir <path>', 'Path to the project to upgrade')
253
266
  .option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
254
267
  .option('--frontend-components', 'Scope upgrade to scaffold-tracked frontend files only (opens a PR with Chromatic visual diff)', false)
255
268
  .action(async (version, opts) => {
256
- await runUpgrade(version, { dryRun: opts.dryRun, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
269
+ await runUpgrade(version, { dryRun: opts.dryRun, check: opts.check, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
257
270
  });
258
271
  // ── New W4 commands ───────────────────────────────────────────────
259
272
  program
@@ -339,6 +352,50 @@ program
339
352
  .action(async (opts) => {
340
353
  await runBuildUi({ projectRoot: opts.projectRoot });
341
354
  });
355
+ program
356
+ .command('why')
357
+ .description('Print current scaffold version, enabled modules, and pending upgrade diff to stdout')
358
+ .option('--project-root <path>', 'Path to the project root (defaults to cwd)')
359
+ .option('--scaffold-bucket <name>', 'S3 bucket to fetch versions.json from (defaults to mc-scaffold)')
360
+ .option('--json', 'Output JSON instead of a human-readable report', false)
361
+ .action(async (opts) => {
362
+ await runWhyCli({
363
+ projectRoot: opts.projectRoot,
364
+ scaffoldBucket: opts.scaffoldBucket,
365
+ json: opts.json,
366
+ });
367
+ });
368
+ program
369
+ .command('generate-openapi <domain>')
370
+ .description('Generate an OpenAPI 3.1 spec from a domain registry (.mc/<domain>-registry.json)')
371
+ .option('--project-root <path>', 'Path to the project root (defaults to cwd)')
372
+ .option('--output <path>', 'Override the output file path')
373
+ .action(async (domain, opts) => {
374
+ await runGenerateOpenapiCli(domain, { projectRoot: opts.projectRoot, output: opts.output });
375
+ });
376
+ program
377
+ .command('generate-sdk <domain>')
378
+ .description('Generate a typed TypeScript API client from a domain\'s OpenAPI spec, returning Result<T, AppError> from every call')
379
+ .option('--project-root <path>', 'Path to the project root (defaults to cwd)')
380
+ .option('--input <path>', 'Path to the OpenAPI spec (defaults to domains/<domain>/api/openapi.generated.json)')
381
+ .option('--output <path>', 'Override the output directory')
382
+ .action(async (domain, opts) => {
383
+ await runGenerateSdkCli(domain, { projectRoot: opts.projectRoot, input: opts.input, output: opts.output });
384
+ });
385
+ program
386
+ .command('add-fixture-factory <domain> <api-id>')
387
+ .description('Generate a test data factory with @faker-js/faker for an existing API handler')
388
+ .option('--project-root <path>', 'Path to the project root (defaults to cwd)')
389
+ .action(async (domain, apiId, opts) => {
390
+ await runAddFixtureFactoryCli(domain, apiId, { projectRoot: opts.projectRoot });
391
+ });
392
+ program
393
+ .command('init')
394
+ .description('Bootstrap a freshly scaffolded project: npm install, build CLI, run doctor baseline')
395
+ .option('--project-root <path>', 'Path to the project root (defaults to cwd)')
396
+ .action(async (opts) => {
397
+ await runInitCli({ projectRoot: opts.projectRoot });
398
+ });
342
399
  program.parseAsync(process.argv).catch((err) => {
343
400
  // eslint-disable-next-line no-console
344
401
  console.error(err);
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Valid API pattern IDs.
7
7
  */
8
- export declare const API_PATTERNS: readonly ["simple-crud", "paginated-list", "create-with-event", "idempotent-mutation", "webhook-receiver-style", "system-admin"];
8
+ export declare const API_PATTERNS: readonly ["simple-crud", "paginated-list", "create-with-event", "idempotent-mutation", "webhook-receiver-style", "system-admin", "streaming-list"];
9
9
  export type ApiPattern = (typeof API_PATTERNS)[number];
10
10
  /**
11
11
  * Options for the add-api command.
@@ -5,12 +5,13 @@
5
5
  import { writeFile, access, mkdir } from 'node:fs/promises';
6
6
  import { join, resolve } from 'node:path';
7
7
  import { apiSkeletonTemplate, apiFixtureSkeleton } from '../templates/api-skeleton.js';
8
- import { simpleCrudTemplate, simpleCrudListTemplate } from '../templates/patterns/api/simple-crud.js';
9
- import { paginatedListTemplate } from '../templates/patterns/api/paginated-list.js';
10
- import { createWithEventTemplate } from '../templates/patterns/api/create-with-event.js';
11
- import { idempotentMutationTemplate } from '../templates/patterns/api/idempotent-mutation.js';
12
- import { webhookReceiverStyleTemplate } from '../templates/patterns/api/webhook-receiver-style.js';
13
- import { systemAdminTemplate } from '../templates/patterns/api/system-admin.js';
8
+ import { simpleCrudTemplate, simpleCrudListTemplate, simpleCrudExampleBody, } from '../templates/patterns/api/simple-crud.js';
9
+ import { paginatedListTemplate, paginatedListExampleBody, } from '../templates/patterns/api/paginated-list.js';
10
+ import { createWithEventTemplate, createWithEventExampleBody, } from '../templates/patterns/api/create-with-event.js';
11
+ import { idempotentMutationTemplate, idempotentMutationExampleBody, } from '../templates/patterns/api/idempotent-mutation.js';
12
+ import { webhookReceiverStyleTemplate, webhookReceiverStyleExampleBody, } from '../templates/patterns/api/webhook-receiver-style.js';
13
+ import { systemAdminTemplate, systemAdminExampleBody, } from '../templates/patterns/api/system-admin.js';
14
+ import { streamingListPattern, } from '../templates/patterns/api/streaming-list.js';
14
15
  import { cliLogger } from '../utils/logger.js';
15
16
  /**
16
17
  * Valid API pattern IDs.
@@ -22,6 +23,7 @@ export const API_PATTERNS = [
22
23
  'idempotent-mutation',
23
24
  'webhook-receiver-style',
24
25
  'system-admin',
26
+ 'streaming-list',
25
27
  ];
26
28
  const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
27
29
  /**
@@ -59,8 +61,11 @@ export async function runAddApi(opts) {
59
61
  }
60
62
  // Resolve template content
61
63
  let apiContent;
64
+ let exampleBody = '{}';
62
65
  if (opts.pattern) {
63
- apiContent = resolveApiPattern(opts.pattern, opts.domain, opts.id, opts.tenancy);
66
+ const resolved = resolveApiPattern(opts.pattern, opts.domain, opts.id, opts.tenancy);
67
+ apiContent = resolved.apiContent;
68
+ exampleBody = resolved.exampleBody;
64
69
  cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
65
70
  }
66
71
  else {
@@ -68,10 +73,10 @@ export async function runAddApi(opts) {
68
73
  }
69
74
  // Write API file
70
75
  await writeFile(apiFilePath, apiContent);
71
- // Create fixture
76
+ // Create fixture with the input schema's default shape as the example payload
72
77
  const apiTestDir = join(domainDir, 'api', '__tests__');
73
78
  await mkdir(apiTestDir, { recursive: true });
74
- await writeFile(join(apiTestDir, `${opts.id}.fixture.json`), apiFixtureSkeleton(opts.domain, opts.id));
79
+ await writeFile(join(apiTestDir, `${opts.id}.fixture.json`), apiFixtureSkeleton(opts.domain, opts.id, exampleBody));
75
80
  cliLogger.info({ domain: opts.domain, api: opts.id, method: opts.method ?? 'GET', pattern: opts.pattern ?? 'skeleton' }, 'API added');
76
81
  // eslint-disable-next-line no-console
77
82
  console.log(`\n✓ API "${opts.id}" added to domain "${opts.domain}" at ${apiFilePath}`);
@@ -87,19 +92,42 @@ export async function runAddApi(opts) {
87
92
  function resolveApiPattern(pattern, domain, id, tenancy) {
88
93
  switch (pattern) {
89
94
  case 'simple-crud':
90
- return simpleCrudTemplate(domain, id, tenancy) +
91
- '\n' +
92
- simpleCrudListTemplate(domain, id, tenancy);
95
+ return {
96
+ apiContent: simpleCrudTemplate(domain, id, tenancy) +
97
+ '\n' +
98
+ simpleCrudListTemplate(domain, id, tenancy),
99
+ exampleBody: simpleCrudExampleBody,
100
+ };
93
101
  case 'paginated-list':
94
- return paginatedListTemplate(domain, id, tenancy);
102
+ return {
103
+ apiContent: paginatedListTemplate(domain, id, tenancy),
104
+ exampleBody: paginatedListExampleBody,
105
+ };
95
106
  case 'create-with-event':
96
- return createWithEventTemplate(domain, id, tenancy);
107
+ return {
108
+ apiContent: createWithEventTemplate(domain, id, tenancy),
109
+ exampleBody: createWithEventExampleBody,
110
+ };
97
111
  case 'idempotent-mutation':
98
- return idempotentMutationTemplate(domain, id, tenancy);
112
+ return {
113
+ apiContent: idempotentMutationTemplate(domain, id, tenancy),
114
+ exampleBody: idempotentMutationExampleBody,
115
+ };
99
116
  case 'webhook-receiver-style':
100
- return webhookReceiverStyleTemplate(domain, id, tenancy);
117
+ return {
118
+ apiContent: webhookReceiverStyleTemplate(domain, id, tenancy),
119
+ exampleBody: webhookReceiverStyleExampleBody,
120
+ };
101
121
  case 'system-admin':
102
- return systemAdminTemplate(domain, id, tenancy);
122
+ return {
123
+ apiContent: systemAdminTemplate(domain, id, tenancy),
124
+ exampleBody: systemAdminExampleBody,
125
+ };
126
+ case 'streaming-list':
127
+ return {
128
+ apiContent: streamingListPattern(domain, id, 'item'),
129
+ exampleBody: '{}',
130
+ };
103
131
  default:
104
132
  throw new Error(`Unknown API pattern "${pattern}". Available: ${API_PATTERNS.join(', ')}`);
105
133
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * add-fixture-factory — generate a test data factory alongside an
3
+ * existing API handler. The factory provides `valid()` and `invalid()`
4
+ * functions with realistic test data powered by @faker-js/faker.
5
+ *
6
+ * Usage: npx mc-domain-module add-fixture-factory <domain> <api-id>
7
+ */
8
+ export interface AddFixtureFactoryOptions {
9
+ domain: string;
10
+ apiId: string;
11
+ projectRoot?: string;
12
+ }
13
+ export declare function runAddFixtureFactory(options: AddFixtureFactoryOptions): Promise<string>;
14
+ export declare function runAddFixtureFactoryCli(domain: string, apiId: string, opts?: {
15
+ projectRoot?: string;
16
+ }): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * add-fixture-factory — generate a test data factory alongside an
3
+ * existing API handler. The factory provides `valid()` and `invalid()`
4
+ * functions with realistic test data powered by @faker-js/faker.
5
+ *
6
+ * Usage: npx mc-domain-module add-fixture-factory <domain> <api-id>
7
+ */
8
+ import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { cliLogger } from '../utils/logger.js';
11
+ const FACTORY_TEMPLATE = `// Generated fixture factory for {domain}.{apiId}
12
+ // Uses @faker-js/faker for realistic test data. Override any field
13
+ // by passing overrides to ` + '`valid()`' + `.
14
+ import { faker } from '@faker-js/faker';
15
+
16
+ /** The input shape for {domain}.{apiId} — derive from the Zod schema. */
17
+ type Input = Record<string, unknown>;
18
+
19
+ /** Full, valid input with sensible defaults. */
20
+ export function valid(overrides: Partial<Input> = {}): Input {
21
+ return {
22
+ name: faker.company.name(),
23
+ email: faker.internet.email(),
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ /** Intentionally invalid input — use to test validation failures. */
29
+ export function invalid(): Partial<Input> {
30
+ return {
31
+ name: '', // empty — should fail minLength if required
32
+ email: 'not-an-email',
33
+ };
34
+ }
35
+ `;
36
+ export async function runAddFixtureFactory(options) {
37
+ const projectRoot = options.projectRoot ?? process.cwd();
38
+ const domain = options.domain;
39
+ const apiId = options.apiId;
40
+ const apiFile = join(projectRoot, 'domains', domain, 'api', `${apiId}.ts`);
41
+ await access(apiFile).catch(() => {
42
+ throw new Error(`API file not found: ${apiFile}. Run add-api first.`);
43
+ });
44
+ // Read the API file to extract the input type name
45
+ const content = await readFile(apiFile, 'utf8');
46
+ const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
47
+ const factoryFile = join(projectRoot, 'domains', domain, 'api', '__tests__', `${apiId}.factory.ts`);
48
+ const factoryContent = FACTORY_TEMPLATE
49
+ .replace(/\{domain\}/g, domain)
50
+ .replace(/\{apiId\}/g, apiId);
51
+ await mkdir(join(projectRoot, 'domains', domain, 'api', '__tests__'), { recursive: true });
52
+ await writeFile(factoryFile, factoryContent, 'utf8');
53
+ cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
54
+ return factoryFile;
55
+ }
56
+ export async function runAddFixtureFactoryCli(domain, apiId, opts = {}) {
57
+ const factoryFile = await runAddFixtureFactory({ domain, apiId, ...opts });
58
+ // eslint-disable-next-line no-console
59
+ console.log(`Fixture factory written to: ${factoryFile}`);
60
+ }
@@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
2
2
  import { join } from 'node:path';
3
3
  import { Readable } from 'node:stream';
4
4
  import { Parser as TarParser } from 'tar';
5
+ import ky from 'ky';
5
6
  import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
6
7
  import { readManifest, writeManifest, upsertManifestFile, inferPolicyFromPath } from '../utils/manifest.js';
7
8
  import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
@@ -65,18 +66,16 @@ async function detectRepoInfo() {
65
66
  }
66
67
  async function createGitHubPR(owner, repo, branchName, title, body, token) {
67
68
  try {
68
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
69
- method: 'POST',
69
+ const response = await ky.post(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
70
70
  headers: {
71
71
  Authorization: `Bearer ${token}`,
72
- 'Content-Type': 'application/json',
73
72
  },
74
- body: JSON.stringify({
73
+ json: {
75
74
  title,
76
75
  body,
77
76
  head: branchName,
78
77
  base: 'develop',
79
- }),
78
+ },
80
79
  });
81
80
  if (!response.ok) {
82
81
  const error = await response.json();
@@ -3,6 +3,7 @@ import { join, resolve, dirname } from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { createGunzip } from 'node:zlib';
5
5
  import { Readable } from 'node:stream';
6
+ import ky from 'ky';
6
7
  import { cliLogger } from '../utils/logger.js';
7
8
  import { fetchVersionsJson, fetchModulesJson, fetchModuleTarball, } from '../utils/s3-fetch.js';
8
9
  import { readManifest } from '../utils/manifest.js';
@@ -30,20 +31,18 @@ function detectGitRemote(projectDir) {
30
31
  }
31
32
  async function createGitHubPR(owner, repo, token, branch, title, body) {
32
33
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
33
- const res = await fetch(url, {
34
- method: 'POST',
34
+ const res = await ky.post(url, {
35
35
  headers: {
36
36
  Authorization: `Bearer ${token}`,
37
37
  Accept: 'application/vnd.github+json',
38
- 'Content-Type': 'application/json',
39
38
  'X-GitHub-Api-Version': '2022-11-28',
40
39
  },
41
- body: JSON.stringify({
40
+ json: {
42
41
  title,
43
42
  body,
44
43
  head: branch,
45
44
  base: 'develop',
46
- }),
45
+ },
47
46
  });
48
47
  if (!res.ok) {
49
48
  const err = await res.text();
@@ -57,7 +57,7 @@ const FlowConfigSchema = z.object({
57
57
  steps: z.array(FlowStepSchema).min(1),
58
58
  trigger: z.object({
59
59
  type: z.literal('event'),
60
- eventId: z.string().includes('.'),
60
+ eventId: z.string().refine(val => val.includes('.'), { message: 'eventId must be namespaced (e.g. domain.event)' }),
61
61
  semverRange: z.string().min(1),
62
62
  }).optional(),
63
63
  });
@@ -1,3 +1,5 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
+ import type { FastifyInstance } from 'fastify';
1
3
  /**
2
4
  * Options for the dev command.
3
5
  */
@@ -6,10 +8,35 @@ export interface DevOptions {
6
8
  domainRoot: string;
7
9
  /** Port for the local HTTP server. Defaults to 3000. */
8
10
  port?: number;
11
+ /** Absolute path to the project root. Defaults to process.cwd(). */
12
+ projectRoot?: string;
13
+ /** Port for the Vite frontend dev server. Defaults to 3001. */
14
+ frontendPort?: number;
15
+ /** Override the spawn function (used for tests). Defaults to node:child_process.spawn. */
16
+ spawnFn?: typeof spawn;
9
17
  }
10
18
  /**
11
- * Execute the dev command: build the domain registry and start the local Fastify
12
- * HTTP server. Keeps the process running. Shuts down gracefully on SIGINT/SIGTERM.
13
- * @param options - DevOptions specifying domain root and optional port.
19
+ * Handle returned by startDev, exposing the live processes and a shutdown function.
20
+ */
21
+ export interface DevHandle {
22
+ /** The Fastify API server instance. */
23
+ server: FastifyInstance;
24
+ /** The Vite child process, or null when no frontend/ directory was found. */
25
+ vite: ChildProcess | null;
26
+ /** Shut down both processes gracefully. Idempotent. */
27
+ shutdown: () => Promise<void>;
28
+ }
29
+ /**
30
+ * Start the dev servers (Fastify API + Vite frontend if present) and return a handle.
31
+ * This is the testable entry point — the signal-handling logic lives in runDev.
32
+ * @param options - DevOptions specifying domain root, ports, and project root.
33
+ * @returns A DevHandle with the running server, vite child (or null), and shutdown.
34
+ */
35
+ export declare function startDev(options: DevOptions): Promise<DevHandle>;
36
+ /**
37
+ * Execute the dev command: build the domain registry, start the local Fastify
38
+ * HTTP server, and (if frontend/ exists) start a Vite dev server alongside it.
39
+ * Keeps the process running. Shuts down both children gracefully on SIGINT/SIGTERM.
40
+ * @param options - DevOptions specifying domain root, ports, and project root.
14
41
  */
15
42
  export declare function runDev(options: DevOptions): Promise<void>;
@@ -1,15 +1,21 @@
1
- import { resolve } from 'node:path';
1
+ import { resolve, join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
2
4
  import { buildRegistry } from '../builder/build-registry.js';
3
5
  import { createApiServer } from '../server/api-server.js';
4
6
  import { cliLogger } from '../utils/logger.js';
5
7
  /**
6
- * Execute the dev command: build the domain registry and start the local Fastify
7
- * HTTP server. Keeps the process running. Shuts down gracefully on SIGINT/SIGTERM.
8
- * @param options - DevOptions specifying domain root and optional port.
8
+ * Start the dev servers (Fastify API + Vite frontend if present) and return a handle.
9
+ * This is the testable entry point the signal-handling logic lives in runDev.
10
+ * @param options - DevOptions specifying domain root, ports, and project root.
11
+ * @returns A DevHandle with the running server, vite child (or null), and shutdown.
9
12
  */
10
- export async function runDev(options) {
13
+ export async function startDev(options) {
14
+ const projectRoot = options.projectRoot ?? process.cwd();
11
15
  const domainRoot = resolve(options.domainRoot);
12
16
  const port = options.port ?? 3000;
17
+ const frontendPort = options.frontendPort ?? 3001;
18
+ const spawnFn = options.spawnFn ?? spawn;
13
19
  cliLogger.info({ domainRoot }, 'Building registry for dev server');
14
20
  const { registry, warnings } = await buildRegistry(domainRoot);
15
21
  for (const w of warnings) {
@@ -17,11 +23,46 @@ export async function runDev(options) {
17
23
  }
18
24
  cliLogger.info({ domain: registry.domain.id, apis: registry.apis.length, port }, 'Starting local dev server');
19
25
  const server = await createApiServer({ port, registry, domainRoot });
20
- async function shutdown() {
21
- cliLogger.info('Shutting down dev server');
22
- await server.close();
23
- process.exit(0);
26
+ // Spawn Vite if frontend/ exists; skip with warning otherwise.
27
+ const frontendDir = join(projectRoot, 'frontend');
28
+ let vite = null;
29
+ if (existsSync(frontendDir)) {
30
+ cliLogger.info({ frontendDir, port: frontendPort }, 'Starting Vite frontend dev server');
31
+ vite = spawnFn('npx', ['vite', '--port', String(frontendPort), '--strictPort'], {
32
+ cwd: frontendDir,
33
+ stdio: 'inherit',
34
+ });
35
+ vite.on('error', (err) => {
36
+ cliLogger.warn({ err: err.message }, 'Vite dev server failed to start');
37
+ });
24
38
  }
25
- process.on('SIGINT', () => { void shutdown(); });
26
- process.on('SIGTERM', () => { void shutdown(); });
39
+ else {
40
+ cliLogger.warn({ frontendDir }, 'No frontend/ directory found skipping Vite dev server');
41
+ }
42
+ let shuttingDown = false;
43
+ const shutdown = async () => {
44
+ if (shuttingDown)
45
+ return;
46
+ shuttingDown = true;
47
+ cliLogger.info('Shutting down dev servers');
48
+ if (vite && !vite.killed) {
49
+ vite.kill('SIGTERM');
50
+ }
51
+ await server.close();
52
+ };
53
+ return { server, vite, shutdown };
54
+ }
55
+ /**
56
+ * Execute the dev command: build the domain registry, start the local Fastify
57
+ * HTTP server, and (if frontend/ exists) start a Vite dev server alongside it.
58
+ * Keeps the process running. Shuts down both children gracefully on SIGINT/SIGTERM.
59
+ * @param options - DevOptions specifying domain root, ports, and project root.
60
+ */
61
+ export async function runDev(options) {
62
+ const handle = await startDev(options);
63
+ const onSignal = () => {
64
+ void handle.shutdown().then(() => process.exit(0));
65
+ };
66
+ process.on('SIGINT', onSignal);
67
+ process.on('SIGTERM', onSignal);
27
68
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Canonical option-string constant for the --fix flag on the doctor command.
3
+ * Exported so the cli-integration task (packages/domain-cli/src/index.ts) can
4
+ * register the same flag string in commander without string drift.
5
+ */
6
+ export declare const DOCTOR_FIX_FLAG = "--fix";
1
7
  /**
2
8
  * Options for the doctor command.
3
9
  * Doctor is ALWAYS strict — any FAIL causes exit code 1.
@@ -7,6 +13,22 @@ export interface DoctorOptions {
7
13
  projectRoot?: string;
8
14
  /** Flag to run relocation of old-layout scaffold files to new layout. */
9
15
  relocate?: boolean;
16
+ /**
17
+ * Auto-remediate before reporting. --fix is a superset of --relocate: it
18
+ * first runs the relocation routine (auto-moves owned scaffold files outside
19
+ * .mc/ into .mc/ and updates .mc/manifest.json), then continues with the
20
+ * full doctor report. On a project that is already clean, --fix is a no-op
21
+ * (0 files moved, exit 0). Does NOT auto-generate missing fixtures
22
+ * (deferred).
23
+ */
24
+ fix?: boolean;
25
+ /**
26
+ * Explicit strict mode. Doctor is ALWAYS strict — any FAIL causes exit 1.
27
+ * This flag exists so CI can pass `--strict` as a self-documenting,
28
+ * fail-closed gate. Currently informational only; behaviour is identical
29
+ * with or without the flag.
30
+ */
31
+ strict?: boolean;
10
32
  }
11
33
  /**
12
34
  * A single structural check result.