@mettlecast/domain-cli 0.2.22 → 0.2.23
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.
- package/dist/cli.js +60 -3
- package/dist/commands/add-api.d.ts +1 -1
- package/dist/commands/add-api.js +45 -17
- package/dist/commands/add-fixture-factory.d.ts +16 -0
- package/dist/commands/add-fixture-factory.js +60 -0
- package/dist/commands/add-module.js +4 -5
- package/dist/commands/add-seed-page.js +4 -5
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/dev.d.ts +30 -3
- package/dist/commands/dev.js +52 -11
- package/dist/commands/doctor.d.ts +22 -0
- package/dist/commands/doctor.js +341 -6
- package/dist/commands/generate-openapi.d.ts +21 -0
- package/dist/commands/generate-openapi.js +117 -0
- package/dist/commands/generate-sdk.d.ts +25 -0
- package/dist/commands/generate-sdk.js +98 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +62 -0
- package/dist/commands/reseed-page.js +5 -6
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +28 -6
- package/dist/commands/why.d.ts +47 -0
- package/dist/commands/why.js +129 -0
- package/dist/templates/api-skeleton.d.ts +5 -1
- package/dist/templates/api-skeleton.js +28 -6
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +14 -1
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +20 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +12 -2
- package/dist/templates/patterns/api/simple-crud.d.ts +5 -0
- package/dist/templates/patterns/api/simple-crud.js +22 -4
- package/dist/templates/patterns/api/streaming-list.d.ts +27 -0
- package/dist/templates/patterns/api/streaming-list.js +91 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +14 -4
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +22 -0
- package/dist/utils/s3-fetch.js +23 -32
- package/package.json +4 -1
- package/src/__tests__/commands/add-api.test.ts +160 -0
- package/src/__tests__/commands/dev.test.ts +162 -0
- package/src/__tests__/commands/why.test.ts +199 -0
- package/src/__tests__/doctor.test.ts +336 -1
- package/src/__tests__/smoke/scaffold.test.ts +574 -0
- package/src/cli.ts +67 -5
- package/src/commands/add-api.ts +68 -19
- package/src/commands/add-fixture-factory.ts +75 -0
- package/src/commands/add-module.ts +4 -5
- package/src/commands/add-seed-page.ts +4 -5
- package/src/commands/build-flows.ts +2 -2
- package/src/commands/dev.ts +78 -11
- package/src/commands/doctor.ts +379 -12
- package/src/commands/generate-openapi.ts +154 -0
- package/src/commands/generate-sdk.ts +125 -0
- package/src/commands/init.ts +78 -0
- package/src/commands/reseed-page.ts +5 -6
- package/src/commands/upgrade.ts +26 -6
- package/src/commands/why.ts +171 -0
- package/src/templates/api-skeleton.ts +32 -6
- package/src/templates/patterns/api/create-with-event.ts +15 -1
- package/src/templates/patterns/api/idempotent-mutation.ts +21 -0
- package/src/templates/patterns/api/paginated-list.ts +13 -2
- package/src/templates/patterns/api/simple-crud.ts +23 -4
- package/src/templates/patterns/api/streaming-list.ts +91 -0
- package/src/templates/patterns/api/system-admin.ts +15 -4
- package/src/templates/patterns/api/webhook-receiver-style.ts +23 -0
- 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
|
-
|
|
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
|
|
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.
|
package/dist/commands/add-api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
102
|
+
return {
|
|
103
|
+
apiContent: paginatedListTemplate(domain, id, tenancy),
|
|
104
|
+
exampleBody: paginatedListExampleBody,
|
|
105
|
+
};
|
|
95
106
|
case 'create-with-event':
|
|
96
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
apiContent: createWithEventTemplate(domain, id, tenancy),
|
|
109
|
+
exampleBody: createWithEventExampleBody,
|
|
110
|
+
};
|
|
97
111
|
case 'idempotent-mutation':
|
|
98
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
apiContent: idempotentMutationTemplate(domain, id, tenancy),
|
|
114
|
+
exampleBody: idempotentMutationExampleBody,
|
|
115
|
+
};
|
|
99
116
|
case 'webhook-receiver-style':
|
|
100
|
-
return
|
|
117
|
+
return {
|
|
118
|
+
apiContent: webhookReceiverStyleTemplate(domain, id, tenancy),
|
|
119
|
+
exampleBody: webhookReceiverStyleExampleBody,
|
|
120
|
+
};
|
|
101
121
|
case 'system-admin':
|
|
102
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
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>;
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param options - DevOptions specifying domain root and
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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.
|