ai-spec-dev 0.17.0 → 0.25.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 +82 -17
- package/RELEASE_LOG.md +426 -0
- package/cli/index.ts +200 -42
- package/core/code-generator.ts +266 -46
- package/core/error-feedback.ts +14 -6
- package/core/knowledge-memory.ts +54 -0
- package/core/mock-server-generator.ts +210 -0
- package/core/task-generator.ts +6 -2
- package/dist/cli/index.js +553 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +553 -57
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +214 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +11 -3
- package/prompts/consolidate.prompt.ts +3 -1
- package/prompts/tasks.prompt.ts +28 -5
- package/purpose.md +174 -101
- package/.claude/settings.local.json +0 -18
package/cli/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { DslExtractor } from "../core/dsl-extractor";
|
|
|
38
38
|
import { TestGenerator } from "../core/test-generator";
|
|
39
39
|
import { runErrorFeedback } from "../core/error-feedback";
|
|
40
40
|
import { assessSpec, printSpecAssessment } from "../core/spec-assessor";
|
|
41
|
-
import { accumulateReviewKnowledge } from "../core/knowledge-memory";
|
|
41
|
+
import { accumulateReviewKnowledge, appendDirectLesson } from "../core/knowledge-memory";
|
|
42
42
|
import {
|
|
43
43
|
WorkspaceLoader,
|
|
44
44
|
WorkspaceConfig,
|
|
@@ -62,7 +62,14 @@ import { buildFrontendApiContract, buildContractContextSection } from "../core/c
|
|
|
62
62
|
import { loadFrontendContext, buildFrontendContextSection } from "../core/frontend-context-loader";
|
|
63
63
|
import { buildFrontendSpecPrompt, frontendSpecSystemPrompt } from "../prompts/frontend-spec.prompt";
|
|
64
64
|
import { SpecDSL } from "../core/dsl-types";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
generateMockAssets,
|
|
67
|
+
findLatestDslFile,
|
|
68
|
+
applyMockProxy,
|
|
69
|
+
restoreMockProxy,
|
|
70
|
+
startMockServerBackground,
|
|
71
|
+
saveMockServerPid,
|
|
72
|
+
} from "../core/mock-server-generator";
|
|
66
73
|
import { SpecUpdater } from "../core/spec-updater";
|
|
67
74
|
import { exportOpenApi } from "../core/openapi-exporter";
|
|
68
75
|
|
|
@@ -74,6 +81,8 @@ interface AiSpecConfig {
|
|
|
74
81
|
codegen?: CodeGenMode;
|
|
75
82
|
codegenProvider?: string;
|
|
76
83
|
codegenModel?: string;
|
|
84
|
+
/** Minimum overall spec score (1-10) required to pass Approval Gate. 0 = disabled (default). */
|
|
85
|
+
minSpecScore?: number;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
const CONFIG_FILE = ".ai-spec.json";
|
|
@@ -86,22 +95,6 @@ async function loadConfig(dir: string): Promise<AiSpecConfig> {
|
|
|
86
95
|
return {};
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
// ─── Constitution Length Warning ──────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
const CONSTITUTION_WARN_CHARS = 6000;
|
|
92
|
-
|
|
93
|
-
function warnIfConstitutionLong(context: { constitution?: string }): void {
|
|
94
|
-
if (!context.constitution) return;
|
|
95
|
-
const len = context.constitution.length;
|
|
96
|
-
if (len > CONSTITUTION_WARN_CHARS) {
|
|
97
|
-
console.log(
|
|
98
|
-
chalk.yellow(
|
|
99
|
-
` ⚠ Constitution is long (${len.toLocaleString()} chars). Consider running: ai-spec init --consolidate`
|
|
100
|
-
)
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
98
|
// ─── API Key Resolution ────────────────────────────────────────────────────────
|
|
106
99
|
|
|
107
100
|
async function resolveApiKey(
|
|
@@ -205,6 +198,8 @@ program
|
|
|
205
198
|
.option("--skip-error-feedback", "Skip error feedback loop (test/lint auto-fix)")
|
|
206
199
|
.option("--tdd", "TDD mode: generate failing tests first, then generate implementation to pass them")
|
|
207
200
|
.option("--skip-assessment", "Skip spec quality pre-assessment before the Approval Gate")
|
|
201
|
+
.option("--force", "Bypass the spec quality score gate even if score is below minSpecScore")
|
|
202
|
+
.option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy")
|
|
208
203
|
.action(async (idea: string | undefined, opts) => {
|
|
209
204
|
const currentDir = process.cwd();
|
|
210
205
|
const config = await loadConfig(currentDir);
|
|
@@ -224,7 +219,49 @@ program
|
|
|
224
219
|
if (workspaceConfig) {
|
|
225
220
|
console.log(chalk.cyan(`\n[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
226
221
|
console.log(chalk.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
227
|
-
await runMultiRepoPipeline(idea!, workspaceConfig, opts, currentDir, config);
|
|
222
|
+
const pipelineResults = await runMultiRepoPipeline(idea!, workspaceConfig, opts, currentDir, config);
|
|
223
|
+
|
|
224
|
+
// ── Auto-serve: start mock server + patch frontend proxy ──────────────
|
|
225
|
+
if (opts.serve) {
|
|
226
|
+
console.log(chalk.blue("\n─── Auto-serve: starting mock server ───────────"));
|
|
227
|
+
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
228
|
+
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
229
|
+
|
|
230
|
+
if (!backendResult) {
|
|
231
|
+
console.log(chalk.yellow(" No successful backend with DSL found — skipping auto-serve."));
|
|
232
|
+
} else {
|
|
233
|
+
const mockPort = 3001;
|
|
234
|
+
// Generate mock assets in backend repo
|
|
235
|
+
const mockResult = await generateMockAssets(backendResult.dsl!, backendResult.repoAbsPath, { port: mockPort });
|
|
236
|
+
const serverJsPath = path.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
237
|
+
console.log(chalk.green(` ✔ Mock assets generated (${mockResult.files.length} file(s))`));
|
|
238
|
+
|
|
239
|
+
// Start mock server
|
|
240
|
+
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
241
|
+
console.log(chalk.green(` ✔ Mock server started (PID ${pid}) → http://localhost:${mockPort}`));
|
|
242
|
+
|
|
243
|
+
// Patch frontend proxy
|
|
244
|
+
if (frontendResult) {
|
|
245
|
+
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl!.endpoints);
|
|
246
|
+
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
247
|
+
if (proxyResult.applied) {
|
|
248
|
+
console.log(chalk.green(` ✔ Frontend proxy patched (${proxyResult.framework})`));
|
|
249
|
+
console.log(chalk.bold.cyan(`\n Ready! Run your frontend dev server:`));
|
|
250
|
+
console.log(chalk.white(` cd ${frontendResult.repoAbsPath}`));
|
|
251
|
+
console.log(chalk.white(` ${proxyResult.devCommand}`));
|
|
252
|
+
console.log(chalk.gray(`\n When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
253
|
+
} else {
|
|
254
|
+
console.log(chalk.yellow(` ⚠ Auto-patch not available for ${proxyResult.framework}.`));
|
|
255
|
+
if (proxyResult.note) console.log(chalk.gray(` ${proxyResult.note}`));
|
|
256
|
+
console.log(chalk.gray(` Mock server: http://localhost:${mockPort}`));
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(chalk.gray(` No frontend repo found — mock server is running at http://localhost:${mockPort}`));
|
|
260
|
+
console.log(chalk.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
228
265
|
return;
|
|
229
266
|
}
|
|
230
267
|
|
|
@@ -263,13 +300,15 @@ program
|
|
|
263
300
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
264
301
|
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
265
302
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
266
|
-
warnIfConstitutionLong(context);
|
|
267
303
|
console.log(chalk.gray(` API files : ${context.apiStructure.length} files`));
|
|
268
304
|
if (context.schema) {
|
|
269
305
|
console.log(chalk.gray(` Prisma schema: found`));
|
|
270
306
|
}
|
|
271
307
|
if (context.constitution) {
|
|
272
308
|
console.log(chalk.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
309
|
+
if (context.constitution.length > 6000) {
|
|
310
|
+
console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
311
|
+
}
|
|
273
312
|
} else {
|
|
274
313
|
// Auto-run init: generate constitution before proceeding
|
|
275
314
|
console.log(chalk.yellow(" Constitution : not found — auto-generating..."));
|
|
@@ -331,13 +370,35 @@ program
|
|
|
331
370
|
const featureSlug = slugify(idea!);
|
|
332
371
|
|
|
333
372
|
// ── Step 3.4: Spec Quality Pre-Assessment ────────────────────────────────
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
373
|
+
// Skipped in --skip-assessment mode.
|
|
374
|
+
// In --auto mode: skipped UNLESS minSpecScore is configured — the hard gate
|
|
375
|
+
// must still enforce even in CI/auto runs (only the interactive display is skipped).
|
|
376
|
+
const minScore = config.minSpecScore ?? 0;
|
|
377
|
+
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
378
|
+
|
|
379
|
+
if (shouldRunAssessment) {
|
|
380
|
+
if (!opts.auto) {
|
|
381
|
+
console.log(chalk.blue("\n[3.4/6] Spec quality assessment..."));
|
|
382
|
+
}
|
|
337
383
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? undefined);
|
|
338
384
|
if (assessment) {
|
|
339
|
-
printSpecAssessment(assessment);
|
|
340
|
-
|
|
385
|
+
if (!opts.auto) printSpecAssessment(assessment);
|
|
386
|
+
|
|
387
|
+
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
388
|
+
if (opts.force) {
|
|
389
|
+
console.log(chalk.yellow(`\n ⚠ Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 — bypassed with --force.`));
|
|
390
|
+
} else {
|
|
391
|
+
console.log(chalk.red(`\n ✘ Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
392
|
+
if (!opts.auto) {
|
|
393
|
+
console.log(chalk.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
394
|
+
} else {
|
|
395
|
+
console.log(chalk.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
396
|
+
}
|
|
397
|
+
console.log(chalk.gray(` Gate threshold set in .ai-spec.json → "minSpecScore": ${minScore}`));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else if (!opts.auto) {
|
|
341
402
|
console.log(chalk.gray(" (Assessment skipped — AI call failed or timed out)"));
|
|
342
403
|
}
|
|
343
404
|
}
|
|
@@ -792,6 +853,7 @@ program
|
|
|
792
853
|
)
|
|
793
854
|
.option("--codegen-provider <name>", "Default provider for code generation")
|
|
794
855
|
.option("--codegen-model <name>", "Default model for code generation")
|
|
856
|
+
.option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)")
|
|
795
857
|
.option("--show", "Print current configuration")
|
|
796
858
|
.option("--reset", "Reset configuration to empty")
|
|
797
859
|
.option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json")
|
|
@@ -853,6 +915,14 @@ program
|
|
|
853
915
|
if (opts.codegen) updated.codegen = opts.codegen as CodeGenMode;
|
|
854
916
|
if (opts.codegenProvider) updated.codegenProvider = opts.codegenProvider;
|
|
855
917
|
if (opts.codegenModel) updated.codegenModel = opts.codegenModel;
|
|
918
|
+
if (opts.minSpecScore !== undefined) {
|
|
919
|
+
const score = parseInt(opts.minSpecScore, 10);
|
|
920
|
+
if (isNaN(score) || score < 0 || score > 10) {
|
|
921
|
+
console.error(chalk.red(" --min-spec-score must be a number between 0 and 10"));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
updated.minSpecScore = score;
|
|
925
|
+
}
|
|
856
926
|
|
|
857
927
|
await fs.writeJson(configPath, updated, { spaces: 2 });
|
|
858
928
|
console.log(chalk.green(`✔ Config saved to ${configPath}`));
|
|
@@ -1055,7 +1125,9 @@ async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
1055
1125
|
|
|
1056
1126
|
console.log(chalk.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
1057
1127
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
1058
|
-
|
|
1128
|
+
if (context.constitution && context.constitution.length > 6000) {
|
|
1129
|
+
console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
1130
|
+
}
|
|
1059
1131
|
|
|
1060
1132
|
if (!context.constitution) {
|
|
1061
1133
|
console.log(chalk.yellow(` Constitution: not found — auto-generating...`));
|
|
@@ -1194,13 +1266,22 @@ async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
1194
1266
|
/**
|
|
1195
1267
|
* Multi-repo pipeline: decompose → order repos → run each repo in order → bridge contracts.
|
|
1196
1268
|
*/
|
|
1269
|
+
type MultiRepoResult = {
|
|
1270
|
+
repoName: string;
|
|
1271
|
+
status: "success" | "failed" | "skipped";
|
|
1272
|
+
specFile: string | null;
|
|
1273
|
+
dsl: SpecDSL | null;
|
|
1274
|
+
repoAbsPath: string;
|
|
1275
|
+
role: string;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1197
1278
|
async function runMultiRepoPipeline(
|
|
1198
1279
|
idea: string,
|
|
1199
1280
|
workspace: WorkspaceConfig,
|
|
1200
1281
|
opts: Record<string, unknown>,
|
|
1201
1282
|
currentDir: string,
|
|
1202
1283
|
config: AiSpecConfig
|
|
1203
|
-
): Promise<
|
|
1284
|
+
): Promise<MultiRepoResult[]> {
|
|
1204
1285
|
// ── Resolve providers ──────────────────────────────────────────────────────
|
|
1205
1286
|
const specProviderName = (opts.provider as string) || config.provider || "gemini";
|
|
1206
1287
|
const specModelName = (opts.model as string) || config.model || DEFAULT_MODELS[specProviderName];
|
|
@@ -1239,7 +1320,6 @@ async function runMultiRepoPipeline(
|
|
|
1239
1320
|
try {
|
|
1240
1321
|
const loader = new ContextLoader(repoAbsPath);
|
|
1241
1322
|
const ctx = await loader.loadProjectContext();
|
|
1242
|
-
warnIfConstitutionLong(ctx);
|
|
1243
1323
|
contexts.set(repo.name, ctx);
|
|
1244
1324
|
|
|
1245
1325
|
// Load frontend context for frontend/mobile repos
|
|
@@ -1334,19 +1414,14 @@ async function runMultiRepoPipeline(
|
|
|
1334
1414
|
// ── Step 5: Run each repo's pipeline ──────────────────────────────────────
|
|
1335
1415
|
console.log(chalk.blue(`\n[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
1336
1416
|
|
|
1337
|
-
const results:
|
|
1338
|
-
repoName: string;
|
|
1339
|
-
status: "success" | "failed" | "skipped";
|
|
1340
|
-
specFile: string | null;
|
|
1341
|
-
dsl: SpecDSL | null;
|
|
1342
|
-
}> = [];
|
|
1417
|
+
const results: MultiRepoResult[] = [];
|
|
1343
1418
|
|
|
1344
1419
|
for (const repoReq of sortedRepoRequirements) {
|
|
1345
1420
|
// Find repo config in workspace
|
|
1346
1421
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
1347
1422
|
if (!repoConfig) {
|
|
1348
1423
|
console.log(chalk.yellow(` Skipping ${repoReq.repoName} — not found in workspace config.`));
|
|
1349
|
-
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null });
|
|
1424
|
+
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
1350
1425
|
continue;
|
|
1351
1426
|
}
|
|
1352
1427
|
|
|
@@ -1413,11 +1488,11 @@ async function runMultiRepoPipeline(
|
|
|
1413
1488
|
console.log(chalk.green(` Contract stored for downstream repos.`));
|
|
1414
1489
|
}
|
|
1415
1490
|
|
|
1416
|
-
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl });
|
|
1491
|
+
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
1417
1492
|
console.log(chalk.green(` ✔ ${repoReq.repoName} complete`));
|
|
1418
1493
|
} catch (err) {
|
|
1419
1494
|
console.error(chalk.red(` ✘ ${repoReq.repoName} failed: ${(err as Error).message}`));
|
|
1420
|
-
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null });
|
|
1495
|
+
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
1421
1496
|
// Continue — don't abort other repos
|
|
1422
1497
|
}
|
|
1423
1498
|
}
|
|
@@ -1432,6 +1507,8 @@ async function runMultiRepoPipeline(
|
|
|
1432
1507
|
const specInfo = r.specFile ? chalk.gray(` → ${r.specFile}`) : "";
|
|
1433
1508
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
1434
1509
|
}
|
|
1510
|
+
|
|
1511
|
+
return results;
|
|
1435
1512
|
}
|
|
1436
1513
|
|
|
1437
1514
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1703,7 +1780,9 @@ program
|
|
|
1703
1780
|
console.log(chalk.gray(" Loading project context..."));
|
|
1704
1781
|
const loader = new ContextLoader(currentDir);
|
|
1705
1782
|
const context = await loader.loadProjectContext();
|
|
1706
|
-
|
|
1783
|
+
if (context.constitution && context.constitution.length > 6000) {
|
|
1784
|
+
console.log(chalk.yellow(` ⚠ Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
1785
|
+
}
|
|
1707
1786
|
|
|
1708
1787
|
// ── Detect repo type ──────────────────────────────────────────────────────
|
|
1709
1788
|
const { detectRepoType: _detectRepoType } = await import("../core/workspace-loader");
|
|
@@ -1866,16 +1945,28 @@ program
|
|
|
1866
1945
|
.option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/")
|
|
1867
1946
|
.option("--proxy", "Also generate frontend proxy config snippet")
|
|
1868
1947
|
.option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)")
|
|
1869
|
-
.option(
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
)
|
|
1948
|
+
.option("--workspace", "Generate mock assets for all backend repos in the workspace")
|
|
1949
|
+
.option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)")
|
|
1950
|
+
.option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)")
|
|
1951
|
+
.option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)")
|
|
1873
1952
|
.action(async (opts) => {
|
|
1874
1953
|
const currentDir = process.cwd();
|
|
1875
1954
|
const port = parseInt(opts.port, 10) || 3001;
|
|
1876
1955
|
|
|
1877
1956
|
console.log(chalk.blue("\n─── ai-spec mock ───────────────────────────────"));
|
|
1878
1957
|
|
|
1958
|
+
// ── Restore mode ──────────────────────────────────────────────────────────
|
|
1959
|
+
if (opts.restore) {
|
|
1960
|
+
const frontendDir = opts.frontend ? path.resolve(opts.frontend) : currentDir;
|
|
1961
|
+
const r = await restoreMockProxy(frontendDir);
|
|
1962
|
+
if (r.restored) {
|
|
1963
|
+
console.log(chalk.green(" ✔ Proxy restored and mock server stopped."));
|
|
1964
|
+
} else {
|
|
1965
|
+
console.log(chalk.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
1966
|
+
}
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1879
1970
|
// ── Workspace mode ────────────────────────────────────────────────────────
|
|
1880
1971
|
if (opts.workspace) {
|
|
1881
1972
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
@@ -1952,11 +2043,47 @@ program
|
|
|
1952
2043
|
console.log(chalk.gray(` ${f.description}`));
|
|
1953
2044
|
}
|
|
1954
2045
|
|
|
2046
|
+
// ── Serve mode: start mock server + patch frontend proxy ──────────────────
|
|
2047
|
+
if (opts.serve) {
|
|
2048
|
+
const serverJsPath = path.join(currentDir, "mock", "server.js");
|
|
2049
|
+
if (!(await fs.pathExists(serverJsPath))) {
|
|
2050
|
+
console.error(chalk.red(" mock/server.js not found — generation may have failed."));
|
|
2051
|
+
process.exit(1);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Start mock server in background
|
|
2055
|
+
const pid = startMockServerBackground(serverJsPath, port);
|
|
2056
|
+
console.log(chalk.green(`\n ✔ Mock server started (PID ${pid}) → http://localhost:${port}`));
|
|
2057
|
+
|
|
2058
|
+
// Apply frontend proxy patch
|
|
2059
|
+
if (opts.frontend) {
|
|
2060
|
+
const frontendDir = path.resolve(opts.frontend);
|
|
2061
|
+
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
2062
|
+
await saveMockServerPid(frontendDir, pid);
|
|
2063
|
+
|
|
2064
|
+
if (proxyResult.applied) {
|
|
2065
|
+
console.log(chalk.green(` ✔ Frontend proxy patched (${proxyResult.framework})`));
|
|
2066
|
+
console.log(chalk.bold.cyan(`\n Ready! Open a new terminal and run:`));
|
|
2067
|
+
console.log(chalk.white(` cd ${frontendDir}`));
|
|
2068
|
+
console.log(chalk.white(` ${proxyResult.devCommand}`));
|
|
2069
|
+
console.log(chalk.gray(`\n When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
2070
|
+
} else {
|
|
2071
|
+
console.log(chalk.yellow(` ⚠ Auto-patch not available for ${proxyResult.framework}.`));
|
|
2072
|
+
if (proxyResult.note) console.log(chalk.gray(` ${proxyResult.note}`));
|
|
2073
|
+
}
|
|
2074
|
+
} else {
|
|
2075
|
+
console.log(chalk.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
2076
|
+
console.log(chalk.gray(` Mock server: http://localhost:${port}`));
|
|
2077
|
+
}
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1955
2081
|
console.log(chalk.blue("\n─── Quick start ────────────────────────────────"));
|
|
1956
2082
|
console.log(chalk.white(` 1. Install express (if not already):`));
|
|
1957
2083
|
console.log(chalk.gray(` npm install --save-dev express`));
|
|
1958
2084
|
console.log(chalk.white(` 2. Start mock server:`));
|
|
1959
2085
|
console.log(chalk.gray(` node mock/server.js`));
|
|
2086
|
+
console.log(chalk.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
1960
2087
|
console.log(chalk.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
1961
2088
|
console.log(chalk.gray(` http://localhost:${port}`));
|
|
1962
2089
|
if (opts.proxy) {
|
|
@@ -1969,6 +2096,37 @@ program
|
|
|
1969
2096
|
}
|
|
1970
2097
|
});
|
|
1971
2098
|
|
|
2099
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2100
|
+
// Command: learn — zero-friction knowledge injection into constitution §9
|
|
2101
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2102
|
+
|
|
2103
|
+
program
|
|
2104
|
+
.command("learn")
|
|
2105
|
+
.description("Append a lesson or engineering decision directly to constitution §9")
|
|
2106
|
+
.argument("[lesson]", "The lesson or decision to record (prompted if omitted)")
|
|
2107
|
+
.action(async (lesson: string | undefined) => {
|
|
2108
|
+
const currentDir = process.cwd();
|
|
2109
|
+
|
|
2110
|
+
if (!lesson) {
|
|
2111
|
+
const { default: inquirerInput } = await import("@inquirer/prompts").then(
|
|
2112
|
+
(m) => ({ default: m.input })
|
|
2113
|
+
);
|
|
2114
|
+
lesson = await inquirerInput({
|
|
2115
|
+
message: "What lesson or engineering decision should be recorded?",
|
|
2116
|
+
validate: (v) => v.trim().length > 0 || "Please enter a lesson",
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
2121
|
+
|
|
2122
|
+
if (result.appended) {
|
|
2123
|
+
console.log(chalk.green(`\n ✔ Lesson appended to constitution §9`));
|
|
2124
|
+
console.log(chalk.gray(` File: .ai-spec-constitution.md`));
|
|
2125
|
+
} else {
|
|
2126
|
+
console.log(chalk.yellow(`\n ⚠ Not appended: ${result.reason}`));
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
|
|
1972
2130
|
// Show welcome screen when invoked with no arguments
|
|
1973
2131
|
if (process.argv.length <= 2) {
|
|
1974
2132
|
(async () => {
|