@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/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
|
}
|