@mettlecast/domain-cli 0.2.21 → 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/builder/load-module.js +5 -1
- 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/builder/load-module.ts +5 -1
- 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/src/cli.ts
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
|
/**
|
|
36
41
|
* Root CLI program for the TIB Domain Module local developer toolchain.
|
|
@@ -152,12 +157,19 @@ program
|
|
|
152
157
|
.description('Run all structural checks for the current TIB project (strict by default — any FAIL causes exit code 1)')
|
|
153
158
|
.option('--strict', 'DEPRECATED: Doctor is always strict now. This flag is a no-op.', false)
|
|
154
159
|
.option('--json', 'Output JSON instead of pretty table', false)
|
|
155
|
-
.
|
|
160
|
+
.option('--fix', 'Attempt to automatically fix issues that have a safe automatic fix', false)
|
|
161
|
+
.action(async (opts: { strict: boolean; json: boolean; fix: boolean }) => {
|
|
156
162
|
if (opts.strict) {
|
|
157
163
|
// eslint-disable-next-line no-console
|
|
158
164
|
console.warn('⚠ --strict is deprecated: doctor is always strict now. This flag is a no-op.');
|
|
159
165
|
}
|
|
160
|
-
|
|
166
|
+
// `fix` is part of DoctorOptions in Wave 1 (Task doctor-fix). Cast through
|
|
167
|
+
// `unknown` so this file type-checks today and after the doctor-fix worker
|
|
168
|
+
// adds the `fix?: boolean` field to DoctorOptions.
|
|
169
|
+
const report = await runDoctor({
|
|
170
|
+
projectRoot: process.cwd(),
|
|
171
|
+
fix: opts.fix,
|
|
172
|
+
} as unknown as Parameters<typeof runDoctor>[0]);
|
|
161
173
|
if (opts.json) {
|
|
162
174
|
// eslint-disable-next-line no-console
|
|
163
175
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -284,13 +296,14 @@ program
|
|
|
284
296
|
|
|
285
297
|
program
|
|
286
298
|
.command('upgrade [version]')
|
|
287
|
-
.description('Upgrade scaffold to a new version — diffs S3 tarballs
|
|
299
|
+
.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')
|
|
288
300
|
.option('--dry-run', 'Preview changes without writing files or creating PR', false)
|
|
301
|
+
.option('--check', 'Read-only CI gate: compute upgrade diff, exit 0 for no drift or 1 if drift detected. Writes nothing.', false)
|
|
289
302
|
.option('--project-dir <path>', 'Path to the project to upgrade')
|
|
290
303
|
.option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
|
|
291
304
|
.option('--frontend-components', 'Scope upgrade to scaffold-tracked frontend files only (opens a PR with Chromatic visual diff)', false)
|
|
292
|
-
.action(async (version: string | undefined, opts: { dryRun: boolean; projectDir?: string; githubToken?: string; frontendComponents: boolean }) => {
|
|
293
|
-
await runUpgrade(version, { dryRun: opts.dryRun, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
|
|
305
|
+
.action(async (version: string | undefined, opts: { dryRun: boolean; check: boolean; projectDir?: string; githubToken?: string; frontendComponents: boolean }) => {
|
|
306
|
+
await runUpgrade(version, { dryRun: opts.dryRun, check: opts.check, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
|
|
294
307
|
});
|
|
295
308
|
|
|
296
309
|
// ── New W4 commands ───────────────────────────────────────────────
|
|
@@ -389,6 +402,55 @@ program
|
|
|
389
402
|
await runBuildUi({ projectRoot: opts.projectRoot });
|
|
390
403
|
});
|
|
391
404
|
|
|
405
|
+
program
|
|
406
|
+
.command('why')
|
|
407
|
+
.description('Print current scaffold version, enabled modules, and pending upgrade diff to stdout')
|
|
408
|
+
.option('--project-root <path>', 'Path to the project root (defaults to cwd)')
|
|
409
|
+
.option('--scaffold-bucket <name>', 'S3 bucket to fetch versions.json from (defaults to mc-scaffold)')
|
|
410
|
+
.option('--json', 'Output JSON instead of a human-readable report', false)
|
|
411
|
+
.action(async (opts: { projectRoot?: string; scaffoldBucket?: string; json: boolean }) => {
|
|
412
|
+
await runWhyCli({
|
|
413
|
+
projectRoot: opts.projectRoot,
|
|
414
|
+
scaffoldBucket: opts.scaffoldBucket,
|
|
415
|
+
json: opts.json,
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
program
|
|
420
|
+
.command('generate-openapi <domain>')
|
|
421
|
+
.description('Generate an OpenAPI 3.1 spec from a domain registry (.mc/<domain>-registry.json)')
|
|
422
|
+
.option('--project-root <path>', 'Path to the project root (defaults to cwd)')
|
|
423
|
+
.option('--output <path>', 'Override the output file path')
|
|
424
|
+
.action(async (domain: string, opts: { projectRoot?: string; output?: string }) => {
|
|
425
|
+
await runGenerateOpenapiCli(domain, { projectRoot: opts.projectRoot, output: opts.output });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
program
|
|
429
|
+
.command('generate-sdk <domain>')
|
|
430
|
+
.description('Generate a typed TypeScript API client from a domain\'s OpenAPI spec, returning Result<T, AppError> from every call')
|
|
431
|
+
.option('--project-root <path>', 'Path to the project root (defaults to cwd)')
|
|
432
|
+
.option('--input <path>', 'Path to the OpenAPI spec (defaults to domains/<domain>/api/openapi.generated.json)')
|
|
433
|
+
.option('--output <path>', 'Override the output directory')
|
|
434
|
+
.action(async (domain: string, opts: { projectRoot?: string; input?: string; output?: string }) => {
|
|
435
|
+
await runGenerateSdkCli(domain, { projectRoot: opts.projectRoot, input: opts.input, output: opts.output });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
program
|
|
439
|
+
.command('add-fixture-factory <domain> <api-id>')
|
|
440
|
+
.description('Generate a test data factory with @faker-js/faker for an existing API handler')
|
|
441
|
+
.option('--project-root <path>', 'Path to the project root (defaults to cwd)')
|
|
442
|
+
.action(async (domain: string, apiId: string, opts: { projectRoot?: string }) => {
|
|
443
|
+
await runAddFixtureFactoryCli(domain, apiId, { projectRoot: opts.projectRoot });
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
program
|
|
447
|
+
.command('init')
|
|
448
|
+
.description('Bootstrap a freshly scaffolded project: npm install, build CLI, run doctor baseline')
|
|
449
|
+
.option('--project-root <path>', 'Path to the project root (defaults to cwd)')
|
|
450
|
+
.action(async (opts: { projectRoot?: string }) => {
|
|
451
|
+
await runInitCli({ projectRoot: opts.projectRoot });
|
|
452
|
+
});
|
|
453
|
+
|
|
392
454
|
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
393
455
|
// eslint-disable-next-line no-console
|
|
394
456
|
console.error(err);
|
package/src/commands/add-api.ts
CHANGED
|
@@ -6,12 +6,34 @@
|
|
|
6
6
|
import { writeFile, access, mkdir } from 'node:fs/promises';
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
8
|
import { apiSkeletonTemplate, apiFixtureSkeleton } from '../templates/api-skeleton.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
simpleCrudTemplate,
|
|
11
|
+
simpleCrudListTemplate,
|
|
12
|
+
simpleCrudExampleBody,
|
|
13
|
+
} from '../templates/patterns/api/simple-crud.js';
|
|
14
|
+
import {
|
|
15
|
+
paginatedListTemplate,
|
|
16
|
+
paginatedListExampleBody,
|
|
17
|
+
} from '../templates/patterns/api/paginated-list.js';
|
|
18
|
+
import {
|
|
19
|
+
createWithEventTemplate,
|
|
20
|
+
createWithEventExampleBody,
|
|
21
|
+
} from '../templates/patterns/api/create-with-event.js';
|
|
22
|
+
import {
|
|
23
|
+
idempotentMutationTemplate,
|
|
24
|
+
idempotentMutationExampleBody,
|
|
25
|
+
} from '../templates/patterns/api/idempotent-mutation.js';
|
|
26
|
+
import {
|
|
27
|
+
webhookReceiverStyleTemplate,
|
|
28
|
+
webhookReceiverStyleExampleBody,
|
|
29
|
+
} from '../templates/patterns/api/webhook-receiver-style.js';
|
|
30
|
+
import {
|
|
31
|
+
systemAdminTemplate,
|
|
32
|
+
systemAdminExampleBody,
|
|
33
|
+
} from '../templates/patterns/api/system-admin.js';
|
|
34
|
+
import {
|
|
35
|
+
streamingListPattern,
|
|
36
|
+
} from '../templates/patterns/api/streaming-list.js';
|
|
15
37
|
import { cliLogger } from '../utils/logger.js';
|
|
16
38
|
|
|
17
39
|
/**
|
|
@@ -24,6 +46,7 @@ export const API_PATTERNS = [
|
|
|
24
46
|
'idempotent-mutation',
|
|
25
47
|
'webhook-receiver-style',
|
|
26
48
|
'system-admin',
|
|
49
|
+
'streaming-list',
|
|
27
50
|
] as const;
|
|
28
51
|
|
|
29
52
|
export type ApiPattern = (typeof API_PATTERNS)[number];
|
|
@@ -87,8 +110,11 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
87
110
|
|
|
88
111
|
// Resolve template content
|
|
89
112
|
let apiContent: string;
|
|
113
|
+
let exampleBody: string = '{}';
|
|
90
114
|
if (opts.pattern) {
|
|
91
|
-
|
|
115
|
+
const resolved = resolveApiPattern(opts.pattern, opts.domain, opts.id, opts.tenancy);
|
|
116
|
+
apiContent = resolved.apiContent;
|
|
117
|
+
exampleBody = resolved.exampleBody;
|
|
92
118
|
cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
|
|
93
119
|
} else {
|
|
94
120
|
apiContent = apiSkeletonTemplate(opts.domain, opts.id, opts.tenancy);
|
|
@@ -97,12 +123,12 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
97
123
|
// Write API file
|
|
98
124
|
await writeFile(apiFilePath, apiContent);
|
|
99
125
|
|
|
100
|
-
// Create fixture
|
|
126
|
+
// Create fixture with the input schema's default shape as the example payload
|
|
101
127
|
const apiTestDir = join(domainDir, 'api', '__tests__');
|
|
102
128
|
await mkdir(apiTestDir, { recursive: true });
|
|
103
129
|
await writeFile(
|
|
104
130
|
join(apiTestDir, `${opts.id}.fixture.json`),
|
|
105
|
-
apiFixtureSkeleton(opts.domain, opts.id)
|
|
131
|
+
apiFixtureSkeleton(opts.domain, opts.id, exampleBody)
|
|
106
132
|
);
|
|
107
133
|
|
|
108
134
|
cliLogger.info({ domain: opts.domain, api: opts.id, method: opts.method ?? 'GET', pattern: opts.pattern ?? 'skeleton' }, 'API added');
|
|
@@ -123,22 +149,45 @@ function resolveApiPattern(
|
|
|
123
149
|
domain: string,
|
|
124
150
|
id: string,
|
|
125
151
|
tenancy: string,
|
|
126
|
-
): string {
|
|
152
|
+
): { apiContent: string; exampleBody: string } {
|
|
127
153
|
switch (pattern) {
|
|
128
154
|
case 'simple-crud':
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
return {
|
|
156
|
+
apiContent: simpleCrudTemplate(domain, id, tenancy) +
|
|
157
|
+
'\n' +
|
|
158
|
+
simpleCrudListTemplate(domain, id, tenancy),
|
|
159
|
+
exampleBody: simpleCrudExampleBody,
|
|
160
|
+
};
|
|
132
161
|
case 'paginated-list':
|
|
133
|
-
return
|
|
162
|
+
return {
|
|
163
|
+
apiContent: paginatedListTemplate(domain, id, tenancy),
|
|
164
|
+
exampleBody: paginatedListExampleBody,
|
|
165
|
+
};
|
|
134
166
|
case 'create-with-event':
|
|
135
|
-
return
|
|
167
|
+
return {
|
|
168
|
+
apiContent: createWithEventTemplate(domain, id, tenancy),
|
|
169
|
+
exampleBody: createWithEventExampleBody,
|
|
170
|
+
};
|
|
136
171
|
case 'idempotent-mutation':
|
|
137
|
-
return
|
|
172
|
+
return {
|
|
173
|
+
apiContent: idempotentMutationTemplate(domain, id, tenancy),
|
|
174
|
+
exampleBody: idempotentMutationExampleBody,
|
|
175
|
+
};
|
|
138
176
|
case 'webhook-receiver-style':
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
177
|
+
return {
|
|
178
|
+
apiContent: webhookReceiverStyleTemplate(domain, id, tenancy),
|
|
179
|
+
exampleBody: webhookReceiverStyleExampleBody,
|
|
180
|
+
};
|
|
181
|
+
case 'system-admin':
|
|
182
|
+
return {
|
|
183
|
+
apiContent: systemAdminTemplate(domain, id, tenancy),
|
|
184
|
+
exampleBody: systemAdminExampleBody,
|
|
185
|
+
};
|
|
186
|
+
case 'streaming-list':
|
|
187
|
+
return {
|
|
188
|
+
apiContent: streamingListPattern(domain, id, 'item'),
|
|
189
|
+
exampleBody: '{}',
|
|
190
|
+
};
|
|
142
191
|
default:
|
|
143
192
|
throw new Error(`Unknown API pattern "${pattern}". Available: ${API_PATTERNS.join(', ')}`);
|
|
144
193
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
|
|
9
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { cliLogger } from '../utils/logger.js';
|
|
12
|
+
|
|
13
|
+
export interface AddFixtureFactoryOptions {
|
|
14
|
+
domain: string;
|
|
15
|
+
apiId: string;
|
|
16
|
+
projectRoot?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FACTORY_TEMPLATE = `// Generated fixture factory for {domain}.{apiId}
|
|
20
|
+
// Uses @faker-js/faker for realistic test data. Override any field
|
|
21
|
+
// by passing overrides to ` + '`valid()`' + `.
|
|
22
|
+
import { faker } from '@faker-js/faker';
|
|
23
|
+
|
|
24
|
+
/** The input shape for {domain}.{apiId} — derive from the Zod schema. */
|
|
25
|
+
type Input = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
/** Full, valid input with sensible defaults. */
|
|
28
|
+
export function valid(overrides: Partial<Input> = {}): Input {
|
|
29
|
+
return {
|
|
30
|
+
name: faker.company.name(),
|
|
31
|
+
email: faker.internet.email(),
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Intentionally invalid input — use to test validation failures. */
|
|
37
|
+
export function invalid(): Partial<Input> {
|
|
38
|
+
return {
|
|
39
|
+
name: '', // empty — should fail minLength if required
|
|
40
|
+
email: 'not-an-email',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
export async function runAddFixtureFactory(options: AddFixtureFactoryOptions): Promise<string> {
|
|
46
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
47
|
+
const domain = options.domain;
|
|
48
|
+
const apiId = options.apiId;
|
|
49
|
+
|
|
50
|
+
const apiFile = join(projectRoot, 'domains', domain, 'api', `${apiId}.ts`);
|
|
51
|
+
await access(apiFile).catch(() => {
|
|
52
|
+
throw new Error(`API file not found: ${apiFile}. Run add-api first.`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Read the API file to extract the input type name
|
|
56
|
+
const content = await readFile(apiFile, 'utf8');
|
|
57
|
+
const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
|
|
58
|
+
|
|
59
|
+
const factoryFile = join(projectRoot, 'domains', domain, 'api', '__tests__', `${apiId}.factory.ts`);
|
|
60
|
+
const factoryContent = FACTORY_TEMPLATE
|
|
61
|
+
.replace(/\{domain\}/g, domain)
|
|
62
|
+
.replace(/\{apiId\}/g, apiId);
|
|
63
|
+
|
|
64
|
+
await mkdir(join(projectRoot, 'domains', domain, 'api', '__tests__'), { recursive: true });
|
|
65
|
+
await writeFile(factoryFile, factoryContent, 'utf8');
|
|
66
|
+
|
|
67
|
+
cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
|
|
68
|
+
return factoryFile;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runAddFixtureFactoryCli(domain: string, apiId: string, opts: { projectRoot?: string } = {}): Promise<void> {
|
|
72
|
+
const factoryFile = await runAddFixtureFactory({ domain, apiId, ...opts });
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log(`Fixture factory written to: ${factoryFile}`);
|
|
75
|
+
}
|
|
@@ -3,6 +3,7 @@ import { mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { Readable } from 'node:stream';
|
|
5
5
|
import { Parser as TarParser } from 'tar';
|
|
6
|
+
import ky from 'ky';
|
|
6
7
|
import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
|
|
7
8
|
import { readManifest, writeManifest, upsertManifestFile, inferPolicyFromPath } from '../utils/manifest.js';
|
|
8
9
|
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
@@ -99,18 +100,16 @@ async function createGitHubPR(
|
|
|
99
100
|
token: string
|
|
100
101
|
): Promise<{ prUrl: string; prNumber: number } | null> {
|
|
101
102
|
try {
|
|
102
|
-
const response = await
|
|
103
|
-
method: 'POST',
|
|
103
|
+
const response = await ky.post(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
|
|
104
104
|
headers: {
|
|
105
105
|
Authorization: `Bearer ${token}`,
|
|
106
|
-
'Content-Type': 'application/json',
|
|
107
106
|
},
|
|
108
|
-
|
|
107
|
+
json: {
|
|
109
108
|
title,
|
|
110
109
|
body,
|
|
111
110
|
head: branchName,
|
|
112
111
|
base: 'develop',
|
|
113
|
-
}
|
|
112
|
+
},
|
|
114
113
|
});
|
|
115
114
|
|
|
116
115
|
if (!response.ok) {
|
|
@@ -4,6 +4,7 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import { createGunzip } from 'node:zlib';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { extract } from 'tar';
|
|
7
|
+
import ky from 'ky';
|
|
7
8
|
import { cliLogger } from '../utils/logger.js';
|
|
8
9
|
import {
|
|
9
10
|
fetchVersionsJson,
|
|
@@ -58,20 +59,18 @@ async function createGitHubPR(
|
|
|
58
59
|
body: string
|
|
59
60
|
): Promise<{ url: string; number: number }> {
|
|
60
61
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
61
|
-
const res = await
|
|
62
|
-
method: 'POST',
|
|
62
|
+
const res = await ky.post(url, {
|
|
63
63
|
headers: {
|
|
64
64
|
Authorization: `Bearer ${token}`,
|
|
65
65
|
Accept: 'application/vnd.github+json',
|
|
66
|
-
'Content-Type': 'application/json',
|
|
67
66
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
68
67
|
},
|
|
69
|
-
|
|
68
|
+
json: {
|
|
70
69
|
title,
|
|
71
70
|
body,
|
|
72
71
|
head: branch,
|
|
73
72
|
base: 'develop',
|
|
74
|
-
}
|
|
73
|
+
},
|
|
75
74
|
});
|
|
76
75
|
|
|
77
76
|
if (!res.ok) {
|
|
@@ -70,12 +70,12 @@ const FlowStepSchema = z.union([
|
|
|
70
70
|
const FlowConfigSchema = z.object({
|
|
71
71
|
id: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
72
72
|
owningDomain: z.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
|
|
73
|
-
type: z.enum(['express', 'standard']).optional(),
|
|
73
|
+
type: z.enum(['express', 'standard'] as const).optional(),
|
|
74
74
|
name: z.string().min(1),
|
|
75
75
|
steps: z.array(FlowStepSchema).min(1),
|
|
76
76
|
trigger: z.object({
|
|
77
77
|
type: z.literal('event'),
|
|
78
|
-
eventId: z.string().includes('.'),
|
|
78
|
+
eventId: z.string().refine(val => val.includes('.'), { message: 'eventId must be namespaced (e.g. domain.event)' }),
|
|
79
79
|
semverRange: z.string().min(1),
|
|
80
80
|
}).optional(),
|
|
81
81
|
});
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
4
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
5
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
3
6
|
import { createApiServer } from '../server/api-server.js';
|
|
4
7
|
import { cliLogger } from '../utils/logger.js';
|
|
@@ -11,16 +14,38 @@ export interface DevOptions {
|
|
|
11
14
|
domainRoot: string;
|
|
12
15
|
/** Port for the local HTTP server. Defaults to 3000. */
|
|
13
16
|
port?: number;
|
|
17
|
+
/** Absolute path to the project root. Defaults to process.cwd(). */
|
|
18
|
+
projectRoot?: string;
|
|
19
|
+
/** Port for the Vite frontend dev server. Defaults to 3001. */
|
|
20
|
+
frontendPort?: number;
|
|
21
|
+
/** Override the spawn function (used for tests). Defaults to node:child_process.spawn. */
|
|
22
|
+
spawnFn?: typeof spawn;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
|
-
*
|
|
18
|
-
* HTTP server. Keeps the process running. Shuts down gracefully on SIGINT/SIGTERM.
|
|
19
|
-
* @param options - DevOptions specifying domain root and optional port.
|
|
26
|
+
* Handle returned by startDev, exposing the live processes and a shutdown function.
|
|
20
27
|
*/
|
|
21
|
-
export
|
|
28
|
+
export interface DevHandle {
|
|
29
|
+
/** The Fastify API server instance. */
|
|
30
|
+
server: FastifyInstance;
|
|
31
|
+
/** The Vite child process, or null when no frontend/ directory was found. */
|
|
32
|
+
vite: ChildProcess | null;
|
|
33
|
+
/** Shut down both processes gracefully. Idempotent. */
|
|
34
|
+
shutdown: () => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start the dev servers (Fastify API + Vite frontend if present) and return a handle.
|
|
39
|
+
* This is the testable entry point — the signal-handling logic lives in runDev.
|
|
40
|
+
* @param options - DevOptions specifying domain root, ports, and project root.
|
|
41
|
+
* @returns A DevHandle with the running server, vite child (or null), and shutdown.
|
|
42
|
+
*/
|
|
43
|
+
export async function startDev(options: DevOptions): Promise<DevHandle> {
|
|
44
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
22
45
|
const domainRoot = resolve(options.domainRoot);
|
|
23
46
|
const port = options.port ?? 3000;
|
|
47
|
+
const frontendPort = options.frontendPort ?? 3001;
|
|
48
|
+
const spawnFn = options.spawnFn ?? spawn;
|
|
24
49
|
|
|
25
50
|
cliLogger.info({ domainRoot }, 'Building registry for dev server');
|
|
26
51
|
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
@@ -36,12 +61,54 @@ export async function runDev(options: DevOptions): Promise<void> {
|
|
|
36
61
|
|
|
37
62
|
const server = await createApiServer({ port, registry, domainRoot });
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
// Spawn Vite if frontend/ exists; skip with warning otherwise.
|
|
65
|
+
const frontendDir = join(projectRoot, 'frontend');
|
|
66
|
+
let vite: ChildProcess | null = null;
|
|
67
|
+
if (existsSync(frontendDir)) {
|
|
68
|
+
cliLogger.info(
|
|
69
|
+
{ frontendDir, port: frontendPort },
|
|
70
|
+
'Starting Vite frontend dev server'
|
|
71
|
+
);
|
|
72
|
+
vite = spawnFn('npx', ['vite', '--port', String(frontendPort), '--strictPort'], {
|
|
73
|
+
cwd: frontendDir,
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
});
|
|
76
|
+
vite.on('error', (err: Error) => {
|
|
77
|
+
cliLogger.warn({ err: err.message }, 'Vite dev server failed to start');
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
cliLogger.warn(
|
|
81
|
+
{ frontendDir },
|
|
82
|
+
'No frontend/ directory found — skipping Vite dev server'
|
|
83
|
+
);
|
|
43
84
|
}
|
|
44
85
|
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
let shuttingDown = false;
|
|
87
|
+
const shutdown = async (): Promise<void> => {
|
|
88
|
+
if (shuttingDown) return;
|
|
89
|
+
shuttingDown = true;
|
|
90
|
+
cliLogger.info('Shutting down dev servers');
|
|
91
|
+
if (vite && !vite.killed) {
|
|
92
|
+
vite.kill('SIGTERM');
|
|
93
|
+
}
|
|
94
|
+
await server.close();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return { server, vite, shutdown };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Execute the dev command: build the domain registry, start the local Fastify
|
|
102
|
+
* HTTP server, and (if frontend/ exists) start a Vite dev server alongside it.
|
|
103
|
+
* Keeps the process running. Shuts down both children gracefully on SIGINT/SIGTERM.
|
|
104
|
+
* @param options - DevOptions specifying domain root, ports, and project root.
|
|
105
|
+
*/
|
|
106
|
+
export async function runDev(options: DevOptions): Promise<void> {
|
|
107
|
+
const handle = await startDev(options);
|
|
108
|
+
|
|
109
|
+
const onSignal = (): void => {
|
|
110
|
+
void handle.shutdown().then(() => process.exit(0));
|
|
111
|
+
};
|
|
112
|
+
process.on('SIGINT', onSignal);
|
|
113
|
+
process.on('SIGTERM', onSignal);
|
|
47
114
|
}
|