ai-spec-dev 0.37.0 → 0.41.0
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/README.md +381 -1796
- package/RELEASE_LOG.md +231 -0
- package/cli/commands/create.ts +9 -1176
- package/cli/commands/dashboard.ts +1 -1
- package/cli/pipeline/helpers.ts +34 -0
- package/cli/pipeline/multi-repo.ts +483 -0
- package/cli/pipeline/single-repo.ts +755 -0
- package/cli/utils.ts +2 -0
- package/core/code-generator.ts +52 -341
- package/core/codegen/helpers.ts +219 -0
- package/core/codegen/topo-sort.ts +98 -0
- package/core/constitution-consolidator.ts +2 -2
- package/core/dsl-coverage-checker.ts +298 -0
- package/core/dsl-extractor.ts +19 -46
- package/core/dsl-feedback.ts +1 -1
- package/core/dsl-validator.ts +74 -0
- package/core/error-feedback.ts +95 -11
- package/core/frontend-context-loader.ts +27 -5
- package/core/knowledge-memory.ts +52 -0
- package/core/mock/fixtures.ts +89 -0
- package/core/mock/proxy.ts +380 -0
- package/core/mock-server-generator.ts +12 -460
- package/core/requirement-decomposer.ts +4 -28
- package/core/reviewer.ts +1 -1
- package/core/safe-json.ts +76 -0
- package/core/spec-updater.ts +5 -21
- package/core/token-budget.ts +124 -0
- package/core/vcr.ts +20 -1
- package/dist/cli/index.js +4110 -3534
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +4237 -3661
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +310 -182
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +308 -180
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/purpose.md +173 -33
- package/tests/auto-consolidation.test.ts +109 -0
- package/tests/combined-generator.test.ts +81 -0
- package/tests/constitution-consolidator.test.ts +161 -0
- package/tests/constitution-generator.test.ts +94 -0
- package/tests/contract-bridge.test.ts +201 -0
- package/tests/design-dialogue.test.ts +108 -0
- package/tests/dsl-coverage-checker.test.ts +230 -0
- package/tests/dsl-feedback.test.ts +45 -0
- package/tests/dsl-validator-xref.test.ts +99 -0
- package/tests/error-feedback-repair.test.ts +319 -0
- package/tests/error-feedback-validation.test.ts +91 -0
- package/tests/frontend-context-loader.test.ts +609 -0
- package/tests/global-constitution.test.ts +110 -0
- package/tests/key-store.test.ts +73 -0
- package/tests/knowledge-memory.test.ts +327 -0
- package/tests/project-index.test.ts +206 -0
- package/tests/prompt-hasher.test.ts +19 -0
- package/tests/requirement-decomposer.test.ts +171 -0
- package/tests/reviewer.test.ts +4 -1
- package/tests/run-logger.test.ts +289 -0
- package/tests/run-snapshot.test.ts +113 -0
- package/tests/safe-json.test.ts +63 -0
- package/tests/spec-updater.test.ts +161 -0
- package/tests/test-generator.test.ts +146 -0
- package/tests/token-budget.test.ts +124 -0
- package/tests/vcr-hash.test.ts +101 -0
- package/tests/workspace-loader.test.ts +277 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { ApiEndpoint } from "../dsl-types";
|
|
5
|
+
|
|
6
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface ProxyApplyResult {
|
|
9
|
+
framework: string;
|
|
10
|
+
applied: boolean;
|
|
11
|
+
devCommand: string | null;
|
|
12
|
+
note?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
16
|
+
|
|
17
|
+
interface MockLock {
|
|
18
|
+
framework: string;
|
|
19
|
+
mockPort: number;
|
|
20
|
+
frontendDir: string;
|
|
21
|
+
mockServerPid?: number;
|
|
22
|
+
actions: Array<
|
|
23
|
+
| { type: "wrote-file"; filePath: string }
|
|
24
|
+
| { type: "patched-pkg-proxy"; originalProxy?: string | null }
|
|
25
|
+
| { type: "added-pkg-script"; key: string; originalValue?: string | null }
|
|
26
|
+
>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Framework Detection ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export function detectFrontendFramework(projectDir: string): "vite" | "next" | "webpack" | "cra" | "unknown" {
|
|
32
|
+
// Check vite.config
|
|
33
|
+
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
34
|
+
if (fs.existsSync(path.join(projectDir, f))) return "vite";
|
|
35
|
+
}
|
|
36
|
+
// Check next.config
|
|
37
|
+
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
38
|
+
if (fs.existsSync(path.join(projectDir, f))) return "next";
|
|
39
|
+
}
|
|
40
|
+
// Check for CRA (react-scripts in package.json)
|
|
41
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
42
|
+
if (fs.existsSync(pkgPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
46
|
+
if (deps["react-scripts"]) return "cra";
|
|
47
|
+
} catch { /* ignore */ }
|
|
48
|
+
}
|
|
49
|
+
// Check webpack.config
|
|
50
|
+
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
51
|
+
if (fs.existsSync(path.join(projectDir, f))) return "webpack";
|
|
52
|
+
}
|
|
53
|
+
return "unknown";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Proxy Config Generators ─────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function generateViteProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
|
|
59
|
+
// Collect unique path prefixes
|
|
60
|
+
const prefixes = new Set<string>();
|
|
61
|
+
for (const ep of endpoints) {
|
|
62
|
+
const parts = ep.path.split("/").filter(Boolean);
|
|
63
|
+
if (parts.length > 0) prefixes.add(`/${parts[0]}`);
|
|
64
|
+
}
|
|
65
|
+
const target = `http://localhost:${mockPort}`;
|
|
66
|
+
const proxyEntries = Array.from(prefixes)
|
|
67
|
+
.map((p) => ` '${p}': { target: '${target}', changeOrigin: true }`)
|
|
68
|
+
.join(",\n");
|
|
69
|
+
|
|
70
|
+
return `// Add this proxy block to your vite.config.ts / vite.config.js
|
|
71
|
+
// Inside the defineConfig({ server: { proxy: { ... } } }) section:
|
|
72
|
+
//
|
|
73
|
+
// server: {
|
|
74
|
+
// proxy: {
|
|
75
|
+
${proxyEntries
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((l) => `// ${l}`)
|
|
78
|
+
.join("\n")}
|
|
79
|
+
// }
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
// ─── Standalone proxy snippet for vite.config.ts ─────────────────────────────
|
|
83
|
+
// import { defineConfig } from 'vite';
|
|
84
|
+
// export default defineConfig({
|
|
85
|
+
// server: {
|
|
86
|
+
// proxy: {
|
|
87
|
+
${proxyEntries
|
|
88
|
+
.split("\n")
|
|
89
|
+
.map((l) => `// ${l.trim()}`)
|
|
90
|
+
.join("\n")}
|
|
91
|
+
// }
|
|
92
|
+
// }
|
|
93
|
+
// });
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function generateNextProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
|
|
98
|
+
const prefixes = new Set<string>();
|
|
99
|
+
for (const ep of endpoints) {
|
|
100
|
+
const parts = ep.path.split("/").filter(Boolean);
|
|
101
|
+
if (parts.length > 0) prefixes.add(`/${parts[0]}`);
|
|
102
|
+
}
|
|
103
|
+
const rewrites = Array.from(prefixes).map(
|
|
104
|
+
(p) => ` { source: '${p}/:path*', destination: 'http://localhost:${mockPort}${p}/:path*' }`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return `// Add this to your next.config.js rewrites():
|
|
108
|
+
//
|
|
109
|
+
// module.exports = {
|
|
110
|
+
// async rewrites() {
|
|
111
|
+
// return [
|
|
112
|
+
${rewrites.map((r) => `// ${r}`).join(",\n")}
|
|
113
|
+
// ];
|
|
114
|
+
// },
|
|
115
|
+
// };
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function generateWebpackProxyBlock(mockPort: number, endpoints: ApiEndpoint[]): string {
|
|
120
|
+
const prefixes = new Set<string>();
|
|
121
|
+
for (const ep of endpoints) {
|
|
122
|
+
const parts = ep.path.split("/").filter(Boolean);
|
|
123
|
+
if (parts.length > 0) prefixes.add(`/${parts[0]}`);
|
|
124
|
+
}
|
|
125
|
+
const proxyEntries = Array.from(prefixes)
|
|
126
|
+
.map(
|
|
127
|
+
(p) =>
|
|
128
|
+
` '${p}': {\n target: 'http://localhost:${mockPort}',\n changeOrigin: true\n }`
|
|
129
|
+
)
|
|
130
|
+
.join(",\n");
|
|
131
|
+
|
|
132
|
+
return `// Add this to your webpack.config.js devServer.proxy section:
|
|
133
|
+
//
|
|
134
|
+
// devServer: {
|
|
135
|
+
// proxy: {
|
|
136
|
+
${proxyEntries
|
|
137
|
+
.split("\n")
|
|
138
|
+
.map((l) => `// ${l}`)
|
|
139
|
+
.join("\n")}
|
|
140
|
+
// }
|
|
141
|
+
// }
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function generateCraProxyBlock(mockPort: number): string {
|
|
146
|
+
return `// For Create React App: add a "proxy" field to package.json
|
|
147
|
+
// This only proxies requests that don't match static files:
|
|
148
|
+
//
|
|
149
|
+
// {
|
|
150
|
+
// "proxy": "http://localhost:${mockPort}"
|
|
151
|
+
// }
|
|
152
|
+
//
|
|
153
|
+
// Or use src/setupProxy.js for per-path control:
|
|
154
|
+
// const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
155
|
+
// module.exports = function(app) {
|
|
156
|
+
// app.use('/api', createProxyMiddleware({ target: 'http://localhost:${mockPort}', changeOrigin: true }));
|
|
157
|
+
// };
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function generateProxyConfig(
|
|
162
|
+
dsl: { endpoints: ApiEndpoint[] },
|
|
163
|
+
mockPort: number,
|
|
164
|
+
projectDir: string
|
|
165
|
+
): { content: string; filename: string } {
|
|
166
|
+
const framework = detectFrontendFramework(projectDir);
|
|
167
|
+
|
|
168
|
+
switch (framework) {
|
|
169
|
+
case "vite":
|
|
170
|
+
return {
|
|
171
|
+
filename: "mock/proxy.vite.comment.txt",
|
|
172
|
+
content: generateViteProxyBlock(mockPort, dsl.endpoints),
|
|
173
|
+
};
|
|
174
|
+
case "next":
|
|
175
|
+
return {
|
|
176
|
+
filename: "mock/proxy.next.comment.txt",
|
|
177
|
+
content: generateNextProxyBlock(mockPort, dsl.endpoints),
|
|
178
|
+
};
|
|
179
|
+
case "cra":
|
|
180
|
+
return {
|
|
181
|
+
filename: "mock/proxy.cra.comment.txt",
|
|
182
|
+
content: generateCraProxyBlock(mockPort),
|
|
183
|
+
};
|
|
184
|
+
default:
|
|
185
|
+
return {
|
|
186
|
+
filename: "mock/proxy.webpack.comment.txt",
|
|
187
|
+
content: generateWebpackProxyBlock(mockPort, dsl.endpoints),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Vite Mock Config ────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function findViteConfigFile(projectDir: string): string | null {
|
|
195
|
+
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
196
|
+
if (fs.existsSync(path.join(projectDir, f))) return f;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildViteProxyEntries(endpoints: ApiEndpoint[], mockPort: number): string {
|
|
202
|
+
const prefixes = new Set<string>();
|
|
203
|
+
for (const ep of endpoints) {
|
|
204
|
+
const parts = ep.path.split("/").filter(Boolean);
|
|
205
|
+
if (parts.length > 0) prefixes.add(`/${parts[0]}`);
|
|
206
|
+
}
|
|
207
|
+
if (prefixes.size === 0) prefixes.add("/api");
|
|
208
|
+
const target = `http://localhost:${mockPort}`;
|
|
209
|
+
return Array.from(prefixes)
|
|
210
|
+
.map((p) => ` '${p}': { target: '${target}', changeOrigin: true },`)
|
|
211
|
+
.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function generateViteMockConfigTs(baseConfigFile: string, mockPort: number, endpoints: ApiEndpoint[]): string {
|
|
215
|
+
const importPath = `./${baseConfigFile.replace(/\.(ts|mts|js|mjs)$/, "")}`;
|
|
216
|
+
const proxyEntries = buildViteProxyEntries(endpoints, mockPort);
|
|
217
|
+
return `// Auto-generated by ai-spec mock --serve
|
|
218
|
+
// LOCAL DEVELOPMENT ONLY — do not commit this file
|
|
219
|
+
// Remove with: ai-spec mock --restore
|
|
220
|
+
import { defineConfig, mergeConfig } from 'vite';
|
|
221
|
+
|
|
222
|
+
export default defineConfig(async (env) => {
|
|
223
|
+
const mod = await import('${importPath}');
|
|
224
|
+
const baseConfigOrFn = mod.default;
|
|
225
|
+
const baseConfig =
|
|
226
|
+
typeof baseConfigOrFn === 'function'
|
|
227
|
+
? await baseConfigOrFn(env)
|
|
228
|
+
: baseConfigOrFn;
|
|
229
|
+
|
|
230
|
+
return mergeConfig(baseConfig ?? {}, {
|
|
231
|
+
server: {
|
|
232
|
+
proxy: {
|
|
233
|
+
${proxyEntries}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Proxy Patching (applyMockProxy / restoreMockProxy) ──────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Patch the frontend project's proxy config to point to the mock server.
|
|
245
|
+
* Vite: writes vite.config.ai-spec-mock.ts + adds "dev:mock" npm script.
|
|
246
|
+
* CRA : patches package.json "proxy" field (original backed up in lock file).
|
|
247
|
+
* Saves .ai-spec-mock.lock.json so restoreMockProxy() can undo all changes.
|
|
248
|
+
*/
|
|
249
|
+
export async function applyMockProxy(
|
|
250
|
+
frontendDir: string,
|
|
251
|
+
mockPort: number,
|
|
252
|
+
endpoints: ApiEndpoint[] = []
|
|
253
|
+
): Promise<ProxyApplyResult> {
|
|
254
|
+
const framework = detectFrontendFramework(frontendDir);
|
|
255
|
+
const actions: MockLock["actions"] = [];
|
|
256
|
+
|
|
257
|
+
if (framework === "vite") {
|
|
258
|
+
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
259
|
+
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
260
|
+
const mockConfigPath = path.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
261
|
+
await fs.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
262
|
+
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
263
|
+
|
|
264
|
+
const pkgPath = path.join(frontendDir, "package.json");
|
|
265
|
+
if (await fs.pathExists(pkgPath)) {
|
|
266
|
+
const pkg = await fs.readJson(pkgPath);
|
|
267
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
268
|
+
const originalValue: string | null = pkg.scripts["dev:mock"] ?? null;
|
|
269
|
+
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
270
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
271
|
+
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lock: MockLock = { framework, mockPort, frontendDir, actions };
|
|
275
|
+
await fs.writeJson(path.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
276
|
+
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (framework === "cra") {
|
|
280
|
+
const pkgPath = path.join(frontendDir, "package.json");
|
|
281
|
+
if (await fs.pathExists(pkgPath)) {
|
|
282
|
+
const pkg = await fs.readJson(pkgPath);
|
|
283
|
+
const originalProxy: string | null = pkg.proxy ?? null;
|
|
284
|
+
pkg.proxy = `http://localhost:${mockPort}`;
|
|
285
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
286
|
+
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
287
|
+
const lock: MockLock = { framework, mockPort, frontendDir, actions };
|
|
288
|
+
await fs.writeJson(path.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
289
|
+
return { framework, applied: true, devCommand: "npm start" };
|
|
290
|
+
}
|
|
291
|
+
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// next / webpack / unknown — save lock but no auto-patch
|
|
295
|
+
const lock: MockLock = { framework, mockPort, frontendDir, actions };
|
|
296
|
+
await fs.writeJson(path.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
297
|
+
const manualNote =
|
|
298
|
+
framework === "next"
|
|
299
|
+
? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}`
|
|
300
|
+
: `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
|
|
301
|
+
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Undo all proxy changes made by applyMockProxy().
|
|
306
|
+
* Also kills the mock server if its PID was stored in the lock file.
|
|
307
|
+
*/
|
|
308
|
+
export async function restoreMockProxy(
|
|
309
|
+
frontendDir: string
|
|
310
|
+
): Promise<{ restored: boolean; note?: string }> {
|
|
311
|
+
const lockPath = path.join(frontendDir, MOCK_LOCK_FILE);
|
|
312
|
+
if (!(await fs.pathExists(lockPath))) {
|
|
313
|
+
return { restored: false, note: "No lock file found — nothing to restore." };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const lock: MockLock = await fs.readJson(lockPath);
|
|
317
|
+
|
|
318
|
+
for (const action of lock.actions) {
|
|
319
|
+
if (action.type === "wrote-file") {
|
|
320
|
+
const fp = path.join(frontendDir, action.filePath);
|
|
321
|
+
if (await fs.pathExists(fp)) await fs.remove(fp);
|
|
322
|
+
} else if (action.type === "added-pkg-script") {
|
|
323
|
+
const pkgPath = path.join(frontendDir, "package.json");
|
|
324
|
+
if (await fs.pathExists(pkgPath)) {
|
|
325
|
+
const pkg = await fs.readJson(pkgPath);
|
|
326
|
+
if (action.originalValue == null) {
|
|
327
|
+
delete pkg.scripts?.[action.key];
|
|
328
|
+
} else {
|
|
329
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
330
|
+
pkg.scripts[action.key] = action.originalValue;
|
|
331
|
+
}
|
|
332
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
333
|
+
}
|
|
334
|
+
} else if (action.type === "patched-pkg-proxy") {
|
|
335
|
+
const pkgPath = path.join(frontendDir, "package.json");
|
|
336
|
+
if (await fs.pathExists(pkgPath)) {
|
|
337
|
+
const pkg = await fs.readJson(pkgPath);
|
|
338
|
+
if (action.originalProxy == null) {
|
|
339
|
+
delete pkg.proxy;
|
|
340
|
+
} else {
|
|
341
|
+
pkg.proxy = action.originalProxy;
|
|
342
|
+
}
|
|
343
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (lock.mockServerPid) {
|
|
349
|
+
try { process.kill(lock.mockServerPid, "SIGTERM"); } catch { /* already dead */ }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await fs.remove(lockPath);
|
|
353
|
+
return { restored: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Start mock/server.js as a detached background process.
|
|
358
|
+
* Returns the spawned PID so it can be stored for later cleanup.
|
|
359
|
+
*/
|
|
360
|
+
export function startMockServerBackground(serverJsPath: string, port: number): number {
|
|
361
|
+
const child = spawn("node", [serverJsPath], {
|
|
362
|
+
detached: true,
|
|
363
|
+
stdio: "ignore",
|
|
364
|
+
env: { ...process.env, MOCK_PORT: String(port) },
|
|
365
|
+
});
|
|
366
|
+
child.unref();
|
|
367
|
+
return child.pid!;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Save the mock server PID into an existing lock file.
|
|
372
|
+
*/
|
|
373
|
+
export async function saveMockServerPid(frontendDir: string, pid: number): Promise<void> {
|
|
374
|
+
const lockPath = path.join(frontendDir, MOCK_LOCK_FILE);
|
|
375
|
+
if (await fs.pathExists(lockPath)) {
|
|
376
|
+
const lock: MockLock = await fs.readJson(lockPath);
|
|
377
|
+
lock.mockServerPid = pid;
|
|
378
|
+
await fs.writeJson(lockPath, lock, { spaces: 2 });
|
|
379
|
+
}
|
|
380
|
+
}
|