ai-spec-dev 0.25.0 → 0.28.1
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/.claude/settings.local.json +10 -0
- package/README.md +11 -6
- package/RELEASE_LOG.md +102 -0
- package/cli/index.ts +50 -2
- package/core/code-generator.ts +13 -1
- package/core/error-feedback.ts +18 -7
- package/core/frontend-context-loader.ts +72 -24
- package/core/provider-utils.ts +90 -0
- package/core/reviewer.ts +46 -9
- package/core/run-logger.ts +136 -0
- package/core/run-snapshot.ts +84 -0
- package/core/spec-generator.ts +83 -67
- package/core/task-generator.ts +11 -3
- package/dist/cli/index.js +1347 -977
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1346 -976
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +432 -231
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +432 -231
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +54 -0
- package/purpose.md +159 -32
package/dist/cli/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ __export(codegen_prompt_exports, {
|
|
|
20
20
|
codeGenSystemPrompt: () => codeGenSystemPrompt,
|
|
21
21
|
getCodeGenSystemPrompt: () => getCodeGenSystemPrompt,
|
|
22
22
|
reviewArchitectureSystemPrompt: () => reviewArchitectureSystemPrompt,
|
|
23
|
+
reviewImpactComplexitySystemPrompt: () => reviewImpactComplexitySystemPrompt,
|
|
23
24
|
reviewImplementationSystemPrompt: () => reviewImplementationSystemPrompt,
|
|
24
25
|
reviewSystemPrompt: () => reviewSystemPrompt
|
|
25
26
|
});
|
|
@@ -39,7 +40,7 @@ function getCodeGenSystemPrompt(repoType) {
|
|
|
39
40
|
return codeGenSystemPrompt;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt, reviewSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt;
|
|
43
|
+
var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt, reviewSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt, reviewImpactComplexitySystemPrompt;
|
|
43
44
|
var init_codegen_prompt = __esm({
|
|
44
45
|
"prompts/codegen.prompt.ts"() {
|
|
45
46
|
"use strict";
|
|
@@ -266,6 +267,48 @@ Actionable, concrete improvements.
|
|
|
266
267
|
Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
|
|
267
268
|
|
|
268
269
|
Be specific. Reference actual code, not vague principles.`;
|
|
270
|
+
reviewImpactComplexitySystemPrompt = `You are a Senior Staff Engineer assessing the RISK and MAINTAINABILITY of a code change.
|
|
271
|
+
|
|
272
|
+
Two previous review passes have already covered architecture compliance and implementation correctness. Do NOT repeat their findings.
|
|
273
|
+
|
|
274
|
+
Your job is to answer exactly two questions:
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## \u{1F30A} \u5F71\u54CD\u9762\u8BC4\u4F30 (Impact Assessment)
|
|
279
|
+
|
|
280
|
+
Evaluate what this change touches beyond its own files:
|
|
281
|
+
|
|
282
|
+
1. **\u76F4\u63A5\u5F71\u54CD\u6587\u4EF6** \u2014 List the files created or modified.
|
|
283
|
+
2. **\u95F4\u63A5\u5F71\u54CD\u8303\u56F4** \u2014 Which existing modules, API consumers, or downstream services will be affected? (e.g., shared utilities modified, public interfaces changed, database schema altered)
|
|
284
|
+
3. **\u7834\u574F\u6027\u53D8\u66F4 (Breaking Changes)** \u2014 Are there changes requiring coordinated updates elsewhere?
|
|
285
|
+
- API endpoint signature changes (path, method, required params)
|
|
286
|
+
- Database schema changes (renamed columns, dropped fields, new NOT NULL constraints)
|
|
287
|
+
- Config / env variable changes
|
|
288
|
+
- Exported function/type renames
|
|
289
|
+
- If none: state "\u65E0\u7834\u574F\u6027\u53D8\u66F4"
|
|
290
|
+
4. **\u5F71\u54CD\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
291
|
+
- \u4F4E: isolated to new files, no shared interface changes
|
|
292
|
+
- \u4E2D: modifies shared utilities or existing public interfaces, backwards compatible
|
|
293
|
+
- \u9AD8: breaking changes, or touches core auth / payment / data integrity paths
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## \u{1F9EE} \u4EE3\u7801\u590D\u6742\u5EA6\u8BC4\u4F30 (Complexity Assessment)
|
|
298
|
+
|
|
299
|
+
Evaluate how hard the new code will be to understand and change in the future:
|
|
300
|
+
|
|
301
|
+
1. **\u8BA4\u77E5\u590D\u6742\u5EA6\u70ED\u70B9** \u2014 Identify the 1-3 most complex functions/methods. For each, explain WHY (deep nesting, multiple early returns, implicit state mutation, mixed abstraction levels).
|
|
302
|
+
2. **\u8026\u5408\u5EA6\u5206\u6790** \u2014 How tightly is the new code coupled to existing modules? Are dependencies injected or hardcoded? Any circular dependency risks?
|
|
303
|
+
3. **\u53EF\u7EF4\u62A4\u6027\u98CE\u9669** \u2014 Flag patterns that will cause pain when requirements change:
|
|
304
|
+
- Magic numbers / hardcoded strings that should be constants
|
|
305
|
+
- Business logic buried in framework lifecycle hooks or constructors
|
|
306
|
+
- Implicit temporal coupling (functions that must be called in a specific order)
|
|
307
|
+
4. **\u590D\u6742\u5EA6\u7B49\u7EA7** \u2014 Rate as \u4F4E / \u4E2D / \u9AD8 with one sentence justification.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
Format using ONLY these two sections. Be specific. Reference file names and line ranges where relevant.`;
|
|
269
312
|
}
|
|
270
313
|
});
|
|
271
314
|
|
|
@@ -276,31 +319,31 @@ __export(workspace_loader_exports, {
|
|
|
276
319
|
WorkspaceLoader: () => WorkspaceLoader,
|
|
277
320
|
detectRepoType: () => detectRepoType
|
|
278
321
|
});
|
|
279
|
-
import * as
|
|
280
|
-
import * as
|
|
322
|
+
import * as fs18 from "fs-extra";
|
|
323
|
+
import * as path17 from "path";
|
|
281
324
|
async function detectRepoType(repoAbsPath) {
|
|
282
|
-
if (await
|
|
325
|
+
if (await fs18.pathExists(path17.join(repoAbsPath, "go.mod"))) {
|
|
283
326
|
return { type: "go", role: "backend" };
|
|
284
327
|
}
|
|
285
|
-
if (await
|
|
328
|
+
if (await fs18.pathExists(path17.join(repoAbsPath, "composer.json"))) {
|
|
286
329
|
return { type: "php", role: "backend" };
|
|
287
330
|
}
|
|
288
|
-
if (await
|
|
331
|
+
if (await fs18.pathExists(path17.join(repoAbsPath, "Cargo.toml"))) {
|
|
289
332
|
return { type: "rust", role: "backend" };
|
|
290
333
|
}
|
|
291
|
-
if (await
|
|
334
|
+
if (await fs18.pathExists(path17.join(repoAbsPath, "pom.xml")) || await fs18.pathExists(path17.join(repoAbsPath, "build.gradle")) || await fs18.pathExists(path17.join(repoAbsPath, "build.gradle.kts"))) {
|
|
292
335
|
return { type: "java", role: "backend" };
|
|
293
336
|
}
|
|
294
|
-
if (await
|
|
337
|
+
if (await fs18.pathExists(path17.join(repoAbsPath, "requirements.txt")) || await fs18.pathExists(path17.join(repoAbsPath, "pyproject.toml")) || await fs18.pathExists(path17.join(repoAbsPath, "setup.py"))) {
|
|
295
338
|
return { type: "python", role: "backend" };
|
|
296
339
|
}
|
|
297
|
-
const pkgPath =
|
|
298
|
-
if (!await
|
|
340
|
+
const pkgPath = path17.join(repoAbsPath, "package.json");
|
|
341
|
+
if (!await fs18.pathExists(pkgPath)) {
|
|
299
342
|
return { type: "unknown", role: "shared" };
|
|
300
343
|
}
|
|
301
344
|
let pkg = {};
|
|
302
345
|
try {
|
|
303
|
-
pkg = await
|
|
346
|
+
pkg = await fs18.readJson(pkgPath);
|
|
304
347
|
} catch {
|
|
305
348
|
return { type: "unknown", role: "shared" };
|
|
306
349
|
}
|
|
@@ -344,13 +387,13 @@ var init_workspace_loader = __esm({
|
|
|
344
387
|
* Returns null if the file does not exist (graceful degradation).
|
|
345
388
|
*/
|
|
346
389
|
async load() {
|
|
347
|
-
const configPath =
|
|
348
|
-
if (!await
|
|
390
|
+
const configPath = path17.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
|
|
391
|
+
if (!await fs18.pathExists(configPath)) {
|
|
349
392
|
return null;
|
|
350
393
|
}
|
|
351
394
|
let raw;
|
|
352
395
|
try {
|
|
353
|
-
raw = await
|
|
396
|
+
raw = await fs18.readJson(configPath);
|
|
354
397
|
} catch (err) {
|
|
355
398
|
throw new Error(
|
|
356
399
|
`Failed to parse ${WORKSPACE_CONFIG_FILE}: ${err.message}`
|
|
@@ -373,15 +416,15 @@ var init_workspace_loader = __esm({
|
|
|
373
416
|
* Auto-detects type and role from dependencies.
|
|
374
417
|
*/
|
|
375
418
|
async autoDetect(names) {
|
|
376
|
-
const entries = await
|
|
419
|
+
const entries = await fs18.readdir(this.workspaceRoot);
|
|
377
420
|
const repos = [];
|
|
378
421
|
for (const entry of entries) {
|
|
379
|
-
const absPath =
|
|
380
|
-
const stat4 = await
|
|
422
|
+
const absPath = path17.join(this.workspaceRoot, entry);
|
|
423
|
+
const stat4 = await fs18.stat(absPath).catch(() => null);
|
|
381
424
|
if (!stat4 || !stat4.isDirectory()) continue;
|
|
382
425
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
383
426
|
if (names && !names.includes(entry)) continue;
|
|
384
|
-
const hasManifest = await
|
|
427
|
+
const hasManifest = await fs18.pathExists(path17.join(absPath, "package.json")) || await fs18.pathExists(path17.join(absPath, "go.mod")) || await fs18.pathExists(path17.join(absPath, "Cargo.toml")) || await fs18.pathExists(path17.join(absPath, "pom.xml")) || await fs18.pathExists(path17.join(absPath, "build.gradle")) || await fs18.pathExists(path17.join(absPath, "requirements.txt")) || await fs18.pathExists(path17.join(absPath, "pyproject.toml")) || await fs18.pathExists(path17.join(absPath, "composer.json"));
|
|
385
428
|
if (!hasManifest) continue;
|
|
386
429
|
const { type, role } = await detectRepoType(absPath);
|
|
387
430
|
repos.push({ name: entry, path: entry, type, role });
|
|
@@ -394,11 +437,11 @@ var init_workspace_loader = __esm({
|
|
|
394
437
|
async resolveRepoPaths(config2) {
|
|
395
438
|
const resolved = [];
|
|
396
439
|
for (const repo of config2.repos) {
|
|
397
|
-
const absPath =
|
|
440
|
+
const absPath = path17.resolve(this.workspaceRoot, repo.path);
|
|
398
441
|
let constitution;
|
|
399
|
-
const constitutionFile =
|
|
400
|
-
if (await
|
|
401
|
-
constitution = await
|
|
442
|
+
const constitutionFile = path17.join(absPath, ".ai-spec-constitution.md");
|
|
443
|
+
if (await fs18.pathExists(constitutionFile)) {
|
|
444
|
+
constitution = await fs18.readFile(constitutionFile, "utf-8");
|
|
402
445
|
}
|
|
403
446
|
resolved.push({ ...repo, constitution });
|
|
404
447
|
}
|
|
@@ -408,19 +451,19 @@ var init_workspace_loader = __esm({
|
|
|
408
451
|
* Save a workspace config to disk.
|
|
409
452
|
*/
|
|
410
453
|
async save(config2) {
|
|
411
|
-
const configPath =
|
|
454
|
+
const configPath = path17.join(this.workspaceRoot, WORKSPACE_CONFIG_FILE);
|
|
412
455
|
const toSave = {
|
|
413
456
|
name: config2.name,
|
|
414
457
|
repos: config2.repos.map(({ constitution: _c, ...rest }) => rest)
|
|
415
458
|
};
|
|
416
|
-
await
|
|
459
|
+
await fs18.writeJson(configPath, toSave, { spaces: 2 });
|
|
417
460
|
return configPath;
|
|
418
461
|
}
|
|
419
462
|
/**
|
|
420
463
|
* Resolve the absolute path of a repo given its config.
|
|
421
464
|
*/
|
|
422
465
|
resolveAbsPath(repo) {
|
|
423
|
-
return
|
|
466
|
+
return path17.resolve(this.workspaceRoot, repo.path);
|
|
424
467
|
}
|
|
425
468
|
/**
|
|
426
469
|
* Find which repos are backend (contract providers) and which depend on them.
|
|
@@ -442,9 +485,9 @@ var init_workspace_loader = __esm({
|
|
|
442
485
|
|
|
443
486
|
// cli/index.ts
|
|
444
487
|
import { Command } from "commander";
|
|
445
|
-
import * as
|
|
446
|
-
import * as
|
|
447
|
-
import
|
|
488
|
+
import * as path23 from "path";
|
|
489
|
+
import * as fs24 from "fs-extra";
|
|
490
|
+
import chalk19 from "chalk";
|
|
448
491
|
import * as dotenv from "dotenv";
|
|
449
492
|
import { input, confirm as confirm2, select as select3 } from "@inquirer/prompts";
|
|
450
493
|
|
|
@@ -559,6 +602,67 @@ model ExampleModel {
|
|
|
559
602
|
|
|
560
603
|
\u6839\u636E\u7528\u6237\u7684\u60F3\u6CD5\u548C\u9879\u76EE\u4E0A\u4E0B\u6587\u751F\u6210\u4E0A\u8FF0\u5B8C\u6574 Spec\u3002\u786E\u4FDD API \u8BBE\u8BA1\u4E0E\u73B0\u6709\u9879\u76EE\u7684\u8DEF\u7531\u98CE\u683C\u3001\u9519\u8BEF\u7801\u89C4\u8303\u4FDD\u6301\u4E00\u81F4\uFF0C\u6570\u636E\u6A21\u578B\u4E0E\u73B0\u6709 Prisma Schema \u534F\u8C03\u3002`;
|
|
561
604
|
|
|
605
|
+
// core/provider-utils.ts
|
|
606
|
+
import chalk from "chalk";
|
|
607
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
608
|
+
var ProviderError = class extends Error {
|
|
609
|
+
constructor(message, kind, originalError) {
|
|
610
|
+
super(message);
|
|
611
|
+
this.kind = kind;
|
|
612
|
+
this.originalError = originalError;
|
|
613
|
+
this.name = "ProviderError";
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function classifyError(err, label) {
|
|
617
|
+
const e = err;
|
|
618
|
+
const status = e.status ?? e.response?.status;
|
|
619
|
+
if (status === 401 || status === 403)
|
|
620
|
+
return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
|
|
621
|
+
if (status === 429)
|
|
622
|
+
return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
|
|
623
|
+
if (e._timeout || e.message?.toLowerCase().includes("timed out"))
|
|
624
|
+
return new ProviderError(`Request timed out (${label})`, "timeout", err);
|
|
625
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
|
|
626
|
+
return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
|
|
627
|
+
return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
|
|
628
|
+
}
|
|
629
|
+
function isRetryable(err) {
|
|
630
|
+
const e = err;
|
|
631
|
+
const status = e.status ?? e.response?.status;
|
|
632
|
+
if (status === 401 || status === 403) return false;
|
|
633
|
+
if (status === 429 || status !== void 0 && status >= 500) return true;
|
|
634
|
+
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED") return true;
|
|
635
|
+
if (e.message?.toLowerCase().includes("timed out")) return true;
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
async function withReliability(fn, opts) {
|
|
639
|
+
const { retries = 2, timeoutMs = 9e4, label = "AI call", onRetry } = opts ?? {};
|
|
640
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
641
|
+
try {
|
|
642
|
+
return await Promise.race([
|
|
643
|
+
fn(),
|
|
644
|
+
new Promise(
|
|
645
|
+
(_2, reject) => setTimeout(
|
|
646
|
+
() => reject(Object.assign(new Error(`timed out after ${timeoutMs / 1e3}s`), { _timeout: true })),
|
|
647
|
+
timeoutMs
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
]);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (!isRetryable(err) || attempt === retries) {
|
|
653
|
+
throw classifyError(err, label);
|
|
654
|
+
}
|
|
655
|
+
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
656
|
+
console.warn(
|
|
657
|
+
chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
|
|
658
|
+
);
|
|
659
|
+
onRetry?.(attempt + 1, err);
|
|
660
|
+
await sleep(waitMs);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
throw new Error("unreachable");
|
|
664
|
+
}
|
|
665
|
+
|
|
562
666
|
// core/spec-generator.ts
|
|
563
667
|
function geminiRequestOptions() {
|
|
564
668
|
const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
@@ -704,12 +808,17 @@ var GeminiProvider = class {
|
|
|
704
808
|
this.modelName = modelName;
|
|
705
809
|
}
|
|
706
810
|
async generate(prompt, systemInstruction) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
811
|
+
return withReliability(
|
|
812
|
+
async () => {
|
|
813
|
+
const model = this.genAI.getGenerativeModel(
|
|
814
|
+
{ model: this.modelName, ...systemInstruction ? { systemInstruction } : {} },
|
|
815
|
+
geminiRequestOptions()
|
|
816
|
+
);
|
|
817
|
+
const result = await model.generateContent(prompt);
|
|
818
|
+
return result.response.text();
|
|
819
|
+
},
|
|
820
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
710
821
|
);
|
|
711
|
-
const result = await model.generateContent(prompt);
|
|
712
|
-
return result.response.text();
|
|
713
822
|
}
|
|
714
823
|
};
|
|
715
824
|
var ClaudeProvider = class {
|
|
@@ -721,15 +830,20 @@ var ClaudeProvider = class {
|
|
|
721
830
|
this.modelName = modelName;
|
|
722
831
|
}
|
|
723
832
|
async generate(prompt, systemInstruction) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
833
|
+
return withReliability(
|
|
834
|
+
async () => {
|
|
835
|
+
const message = await this.client.messages.create({
|
|
836
|
+
model: this.modelName,
|
|
837
|
+
max_tokens: 8192,
|
|
838
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
839
|
+
messages: [{ role: "user", content: prompt }]
|
|
840
|
+
});
|
|
841
|
+
const block = message.content[0];
|
|
842
|
+
if (block.type === "text") return block.text;
|
|
843
|
+
throw new Error("Unexpected response type from Claude API");
|
|
844
|
+
},
|
|
845
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
846
|
+
);
|
|
733
847
|
}
|
|
734
848
|
};
|
|
735
849
|
var OpenAICompatibleProvider = class {
|
|
@@ -749,19 +863,24 @@ var OpenAICompatibleProvider = class {
|
|
|
749
863
|
});
|
|
750
864
|
}
|
|
751
865
|
async generate(prompt, systemInstruction) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
866
|
+
return withReliability(
|
|
867
|
+
async () => {
|
|
868
|
+
const messages = [];
|
|
869
|
+
if (systemInstruction) {
|
|
870
|
+
const isOSeries = /^o[13]/.test(this.modelName);
|
|
871
|
+
const role = isOSeries ? "developer" : this.systemRole;
|
|
872
|
+
messages.push({ role, content: systemInstruction });
|
|
873
|
+
}
|
|
874
|
+
messages.push({ role: "user", content: prompt });
|
|
875
|
+
const completion = await this.client.chat.completions.create({
|
|
876
|
+
model: this.modelName,
|
|
877
|
+
messages,
|
|
878
|
+
...this.extraBody ? { extra_body: this.extraBody } : {}
|
|
879
|
+
});
|
|
880
|
+
return completion.choices[0].message.content ?? "";
|
|
881
|
+
},
|
|
882
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
883
|
+
);
|
|
765
884
|
}
|
|
766
885
|
};
|
|
767
886
|
var MiMoProvider = class {
|
|
@@ -774,32 +893,37 @@ var MiMoProvider = class {
|
|
|
774
893
|
this.modelName = modelName;
|
|
775
894
|
}
|
|
776
895
|
async generate(prompt, systemInstruction) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
896
|
+
return withReliability(
|
|
897
|
+
async () => {
|
|
898
|
+
const body = {
|
|
899
|
+
model: this.modelName,
|
|
900
|
+
max_tokens: 16384,
|
|
901
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
902
|
+
top_p: 0.95,
|
|
903
|
+
stream: false,
|
|
904
|
+
temperature: 1,
|
|
905
|
+
stop_sequences: null
|
|
906
|
+
};
|
|
907
|
+
if (systemInstruction) {
|
|
908
|
+
body.system = systemInstruction;
|
|
909
|
+
}
|
|
910
|
+
const response = await axios.post(this.baseUrl, body, {
|
|
911
|
+
headers: {
|
|
912
|
+
"api-key": this.apiKey,
|
|
913
|
+
"Content-Type": "application/json"
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
const data = response.data;
|
|
917
|
+
const blocks = data?.content ?? [];
|
|
918
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
919
|
+
if (textBlock?.text) return textBlock.text;
|
|
920
|
+
if (data?.stop_reason === "max_tokens") {
|
|
921
|
+
throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
|
|
922
|
+
}
|
|
923
|
+
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
924
|
+
},
|
|
925
|
+
{ label: `${this.providerName}/${this.modelName}` }
|
|
926
|
+
);
|
|
803
927
|
}
|
|
804
928
|
};
|
|
805
929
|
function createProvider(providerName, apiKey, modelName) {
|
|
@@ -4260,10 +4384,10 @@ ${preview}
|
|
|
4260
4384
|
|
|
4261
4385
|
// core/spec-refiner.ts
|
|
4262
4386
|
import { editor, confirm, select } from "@inquirer/prompts";
|
|
4263
|
-
import
|
|
4387
|
+
import chalk3 from "chalk";
|
|
4264
4388
|
|
|
4265
4389
|
// core/spec-versioning.ts
|
|
4266
|
-
import
|
|
4390
|
+
import chalk2 from "chalk";
|
|
4267
4391
|
import * as fs4 from "fs-extra";
|
|
4268
4392
|
import * as path3 from "path";
|
|
4269
4393
|
function slugify(idea) {
|
|
@@ -4348,7 +4472,7 @@ function computeSimpleDiff(oldLines, newLines) {
|
|
|
4348
4472
|
var CONTEXT_LINES = 3;
|
|
4349
4473
|
function printDiff(diff) {
|
|
4350
4474
|
if (diff.added === 0 && diff.removed === 0) {
|
|
4351
|
-
console.log(
|
|
4475
|
+
console.log(chalk2.gray(" (no changes)"));
|
|
4352
4476
|
return;
|
|
4353
4477
|
}
|
|
4354
4478
|
const { lines } = diff;
|
|
@@ -4365,25 +4489,25 @@ function printDiff(diff) {
|
|
|
4365
4489
|
let prevIdx = -2;
|
|
4366
4490
|
for (const idx of sorted) {
|
|
4367
4491
|
if (idx > prevIdx + 1 && prevIdx !== -2) {
|
|
4368
|
-
console.log(
|
|
4492
|
+
console.log(chalk2.cyan(" @@"));
|
|
4369
4493
|
}
|
|
4370
4494
|
const l = lines[idx];
|
|
4371
4495
|
if (l.type === "added") {
|
|
4372
|
-
console.log(
|
|
4496
|
+
console.log(chalk2.green(` + ${l.content}`));
|
|
4373
4497
|
} else if (l.type === "removed") {
|
|
4374
|
-
console.log(
|
|
4498
|
+
console.log(chalk2.red(` - ${l.content}`));
|
|
4375
4499
|
} else {
|
|
4376
|
-
console.log(
|
|
4500
|
+
console.log(chalk2.gray(` ${l.content}`));
|
|
4377
4501
|
}
|
|
4378
4502
|
prevIdx = idx;
|
|
4379
4503
|
}
|
|
4380
4504
|
}
|
|
4381
4505
|
function printDiffSummary(diff, label) {
|
|
4382
4506
|
const parts = [];
|
|
4383
|
-
if (diff.added > 0) parts.push(
|
|
4384
|
-
if (diff.removed > 0) parts.push(
|
|
4385
|
-
if (parts.length === 0) parts.push(
|
|
4386
|
-
console.log(
|
|
4507
|
+
if (diff.added > 0) parts.push(chalk2.green(`+${diff.added}`));
|
|
4508
|
+
if (diff.removed > 0) parts.push(chalk2.red(`-${diff.removed}`));
|
|
4509
|
+
if (parts.length === 0) parts.push(chalk2.gray("no change"));
|
|
4510
|
+
console.log(chalk2.bold(` ${label}: `) + parts.join(" ") + chalk2.gray(` lines`));
|
|
4387
4511
|
}
|
|
4388
4512
|
|
|
4389
4513
|
// core/spec-refiner.ts
|
|
@@ -4395,16 +4519,16 @@ var SpecRefiner = class {
|
|
|
4395
4519
|
let currentSpec = initialSpec;
|
|
4396
4520
|
let round = 1;
|
|
4397
4521
|
while (true) {
|
|
4398
|
-
console.log(
|
|
4522
|
+
console.log(chalk3.cyan(`
|
|
4399
4523
|
\u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
4400
|
-
console.log(
|
|
4524
|
+
console.log(chalk3.gray(" Opening spec in editor. Save and close to continue."));
|
|
4401
4525
|
currentSpec = await editor({
|
|
4402
4526
|
message: "Review and edit the spec:",
|
|
4403
4527
|
default: currentSpec,
|
|
4404
4528
|
postfix: ".md",
|
|
4405
4529
|
waitForUserInput: false
|
|
4406
4530
|
});
|
|
4407
|
-
console.log(
|
|
4531
|
+
console.log(chalk3.green(" \u2714 Spec saved."));
|
|
4408
4532
|
const action = await select({
|
|
4409
4533
|
message: "What would you like to do?",
|
|
4410
4534
|
choices: [
|
|
@@ -4417,7 +4541,7 @@ var SpecRefiner = class {
|
|
|
4417
4541
|
break;
|
|
4418
4542
|
}
|
|
4419
4543
|
if (action === "ai") {
|
|
4420
|
-
console.log(
|
|
4544
|
+
console.log(chalk3.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
|
|
4421
4545
|
try {
|
|
4422
4546
|
const improved = await this.provider.generate(
|
|
4423
4547
|
`Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
|
|
@@ -4427,30 +4551,30 @@ Output ONLY the improved markdown spec, nothing else.
|
|
|
4427
4551
|
${currentSpec}`,
|
|
4428
4552
|
"You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
|
|
4429
4553
|
);
|
|
4430
|
-
console.log(
|
|
4554
|
+
console.log(chalk3.yellow("\n AI has suggested improvements. Opening diff in editor..."));
|
|
4431
4555
|
const acceptImproved = await confirm({
|
|
4432
4556
|
message: "Accept AI improvements? (opens editor so you can review first)",
|
|
4433
4557
|
default: true
|
|
4434
4558
|
});
|
|
4435
4559
|
if (acceptImproved) {
|
|
4436
4560
|
const diff = computeDiff(currentSpec, improved);
|
|
4437
|
-
console.log(
|
|
4561
|
+
console.log(chalk3.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4438
4562
|
printDiffSummary(diff, "AI edits");
|
|
4439
4563
|
printDiff(diff);
|
|
4440
|
-
console.log(
|
|
4564
|
+
console.log(chalk3.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
4441
4565
|
currentSpec = await editor({
|
|
4442
4566
|
message: "Review AI-improved spec (edit if needed, then save):",
|
|
4443
4567
|
default: improved,
|
|
4444
4568
|
postfix: ".md",
|
|
4445
4569
|
waitForUserInput: false
|
|
4446
4570
|
});
|
|
4447
|
-
console.log(
|
|
4571
|
+
console.log(chalk3.green(" \u2714 AI-improved spec accepted."));
|
|
4448
4572
|
} else {
|
|
4449
|
-
console.log(
|
|
4573
|
+
console.log(chalk3.gray(" AI improvements discarded. Keeping your version."));
|
|
4450
4574
|
}
|
|
4451
4575
|
} catch (err) {
|
|
4452
|
-
console.error(
|
|
4453
|
-
console.log(
|
|
4576
|
+
console.error(chalk3.red(" AI improvement failed:"), err);
|
|
4577
|
+
console.log(chalk3.gray(" Continuing with current spec."));
|
|
4454
4578
|
}
|
|
4455
4579
|
}
|
|
4456
4580
|
round++;
|
|
@@ -4460,14 +4584,14 @@ ${currentSpec}`,
|
|
|
4460
4584
|
};
|
|
4461
4585
|
|
|
4462
4586
|
// core/code-generator.ts
|
|
4463
|
-
import
|
|
4587
|
+
import chalk8 from "chalk";
|
|
4464
4588
|
import { execSync } from "child_process";
|
|
4465
|
-
import * as
|
|
4466
|
-
import * as
|
|
4589
|
+
import * as path9 from "path";
|
|
4590
|
+
import * as fs10 from "fs-extra";
|
|
4467
4591
|
init_codegen_prompt();
|
|
4468
4592
|
|
|
4469
4593
|
// core/task-generator.ts
|
|
4470
|
-
import
|
|
4594
|
+
import chalk4 from "chalk";
|
|
4471
4595
|
import * as fs5 from "fs-extra";
|
|
4472
4596
|
import * as path4 from "path";
|
|
4473
4597
|
|
|
@@ -4621,31 +4745,36 @@ function parseTasks(raw) {
|
|
|
4621
4745
|
}
|
|
4622
4746
|
function printTasks(tasks) {
|
|
4623
4747
|
const layerColors = {
|
|
4624
|
-
data:
|
|
4625
|
-
infra:
|
|
4626
|
-
service:
|
|
4627
|
-
api:
|
|
4628
|
-
view:
|
|
4629
|
-
route:
|
|
4630
|
-
test:
|
|
4748
|
+
data: chalk4.magenta,
|
|
4749
|
+
infra: chalk4.gray,
|
|
4750
|
+
service: chalk4.blue,
|
|
4751
|
+
api: chalk4.cyan,
|
|
4752
|
+
view: chalk4.yellow,
|
|
4753
|
+
route: chalk4.white,
|
|
4754
|
+
test: chalk4.green
|
|
4631
4755
|
};
|
|
4632
|
-
console.log(
|
|
4756
|
+
console.log(chalk4.bold(`
|
|
4633
4757
|
Tasks (${tasks.length}):`));
|
|
4634
4758
|
for (const task of tasks) {
|
|
4635
|
-
const color = layerColors[task.layer] ??
|
|
4759
|
+
const color = layerColors[task.layer] ?? chalk4.white;
|
|
4636
4760
|
const badge = color(`[${task.layer}]`);
|
|
4637
|
-
const prio = task.priority === "high" ?
|
|
4638
|
-
console.log(` ${prio} ${
|
|
4761
|
+
const prio = task.priority === "high" ? chalk4.red("\u25CF") : task.priority === "medium" ? chalk4.yellow("\u25CF") : chalk4.gray("\u25CF");
|
|
4762
|
+
console.log(` ${prio} ${chalk4.bold(task.id)} ${badge} ${task.title}`);
|
|
4639
4763
|
}
|
|
4640
4764
|
}
|
|
4641
4765
|
async function loadTasksForSpec(specFilePath) {
|
|
4642
4766
|
const base = path4.basename(specFilePath, ".md");
|
|
4643
4767
|
const dir = path4.dirname(specFilePath);
|
|
4644
4768
|
const tasksFile = path4.join(dir, `${base}-tasks.json`);
|
|
4645
|
-
if (await fs5.pathExists(tasksFile))
|
|
4646
|
-
|
|
4769
|
+
if (!await fs5.pathExists(tasksFile)) return null;
|
|
4770
|
+
try {
|
|
4771
|
+
return await fs5.readJson(tasksFile);
|
|
4772
|
+
} catch {
|
|
4773
|
+
console.warn(
|
|
4774
|
+
chalk4.yellow(` \u26A0 Tasks file is corrupt or unreadable (${path4.basename(tasksFile)}).`) + chalk4.gray(` Re-run \`ai-spec tasks <spec>\` to regenerate.`)
|
|
4775
|
+
);
|
|
4776
|
+
return null;
|
|
4647
4777
|
}
|
|
4648
|
-
return null;
|
|
4649
4778
|
}
|
|
4650
4779
|
async function updateTaskStatus(specFilePath, taskId, status) {
|
|
4651
4780
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
@@ -4659,13 +4788,13 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4659
4788
|
}
|
|
4660
4789
|
|
|
4661
4790
|
// core/dsl-extractor.ts
|
|
4662
|
-
import
|
|
4791
|
+
import chalk6 from "chalk";
|
|
4663
4792
|
import * as fs6 from "fs-extra";
|
|
4664
4793
|
import * as path5 from "path";
|
|
4665
4794
|
import { select as select2 } from "@inquirer/prompts";
|
|
4666
4795
|
|
|
4667
4796
|
// core/dsl-validator.ts
|
|
4668
|
-
import
|
|
4797
|
+
import chalk5 from "chalk";
|
|
4669
4798
|
var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4670
4799
|
var MAX_MODELS = 50;
|
|
4671
4800
|
var MAX_FIELDS_PER_MODEL = 100;
|
|
@@ -4738,221 +4867,221 @@ function validateDsl(raw) {
|
|
|
4738
4867
|
}
|
|
4739
4868
|
return { valid: true, dsl: raw };
|
|
4740
4869
|
}
|
|
4741
|
-
function validateFeature(raw,
|
|
4870
|
+
function validateFeature(raw, path24, errors) {
|
|
4742
4871
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4743
|
-
errors.push({ path:
|
|
4872
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4744
4873
|
return;
|
|
4745
4874
|
}
|
|
4746
4875
|
const f = raw;
|
|
4747
|
-
requireNonEmptyString(f["id"], `${
|
|
4748
|
-
requireNonEmptyString(f["title"], `${
|
|
4749
|
-
requireNonEmptyString(f["description"], `${
|
|
4876
|
+
requireNonEmptyString(f["id"], `${path24}.id`, errors);
|
|
4877
|
+
requireNonEmptyString(f["title"], `${path24}.title`, errors);
|
|
4878
|
+
requireNonEmptyString(f["description"], `${path24}.description`, errors);
|
|
4750
4879
|
}
|
|
4751
|
-
function validateModel(raw,
|
|
4880
|
+
function validateModel(raw, path24, errors) {
|
|
4752
4881
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4753
|
-
errors.push({ path:
|
|
4882
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4754
4883
|
return;
|
|
4755
4884
|
}
|
|
4756
4885
|
const m = raw;
|
|
4757
|
-
requireNonEmptyString(m["name"], `${
|
|
4886
|
+
requireNonEmptyString(m["name"], `${path24}.name`, errors);
|
|
4758
4887
|
if (!Array.isArray(m["fields"])) {
|
|
4759
|
-
errors.push({ path: `${
|
|
4888
|
+
errors.push({ path: `${path24}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4760
4889
|
} else {
|
|
4761
4890
|
const fields = m["fields"];
|
|
4762
4891
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4763
|
-
errors.push({ path: `${
|
|
4892
|
+
errors.push({ path: `${path24}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4764
4893
|
}
|
|
4765
4894
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4766
|
-
validateModelField(fields[j2], `${
|
|
4895
|
+
validateModelField(fields[j2], `${path24}.fields[${j2}]`, errors);
|
|
4767
4896
|
}
|
|
4768
4897
|
}
|
|
4769
4898
|
if (m["relations"] !== void 0) {
|
|
4770
4899
|
if (!Array.isArray(m["relations"])) {
|
|
4771
|
-
errors.push({ path: `${
|
|
4900
|
+
errors.push({ path: `${path24}.relations`, message: "Must be an array of strings if present" });
|
|
4772
4901
|
} else {
|
|
4773
4902
|
const rels = m["relations"];
|
|
4774
4903
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4775
4904
|
if (typeof rels[j2] !== "string") {
|
|
4776
|
-
errors.push({ path: `${
|
|
4905
|
+
errors.push({ path: `${path24}.relations[${j2}]`, message: "Must be a string" });
|
|
4777
4906
|
}
|
|
4778
4907
|
}
|
|
4779
4908
|
}
|
|
4780
4909
|
}
|
|
4781
4910
|
}
|
|
4782
|
-
function validateModelField(raw,
|
|
4911
|
+
function validateModelField(raw, path24, errors) {
|
|
4783
4912
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4784
|
-
errors.push({ path:
|
|
4913
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4785
4914
|
return;
|
|
4786
4915
|
}
|
|
4787
4916
|
const f = raw;
|
|
4788
|
-
requireNonEmptyString(f["name"], `${
|
|
4789
|
-
requireNonEmptyString(f["type"], `${
|
|
4917
|
+
requireNonEmptyString(f["name"], `${path24}.name`, errors);
|
|
4918
|
+
requireNonEmptyString(f["type"], `${path24}.type`, errors);
|
|
4790
4919
|
if (typeof f["required"] !== "boolean") {
|
|
4791
|
-
errors.push({ path: `${
|
|
4920
|
+
errors.push({ path: `${path24}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4792
4921
|
}
|
|
4793
4922
|
}
|
|
4794
|
-
function validateEndpoint(raw,
|
|
4923
|
+
function validateEndpoint(raw, path24, errors) {
|
|
4795
4924
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4796
|
-
errors.push({ path:
|
|
4925
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4797
4926
|
return;
|
|
4798
4927
|
}
|
|
4799
4928
|
const e = raw;
|
|
4800
|
-
requireNonEmptyString(e["id"], `${
|
|
4801
|
-
requireNonEmptyString(e["description"], `${
|
|
4929
|
+
requireNonEmptyString(e["id"], `${path24}.id`, errors);
|
|
4930
|
+
requireNonEmptyString(e["description"], `${path24}.description`, errors);
|
|
4802
4931
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4803
4932
|
errors.push({
|
|
4804
|
-
path: `${
|
|
4933
|
+
path: `${path24}.method`,
|
|
4805
4934
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4806
4935
|
});
|
|
4807
4936
|
}
|
|
4808
4937
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4809
4938
|
errors.push({
|
|
4810
|
-
path: `${
|
|
4939
|
+
path: `${path24}.path`,
|
|
4811
4940
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4812
4941
|
});
|
|
4813
4942
|
}
|
|
4814
4943
|
if (typeof e["auth"] !== "boolean") {
|
|
4815
|
-
errors.push({ path: `${
|
|
4944
|
+
errors.push({ path: `${path24}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4816
4945
|
}
|
|
4817
4946
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4818
4947
|
errors.push({
|
|
4819
|
-
path: `${
|
|
4948
|
+
path: `${path24}.successStatus`,
|
|
4820
4949
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4821
4950
|
});
|
|
4822
4951
|
}
|
|
4823
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4952
|
+
requireNonEmptyString(e["successDescription"], `${path24}.successDescription`, errors);
|
|
4824
4953
|
if (e["request"] !== void 0) {
|
|
4825
|
-
validateRequestSchema(e["request"], `${
|
|
4954
|
+
validateRequestSchema(e["request"], `${path24}.request`, errors);
|
|
4826
4955
|
}
|
|
4827
4956
|
if (e["errors"] !== void 0) {
|
|
4828
4957
|
if (!Array.isArray(e["errors"])) {
|
|
4829
|
-
errors.push({ path: `${
|
|
4958
|
+
errors.push({ path: `${path24}.errors`, message: "Must be an array if present" });
|
|
4830
4959
|
} else {
|
|
4831
4960
|
const errs = e["errors"];
|
|
4832
4961
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4833
|
-
errors.push({ path: `${
|
|
4962
|
+
errors.push({ path: `${path24}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4834
4963
|
}
|
|
4835
4964
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4836
|
-
validateResponseError(errs[j2], `${
|
|
4965
|
+
validateResponseError(errs[j2], `${path24}.errors[${j2}]`, errors);
|
|
4837
4966
|
}
|
|
4838
4967
|
}
|
|
4839
4968
|
}
|
|
4840
4969
|
}
|
|
4841
|
-
function validateRequestSchema(raw,
|
|
4970
|
+
function validateRequestSchema(raw, path24, errors) {
|
|
4842
4971
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4843
|
-
errors.push({ path:
|
|
4972
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4844
4973
|
return;
|
|
4845
4974
|
}
|
|
4846
4975
|
const r = raw;
|
|
4847
4976
|
for (const key of ["body", "query", "params"]) {
|
|
4848
4977
|
if (r[key] !== void 0) {
|
|
4849
|
-
validateFieldMap(r[key], `${
|
|
4978
|
+
validateFieldMap(r[key], `${path24}.${key}`, errors);
|
|
4850
4979
|
}
|
|
4851
4980
|
}
|
|
4852
4981
|
}
|
|
4853
|
-
function validateFieldMap(raw,
|
|
4982
|
+
function validateFieldMap(raw, path24, errors) {
|
|
4854
4983
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4855
|
-
errors.push({ path:
|
|
4984
|
+
errors.push({ path: path24, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4856
4985
|
return;
|
|
4857
4986
|
}
|
|
4858
4987
|
const map = raw;
|
|
4859
4988
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4860
4989
|
if (typeof v2 !== "string") {
|
|
4861
|
-
errors.push({ path: `${
|
|
4990
|
+
errors.push({ path: `${path24}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4862
4991
|
}
|
|
4863
4992
|
}
|
|
4864
4993
|
}
|
|
4865
|
-
function validateResponseError(raw,
|
|
4994
|
+
function validateResponseError(raw, path24, errors) {
|
|
4866
4995
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4867
|
-
errors.push({ path:
|
|
4996
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4868
4997
|
return;
|
|
4869
4998
|
}
|
|
4870
4999
|
const e = raw;
|
|
4871
5000
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
4872
|
-
errors.push({ path: `${
|
|
5001
|
+
errors.push({ path: `${path24}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
4873
5002
|
}
|
|
4874
|
-
requireNonEmptyString(e["code"], `${
|
|
4875
|
-
requireNonEmptyString(e["description"], `${
|
|
5003
|
+
requireNonEmptyString(e["code"], `${path24}.code`, errors);
|
|
5004
|
+
requireNonEmptyString(e["description"], `${path24}.description`, errors);
|
|
4876
5005
|
}
|
|
4877
|
-
function validateBehavior(raw,
|
|
5006
|
+
function validateBehavior(raw, path24, errors) {
|
|
4878
5007
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4879
|
-
errors.push({ path:
|
|
5008
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4880
5009
|
return;
|
|
4881
5010
|
}
|
|
4882
5011
|
const b = raw;
|
|
4883
|
-
requireNonEmptyString(b["id"], `${
|
|
4884
|
-
requireNonEmptyString(b["description"], `${
|
|
5012
|
+
requireNonEmptyString(b["id"], `${path24}.id`, errors);
|
|
5013
|
+
requireNonEmptyString(b["description"], `${path24}.description`, errors);
|
|
4885
5014
|
if (b["constraints"] !== void 0) {
|
|
4886
5015
|
if (!Array.isArray(b["constraints"])) {
|
|
4887
|
-
errors.push({ path: `${
|
|
5016
|
+
errors.push({ path: `${path24}.constraints`, message: "Must be an array of strings if present" });
|
|
4888
5017
|
} else {
|
|
4889
5018
|
const cs2 = b["constraints"];
|
|
4890
5019
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
4891
5020
|
if (typeof cs2[j2] !== "string") {
|
|
4892
|
-
errors.push({ path: `${
|
|
5021
|
+
errors.push({ path: `${path24}.constraints[${j2}]`, message: "Must be a string" });
|
|
4893
5022
|
}
|
|
4894
5023
|
}
|
|
4895
5024
|
}
|
|
4896
5025
|
}
|
|
4897
5026
|
}
|
|
4898
|
-
function validateComponent(raw,
|
|
5027
|
+
function validateComponent(raw, path24, errors) {
|
|
4899
5028
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4900
|
-
errors.push({ path:
|
|
5029
|
+
errors.push({ path: path24, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4901
5030
|
return;
|
|
4902
5031
|
}
|
|
4903
5032
|
const c = raw;
|
|
4904
|
-
requireNonEmptyString(c["id"], `${
|
|
4905
|
-
requireNonEmptyString(c["name"], `${
|
|
4906
|
-
requireNonEmptyString(c["description"], `${
|
|
5033
|
+
requireNonEmptyString(c["id"], `${path24}.id`, errors);
|
|
5034
|
+
requireNonEmptyString(c["name"], `${path24}.name`, errors);
|
|
5035
|
+
requireNonEmptyString(c["description"], `${path24}.description`, errors);
|
|
4907
5036
|
if (c["props"] !== void 0) {
|
|
4908
5037
|
if (!Array.isArray(c["props"])) {
|
|
4909
|
-
errors.push({ path: `${
|
|
5038
|
+
errors.push({ path: `${path24}.props`, message: "Must be an array if present" });
|
|
4910
5039
|
} else {
|
|
4911
5040
|
const props = c["props"];
|
|
4912
5041
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
4913
5042
|
const p = props[j2];
|
|
4914
5043
|
if (typeof p !== "object" || p === null) {
|
|
4915
|
-
errors.push({ path: `${
|
|
5044
|
+
errors.push({ path: `${path24}.props[${j2}]`, message: "Must be an object" });
|
|
4916
5045
|
continue;
|
|
4917
5046
|
}
|
|
4918
|
-
requireNonEmptyString(p["name"], `${
|
|
4919
|
-
requireNonEmptyString(p["type"], `${
|
|
5047
|
+
requireNonEmptyString(p["name"], `${path24}.props[${j2}].name`, errors);
|
|
5048
|
+
requireNonEmptyString(p["type"], `${path24}.props[${j2}].type`, errors);
|
|
4920
5049
|
if (typeof p["required"] !== "boolean") {
|
|
4921
|
-
errors.push({ path: `${
|
|
5050
|
+
errors.push({ path: `${path24}.props[${j2}].required`, message: "Must be boolean" });
|
|
4922
5051
|
}
|
|
4923
5052
|
}
|
|
4924
5053
|
}
|
|
4925
5054
|
}
|
|
4926
5055
|
if (c["events"] !== void 0) {
|
|
4927
5056
|
if (!Array.isArray(c["events"])) {
|
|
4928
|
-
errors.push({ path: `${
|
|
5057
|
+
errors.push({ path: `${path24}.events`, message: "Must be an array if present" });
|
|
4929
5058
|
} else {
|
|
4930
5059
|
const events = c["events"];
|
|
4931
5060
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
4932
5061
|
const e = events[j2];
|
|
4933
5062
|
if (typeof e !== "object" || e === null) {
|
|
4934
|
-
errors.push({ path: `${
|
|
5063
|
+
errors.push({ path: `${path24}.events[${j2}]`, message: "Must be an object" });
|
|
4935
5064
|
continue;
|
|
4936
5065
|
}
|
|
4937
|
-
requireNonEmptyString(e["name"], `${
|
|
5066
|
+
requireNonEmptyString(e["name"], `${path24}.events[${j2}].name`, errors);
|
|
4938
5067
|
}
|
|
4939
5068
|
}
|
|
4940
5069
|
}
|
|
4941
5070
|
if (c["state"] !== void 0) {
|
|
4942
5071
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
4943
|
-
errors.push({ path: `${
|
|
5072
|
+
errors.push({ path: `${path24}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
4944
5073
|
}
|
|
4945
5074
|
}
|
|
4946
5075
|
if (c["apiCalls"] !== void 0) {
|
|
4947
5076
|
if (!Array.isArray(c["apiCalls"])) {
|
|
4948
|
-
errors.push({ path: `${
|
|
5077
|
+
errors.push({ path: `${path24}.apiCalls`, message: "Must be an array of strings if present" });
|
|
4949
5078
|
}
|
|
4950
5079
|
}
|
|
4951
5080
|
}
|
|
4952
|
-
function requireNonEmptyString(v2,
|
|
5081
|
+
function requireNonEmptyString(v2, path24, errors) {
|
|
4953
5082
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
4954
5083
|
errors.push({
|
|
4955
|
-
path:
|
|
5084
|
+
path: path24,
|
|
4956
5085
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
4957
5086
|
});
|
|
4958
5087
|
}
|
|
@@ -4963,29 +5092,29 @@ function typeLabel(v2) {
|
|
|
4963
5092
|
return typeof v2;
|
|
4964
5093
|
}
|
|
4965
5094
|
function printValidationErrors(errors) {
|
|
4966
|
-
console.log(
|
|
5095
|
+
console.log(chalk5.red(`
|
|
4967
5096
|
DSL Validation failed \u2014 ${errors.length} error(s):
|
|
4968
5097
|
`));
|
|
4969
5098
|
for (const err of errors) {
|
|
4970
|
-
console.log(
|
|
5099
|
+
console.log(chalk5.red(` \u2718 ${chalk5.bold(err.path)}: ${err.message}`));
|
|
4971
5100
|
}
|
|
4972
5101
|
console.log();
|
|
4973
5102
|
}
|
|
4974
5103
|
function printDslSummary(dsl) {
|
|
4975
|
-
console.log(
|
|
4976
|
-
console.log(
|
|
4977
|
-
console.log(
|
|
4978
|
-
console.log(
|
|
5104
|
+
console.log(chalk5.green(" \u2714 DSL valid"));
|
|
5105
|
+
console.log(chalk5.gray(` Models : ${dsl.models.length}`));
|
|
5106
|
+
console.log(chalk5.gray(` Endpoints : ${dsl.endpoints.length}`));
|
|
5107
|
+
console.log(chalk5.gray(` Behaviors : ${dsl.behaviors.length}`));
|
|
4979
5108
|
if (dsl.components && dsl.components.length > 0) {
|
|
4980
|
-
console.log(
|
|
5109
|
+
console.log(chalk5.gray(` Components: ${dsl.components.length}`));
|
|
4981
5110
|
for (const cmp of dsl.components) {
|
|
4982
|
-
console.log(
|
|
5111
|
+
console.log(chalk5.gray(` ${cmp.id} ${cmp.name} \u2014 props:${cmp.props.length} events:${cmp.events.length}`));
|
|
4983
5112
|
}
|
|
4984
5113
|
}
|
|
4985
5114
|
if (dsl.endpoints.length > 0) {
|
|
4986
5115
|
for (const ep of dsl.endpoints) {
|
|
4987
|
-
const auth = ep.auth ?
|
|
4988
|
-
console.log(
|
|
5116
|
+
const auth = ep.auth ? chalk5.yellow(" [auth]") : "";
|
|
5117
|
+
console.log(chalk5.gray(` ${ep.method.padEnd(6)} ${ep.path}${auth} \u2014 ${ep.description}`));
|
|
4989
5118
|
}
|
|
4990
5119
|
}
|
|
4991
5120
|
}
|
|
@@ -5238,7 +5367,7 @@ var DslExtractor = class {
|
|
|
5238
5367
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
5239
5368
|
const isRetry = attempt > 1;
|
|
5240
5369
|
if (isRetry) {
|
|
5241
|
-
console.log(
|
|
5370
|
+
console.log(chalk6.yellow(`
|
|
5242
5371
|
Retry ${attempt - 1}/${MAX_RETRIES - 1}: fixing validation errors...`));
|
|
5243
5372
|
}
|
|
5244
5373
|
const activeSystemPrompt = opts.isFrontend ? dslFrontendSystemPrompt : dslSystemPrompt;
|
|
@@ -5247,7 +5376,7 @@ var DslExtractor = class {
|
|
|
5247
5376
|
try {
|
|
5248
5377
|
rawOutput = await this.provider.generate(userPrompt, activeSystemPrompt);
|
|
5249
5378
|
} catch (err) {
|
|
5250
|
-
console.log(
|
|
5379
|
+
console.log(chalk6.red(` \u2718 AI call failed: ${err.message}`));
|
|
5251
5380
|
return this.handleFailure(opts, "AI call failed");
|
|
5252
5381
|
}
|
|
5253
5382
|
lastRawOutput = rawOutput;
|
|
@@ -5255,7 +5384,7 @@ var DslExtractor = class {
|
|
|
5255
5384
|
try {
|
|
5256
5385
|
parsed = parseJsonFromOutput(rawOutput);
|
|
5257
5386
|
} catch (parseErr) {
|
|
5258
|
-
console.log(
|
|
5387
|
+
console.log(chalk6.red(` \u2718 Failed to parse JSON from AI output: ${parseErr.message}`));
|
|
5259
5388
|
lastErrors = [{ path: "root", message: "Output is not valid JSON \u2014 see raw output above" }];
|
|
5260
5389
|
if (attempt < MAX_RETRIES) continue;
|
|
5261
5390
|
return this.handleFailure(opts, "AI produced invalid JSON after retries");
|
|
@@ -5268,7 +5397,7 @@ var DslExtractor = class {
|
|
|
5268
5397
|
printValidationErrors(result.errors);
|
|
5269
5398
|
lastErrors = result.errors;
|
|
5270
5399
|
if (attempt < MAX_RETRIES) {
|
|
5271
|
-
console.log(
|
|
5400
|
+
console.log(chalk6.gray(` Will retry with error feedback...`));
|
|
5272
5401
|
continue;
|
|
5273
5402
|
}
|
|
5274
5403
|
return this.handleFailure(opts, `DSL validation failed after ${MAX_RETRIES} attempts`);
|
|
@@ -5280,10 +5409,10 @@ var DslExtractor = class {
|
|
|
5280
5409
|
* Returns null to skip, or throws to abort the pipeline.
|
|
5281
5410
|
*/
|
|
5282
5411
|
async handleFailure(opts, reason) {
|
|
5283
|
-
console.log(
|
|
5412
|
+
console.log(chalk6.yellow(`
|
|
5284
5413
|
\u26A0 DSL extraction failed: ${reason}`));
|
|
5285
5414
|
if (opts.auto) {
|
|
5286
|
-
console.log(
|
|
5415
|
+
console.log(chalk6.gray(" --auto mode: skipping DSL, continuing without it."));
|
|
5287
5416
|
return null;
|
|
5288
5417
|
}
|
|
5289
5418
|
const action = await select2({
|
|
@@ -5294,10 +5423,10 @@ var DslExtractor = class {
|
|
|
5294
5423
|
]
|
|
5295
5424
|
});
|
|
5296
5425
|
if (action === "abort") {
|
|
5297
|
-
console.log(
|
|
5426
|
+
console.log(chalk6.red(" Pipeline aborted by user."));
|
|
5298
5427
|
process.exit(1);
|
|
5299
5428
|
}
|
|
5300
|
-
console.log(
|
|
5429
|
+
console.log(chalk6.gray(" Continuing without DSL."));
|
|
5301
5430
|
return null;
|
|
5302
5431
|
}
|
|
5303
5432
|
// ─── Save ────────────────────────────────────────────────────────────────────
|
|
@@ -5574,7 +5703,7 @@ ${preview}`);
|
|
|
5574
5703
|
} catch {
|
|
5575
5704
|
}
|
|
5576
5705
|
}
|
|
5577
|
-
const httpImportRegex = /^import\s+(
|
|
5706
|
+
const httpImportRegex = /^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
|
|
5578
5707
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
5579
5708
|
try {
|
|
5580
5709
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
@@ -5587,31 +5716,51 @@ ${preview}`);
|
|
|
5587
5716
|
}
|
|
5588
5717
|
}
|
|
5589
5718
|
const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
|
|
5590
|
-
const paginationInterfaceRegex = new RegExp(
|
|
5591
|
-
`(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
|
|
5592
|
-
"s"
|
|
5593
|
-
);
|
|
5594
|
-
const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
|
|
5595
5719
|
for (const relPath of ctx.existingApiFiles) {
|
|
5596
5720
|
if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
|
|
5597
5721
|
try {
|
|
5598
5722
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5599
5723
|
if (!paginationFieldNames.some((f) => content.includes(f))) continue;
|
|
5600
|
-
const
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5724
|
+
const lines = content.split("\n");
|
|
5725
|
+
let interfaceName = "";
|
|
5726
|
+
let interfaceBlock = "";
|
|
5727
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5728
|
+
const m = lines[i].match(/(?:interface|type)\s+(\w*(?:Params|Query|Request|Filter|Page)\w*)\s*[={<]/);
|
|
5729
|
+
if (!m) continue;
|
|
5730
|
+
const blockLines = [];
|
|
5731
|
+
let depth = 0;
|
|
5732
|
+
for (let j2 = i; j2 < Math.min(i + 40, lines.length); j2++) {
|
|
5733
|
+
blockLines.push(lines[j2]);
|
|
5734
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5735
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5736
|
+
if (depth === 0 && j2 > i) break;
|
|
5737
|
+
}
|
|
5738
|
+
const blockText = blockLines.join("\n");
|
|
5739
|
+
if (!paginationFieldNames.some((f) => new RegExp(`\\b${f}\\b`).test(blockText))) continue;
|
|
5740
|
+
interfaceName = m[1];
|
|
5741
|
+
interfaceBlock = blockText;
|
|
5742
|
+
break;
|
|
5743
|
+
}
|
|
5744
|
+
if (!interfaceName) continue;
|
|
5745
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5746
|
+
const line = lines[i];
|
|
5747
|
+
if (!/export\s+(async\s+)?(function|const)/.test(line)) continue;
|
|
5748
|
+
if (!line.includes(interfaceName)) continue;
|
|
5749
|
+
const fnLines = [];
|
|
5750
|
+
let depth = 0;
|
|
5751
|
+
for (let j2 = i; j2 < Math.min(i + 30, lines.length); j2++) {
|
|
5752
|
+
fnLines.push(lines[j2]);
|
|
5753
|
+
depth += (lines[j2].match(/\{/g) ?? []).length;
|
|
5754
|
+
depth -= (lines[j2].match(/\}/g) ?? []).length;
|
|
5755
|
+
if (depth === 0 && j2 > i) break;
|
|
5756
|
+
}
|
|
5609
5757
|
ctx.paginationExample = `// From ${relPath}
|
|
5610
|
-
${
|
|
5758
|
+
${interfaceBlock}
|
|
5611
5759
|
|
|
5612
|
-
${
|
|
5760
|
+
${fnLines.join("\n")}`;
|
|
5613
5761
|
break;
|
|
5614
5762
|
}
|
|
5763
|
+
if (ctx.paginationExample) break;
|
|
5615
5764
|
} catch {
|
|
5616
5765
|
}
|
|
5617
5766
|
}
|
|
@@ -5824,6 +5973,152 @@ Shared component structure patterns:`);
|
|
|
5824
5973
|
return lines.join("\n");
|
|
5825
5974
|
}
|
|
5826
5975
|
|
|
5976
|
+
// core/run-snapshot.ts
|
|
5977
|
+
import * as fs8 from "fs-extra";
|
|
5978
|
+
import * as path7 from "path";
|
|
5979
|
+
var BACKUP_DIR = ".ai-spec-backup";
|
|
5980
|
+
var RunSnapshot = class {
|
|
5981
|
+
constructor(workingDir, runId) {
|
|
5982
|
+
this.workingDir = workingDir;
|
|
5983
|
+
this.runId = runId;
|
|
5984
|
+
this.backupRoot = path7.join(workingDir, BACKUP_DIR, runId);
|
|
5985
|
+
}
|
|
5986
|
+
backupRoot;
|
|
5987
|
+
snapshotted = /* @__PURE__ */ new Set();
|
|
5988
|
+
/**
|
|
5989
|
+
* Snapshot a file before it gets overwritten.
|
|
5990
|
+
* No-op if the file does not exist yet (new file — nothing to restore).
|
|
5991
|
+
* No-op if this file was already snapshotted in this run.
|
|
5992
|
+
*/
|
|
5993
|
+
async snapshotFile(filePath) {
|
|
5994
|
+
const fullPath = path7.isAbsolute(filePath) ? filePath : path7.join(this.workingDir, filePath);
|
|
5995
|
+
if (!await fs8.pathExists(fullPath)) return;
|
|
5996
|
+
if (this.snapshotted.has(fullPath)) return;
|
|
5997
|
+
const relative7 = path7.relative(this.workingDir, fullPath);
|
|
5998
|
+
const dest = path7.join(this.backupRoot, relative7);
|
|
5999
|
+
await fs8.ensureDir(path7.dirname(dest));
|
|
6000
|
+
await fs8.copy(fullPath, dest);
|
|
6001
|
+
this.snapshotted.add(fullPath);
|
|
6002
|
+
}
|
|
6003
|
+
/** Restore all snapshotted files. Returns list of restored relative paths. */
|
|
6004
|
+
async restore() {
|
|
6005
|
+
if (!await fs8.pathExists(this.backupRoot)) return [];
|
|
6006
|
+
const restored = [];
|
|
6007
|
+
const walk = async (dir) => {
|
|
6008
|
+
for (const entry of await fs8.readdir(dir, { withFileTypes: true })) {
|
|
6009
|
+
const full = path7.join(dir, entry.name);
|
|
6010
|
+
if (entry.isDirectory()) {
|
|
6011
|
+
await walk(full);
|
|
6012
|
+
} else {
|
|
6013
|
+
const relative7 = path7.relative(this.backupRoot, full);
|
|
6014
|
+
const dest = path7.join(this.workingDir, relative7);
|
|
6015
|
+
await fs8.ensureDir(path7.dirname(dest));
|
|
6016
|
+
await fs8.copy(full, dest, { overwrite: true });
|
|
6017
|
+
restored.push(relative7);
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
};
|
|
6021
|
+
await walk(this.backupRoot);
|
|
6022
|
+
return restored;
|
|
6023
|
+
}
|
|
6024
|
+
get fileCount() {
|
|
6025
|
+
return this.snapshotted.size;
|
|
6026
|
+
}
|
|
6027
|
+
};
|
|
6028
|
+
var _activeSnapshot = null;
|
|
6029
|
+
function setActiveSnapshot(snapshot) {
|
|
6030
|
+
_activeSnapshot = snapshot;
|
|
6031
|
+
}
|
|
6032
|
+
function getActiveSnapshot() {
|
|
6033
|
+
return _activeSnapshot;
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
// core/run-logger.ts
|
|
6037
|
+
import * as fs9 from "fs-extra";
|
|
6038
|
+
import * as path8 from "path";
|
|
6039
|
+
import chalk7 from "chalk";
|
|
6040
|
+
var LOG_DIR = ".ai-spec-logs";
|
|
6041
|
+
var RunLogger = class {
|
|
6042
|
+
constructor(workingDir, runId, meta) {
|
|
6043
|
+
this.workingDir = workingDir;
|
|
6044
|
+
this.runId = runId;
|
|
6045
|
+
this.startMs = Date.now();
|
|
6046
|
+
this.logPath = path8.join(workingDir, LOG_DIR, `${runId}.json`);
|
|
6047
|
+
this.log = {
|
|
6048
|
+
runId,
|
|
6049
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6050
|
+
workingDir,
|
|
6051
|
+
...meta,
|
|
6052
|
+
entries: [],
|
|
6053
|
+
filesWritten: [],
|
|
6054
|
+
errors: []
|
|
6055
|
+
};
|
|
6056
|
+
this.flush();
|
|
6057
|
+
}
|
|
6058
|
+
log;
|
|
6059
|
+
startMs;
|
|
6060
|
+
logPath;
|
|
6061
|
+
stageStartMs = /* @__PURE__ */ new Map();
|
|
6062
|
+
stageStart(event, data) {
|
|
6063
|
+
this.stageStartMs.set(event, Date.now());
|
|
6064
|
+
this.push(event, data);
|
|
6065
|
+
}
|
|
6066
|
+
stageEnd(event, data) {
|
|
6067
|
+
const start = this.stageStartMs.get(event);
|
|
6068
|
+
const durationMs = start !== void 0 ? Date.now() - start : void 0;
|
|
6069
|
+
this.push(`${event}:done`, { ...data, durationMs });
|
|
6070
|
+
}
|
|
6071
|
+
stageFail(event, error, data) {
|
|
6072
|
+
const start = this.stageStartMs.get(event);
|
|
6073
|
+
const durationMs = start !== void 0 ? Date.now() - start : void 0;
|
|
6074
|
+
this.push(`${event}:failed`, { ...data, error, durationMs });
|
|
6075
|
+
this.log.errors.push(`[${event}] ${error}`);
|
|
6076
|
+
this.flush();
|
|
6077
|
+
}
|
|
6078
|
+
fileWritten(filePath) {
|
|
6079
|
+
if (!this.log.filesWritten.includes(filePath)) {
|
|
6080
|
+
this.log.filesWritten.push(filePath);
|
|
6081
|
+
this.flush();
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
finish() {
|
|
6085
|
+
this.log.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6086
|
+
this.log.totalDurationMs = Date.now() - this.startMs;
|
|
6087
|
+
this.flush();
|
|
6088
|
+
}
|
|
6089
|
+
printSummary() {
|
|
6090
|
+
const dur = this.log.totalDurationMs ? ` in ${(this.log.totalDurationMs / 1e3).toFixed(1)}s` : "";
|
|
6091
|
+
const errPart = this.log.errors.length > 0 ? chalk7.yellow(` \xB7 ${this.log.errors.length} error(s)`) : "";
|
|
6092
|
+
console.log(
|
|
6093
|
+
chalk7.gray(`
|
|
6094
|
+
Run ID: ${chalk7.white(this.runId)}${dur}`) + chalk7.gray(` \xB7 ${this.log.filesWritten.length} file(s) written`) + errPart
|
|
6095
|
+
);
|
|
6096
|
+
console.log(chalk7.gray(` Log : ${path8.relative(this.workingDir, this.logPath)}`));
|
|
6097
|
+
}
|
|
6098
|
+
push(event, data) {
|
|
6099
|
+
this.log.entries.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data ? { data } : {} });
|
|
6100
|
+
this.flush();
|
|
6101
|
+
}
|
|
6102
|
+
flush() {
|
|
6103
|
+
fs9.ensureDir(path8.dirname(this.logPath)).then(() => fs9.writeJson(this.logPath, this.log, { spaces: 2 })).catch(() => {
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
};
|
|
6107
|
+
function generateRunId() {
|
|
6108
|
+
const now = /* @__PURE__ */ new Date();
|
|
6109
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
6110
|
+
const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
|
|
6111
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
6112
|
+
return `${date}-${time}-${rand}`;
|
|
6113
|
+
}
|
|
6114
|
+
var _activeLogger = null;
|
|
6115
|
+
function setActiveLogger(logger) {
|
|
6116
|
+
_activeLogger = logger;
|
|
6117
|
+
}
|
|
6118
|
+
function getActiveLogger() {
|
|
6119
|
+
return _activeLogger;
|
|
6120
|
+
}
|
|
6121
|
+
|
|
5827
6122
|
// core/code-generator.ts
|
|
5828
6123
|
function buildSharedConfigSection(context) {
|
|
5829
6124
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
@@ -5990,13 +6285,13 @@ var CodeGenerator = class {
|
|
|
5990
6285
|
let effectiveMode = this.mode;
|
|
5991
6286
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5992
6287
|
console.log(
|
|
5993
|
-
|
|
6288
|
+
chalk8.yellow(
|
|
5994
6289
|
`
|
|
5995
6290
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5996
6291
|
)
|
|
5997
6292
|
);
|
|
5998
|
-
console.log(
|
|
5999
|
-
console.log(
|
|
6293
|
+
console.log(chalk8.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
6294
|
+
console.log(chalk8.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
6000
6295
|
`));
|
|
6001
6296
|
effectiveMode = "api";
|
|
6002
6297
|
}
|
|
@@ -6021,16 +6316,16 @@ var CodeGenerator = class {
|
|
|
6021
6316
|
}
|
|
6022
6317
|
}
|
|
6023
6318
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
6024
|
-
console.log(
|
|
6319
|
+
console.log(chalk8.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6025
6320
|
if (!this.isClaudeCLIAvailable()) {
|
|
6026
|
-
console.log(
|
|
6027
|
-
console.log(
|
|
6321
|
+
console.log(chalk8.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
6322
|
+
console.log(chalk8.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
6028
6323
|
return this.runPlanMode(specFilePath);
|
|
6029
6324
|
}
|
|
6030
6325
|
const rtkAvailable = isRtkAvailable();
|
|
6031
6326
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
6032
6327
|
if (rtkAvailable) {
|
|
6033
|
-
console.log(
|
|
6328
|
+
console.log(chalk8.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
6034
6329
|
}
|
|
6035
6330
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6036
6331
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -6043,30 +6338,30 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6043
6338
|
Files: ${t.filesToTouch.join(", ")}
|
|
6044
6339
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
6045
6340
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
6046
|
-
const promptFile =
|
|
6047
|
-
await
|
|
6341
|
+
const promptFile = path9.join(workingDir, ".claude-prompt.txt");
|
|
6342
|
+
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
6048
6343
|
if (options.auto) {
|
|
6049
|
-
console.log(
|
|
6050
|
-
console.log(
|
|
6344
|
+
console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6345
|
+
console.log(chalk8.gray(` Spec: ${specFilePath}`));
|
|
6051
6346
|
try {
|
|
6052
6347
|
execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
|
|
6053
6348
|
cwd: workingDir,
|
|
6054
6349
|
stdio: "inherit"
|
|
6055
6350
|
});
|
|
6056
|
-
console.log(
|
|
6351
|
+
console.log(chalk8.green("\n \u2714 Claude Code completed."));
|
|
6057
6352
|
} catch {
|
|
6058
|
-
console.log(
|
|
6353
|
+
console.log(chalk8.yellow("\n Claude Code exited. Check output above."));
|
|
6059
6354
|
}
|
|
6060
6355
|
} else {
|
|
6061
|
-
console.log(
|
|
6062
|
-
console.log(
|
|
6063
|
-
if (tasks) console.log(
|
|
6064
|
-
console.log(
|
|
6356
|
+
console.log(chalk8.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
6357
|
+
console.log(chalk8.gray(` Spec: ${specFilePath}`));
|
|
6358
|
+
if (tasks) console.log(chalk8.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
6359
|
+
console.log(chalk8.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
6065
6360
|
try {
|
|
6066
6361
|
execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
6067
|
-
console.log(
|
|
6362
|
+
console.log(chalk8.green("\n \u2714 Claude Code session completed."));
|
|
6068
6363
|
} catch {
|
|
6069
|
-
console.log(
|
|
6364
|
+
console.log(chalk8.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
6070
6365
|
}
|
|
6071
6366
|
}
|
|
6072
6367
|
}
|
|
@@ -6079,10 +6374,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6079
6374
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
6080
6375
|
const doneCount = tasks.length - pending.length;
|
|
6081
6376
|
if (options.resume && doneCount > 0) {
|
|
6082
|
-
console.log(
|
|
6377
|
+
console.log(chalk8.cyan(`
|
|
6083
6378
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
6084
6379
|
} else {
|
|
6085
|
-
console.log(
|
|
6380
|
+
console.log(chalk8.cyan(`
|
|
6086
6381
|
Incremental mode: ${tasks.length} tasks`));
|
|
6087
6382
|
}
|
|
6088
6383
|
let completed = doneCount;
|
|
@@ -6110,32 +6405,32 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
6110
6405
|
completed++;
|
|
6111
6406
|
} catch {
|
|
6112
6407
|
taskStatus = "failed";
|
|
6113
|
-
console.log(
|
|
6408
|
+
console.log(chalk8.yellow(`
|
|
6114
6409
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
6115
6410
|
}
|
|
6116
6411
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
6117
6412
|
}
|
|
6118
6413
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
6119
6414
|
console.log(
|
|
6120
|
-
|
|
6415
|
+
chalk8.bold(
|
|
6121
6416
|
`
|
|
6122
|
-
${successCount === tasks.length ?
|
|
6417
|
+
${successCount === tasks.length ? chalk8.green("\u2714") : chalk8.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
6123
6418
|
)
|
|
6124
6419
|
);
|
|
6125
6420
|
}
|
|
6126
6421
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
6127
6422
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
6128
6423
|
console.log(
|
|
6129
|
-
|
|
6424
|
+
chalk8.blue(
|
|
6130
6425
|
`
|
|
6131
6426
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
6132
6427
|
)
|
|
6133
6428
|
);
|
|
6134
6429
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
6135
6430
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
6136
|
-
console.log(
|
|
6431
|
+
console.log(chalk8.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
6137
6432
|
}
|
|
6138
|
-
const spec = await
|
|
6433
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6139
6434
|
const constitutionSection = context?.constitution ? `
|
|
6140
6435
|
=== Project Constitution (MUST follow) ===
|
|
6141
6436
|
${context.constitution}
|
|
@@ -6151,7 +6446,7 @@ ${buildDslContextSection(dsl)}
|
|
|
6151
6446
|
if (dsl) {
|
|
6152
6447
|
const cmpCount = dsl.components?.length ?? 0;
|
|
6153
6448
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
6154
|
-
console.log(
|
|
6449
|
+
console.log(chalk8.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
6155
6450
|
}
|
|
6156
6451
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
6157
6452
|
let frontendSection = "";
|
|
@@ -6160,13 +6455,13 @@ ${buildDslContextSection(dsl)}
|
|
|
6160
6455
|
frontendSection = `
|
|
6161
6456
|
${buildFrontendContextSection(fctx)}
|
|
6162
6457
|
`;
|
|
6163
|
-
console.log(
|
|
6458
|
+
console.log(chalk8.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6164
6459
|
}
|
|
6165
6460
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6166
6461
|
if (tasks && tasks.length > 0) {
|
|
6167
6462
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6168
6463
|
}
|
|
6169
|
-
console.log(
|
|
6464
|
+
console.log(chalk8.gray(" [1/2] Planning implementation files..."));
|
|
6170
6465
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
6171
6466
|
|
|
6172
6467
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -6189,18 +6484,18 @@ Output ONLY a valid JSON array:
|
|
|
6189
6484
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
6190
6485
|
filePlan = parseJsonArray(planResponse);
|
|
6191
6486
|
} catch (err) {
|
|
6192
|
-
console.error(
|
|
6487
|
+
console.error(chalk8.red(" Failed to generate file plan:"), err);
|
|
6193
6488
|
}
|
|
6194
6489
|
if (filePlan.length === 0) {
|
|
6195
|
-
console.log(
|
|
6490
|
+
console.log(chalk8.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
6196
6491
|
await this.runPlanMode(specFilePath);
|
|
6197
6492
|
return [];
|
|
6198
6493
|
}
|
|
6199
|
-
console.log(
|
|
6494
|
+
console.log(chalk8.cyan(`
|
|
6200
6495
|
Plan: ${filePlan.length} file(s) to process`));
|
|
6201
6496
|
filePlan.forEach((item) => {
|
|
6202
|
-
const icon = item.action === "create" ?
|
|
6203
|
-
console.log(` ${icon} ${item.file}: ${
|
|
6497
|
+
const icon = item.action === "create" ? chalk8.green("+") : chalk8.yellow("~");
|
|
6498
|
+
console.log(` ${icon} ${item.file}: ${chalk8.gray(item.description)}`);
|
|
6204
6499
|
});
|
|
6205
6500
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6206
6501
|
return files;
|
|
@@ -6209,13 +6504,13 @@ Output ONLY a valid JSON array:
|
|
|
6209
6504
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
6210
6505
|
const doneCount = tasks.length - pendingTasks.length;
|
|
6211
6506
|
if (options.resume && doneCount > 0) {
|
|
6212
|
-
console.log(
|
|
6213
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
6507
|
+
console.log(chalk8.cyan(`
|
|
6508
|
+
Task-based generation (resume): ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, skipping)`));
|
|
6214
6509
|
} else if (doneCount > 0) {
|
|
6215
|
-
console.log(
|
|
6216
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
6510
|
+
console.log(chalk8.cyan(`
|
|
6511
|
+
Task-based generation: ${tasks.length} tasks (${chalk8.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
6217
6512
|
} else {
|
|
6218
|
-
console.log(
|
|
6513
|
+
console.log(chalk8.cyan(`
|
|
6219
6514
|
Task-based generation: ${tasks.length} tasks`));
|
|
6220
6515
|
}
|
|
6221
6516
|
const sharedConfigPaths = new Set(
|
|
@@ -6247,9 +6542,9 @@ Output ONLY a valid JSON array:
|
|
|
6247
6542
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
6248
6543
|
const barWidth = 20;
|
|
6249
6544
|
const filled = Math.round(pct / 100 * barWidth);
|
|
6250
|
-
const bar =
|
|
6545
|
+
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
6251
6546
|
console.log(
|
|
6252
|
-
|
|
6547
|
+
chalk8.bold(`
|
|
6253
6548
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
6254
6549
|
);
|
|
6255
6550
|
} else {
|
|
@@ -6257,12 +6552,12 @@ Output ONLY a valid JSON array:
|
|
|
6257
6552
|
}
|
|
6258
6553
|
const executeTask = async (task, batchIsParallel) => {
|
|
6259
6554
|
if (task.filesToTouch.length === 0) {
|
|
6260
|
-
if (!batchIsParallel) console.log(
|
|
6555
|
+
if (!batchIsParallel) console.log(chalk8.gray(" No files specified, skipping."));
|
|
6261
6556
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6262
6557
|
}
|
|
6263
6558
|
const filePlan = await Promise.all(
|
|
6264
6559
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
6265
|
-
const exists = await
|
|
6560
|
+
const exists = await fs10.pathExists(path9.join(workingDir, f));
|
|
6266
6561
|
return {
|
|
6267
6562
|
file: f,
|
|
6268
6563
|
action: exists ? "modify" : "create",
|
|
@@ -6302,7 +6597,7 @@ ${taskContext}`,
|
|
|
6302
6597
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
6303
6598
|
if (isCodeFile || isViewFile) {
|
|
6304
6599
|
try {
|
|
6305
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
6600
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs10.readFile(path9.join(workingDir, writtenFile), "utf-8");
|
|
6306
6601
|
generatedFileCache.set(writtenFile, content);
|
|
6307
6602
|
} catch {
|
|
6308
6603
|
}
|
|
@@ -6314,7 +6609,12 @@ ${taskContext}`,
|
|
|
6314
6609
|
const layerResults = [];
|
|
6315
6610
|
for (const batch of taskBatches) {
|
|
6316
6611
|
const batchIsParallel = batch.length > 1;
|
|
6317
|
-
const batchResultPromises = batch.map(
|
|
6612
|
+
const batchResultPromises = batch.map(
|
|
6613
|
+
(task) => executeTask(task, batchIsParallel).catch((err) => {
|
|
6614
|
+
console.log(chalk8.yellow(` \u26A0 ${task.id} threw unexpectedly: ${err.message}`));
|
|
6615
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6616
|
+
})
|
|
6617
|
+
);
|
|
6318
6618
|
const batchResults = await Promise.all(batchResultPromises);
|
|
6319
6619
|
layerResults.push(...batchResults);
|
|
6320
6620
|
await updateCacheFromBatch(batchResults);
|
|
@@ -6327,14 +6627,14 @@ ${taskContext}`,
|
|
|
6327
6627
|
totalFiles += result.total;
|
|
6328
6628
|
allGeneratedFiles.push(...result.files);
|
|
6329
6629
|
if (isParallel) {
|
|
6330
|
-
const icon = result.success === result.total ?
|
|
6630
|
+
const icon = result.success === result.total ? chalk8.green("\u2714") : chalk8.yellow("!");
|
|
6331
6631
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
6332
6632
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
6333
6633
|
}
|
|
6334
6634
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
6335
6635
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
6336
6636
|
if (taskStatus === "failed") {
|
|
6337
|
-
console.log(
|
|
6637
|
+
console.log(chalk8.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
6338
6638
|
}
|
|
6339
6639
|
}
|
|
6340
6640
|
completedTasks += layerTasks.length;
|
|
@@ -6343,13 +6643,13 @@ ${taskContext}`,
|
|
|
6343
6643
|
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
6344
6644
|
for (const sharedFile of context.sharedConfigFiles) {
|
|
6345
6645
|
if (processedSharedConfigs.has(sharedFile.path)) continue;
|
|
6346
|
-
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) =>
|
|
6646
|
+
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path9.basename(f).replace(/\.[jt]sx?$/, ""));
|
|
6347
6647
|
if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
|
|
6348
6648
|
let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
|
|
6349
6649
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
6350
6650
|
purpose = `Add to this file: import ${newModuleNames.join(", ")} from their respective paths and register them in the export/default array. Do NOT remove any existing imports.`;
|
|
6351
6651
|
}
|
|
6352
|
-
console.log(
|
|
6652
|
+
console.log(chalk8.gray(`
|
|
6353
6653
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
6354
6654
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
6355
6655
|
await this.generateFiles(
|
|
@@ -6367,26 +6667,26 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6367
6667
|
}
|
|
6368
6668
|
}
|
|
6369
6669
|
console.log(
|
|
6370
|
-
|
|
6670
|
+
chalk8.bold(
|
|
6371
6671
|
`
|
|
6372
|
-
${totalSuccess === totalFiles ?
|
|
6672
|
+
${totalSuccess === totalFiles ? chalk8.green("\u2714") : chalk8.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
6373
6673
|
)
|
|
6374
6674
|
);
|
|
6375
6675
|
return allGeneratedFiles;
|
|
6376
6676
|
}
|
|
6377
6677
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
6378
|
-
const prefix = taskLabel ? ` [${
|
|
6678
|
+
const prefix = taskLabel ? ` [${chalk8.cyan(taskLabel)}] ` : " ";
|
|
6379
6679
|
if (!taskLabel) {
|
|
6380
|
-
console.log(
|
|
6680
|
+
console.log(chalk8.gray(`
|
|
6381
6681
|
Generating ${filePlan.length} file(s)...`));
|
|
6382
6682
|
}
|
|
6383
6683
|
let successCount = 0;
|
|
6384
6684
|
const writtenFiles = [];
|
|
6385
6685
|
for (const item of filePlan) {
|
|
6386
|
-
const fullPath =
|
|
6686
|
+
const fullPath = path9.join(workingDir, item.file);
|
|
6387
6687
|
let existingContent = "";
|
|
6388
|
-
if (await
|
|
6389
|
-
existingContent = await
|
|
6688
|
+
if (await fs10.pathExists(fullPath)) {
|
|
6689
|
+
existingContent = await fs10.readFile(fullPath, "utf-8");
|
|
6390
6690
|
}
|
|
6391
6691
|
const codePrompt = `Implement this file.
|
|
6392
6692
|
|
|
@@ -6401,19 +6701,21 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6401
6701
|
try {
|
|
6402
6702
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
6403
6703
|
const fileContent = stripCodeFences(raw);
|
|
6404
|
-
await
|
|
6405
|
-
await
|
|
6406
|
-
|
|
6704
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
6705
|
+
await fs10.ensureDir(path9.dirname(fullPath));
|
|
6706
|
+
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
6707
|
+
getActiveLogger()?.fileWritten(item.file);
|
|
6708
|
+
console.log(`${prefix}${existingContent ? chalk8.yellow("~") : chalk8.green("+")} ${chalk8.bold(item.file)} ${chalk8.green("\u2714")}`);
|
|
6407
6709
|
successCount++;
|
|
6408
6710
|
writtenFiles.push(item.file);
|
|
6409
6711
|
} catch (err) {
|
|
6410
|
-
console.log(`${prefix}${
|
|
6712
|
+
console.log(`${prefix}${chalk8.red("\u2718")} ${chalk8.bold(item.file)} \u2014 ${chalk8.red(err.message)}`);
|
|
6411
6713
|
}
|
|
6412
6714
|
}
|
|
6413
6715
|
if (!taskLabel) {
|
|
6414
6716
|
console.log(
|
|
6415
|
-
|
|
6416
|
-
` ${successCount === filePlan.length ?
|
|
6717
|
+
chalk8.bold(
|
|
6718
|
+
` ${successCount === filePlan.length ? chalk8.green("\u2714") : chalk8.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
6417
6719
|
)
|
|
6418
6720
|
);
|
|
6419
6721
|
}
|
|
@@ -6421,8 +6723,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6421
6723
|
}
|
|
6422
6724
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6423
6725
|
async runPlanMode(specFilePath) {
|
|
6424
|
-
console.log(
|
|
6425
|
-
const spec = await
|
|
6726
|
+
console.log(chalk8.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6727
|
+
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6426
6728
|
const plan = await this.provider.generate(
|
|
6427
6729
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
6428
6730
|
Be specific about:
|
|
@@ -6434,7 +6736,7 @@ Be specific about:
|
|
|
6434
6736
|
${spec}`,
|
|
6435
6737
|
"You are a senior developer creating an actionable implementation guide."
|
|
6436
6738
|
);
|
|
6437
|
-
console.log(
|
|
6739
|
+
console.log(chalk8.cyan("\n") + plan);
|
|
6438
6740
|
}
|
|
6439
6741
|
};
|
|
6440
6742
|
function topoSortLayerTasks(tasks) {
|
|
@@ -6485,16 +6787,16 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6485
6787
|
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6486
6788
|
const barWidth = 20;
|
|
6487
6789
|
const filled = Math.round(pct / 100 * barWidth);
|
|
6488
|
-
const bar =
|
|
6790
|
+
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
6489
6791
|
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6490
6792
|
if (mode === "skip") {
|
|
6491
6793
|
console.log(
|
|
6492
|
-
|
|
6794
|
+
chalk8.gray(`
|
|
6493
6795
|
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6494
6796
|
);
|
|
6495
6797
|
} else {
|
|
6496
6798
|
console.log(
|
|
6497
|
-
|
|
6799
|
+
chalk8.bold(`
|
|
6498
6800
|
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6499
6801
|
);
|
|
6500
6802
|
}
|
|
@@ -6502,27 +6804,27 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6502
6804
|
|
|
6503
6805
|
// core/reviewer.ts
|
|
6504
6806
|
init_codegen_prompt();
|
|
6505
|
-
import
|
|
6807
|
+
import chalk9 from "chalk";
|
|
6506
6808
|
import { execSync as execSync2 } from "child_process";
|
|
6507
|
-
import * as
|
|
6508
|
-
import * as
|
|
6809
|
+
import * as path10 from "path";
|
|
6810
|
+
import * as fs11 from "fs-extra";
|
|
6509
6811
|
var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
6510
6812
|
async function loadReviewHistory(projectRoot) {
|
|
6511
|
-
const historyPath =
|
|
6813
|
+
const historyPath = path10.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6512
6814
|
try {
|
|
6513
|
-
if (await
|
|
6514
|
-
return await
|
|
6815
|
+
if (await fs11.pathExists(historyPath)) {
|
|
6816
|
+
return await fs11.readJson(historyPath);
|
|
6515
6817
|
}
|
|
6516
6818
|
} catch {
|
|
6517
6819
|
}
|
|
6518
6820
|
return [];
|
|
6519
6821
|
}
|
|
6520
6822
|
async function appendReviewHistory(projectRoot, entry) {
|
|
6521
|
-
const historyPath =
|
|
6823
|
+
const historyPath = path10.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6522
6824
|
const existing = await loadReviewHistory(projectRoot);
|
|
6523
6825
|
const updated = [...existing, entry].slice(-20);
|
|
6524
6826
|
try {
|
|
6525
|
-
await
|
|
6827
|
+
await fs11.writeJson(historyPath, updated, { spaces: 2 });
|
|
6526
6828
|
} catch {
|
|
6527
6829
|
}
|
|
6528
6830
|
}
|
|
@@ -6530,6 +6832,14 @@ function extractScore(reviewText) {
|
|
|
6530
6832
|
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
6531
6833
|
return match ? parseFloat(match[1]) : 0;
|
|
6532
6834
|
}
|
|
6835
|
+
function extractImpactLevel(reviewText) {
|
|
6836
|
+
const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
|
|
6837
|
+
return match ? match[1] : void 0;
|
|
6838
|
+
}
|
|
6839
|
+
function extractComplexityLevel(reviewText) {
|
|
6840
|
+
const match = reviewText.match(/复杂度等级[::]\s*(低|中|高)/);
|
|
6841
|
+
return match ? match[1] : void 0;
|
|
6842
|
+
}
|
|
6533
6843
|
function extractTopIssues(reviewText) {
|
|
6534
6844
|
const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
|
|
6535
6845
|
return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
@@ -6540,7 +6850,7 @@ function buildHistoryContext(history) {
|
|
|
6540
6850
|
const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
|
|
6541
6851
|
for (const entry of recent) {
|
|
6542
6852
|
lines.push(`
|
|
6543
|
-
[${entry.date}] ${
|
|
6853
|
+
[${entry.date}] ${path10.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
|
|
6544
6854
|
entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
|
|
6545
6855
|
}
|
|
6546
6856
|
return lines.join("\n") + "\n";
|
|
@@ -6575,15 +6885,14 @@ var CodeReviewer = class {
|
|
|
6575
6885
|
};
|
|
6576
6886
|
}
|
|
6577
6887
|
/**
|
|
6578
|
-
*
|
|
6888
|
+
* Three-pass review:
|
|
6579
6889
|
* Pass 1 — architecture (spec compliance, layer separation, auth)
|
|
6580
6890
|
* Pass 2 — implementation details (validation, error handling, edge cases)
|
|
6581
6891
|
* + historical issue recurrence check
|
|
6582
|
-
*
|
|
6583
|
-
* Falls back to single-pass if the two-pass flag is not set.
|
|
6892
|
+
* Pass 3 — impact assessment + code complexity
|
|
6584
6893
|
*/
|
|
6585
|
-
async
|
|
6586
|
-
console.log(
|
|
6894
|
+
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6895
|
+
console.log(chalk9.gray(" Pass 1/3: Architecture review..."));
|
|
6587
6896
|
const archPrompt = `Review the architecture of this change.
|
|
6588
6897
|
|
|
6589
6898
|
=== Feature Spec ===
|
|
@@ -6592,7 +6901,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6592
6901
|
=== Code ===
|
|
6593
6902
|
${codeContext}`;
|
|
6594
6903
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6595
|
-
console.log(
|
|
6904
|
+
console.log(chalk9.gray(" Pass 2/3: Implementation review..."));
|
|
6596
6905
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6597
6906
|
const historyContext = buildHistoryContext(history);
|
|
6598
6907
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6607,61 +6916,85 @@ ${codeContext}
|
|
|
6607
6916
|
${archReview}
|
|
6608
6917
|
${historyContext}`;
|
|
6609
6918
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6610
|
-
|
|
6919
|
+
console.log(chalk9.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6920
|
+
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6611
6921
|
|
|
6612
|
-
|
|
6922
|
+
=== Feature Spec ===
|
|
6923
|
+
${specContent || "(No spec \u2014 review for general code quality)"}
|
|
6924
|
+
|
|
6925
|
+
=== Code ===
|
|
6926
|
+
${codeContext}
|
|
6927
|
+
|
|
6928
|
+
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
6929
|
+
${archReview}
|
|
6613
6930
|
|
|
6931
|
+
=== Implementation Review (Pass 2 \u2014 do NOT repeat) ===
|
|
6614
6932
|
${implReview}`;
|
|
6933
|
+
const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
|
|
6934
|
+
const sep = "\u2500".repeat(52);
|
|
6935
|
+
const combined = `${archReview}
|
|
6936
|
+
|
|
6937
|
+
${sep}
|
|
6938
|
+
|
|
6939
|
+
${implReview}
|
|
6940
|
+
|
|
6941
|
+
${sep}
|
|
6942
|
+
|
|
6943
|
+
${impactReview}`;
|
|
6615
6944
|
const score = extractScore(implReview) || extractScore(archReview);
|
|
6616
6945
|
const topIssues = extractTopIssues(implReview);
|
|
6946
|
+
const impactLevel = extractImpactLevel(impactReview);
|
|
6947
|
+
const complexityLevel = extractComplexityLevel(impactReview);
|
|
6617
6948
|
if (score > 0 && specFile) {
|
|
6618
6949
|
await appendReviewHistory(this.projectRoot, {
|
|
6619
6950
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6620
|
-
specFile:
|
|
6951
|
+
specFile: path10.relative(this.projectRoot, specFile),
|
|
6621
6952
|
score,
|
|
6622
|
-
topIssues
|
|
6953
|
+
topIssues,
|
|
6954
|
+
...impactLevel ? { impactLevel } : {},
|
|
6955
|
+
...complexityLevel ? { complexityLevel } : {}
|
|
6623
6956
|
});
|
|
6624
6957
|
}
|
|
6625
6958
|
return combined;
|
|
6626
6959
|
}
|
|
6627
6960
|
async reviewCode(specContent, specFile) {
|
|
6628
|
-
console.log(
|
|
6961
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6629
6962
|
const diff = this.getGitDiff();
|
|
6630
6963
|
if (!diff.trim()) {
|
|
6631
6964
|
console.log(
|
|
6632
|
-
|
|
6965
|
+
chalk9.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6633
6966
|
);
|
|
6634
|
-
console.log(
|
|
6967
|
+
console.log(chalk9.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6635
6968
|
return "No changes";
|
|
6636
6969
|
}
|
|
6637
6970
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6638
6971
|
console.log(
|
|
6639
|
-
|
|
6972
|
+
chalk9.gray(` Diff: ${files} file(s), ${chalk9.green("+" + added)} ${chalk9.red("-" + removed)}`)
|
|
6640
6973
|
);
|
|
6641
6974
|
console.log(
|
|
6642
|
-
|
|
6975
|
+
chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6643
6976
|
);
|
|
6644
6977
|
const codeContext = diff.slice(0, 1e4);
|
|
6645
|
-
const reviewResult = await this.
|
|
6646
|
-
console.log(
|
|
6978
|
+
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6979
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6647
6980
|
console.log(reviewResult);
|
|
6648
|
-
console.log(
|
|
6981
|
+
console.log(chalk9.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6649
6982
|
return reviewResult;
|
|
6650
6983
|
}
|
|
6651
6984
|
/**
|
|
6652
6985
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6653
6986
|
*/
|
|
6654
6987
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6655
|
-
console.log(
|
|
6656
|
-
console.log(
|
|
6988
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6989
|
+
console.log(chalk9.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6657
6990
|
console.log(
|
|
6658
|
-
|
|
6991
|
+
chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6659
6992
|
);
|
|
6660
6993
|
let filesSection = "";
|
|
6661
6994
|
for (const filePath of filePaths) {
|
|
6662
|
-
const fullPath =
|
|
6995
|
+
const fullPath = path10.join(workingDir, filePath);
|
|
6663
6996
|
try {
|
|
6664
|
-
const content = await
|
|
6997
|
+
const content = await fs11.readFile(fullPath, "utf-8");
|
|
6665
6998
|
filesSection += `
|
|
6666
6999
|
|
|
6667
7000
|
=== ${filePath} ===
|
|
@@ -6675,35 +7008,35 @@ ${content.slice(0, 3e3)}`;
|
|
|
6675
7008
|
(file not found)`;
|
|
6676
7009
|
}
|
|
6677
7010
|
}
|
|
6678
|
-
const reviewResult = await this.
|
|
6679
|
-
console.log(
|
|
7011
|
+
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
7012
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6680
7013
|
console.log(reviewResult);
|
|
6681
|
-
console.log(
|
|
7014
|
+
console.log(chalk9.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6682
7015
|
return reviewResult;
|
|
6683
7016
|
}
|
|
6684
7017
|
/** Print score trend from history (last N reviews) */
|
|
6685
7018
|
async printScoreTrend(limit = 5) {
|
|
6686
7019
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6687
7020
|
if (history.length === 0) {
|
|
6688
|
-
console.log(
|
|
7021
|
+
console.log(chalk9.gray(" No review history yet."));
|
|
6689
7022
|
return;
|
|
6690
7023
|
}
|
|
6691
7024
|
const recent = history.slice(-limit);
|
|
6692
|
-
console.log(
|
|
7025
|
+
console.log(chalk9.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6693
7026
|
for (const entry of recent) {
|
|
6694
7027
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6695
|
-
const color = entry.score >= 8 ?
|
|
6696
|
-
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${
|
|
7028
|
+
const color = entry.score >= 8 ? chalk9.green : entry.score >= 6 ? chalk9.yellow : chalk9.red;
|
|
7029
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path10.basename(entry.specFile)}`);
|
|
6697
7030
|
}
|
|
6698
|
-
console.log(
|
|
7031
|
+
console.log(chalk9.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6699
7032
|
}
|
|
6700
7033
|
};
|
|
6701
7034
|
|
|
6702
7035
|
// git/worktree.ts
|
|
6703
7036
|
import { execSync as execSync3 } from "child_process";
|
|
6704
|
-
import * as
|
|
6705
|
-
import * as
|
|
6706
|
-
import
|
|
7037
|
+
import * as path11 from "path";
|
|
7038
|
+
import * as fs12 from "fs-extra";
|
|
7039
|
+
import chalk10 from "chalk";
|
|
6707
7040
|
var GitWorktreeManager = class {
|
|
6708
7041
|
constructor(baseDir) {
|
|
6709
7042
|
this.baseDir = baseDir;
|
|
@@ -6728,32 +7061,32 @@ var GitWorktreeManager = class {
|
|
|
6728
7061
|
async linkDependencies(worktreePath) {
|
|
6729
7062
|
const candidates = ["node_modules", "vendor"];
|
|
6730
7063
|
for (const dir of candidates) {
|
|
6731
|
-
const src =
|
|
6732
|
-
const dest =
|
|
6733
|
-
if (!await
|
|
6734
|
-
if (await
|
|
7064
|
+
const src = path11.join(this.baseDir, dir);
|
|
7065
|
+
const dest = path11.join(worktreePath, dir);
|
|
7066
|
+
if (!await fs12.pathExists(src)) continue;
|
|
7067
|
+
if (await fs12.pathExists(dest)) continue;
|
|
6735
7068
|
try {
|
|
6736
|
-
await
|
|
6737
|
-
console.log(
|
|
7069
|
+
await fs12.ensureSymlink(src, dest, "dir");
|
|
7070
|
+
console.log(chalk10.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6738
7071
|
} catch (err) {
|
|
6739
|
-
console.log(
|
|
6740
|
-
console.log(
|
|
7072
|
+
console.log(chalk10.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
7073
|
+
console.log(chalk10.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6741
7074
|
}
|
|
6742
7075
|
}
|
|
6743
7076
|
}
|
|
6744
7077
|
async createWorktree(idea) {
|
|
6745
7078
|
if (!this.isGitRepo()) {
|
|
6746
|
-
console.log(
|
|
7079
|
+
console.log(chalk10.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6747
7080
|
return null;
|
|
6748
7081
|
}
|
|
6749
7082
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6750
7083
|
const branchName = `feature/${featureName}`;
|
|
6751
|
-
const repoName =
|
|
6752
|
-
const worktreePath =
|
|
6753
|
-
console.log(
|
|
7084
|
+
const repoName = path11.basename(this.baseDir);
|
|
7085
|
+
const worktreePath = path11.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
7086
|
+
console.log(chalk10.cyan(`
|
|
6754
7087
|
--- Setting up Git Worktree ---`));
|
|
6755
|
-
if (await
|
|
6756
|
-
console.log(
|
|
7088
|
+
if (await fs12.pathExists(worktreePath)) {
|
|
7089
|
+
console.log(chalk10.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6757
7090
|
await this.linkDependencies(worktreePath);
|
|
6758
7091
|
return worktreePath;
|
|
6759
7092
|
}
|
|
@@ -6767,7 +7100,7 @@ var GitWorktreeManager = class {
|
|
|
6767
7100
|
branchExists = true;
|
|
6768
7101
|
} catch {
|
|
6769
7102
|
}
|
|
6770
|
-
console.log(
|
|
7103
|
+
console.log(chalk10.gray(`Creating worktree at: ${worktreePath}`));
|
|
6771
7104
|
if (branchExists) {
|
|
6772
7105
|
execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6773
7106
|
cwd: this.baseDir,
|
|
@@ -6780,21 +7113,21 @@ var GitWorktreeManager = class {
|
|
|
6780
7113
|
});
|
|
6781
7114
|
}
|
|
6782
7115
|
console.log(
|
|
6783
|
-
|
|
7116
|
+
chalk10.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6784
7117
|
);
|
|
6785
7118
|
await this.linkDependencies(worktreePath);
|
|
6786
7119
|
return worktreePath;
|
|
6787
7120
|
} catch (error) {
|
|
6788
|
-
console.error(
|
|
7121
|
+
console.error(chalk10.red("Failed to create git worktree:"), error);
|
|
6789
7122
|
return null;
|
|
6790
7123
|
}
|
|
6791
7124
|
}
|
|
6792
7125
|
};
|
|
6793
7126
|
|
|
6794
7127
|
// core/constitution-generator.ts
|
|
6795
|
-
import
|
|
6796
|
-
import * as
|
|
6797
|
-
import * as
|
|
7128
|
+
import chalk11 from "chalk";
|
|
7129
|
+
import * as fs13 from "fs-extra";
|
|
7130
|
+
import * as path12 from "path";
|
|
6798
7131
|
|
|
6799
7132
|
// prompts/constitution.prompt.ts
|
|
6800
7133
|
var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
|
|
@@ -6874,8 +7207,8 @@ var ConstitutionGenerator = class {
|
|
|
6874
7207
|
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
6875
7208
|
}
|
|
6876
7209
|
async saveConstitution(projectRoot, content) {
|
|
6877
|
-
const filePath =
|
|
6878
|
-
await
|
|
7210
|
+
const filePath = path12.join(projectRoot, CONSTITUTION_FILE);
|
|
7211
|
+
await fs13.writeFile(filePath, content, "utf-8");
|
|
6879
7212
|
return filePath;
|
|
6880
7213
|
}
|
|
6881
7214
|
};
|
|
@@ -6934,9 +7267,9 @@ ${sections.join("\n")}
|
|
|
6934
7267
|
}
|
|
6935
7268
|
|
|
6936
7269
|
// core/constitution-consolidator.ts
|
|
6937
|
-
import
|
|
6938
|
-
import * as
|
|
6939
|
-
import * as
|
|
7270
|
+
import chalk12 from "chalk";
|
|
7271
|
+
import * as fs14 from "fs-extra";
|
|
7272
|
+
import * as path13 from "path";
|
|
6940
7273
|
|
|
6941
7274
|
// prompts/consolidate.prompt.ts
|
|
6942
7275
|
var consolidateSystemPrompt = `You are a Principal Engineer maintaining a living "Project Constitution" document.
|
|
@@ -6999,26 +7332,26 @@ var ConstitutionConsolidator = class {
|
|
|
6999
7332
|
}
|
|
7000
7333
|
async consolidate(projectRoot, opts = {}) {
|
|
7001
7334
|
const minLessons = opts.minLessons ?? 5;
|
|
7002
|
-
const constitutionPath =
|
|
7003
|
-
if (!await
|
|
7335
|
+
const constitutionPath = path13.join(projectRoot, CONSTITUTION_FILE);
|
|
7336
|
+
if (!await fs14.pathExists(constitutionPath)) {
|
|
7004
7337
|
throw new Error(`No constitution file found at ${constitutionPath}. Run \`ai-spec init\` first.`);
|
|
7005
7338
|
}
|
|
7006
|
-
const original = await
|
|
7339
|
+
const original = await fs14.readFile(constitutionPath, "utf-8");
|
|
7007
7340
|
const before = parseConstitutionStats(original);
|
|
7008
|
-
console.log(
|
|
7009
|
-
console.log(
|
|
7010
|
-
console.log(
|
|
7011
|
-
console.log(
|
|
7341
|
+
console.log(chalk12.blue("\n\u2500\u2500\u2500 Constitution Consolidation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7342
|
+
console.log(chalk12.gray(` File : ${CONSTITUTION_FILE}`));
|
|
7343
|
+
console.log(chalk12.gray(` Size : ${before.totalLines} lines`));
|
|
7344
|
+
console.log(chalk12.gray(` \xA79 items: ${before.lessonCount} accumulated lessons`));
|
|
7012
7345
|
if (before.lessonCount < minLessons) {
|
|
7013
7346
|
console.log(
|
|
7014
|
-
|
|
7347
|
+
chalk12.green(
|
|
7015
7348
|
`
|
|
7016
7349
|
\u2714 \xA79 has only ${before.lessonCount} lesson(s) \u2014 no consolidation needed yet (threshold: ${minLessons}).`
|
|
7017
7350
|
)
|
|
7018
7351
|
);
|
|
7019
7352
|
return { written: false, before, after: before, backupPath: null };
|
|
7020
7353
|
}
|
|
7021
|
-
console.log(
|
|
7354
|
+
console.log(chalk12.cyan(`
|
|
7022
7355
|
Consolidating ${before.lessonCount} lesson(s) with AI...`));
|
|
7023
7356
|
const prompt = buildConsolidatePrompt(original, before.lessonCount);
|
|
7024
7357
|
let consolidated;
|
|
@@ -7030,28 +7363,28 @@ var ConstitutionConsolidator = class {
|
|
|
7030
7363
|
}
|
|
7031
7364
|
const after = parseConstitutionStats(consolidated);
|
|
7032
7365
|
const diff = computeDiff(original, consolidated);
|
|
7033
|
-
console.log(
|
|
7366
|
+
console.log(chalk12.blue("\n Changes preview:"));
|
|
7034
7367
|
printDiff(diff, 4);
|
|
7035
7368
|
printDiffSummary(diff);
|
|
7036
|
-
console.log(
|
|
7037
|
-
console.log(
|
|
7038
|
-
console.log(
|
|
7369
|
+
console.log(chalk12.cyan("\n After consolidation:"));
|
|
7370
|
+
console.log(chalk12.gray(` Size : ${after.totalLines} lines (was ${before.totalLines})`));
|
|
7371
|
+
console.log(chalk12.gray(` \xA79 items: ${after.lessonCount} remaining (was ${before.lessonCount})`));
|
|
7039
7372
|
const liftedCount = Math.max(0, before.lessonCount - after.lessonCount);
|
|
7040
7373
|
if (liftedCount > 0) {
|
|
7041
|
-
console.log(
|
|
7374
|
+
console.log(chalk12.green(` \u2714 ~${liftedCount} lesson(s) lifted into \xA71\u2013\xA78 or removed`));
|
|
7042
7375
|
}
|
|
7043
7376
|
if (opts.dryRun) {
|
|
7044
|
-
console.log(
|
|
7377
|
+
console.log(chalk12.yellow("\n [dry-run] No changes written."));
|
|
7045
7378
|
return { written: false, before, after, backupPath: null };
|
|
7046
7379
|
}
|
|
7047
7380
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[T:]/g, "-");
|
|
7048
7381
|
const backupName = `.ai-spec-constitution.backup-${timestamp}.md`;
|
|
7049
|
-
const backupPath =
|
|
7050
|
-
await
|
|
7051
|
-
console.log(
|
|
7382
|
+
const backupPath = path13.join(projectRoot, backupName);
|
|
7383
|
+
await fs14.writeFile(backupPath, original, "utf-8");
|
|
7384
|
+
console.log(chalk12.gray(`
|
|
7052
7385
|
Backup : ${backupName}`));
|
|
7053
|
-
await
|
|
7054
|
-
console.log(
|
|
7386
|
+
await fs14.writeFile(constitutionPath, consolidated, "utf-8");
|
|
7387
|
+
console.log(chalk12.green(` \u2714 Constitution updated: ${CONSTITUTION_FILE}`));
|
|
7055
7388
|
return { written: true, before, after, backupPath };
|
|
7056
7389
|
}
|
|
7057
7390
|
};
|
|
@@ -7092,9 +7425,9 @@ function parseSpecAndTasks(raw) {
|
|
|
7092
7425
|
}
|
|
7093
7426
|
|
|
7094
7427
|
// core/test-generator.ts
|
|
7095
|
-
import
|
|
7096
|
-
import * as
|
|
7097
|
-
import * as
|
|
7428
|
+
import chalk13 from "chalk";
|
|
7429
|
+
import * as fs15 from "fs-extra";
|
|
7430
|
+
import * as path14 from "path";
|
|
7098
7431
|
|
|
7099
7432
|
// prompts/testgen.prompt.ts
|
|
7100
7433
|
var testGenSystemPrompt = `You are a Senior QA Engineer generating test skeletons for a Node.js/TypeScript project.
|
|
@@ -7287,10 +7620,10 @@ function parseTestFiles(raw) {
|
|
|
7287
7620
|
return [];
|
|
7288
7621
|
}
|
|
7289
7622
|
async function isFrontendProject(workingDir) {
|
|
7290
|
-
const pkgPath =
|
|
7291
|
-
if (!await
|
|
7623
|
+
const pkgPath = path14.join(workingDir, "package.json");
|
|
7624
|
+
if (!await fs15.pathExists(pkgPath)) return false;
|
|
7292
7625
|
try {
|
|
7293
|
-
const pkg = await
|
|
7626
|
+
const pkg = await fs15.readJson(pkgPath);
|
|
7294
7627
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
7295
7628
|
const keys = Object.keys(deps);
|
|
7296
7629
|
return keys.some((k2) => FRONTEND_FRAMEWORKS.includes(k2));
|
|
@@ -7308,20 +7641,20 @@ var TestGenerator = class {
|
|
|
7308
7641
|
* Returns the list of test file paths written.
|
|
7309
7642
|
*/
|
|
7310
7643
|
async generate(dsl, workingDir) {
|
|
7311
|
-
console.log(
|
|
7644
|
+
console.log(chalk13.blue("\n\u2500\u2500\u2500 Test Generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7312
7645
|
const testDir = await this.detectTestDir(workingDir);
|
|
7313
7646
|
const frontend = await isFrontendProject(workingDir);
|
|
7314
7647
|
let prompt;
|
|
7315
7648
|
let systemPrompt;
|
|
7316
7649
|
if (frontend) {
|
|
7317
7650
|
const ctx = await loadFrontendContext(workingDir);
|
|
7318
|
-
console.log(
|
|
7319
|
-
console.log(
|
|
7651
|
+
console.log(chalk13.gray(` Mode: frontend (${ctx.framework} / ${ctx.testFramework})`));
|
|
7652
|
+
console.log(chalk13.gray(` Test directory: ${testDir}`));
|
|
7320
7653
|
prompt = buildFrontendTestGenPrompt(dsl, testDir, ctx);
|
|
7321
7654
|
systemPrompt = testGenFrontendSystemPrompt;
|
|
7322
7655
|
} else {
|
|
7323
|
-
console.log(
|
|
7324
|
-
console.log(
|
|
7656
|
+
console.log(chalk13.gray(` Mode: backend`));
|
|
7657
|
+
console.log(chalk13.gray(` Test directory: ${testDir}`));
|
|
7325
7658
|
prompt = buildBackendTestGenPrompt(dsl, testDir);
|
|
7326
7659
|
systemPrompt = testGenSystemPrompt;
|
|
7327
7660
|
}
|
|
@@ -7329,23 +7662,23 @@ var TestGenerator = class {
|
|
|
7329
7662
|
try {
|
|
7330
7663
|
rawOutput = await this.provider.generate(prompt, systemPrompt);
|
|
7331
7664
|
} catch (err) {
|
|
7332
|
-
console.log(
|
|
7665
|
+
console.log(chalk13.yellow(` \u26A0 Test generation AI call failed: ${err.message}`));
|
|
7333
7666
|
return [];
|
|
7334
7667
|
}
|
|
7335
7668
|
const testFiles = parseTestFiles(rawOutput);
|
|
7336
7669
|
if (testFiles.length === 0) {
|
|
7337
|
-
console.log(
|
|
7670
|
+
console.log(chalk13.yellow(" \u26A0 Could not parse test files from AI output. Skipping."));
|
|
7338
7671
|
return [];
|
|
7339
7672
|
}
|
|
7340
7673
|
const writtenFiles = [];
|
|
7341
7674
|
for (const tf of testFiles) {
|
|
7342
|
-
const fullPath =
|
|
7343
|
-
await
|
|
7344
|
-
await
|
|
7345
|
-
console.log(
|
|
7675
|
+
const fullPath = path14.join(workingDir, tf.file);
|
|
7676
|
+
await fs15.ensureDir(path14.dirname(fullPath));
|
|
7677
|
+
await fs15.writeFile(fullPath, tf.content, "utf-8");
|
|
7678
|
+
console.log(chalk13.green(` + ${tf.file}`));
|
|
7346
7679
|
writtenFiles.push(tf.file);
|
|
7347
7680
|
}
|
|
7348
|
-
console.log(
|
|
7681
|
+
console.log(chalk13.green(` \u2714 ${writtenFiles.length} test file(s) generated.`));
|
|
7349
7682
|
return writtenFiles;
|
|
7350
7683
|
}
|
|
7351
7684
|
/**
|
|
@@ -7354,40 +7687,40 @@ var TestGenerator = class {
|
|
|
7354
7687
|
* Only supports backend projects (uses supertest + DSL endpoints/models).
|
|
7355
7688
|
*/
|
|
7356
7689
|
async generateTdd(dsl, workingDir) {
|
|
7357
|
-
console.log(
|
|
7690
|
+
console.log(chalk13.blue("\n\u2500\u2500\u2500 TDD Test Generation (pre-implementation) \u2500\u2500\u2500\u2500"));
|
|
7358
7691
|
const testDir = await this.detectTestDir(workingDir);
|
|
7359
|
-
console.log(
|
|
7360
|
-
console.log(
|
|
7692
|
+
console.log(chalk13.gray(` Mode: TDD (real assertions \u2014 tests will fail until implementation is complete)`));
|
|
7693
|
+
console.log(chalk13.gray(` Test directory: ${testDir}`));
|
|
7361
7694
|
const prompt = buildBackendTestGenPrompt(dsl, testDir);
|
|
7362
7695
|
let rawOutput;
|
|
7363
7696
|
try {
|
|
7364
7697
|
rawOutput = await this.provider.generate(prompt, tddTestGenSystemPrompt);
|
|
7365
7698
|
} catch (err) {
|
|
7366
|
-
console.log(
|
|
7699
|
+
console.log(chalk13.yellow(` \u26A0 TDD test generation failed: ${err.message}`));
|
|
7367
7700
|
return [];
|
|
7368
7701
|
}
|
|
7369
7702
|
const testFiles = parseTestFiles(rawOutput);
|
|
7370
7703
|
if (testFiles.length === 0) {
|
|
7371
|
-
console.log(
|
|
7704
|
+
console.log(chalk13.yellow(" \u26A0 Could not parse TDD test files from AI output. Skipping."));
|
|
7372
7705
|
return [];
|
|
7373
7706
|
}
|
|
7374
7707
|
const writtenFiles = [];
|
|
7375
7708
|
for (const tf of testFiles) {
|
|
7376
|
-
const fullPath =
|
|
7377
|
-
await
|
|
7378
|
-
await
|
|
7379
|
-
console.log(
|
|
7709
|
+
const fullPath = path14.join(workingDir, tf.file);
|
|
7710
|
+
await fs15.ensureDir(path14.dirname(fullPath));
|
|
7711
|
+
await fs15.writeFile(fullPath, tf.content, "utf-8");
|
|
7712
|
+
console.log(chalk13.green(` + ${tf.file}`));
|
|
7380
7713
|
writtenFiles.push(tf.file);
|
|
7381
7714
|
}
|
|
7382
7715
|
console.log(
|
|
7383
|
-
|
|
7716
|
+
chalk13.green(` \u2714 ${writtenFiles.length} TDD test file(s) written.`) + chalk13.gray(" (expected to fail \u2014 implementation will make them pass)")
|
|
7384
7717
|
);
|
|
7385
7718
|
return writtenFiles;
|
|
7386
7719
|
}
|
|
7387
7720
|
async detectTestDir(workingDir) {
|
|
7388
7721
|
const candidates = ["tests", "test", "__tests__", "src/__tests__", "spec"];
|
|
7389
7722
|
for (const c of candidates) {
|
|
7390
|
-
if (await
|
|
7723
|
+
if (await fs15.pathExists(path14.join(workingDir, c))) return c;
|
|
7391
7724
|
}
|
|
7392
7725
|
return "tests";
|
|
7393
7726
|
}
|
|
@@ -7395,10 +7728,10 @@ var TestGenerator = class {
|
|
|
7395
7728
|
|
|
7396
7729
|
// core/error-feedback.ts
|
|
7397
7730
|
init_codegen_prompt();
|
|
7398
|
-
import
|
|
7731
|
+
import chalk14 from "chalk";
|
|
7399
7732
|
import { execSync as execSync4 } from "child_process";
|
|
7400
|
-
import * as
|
|
7401
|
-
import * as
|
|
7733
|
+
import * as fs16 from "fs-extra";
|
|
7734
|
+
import * as path15 from "path";
|
|
7402
7735
|
function runCommand(cmd, cwd) {
|
|
7403
7736
|
try {
|
|
7404
7737
|
const output = execSync4(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
|
|
@@ -7409,10 +7742,10 @@ function runCommand(cmd, cwd) {
|
|
|
7409
7742
|
}
|
|
7410
7743
|
}
|
|
7411
7744
|
function detectBuildCommand(workingDir) {
|
|
7412
|
-
if (!
|
|
7413
|
-
const pkgPath =
|
|
7745
|
+
if (!fs16.existsSync(path15.join(workingDir, "tsconfig.json"))) return null;
|
|
7746
|
+
const pkgPath = path15.join(workingDir, "package.json");
|
|
7414
7747
|
try {
|
|
7415
|
-
const pkg = JSON.parse(
|
|
7748
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
|
|
7416
7749
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7417
7750
|
if (allDeps["vue-tsc"]) return "npx vue-tsc --noEmit";
|
|
7418
7751
|
if (pkg.scripts?.["type-check"]) return "npm run type-check";
|
|
@@ -7422,53 +7755,53 @@ function detectBuildCommand(workingDir) {
|
|
|
7422
7755
|
return "npx tsc --noEmit";
|
|
7423
7756
|
}
|
|
7424
7757
|
function detectTestCommand(workingDir) {
|
|
7425
|
-
if (
|
|
7426
|
-
if (
|
|
7427
|
-
return
|
|
7758
|
+
if (fs16.existsSync(path15.join(workingDir, "go.mod"))) return "go test ./...";
|
|
7759
|
+
if (fs16.existsSync(path15.join(workingDir, "composer.json"))) {
|
|
7760
|
+
return fs16.existsSync(path15.join(workingDir, "vendor", "bin", "phpunit")) ? "./vendor/bin/phpunit --colors=never" : "php artisan test --no-ansi";
|
|
7428
7761
|
}
|
|
7429
|
-
if (
|
|
7430
|
-
if (
|
|
7431
|
-
if (
|
|
7762
|
+
if (fs16.existsSync(path15.join(workingDir, "Cargo.toml"))) return "cargo test";
|
|
7763
|
+
if (fs16.existsSync(path15.join(workingDir, "pom.xml"))) return "mvn test -q";
|
|
7764
|
+
if (fs16.existsSync(path15.join(workingDir, "build.gradle")) || fs16.existsSync(path15.join(workingDir, "build.gradle.kts"))) {
|
|
7432
7765
|
return "./gradlew test";
|
|
7433
7766
|
}
|
|
7434
|
-
if (
|
|
7767
|
+
if (fs16.existsSync(path15.join(workingDir, "requirements.txt")) || fs16.existsSync(path15.join(workingDir, "pyproject.toml")) || fs16.existsSync(path15.join(workingDir, "setup.py"))) {
|
|
7435
7768
|
return "pytest";
|
|
7436
7769
|
}
|
|
7437
|
-
const pkgPath =
|
|
7770
|
+
const pkgPath = path15.join(workingDir, "package.json");
|
|
7438
7771
|
try {
|
|
7439
|
-
const pkg = JSON.parse(
|
|
7772
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
|
|
7440
7773
|
if (pkg.scripts?.test) return "npm test";
|
|
7441
7774
|
if (pkg.scripts?.vitest) return "npx vitest run";
|
|
7442
7775
|
} catch {
|
|
7443
7776
|
}
|
|
7444
7777
|
for (const f of ["vitest.config.ts", "vitest.config.js", "jest.config.ts", "jest.config.js"]) {
|
|
7445
|
-
if (
|
|
7778
|
+
if (fs16.existsSync(path15.join(workingDir, f))) {
|
|
7446
7779
|
return f.startsWith("vitest") ? "npx vitest run" : "npx jest --forceExit";
|
|
7447
7780
|
}
|
|
7448
7781
|
}
|
|
7449
7782
|
return null;
|
|
7450
7783
|
}
|
|
7451
7784
|
function detectLintCommand(workingDir) {
|
|
7452
|
-
if (
|
|
7785
|
+
if (fs16.existsSync(path15.join(workingDir, "go.mod"))) {
|
|
7453
7786
|
return "go vet ./...";
|
|
7454
7787
|
}
|
|
7455
|
-
if (
|
|
7456
|
-
return
|
|
7788
|
+
if (fs16.existsSync(path15.join(workingDir, "composer.json"))) {
|
|
7789
|
+
return fs16.existsSync(path15.join(workingDir, "vendor", "bin", "phpstan")) ? "./vendor/bin/phpstan analyse --no-progress --memory-limit=512M" : null;
|
|
7457
7790
|
}
|
|
7458
|
-
if (
|
|
7459
|
-
if (
|
|
7791
|
+
if (fs16.existsSync(path15.join(workingDir, "Cargo.toml"))) return "cargo clippy -- -D warnings";
|
|
7792
|
+
if (fs16.existsSync(path15.join(workingDir, "pom.xml")) || fs16.existsSync(path15.join(workingDir, "build.gradle"))) {
|
|
7460
7793
|
return null;
|
|
7461
7794
|
}
|
|
7462
|
-
if (
|
|
7795
|
+
if (fs16.existsSync(path15.join(workingDir, "requirements.txt")) || fs16.existsSync(path15.join(workingDir, "pyproject.toml")) || fs16.existsSync(path15.join(workingDir, "setup.py"))) {
|
|
7463
7796
|
return "ruff check . || flake8 .";
|
|
7464
7797
|
}
|
|
7465
|
-
const pkgPath =
|
|
7798
|
+
const pkgPath = path15.join(workingDir, "package.json");
|
|
7466
7799
|
try {
|
|
7467
|
-
const pkg = JSON.parse(
|
|
7800
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf-8"));
|
|
7468
7801
|
if (pkg.scripts?.lint) return "npm run lint";
|
|
7469
7802
|
} catch {
|
|
7470
7803
|
}
|
|
7471
|
-
if (
|
|
7804
|
+
if (fs16.existsSync(path15.join(workingDir, ".eslintrc")) || fs16.existsSync(path15.join(workingDir, ".eslintrc.js")) || fs16.existsSync(path15.join(workingDir, ".eslintrc.json")) || fs16.existsSync(path15.join(workingDir, "eslint.config.js"))) {
|
|
7472
7805
|
return "npx eslint . --max-warnings=0";
|
|
7473
7806
|
}
|
|
7474
7807
|
return null;
|
|
@@ -7504,10 +7837,10 @@ async function attemptFix(provider, errors, workingDir, dsl) {
|
|
|
7504
7837
|
errorsByFile.get(file).push(err);
|
|
7505
7838
|
}
|
|
7506
7839
|
for (const [file, fileErrors] of errorsByFile) {
|
|
7507
|
-
const fullPath =
|
|
7840
|
+
const fullPath = path15.join(workingDir, file);
|
|
7508
7841
|
let existingContent = "";
|
|
7509
7842
|
try {
|
|
7510
|
-
existingContent = await
|
|
7843
|
+
existingContent = await fs16.readFile(fullPath, "utf-8");
|
|
7511
7844
|
} catch {
|
|
7512
7845
|
results.push({ fixed: false, file, explanation: "File not found \u2014 cannot auto-fix." });
|
|
7513
7846
|
continue;
|
|
@@ -7530,87 +7863,90 @@ Output ONLY the complete fixed file content. No markdown fences, no explanations
|
|
|
7530
7863
|
try {
|
|
7531
7864
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
7532
7865
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
7533
|
-
await
|
|
7866
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
7867
|
+
await fs16.writeFile(fullPath, fixed, "utf-8");
|
|
7534
7868
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
7535
|
-
console.log(
|
|
7869
|
+
console.log(chalk14.green(` \u2714 Auto-fixed: ${file}`));
|
|
7536
7870
|
} catch (err) {
|
|
7537
7871
|
results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
|
|
7538
|
-
console.log(
|
|
7872
|
+
console.log(chalk14.yellow(` \u26A0 Could not auto-fix: ${file}`));
|
|
7539
7873
|
}
|
|
7540
7874
|
}
|
|
7541
7875
|
return results;
|
|
7542
7876
|
}
|
|
7543
7877
|
async function runErrorFeedback(provider, workingDir, dsl, opts = {}) {
|
|
7544
7878
|
const maxCycles = opts.maxCycles ?? 2;
|
|
7545
|
-
console.log(
|
|
7879
|
+
console.log(chalk14.blue("\n\u2500\u2500\u2500 Error Feedback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7546
7880
|
const testCmd = opts.skipTests ? null : detectTestCommand(workingDir);
|
|
7547
7881
|
const lintCmd = opts.skipLint ? null : detectLintCommand(workingDir);
|
|
7548
7882
|
const buildCmd = opts.skipBuild ? null : detectBuildCommand(workingDir);
|
|
7549
7883
|
if (!testCmd && !lintCmd && !buildCmd) {
|
|
7550
|
-
console.log(
|
|
7884
|
+
console.log(chalk14.gray(" No test / lint / type-check commands detected. Skipping error feedback."));
|
|
7551
7885
|
return true;
|
|
7552
7886
|
}
|
|
7553
|
-
if (buildCmd) console.log(
|
|
7887
|
+
if (buildCmd) console.log(chalk14.gray(` Type-check: ${buildCmd}`));
|
|
7554
7888
|
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
7555
7889
|
const allErrors = [];
|
|
7556
7890
|
if (buildCmd) {
|
|
7557
|
-
console.log(
|
|
7891
|
+
console.log(chalk14.gray(`
|
|
7558
7892
|
[cycle ${cycle}/${maxCycles}] Type-check: ${buildCmd}`));
|
|
7559
7893
|
const buildResult = runCommand(buildCmd, workingDir);
|
|
7560
7894
|
if (!buildResult.success) {
|
|
7561
|
-
const
|
|
7895
|
+
const hasUncaughtError = /ReferenceError:|TypeError:|SyntaxError:/.test(buildResult.output);
|
|
7896
|
+
const hasToolStackFrame = buildResult.output.split("\n").some((l) => l.trim().startsWith("at ") && l.includes("node_modules"));
|
|
7897
|
+
const isToolCrash = hasUncaughtError && hasToolStackFrame;
|
|
7562
7898
|
if (isToolCrash) {
|
|
7563
|
-
console.log(
|
|
7564
|
-
console.log(
|
|
7899
|
+
console.log(chalk14.yellow(` \u26A0 Type-check tool crashed (possible version incompatibility). Skipping.`));
|
|
7900
|
+
console.log(chalk14.gray(` Tip: run \`${buildCmd}\` manually to investigate.`));
|
|
7565
7901
|
} else {
|
|
7566
7902
|
const buildErrors = parseErrors(buildResult.output, "build");
|
|
7567
7903
|
allErrors.push(...buildErrors);
|
|
7568
|
-
console.log(
|
|
7904
|
+
console.log(chalk14.yellow(` \u2718 Type errors (${buildErrors.length} captured)`));
|
|
7569
7905
|
}
|
|
7570
7906
|
} else {
|
|
7571
|
-
console.log(
|
|
7907
|
+
console.log(chalk14.green(" \u2714 Type-check passed."));
|
|
7572
7908
|
}
|
|
7573
7909
|
}
|
|
7574
7910
|
if (testCmd) {
|
|
7575
|
-
console.log(
|
|
7911
|
+
console.log(chalk14.gray(`
|
|
7576
7912
|
[cycle ${cycle}/${maxCycles}] Running tests: ${testCmd}`));
|
|
7577
7913
|
const testResult = runCommand(testCmd, workingDir);
|
|
7578
7914
|
if (!testResult.success) {
|
|
7579
7915
|
const testErrors = parseErrors(testResult.output, "test");
|
|
7580
7916
|
allErrors.push(...testErrors);
|
|
7581
|
-
console.log(
|
|
7917
|
+
console.log(chalk14.yellow(` \u2718 Tests failed (${testErrors.length} error(s) captured)`));
|
|
7582
7918
|
} else {
|
|
7583
|
-
console.log(
|
|
7919
|
+
console.log(chalk14.green(" \u2714 Tests passed."));
|
|
7584
7920
|
}
|
|
7585
7921
|
}
|
|
7586
7922
|
if (lintCmd) {
|
|
7587
|
-
console.log(
|
|
7923
|
+
console.log(chalk14.gray(` [cycle ${cycle}/${maxCycles}] Running lint: ${lintCmd}`));
|
|
7588
7924
|
const lintResult = runCommand(lintCmd, workingDir);
|
|
7589
7925
|
if (!lintResult.success) {
|
|
7590
7926
|
const lintErrors = parseErrors(lintResult.output, "lint");
|
|
7591
7927
|
allErrors.push(...lintErrors);
|
|
7592
|
-
console.log(
|
|
7928
|
+
console.log(chalk14.yellow(` \u2718 Lint failed (${lintErrors.length} error(s) captured)`));
|
|
7593
7929
|
} else {
|
|
7594
|
-
console.log(
|
|
7930
|
+
console.log(chalk14.green(" \u2714 Lint passed."));
|
|
7595
7931
|
}
|
|
7596
7932
|
}
|
|
7597
7933
|
if (allErrors.length === 0) {
|
|
7598
|
-
console.log(
|
|
7934
|
+
console.log(chalk14.green(`
|
|
7599
7935
|
\u2714 All checks passed after ${cycle} cycle(s).`));
|
|
7600
7936
|
return true;
|
|
7601
7937
|
}
|
|
7602
7938
|
if (cycle < maxCycles) {
|
|
7603
|
-
console.log(
|
|
7939
|
+
console.log(chalk14.cyan(`
|
|
7604
7940
|
Attempting auto-fix (${allErrors.length} error(s))...`));
|
|
7605
7941
|
await attemptFix(provider, allErrors, workingDir, dsl);
|
|
7606
7942
|
}
|
|
7607
7943
|
}
|
|
7608
|
-
console.log(
|
|
7944
|
+
console.log(chalk14.yellow("\n \u26A0 Some errors remain after auto-fix cycles. Manual intervention needed."));
|
|
7609
7945
|
return false;
|
|
7610
7946
|
}
|
|
7611
7947
|
|
|
7612
7948
|
// core/spec-assessor.ts
|
|
7613
|
-
import
|
|
7949
|
+
import chalk15 from "chalk";
|
|
7614
7950
|
|
|
7615
7951
|
// prompts/spec-assess.prompt.ts
|
|
7616
7952
|
var specAssessSystemPrompt = `You are a Senior Software Architect reviewing a feature specification before it goes to code generation.
|
|
@@ -7665,8 +8001,8 @@ Output ONLY the JSON object \u2014 no explanation, no markdown fences.`;
|
|
|
7665
8001
|
// core/spec-assessor.ts
|
|
7666
8002
|
function scoreBar(score) {
|
|
7667
8003
|
const filled = Math.round(score);
|
|
7668
|
-
const bar =
|
|
7669
|
-
const color = score >= 8 ?
|
|
8004
|
+
const bar = chalk15.green("\u2588".repeat(filled)) + chalk15.gray("\u2591".repeat(10 - filled));
|
|
8005
|
+
const color = score >= 8 ? chalk15.green : score >= 6 ? chalk15.yellow : chalk15.red;
|
|
7670
8006
|
return `[${bar}] ${color(score + "/10")}`;
|
|
7671
8007
|
}
|
|
7672
8008
|
function parseAssessment(raw) {
|
|
@@ -7696,35 +8032,35 @@ ${spec}`;
|
|
|
7696
8032
|
}
|
|
7697
8033
|
}
|
|
7698
8034
|
function printSpecAssessment(assessment) {
|
|
7699
|
-
console.log(
|
|
8035
|
+
console.log(chalk15.blue("\n\u2500\u2500\u2500 Spec Quality Assessment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7700
8036
|
console.log(` Coverage ${scoreBar(assessment.coverageScore)} error handling, edge cases, auth`);
|
|
7701
8037
|
console.log(` Clarity ${scoreBar(assessment.clarityScore)} API contracts, response shapes`);
|
|
7702
8038
|
console.log(` Constitution${scoreBar(assessment.constitutionScore)} naming, error codes, conventions`);
|
|
7703
|
-
console.log(
|
|
8039
|
+
console.log(chalk15.bold(` Overall ${scoreBar(assessment.overallScore)}`));
|
|
7704
8040
|
if (!assessment.dslExtractable) {
|
|
7705
8041
|
console.log(
|
|
7706
|
-
|
|
8042
|
+
chalk15.yellow(
|
|
7707
8043
|
"\n \u26A0 DSL extraction may be unreliable \u2014 clarityScore < 6 or no structured API section."
|
|
7708
8044
|
)
|
|
7709
8045
|
);
|
|
7710
|
-
console.log(
|
|
8046
|
+
console.log(chalk15.gray(" Consider adding explicit request/response shapes before proceeding."));
|
|
7711
8047
|
}
|
|
7712
8048
|
if (assessment.issues.length > 0) {
|
|
7713
|
-
console.log(
|
|
8049
|
+
console.log(chalk15.yellow(`
|
|
7714
8050
|
Issues found (${assessment.issues.length}):`));
|
|
7715
|
-
assessment.issues.forEach((issue) => console.log(
|
|
8051
|
+
assessment.issues.forEach((issue) => console.log(chalk15.yellow(` \xB7 ${issue}`)));
|
|
7716
8052
|
}
|
|
7717
8053
|
if (assessment.suggestions.length > 0) {
|
|
7718
|
-
console.log(
|
|
7719
|
-
assessment.suggestions.forEach((s) => console.log(
|
|
8054
|
+
console.log(chalk15.cyan("\n Suggestions:"));
|
|
8055
|
+
assessment.suggestions.forEach((s) => console.log(chalk15.cyan(` \u{1F4A1} ${s}`)));
|
|
7720
8056
|
}
|
|
7721
|
-
console.log(
|
|
8057
|
+
console.log(chalk15.blue("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7722
8058
|
}
|
|
7723
8059
|
|
|
7724
8060
|
// core/knowledge-memory.ts
|
|
7725
|
-
import
|
|
7726
|
-
import * as
|
|
7727
|
-
import * as
|
|
8061
|
+
import chalk16 from "chalk";
|
|
8062
|
+
import * as fs17 from "fs-extra";
|
|
8063
|
+
import * as path16 from "path";
|
|
7728
8064
|
function extractIssuesFromReview(reviewText) {
|
|
7729
8065
|
const issues = [];
|
|
7730
8066
|
const issuesMatch = reviewText.match(
|
|
@@ -7761,12 +8097,12 @@ var MEMORY_SECTION_HEADER = "\n\n## 9. \u79EF\u7D2F\u6559\u8BAD (Accumulated Les
|
|
|
7761
8097
|
var MEMORY_SECTION_MARKER = "## 9. \u79EF\u7D2F\u6559\u8BAD";
|
|
7762
8098
|
async function appendLessonsToConstitution(projectRoot, issues) {
|
|
7763
8099
|
if (issues.length === 0) return;
|
|
7764
|
-
const constitutionPath =
|
|
8100
|
+
const constitutionPath = path16.join(projectRoot, CONSTITUTION_FILE);
|
|
7765
8101
|
let content = "";
|
|
7766
8102
|
try {
|
|
7767
|
-
content = await
|
|
8103
|
+
content = await fs17.readFile(constitutionPath, "utf-8");
|
|
7768
8104
|
} catch {
|
|
7769
|
-
console.log(
|
|
8105
|
+
console.log(chalk16.gray(" No constitution file \u2014 skipping knowledge memory."));
|
|
7770
8106
|
return;
|
|
7771
8107
|
}
|
|
7772
8108
|
const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
|
|
@@ -7779,7 +8115,7 @@ async function appendLessonsToConstitution(projectRoot, issues) {
|
|
|
7779
8115
|
newEntries.push(`- ${badge} **[${date}]** ${issue.description}`);
|
|
7780
8116
|
}
|
|
7781
8117
|
if (newEntries.length === 0) {
|
|
7782
|
-
console.log(
|
|
8118
|
+
console.log(chalk16.gray(" No new lessons to add (all deduplicated)."));
|
|
7783
8119
|
return;
|
|
7784
8120
|
}
|
|
7785
8121
|
let updatedContent;
|
|
@@ -7792,22 +8128,22 @@ async function appendLessonsToConstitution(projectRoot, issues) {
|
|
|
7792
8128
|
} else {
|
|
7793
8129
|
updatedContent = content + MEMORY_SECTION_HEADER + newEntries.join("\n") + "\n";
|
|
7794
8130
|
}
|
|
7795
|
-
await
|
|
7796
|
-
console.log(
|
|
8131
|
+
await fs17.writeFile(constitutionPath, updatedContent, "utf-8");
|
|
8132
|
+
console.log(chalk16.green(` \u2714 ${newEntries.length} lesson(s) appended to constitution (\xA79).`));
|
|
7797
8133
|
const stats = parseConstitutionStats(updatedContent);
|
|
7798
8134
|
if (stats.lessonCount >= 8) {
|
|
7799
8135
|
console.log(
|
|
7800
|
-
|
|
8136
|
+
chalk16.yellow(
|
|
7801
8137
|
` \u26A0 \xA79 now has ${stats.lessonCount} accumulated lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
|
|
7802
8138
|
)
|
|
7803
8139
|
);
|
|
7804
8140
|
}
|
|
7805
8141
|
}
|
|
7806
8142
|
async function appendDirectLesson(projectRoot, lessonText) {
|
|
7807
|
-
const constitutionPath =
|
|
8143
|
+
const constitutionPath = path16.join(projectRoot, CONSTITUTION_FILE);
|
|
7808
8144
|
let content = "";
|
|
7809
8145
|
try {
|
|
7810
|
-
content = await
|
|
8146
|
+
content = await fs17.readFile(constitutionPath, "utf-8");
|
|
7811
8147
|
} catch {
|
|
7812
8148
|
return { appended: false, reason: "No constitution file found. Run `ai-spec init` first." };
|
|
7813
8149
|
}
|
|
@@ -7828,11 +8164,11 @@ async function appendDirectLesson(projectRoot, lessonText) {
|
|
|
7828
8164
|
} else {
|
|
7829
8165
|
updatedContent = content + MEMORY_SECTION_HEADER + entry + "\n";
|
|
7830
8166
|
}
|
|
7831
|
-
await
|
|
8167
|
+
await fs17.writeFile(constitutionPath, updatedContent, "utf-8");
|
|
7832
8168
|
const stats = parseConstitutionStats(updatedContent);
|
|
7833
8169
|
if (stats.lessonCount >= 8) {
|
|
7834
8170
|
console.log(
|
|
7835
|
-
|
|
8171
|
+
chalk16.yellow(
|
|
7836
8172
|
` \u26A0 \xA79 now has ${stats.lessonCount} lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
|
|
7837
8173
|
)
|
|
7838
8174
|
);
|
|
@@ -7840,15 +8176,15 @@ async function appendDirectLesson(projectRoot, lessonText) {
|
|
|
7840
8176
|
return { appended: true };
|
|
7841
8177
|
}
|
|
7842
8178
|
async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
|
|
7843
|
-
console.log(
|
|
8179
|
+
console.log(chalk16.blue("\n\u2500\u2500\u2500 Knowledge Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7844
8180
|
const issues = extractIssuesFromReview(reviewText);
|
|
7845
8181
|
if (issues.length === 0) {
|
|
7846
|
-
console.log(
|
|
8182
|
+
console.log(chalk16.gray(" No actionable issues found in review. Skipping."));
|
|
7847
8183
|
return;
|
|
7848
8184
|
}
|
|
7849
|
-
console.log(
|
|
8185
|
+
console.log(chalk16.gray(` Extracted ${issues.length} issue(s) from review:`));
|
|
7850
8186
|
for (const issue of issues) {
|
|
7851
|
-
console.log(
|
|
8187
|
+
console.log(chalk16.gray(` - [${issue.category}] ${issue.description.slice(0, 80)}`));
|
|
7852
8188
|
}
|
|
7853
8189
|
await appendLessonsToConstitution(projectRoot, issues);
|
|
7854
8190
|
}
|
|
@@ -7857,22 +8193,22 @@ async function accumulateReviewKnowledge(provider, projectRoot, reviewText) {
|
|
|
7857
8193
|
init_workspace_loader();
|
|
7858
8194
|
|
|
7859
8195
|
// core/key-store.ts
|
|
7860
|
-
import * as
|
|
7861
|
-
import * as
|
|
8196
|
+
import * as fs19 from "fs-extra";
|
|
8197
|
+
import * as path18 from "path";
|
|
7862
8198
|
import * as os3 from "os";
|
|
7863
|
-
var KEY_STORE_FILE =
|
|
8199
|
+
var KEY_STORE_FILE = path18.join(os3.homedir(), ".ai-spec-keys.json");
|
|
7864
8200
|
async function readStore() {
|
|
7865
8201
|
try {
|
|
7866
|
-
if (await
|
|
7867
|
-
return await
|
|
8202
|
+
if (await fs19.pathExists(KEY_STORE_FILE)) {
|
|
8203
|
+
return await fs19.readJson(KEY_STORE_FILE);
|
|
7868
8204
|
}
|
|
7869
8205
|
} catch {
|
|
7870
8206
|
}
|
|
7871
8207
|
return {};
|
|
7872
8208
|
}
|
|
7873
8209
|
async function writeStore(store) {
|
|
7874
|
-
await
|
|
7875
|
-
await
|
|
8210
|
+
await fs19.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
|
|
8211
|
+
await fs19.chmod(KEY_STORE_FILE, 384);
|
|
7876
8212
|
}
|
|
7877
8213
|
async function getSavedKey(provider) {
|
|
7878
8214
|
const store = await readStore();
|
|
@@ -7884,8 +8220,8 @@ async function saveKey(provider, key) {
|
|
|
7884
8220
|
await writeStore(store);
|
|
7885
8221
|
}
|
|
7886
8222
|
async function clearAllKeys() {
|
|
7887
|
-
if (await
|
|
7888
|
-
await
|
|
8223
|
+
if (await fs19.pathExists(KEY_STORE_FILE)) {
|
|
8224
|
+
await fs19.remove(KEY_STORE_FILE);
|
|
7889
8225
|
}
|
|
7890
8226
|
}
|
|
7891
8227
|
async function clearKey(provider) {
|
|
@@ -7895,19 +8231,19 @@ async function clearKey(provider) {
|
|
|
7895
8231
|
}
|
|
7896
8232
|
|
|
7897
8233
|
// cli/welcome.ts
|
|
7898
|
-
import
|
|
8234
|
+
import chalk17 from "chalk";
|
|
7899
8235
|
import * as os4 from "os";
|
|
7900
|
-
import * as
|
|
7901
|
-
import * as
|
|
8236
|
+
import * as path19 from "path";
|
|
8237
|
+
import * as fs20 from "fs-extra";
|
|
7902
8238
|
var VERSION = "0.14.1";
|
|
7903
8239
|
var TOTAL_W = 76;
|
|
7904
8240
|
var L_WIDTH = 44;
|
|
7905
8241
|
var R_WIDTH = TOTAL_W - L_WIDTH - 4;
|
|
7906
|
-
var ROBOT_COLOR =
|
|
8242
|
+
var ROBOT_COLOR = chalk17.hex("#E8885A");
|
|
7907
8243
|
var ROBOT = [
|
|
7908
8244
|
ROBOT_COLOR(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"),
|
|
7909
|
-
ROBOT_COLOR(" \u2502") +
|
|
7910
|
-
ROBOT_COLOR(" \u2502") +
|
|
8245
|
+
ROBOT_COLOR(" \u2502") + chalk17.bold.white(" \u25C9 ") + ROBOT_COLOR(" ") + chalk17.bold.white("\u25C9 ") + ROBOT_COLOR("\u2502"),
|
|
8246
|
+
ROBOT_COLOR(" \u2502") + chalk17.dim(" \u2570\u2500\u256F ") + ROBOT_COLOR("\u2502"),
|
|
7911
8247
|
ROBOT_COLOR(" \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518"),
|
|
7912
8248
|
ROBOT_COLOR(" \u2502"),
|
|
7913
8249
|
ROBOT_COLOR(" \u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500")
|
|
@@ -7920,7 +8256,7 @@ function padR(s, width) {
|
|
|
7920
8256
|
return vl >= width ? s : s + " ".repeat(width - vl);
|
|
7921
8257
|
}
|
|
7922
8258
|
function row(left, right) {
|
|
7923
|
-
return padR(left, L_WIDTH) + " " +
|
|
8259
|
+
return padR(left, L_WIDTH) + " " + chalk17.gray("\u2502") + " " + padR(right, R_WIDTH);
|
|
7924
8260
|
}
|
|
7925
8261
|
function center(s, width) {
|
|
7926
8262
|
const vl = visLen(s);
|
|
@@ -7928,14 +8264,14 @@ function center(s, width) {
|
|
|
7928
8264
|
return " ".repeat(pad) + s;
|
|
7929
8265
|
}
|
|
7930
8266
|
async function getRecentSpecs(dir) {
|
|
7931
|
-
const specsDir =
|
|
7932
|
-
if (!await
|
|
8267
|
+
const specsDir = path19.join(dir, "specs");
|
|
8268
|
+
if (!await fs20.pathExists(specsDir)) return [];
|
|
7933
8269
|
try {
|
|
7934
|
-
const files = await
|
|
8270
|
+
const files = await fs20.readdir(specsDir);
|
|
7935
8271
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
7936
8272
|
const withStats = await Promise.all(
|
|
7937
8273
|
mdFiles.map(async (f) => {
|
|
7938
|
-
const stat4 = await
|
|
8274
|
+
const stat4 = await fs20.stat(path19.join(specsDir, f));
|
|
7939
8275
|
return { name: f, mtime: stat4.mtime.getTime() };
|
|
7940
8276
|
})
|
|
7941
8277
|
);
|
|
@@ -7946,7 +8282,7 @@ async function getRecentSpecs(dir) {
|
|
|
7946
8282
|
const days = Math.floor(hours / 24);
|
|
7947
8283
|
const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
|
|
7948
8284
|
const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
|
|
7949
|
-
return
|
|
8285
|
+
return chalk17.white(slug) + chalk17.dim(" " + age);
|
|
7950
8286
|
});
|
|
7951
8287
|
} catch {
|
|
7952
8288
|
return [];
|
|
@@ -7961,13 +8297,13 @@ async function printWelcome(currentDir, config2) {
|
|
|
7961
8297
|
const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" \xB7 ");
|
|
7962
8298
|
const maxInfoLen = L_WIDTH - 2;
|
|
7963
8299
|
const bottomTruncated = bottomRaw.length > maxInfoLen ? bottomRaw.slice(0, maxInfoLen - 1) + "\u2026" : bottomRaw;
|
|
7964
|
-
const bottomLine = " " +
|
|
8300
|
+
const bottomLine = " " + chalk17.dim(bottomTruncated);
|
|
7965
8301
|
const titleInner = `ai-spec v${VERSION} `;
|
|
7966
8302
|
const titleDashes = "\u2500".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
|
|
7967
8303
|
console.log(
|
|
7968
|
-
"\n" +
|
|
8304
|
+
"\n" + chalk17.hex("#FF6B35")("\u2500\u2500\u2500 " + titleInner + titleDashes)
|
|
7969
8305
|
);
|
|
7970
|
-
const welcomeText = "Welcome back, " +
|
|
8306
|
+
const welcomeText = "Welcome back, " + chalk17.bold.white(username) + "!";
|
|
7971
8307
|
const leftLines = [
|
|
7972
8308
|
"",
|
|
7973
8309
|
center(welcomeText, L_WIDTH),
|
|
@@ -7978,21 +8314,21 @@ async function printWelcome(currentDir, config2) {
|
|
|
7978
8314
|
""
|
|
7979
8315
|
];
|
|
7980
8316
|
const rightLines = [
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
8317
|
+
chalk17.hex("#FF8C00").bold("Tips for getting started"),
|
|
8318
|
+
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8319
|
+
chalk17.white('ai-spec create "feature"'),
|
|
8320
|
+
chalk17.gray("ai-spec workspace run"),
|
|
8321
|
+
chalk17.gray('ai-spec update "change"'),
|
|
7986
8322
|
"",
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
...recentSpecs.length > 0 ? recentSpecs : [
|
|
8323
|
+
chalk17.hex("#FF8C00").bold("Recent activity"),
|
|
8324
|
+
chalk17.gray("\u2500".repeat(R_WIDTH)),
|
|
8325
|
+
...recentSpecs.length > 0 ? recentSpecs : [chalk17.dim("No recent activity")]
|
|
7990
8326
|
];
|
|
7991
8327
|
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
7992
8328
|
for (let i = 0; i < maxLines; i++) {
|
|
7993
8329
|
console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
|
|
7994
8330
|
}
|
|
7995
|
-
console.log(
|
|
8331
|
+
console.log(chalk17.gray("\u2500".repeat(TOTAL_W)));
|
|
7996
8332
|
console.log();
|
|
7997
8333
|
}
|
|
7998
8334
|
|
|
@@ -8505,8 +8841,8 @@ Existing API/service files:`);
|
|
|
8505
8841
|
}
|
|
8506
8842
|
|
|
8507
8843
|
// core/mock-server-generator.ts
|
|
8508
|
-
import * as
|
|
8509
|
-
import * as
|
|
8844
|
+
import * as path20 from "path";
|
|
8845
|
+
import * as fs21 from "fs-extra";
|
|
8510
8846
|
import { spawn } from "child_process";
|
|
8511
8847
|
function typeToFixture(fieldName, typeDesc) {
|
|
8512
8848
|
const t = typeDesc.toLowerCase();
|
|
@@ -8642,22 +8978,22 @@ function generateMockServerJs(dsl, port) {
|
|
|
8642
8978
|
}
|
|
8643
8979
|
function detectFrontendFramework(projectDir) {
|
|
8644
8980
|
for (const f of ["vite.config.ts", "vite.config.js", "vite.config.mts"]) {
|
|
8645
|
-
if (
|
|
8981
|
+
if (fs21.existsSync(path20.join(projectDir, f))) return "vite";
|
|
8646
8982
|
}
|
|
8647
8983
|
for (const f of ["next.config.js", "next.config.ts", "next.config.mjs"]) {
|
|
8648
|
-
if (
|
|
8984
|
+
if (fs21.existsSync(path20.join(projectDir, f))) return "next";
|
|
8649
8985
|
}
|
|
8650
|
-
const pkgPath =
|
|
8651
|
-
if (
|
|
8986
|
+
const pkgPath = path20.join(projectDir, "package.json");
|
|
8987
|
+
if (fs21.existsSync(pkgPath)) {
|
|
8652
8988
|
try {
|
|
8653
|
-
const pkg = JSON.parse(
|
|
8989
|
+
const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
|
|
8654
8990
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
8655
8991
|
if (deps["react-scripts"]) return "cra";
|
|
8656
8992
|
} catch {
|
|
8657
8993
|
}
|
|
8658
8994
|
}
|
|
8659
8995
|
for (const f of ["webpack.config.js", "webpack.config.ts"]) {
|
|
8660
|
-
if (
|
|
8996
|
+
if (fs21.existsSync(path20.join(projectDir, f))) return "webpack";
|
|
8661
8997
|
}
|
|
8662
8998
|
return "unknown";
|
|
8663
8999
|
}
|
|
@@ -8837,7 +9173,7 @@ export const worker = setupWorker(...handlers);
|
|
|
8837
9173
|
var MOCK_LOCK_FILE = ".ai-spec-mock.lock.json";
|
|
8838
9174
|
function findViteConfigFile(projectDir) {
|
|
8839
9175
|
for (const f of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
8840
|
-
if (
|
|
9176
|
+
if (fs21.existsSync(path20.join(projectDir, f))) return f;
|
|
8841
9177
|
}
|
|
8842
9178
|
return null;
|
|
8843
9179
|
}
|
|
@@ -8883,73 +9219,73 @@ async function applyMockProxy(frontendDir, mockPort, endpoints = []) {
|
|
|
8883
9219
|
if (framework === "vite") {
|
|
8884
9220
|
const viteConfigFile = findViteConfigFile(frontendDir) ?? "vite.config.ts";
|
|
8885
9221
|
const mockConfigContent = generateViteMockConfigTs(viteConfigFile, mockPort, endpoints);
|
|
8886
|
-
const mockConfigPath =
|
|
8887
|
-
await
|
|
9222
|
+
const mockConfigPath = path20.join(frontendDir, "vite.config.ai-spec-mock.ts");
|
|
9223
|
+
await fs21.writeFile(mockConfigPath, mockConfigContent, "utf-8");
|
|
8888
9224
|
actions.push({ type: "wrote-file", filePath: "vite.config.ai-spec-mock.ts" });
|
|
8889
|
-
const pkgPath =
|
|
8890
|
-
if (await
|
|
8891
|
-
const pkg = await
|
|
9225
|
+
const pkgPath = path20.join(frontendDir, "package.json");
|
|
9226
|
+
if (await fs21.pathExists(pkgPath)) {
|
|
9227
|
+
const pkg = await fs21.readJson(pkgPath);
|
|
8892
9228
|
pkg.scripts = pkg.scripts ?? {};
|
|
8893
9229
|
const originalValue = pkg.scripts["dev:mock"] ?? null;
|
|
8894
9230
|
pkg.scripts["dev:mock"] = "vite --config vite.config.ai-spec-mock.ts";
|
|
8895
|
-
await
|
|
9231
|
+
await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8896
9232
|
actions.push({ type: "added-pkg-script", key: "dev:mock", originalValue });
|
|
8897
9233
|
}
|
|
8898
9234
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
8899
|
-
await
|
|
9235
|
+
await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
8900
9236
|
return { framework, applied: true, devCommand: "npm run dev:mock" };
|
|
8901
9237
|
}
|
|
8902
9238
|
if (framework === "cra") {
|
|
8903
|
-
const pkgPath =
|
|
8904
|
-
if (await
|
|
8905
|
-
const pkg = await
|
|
9239
|
+
const pkgPath = path20.join(frontendDir, "package.json");
|
|
9240
|
+
if (await fs21.pathExists(pkgPath)) {
|
|
9241
|
+
const pkg = await fs21.readJson(pkgPath);
|
|
8906
9242
|
const originalProxy = pkg.proxy ?? null;
|
|
8907
9243
|
pkg.proxy = `http://localhost:${mockPort}`;
|
|
8908
|
-
await
|
|
9244
|
+
await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8909
9245
|
actions.push({ type: "patched-pkg-proxy", originalProxy });
|
|
8910
9246
|
const lock2 = { framework, mockPort, frontendDir, actions };
|
|
8911
|
-
await
|
|
9247
|
+
await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock2, { spaces: 2 });
|
|
8912
9248
|
return { framework, applied: true, devCommand: "npm start" };
|
|
8913
9249
|
}
|
|
8914
9250
|
return { framework, applied: false, devCommand: null, note: "No package.json found." };
|
|
8915
9251
|
}
|
|
8916
9252
|
const lock = { framework, mockPort, frontendDir, actions };
|
|
8917
|
-
await
|
|
9253
|
+
await fs21.writeJson(path20.join(frontendDir, MOCK_LOCK_FILE), lock, { spaces: 2 });
|
|
8918
9254
|
const manualNote = framework === "next" ? `Add rewrites in next.config.js to proxy API calls to http://localhost:${mockPort}` : `Add proxy in webpack.config.js devServer to target http://localhost:${mockPort}`;
|
|
8919
9255
|
return { framework, applied: false, devCommand: null, note: manualNote };
|
|
8920
9256
|
}
|
|
8921
9257
|
async function restoreMockProxy(frontendDir) {
|
|
8922
|
-
const lockPath =
|
|
8923
|
-
if (!await
|
|
9258
|
+
const lockPath = path20.join(frontendDir, MOCK_LOCK_FILE);
|
|
9259
|
+
if (!await fs21.pathExists(lockPath)) {
|
|
8924
9260
|
return { restored: false, note: "No lock file found \u2014 nothing to restore." };
|
|
8925
9261
|
}
|
|
8926
|
-
const lock = await
|
|
9262
|
+
const lock = await fs21.readJson(lockPath);
|
|
8927
9263
|
for (const action of lock.actions) {
|
|
8928
9264
|
if (action.type === "wrote-file") {
|
|
8929
|
-
const fp =
|
|
8930
|
-
if (await
|
|
9265
|
+
const fp = path20.join(frontendDir, action.filePath);
|
|
9266
|
+
if (await fs21.pathExists(fp)) await fs21.remove(fp);
|
|
8931
9267
|
} else if (action.type === "added-pkg-script") {
|
|
8932
|
-
const pkgPath =
|
|
8933
|
-
if (await
|
|
8934
|
-
const pkg = await
|
|
9268
|
+
const pkgPath = path20.join(frontendDir, "package.json");
|
|
9269
|
+
if (await fs21.pathExists(pkgPath)) {
|
|
9270
|
+
const pkg = await fs21.readJson(pkgPath);
|
|
8935
9271
|
if (action.originalValue == null) {
|
|
8936
9272
|
delete pkg.scripts?.[action.key];
|
|
8937
9273
|
} else {
|
|
8938
9274
|
pkg.scripts = pkg.scripts ?? {};
|
|
8939
9275
|
pkg.scripts[action.key] = action.originalValue;
|
|
8940
9276
|
}
|
|
8941
|
-
await
|
|
9277
|
+
await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8942
9278
|
}
|
|
8943
9279
|
} else if (action.type === "patched-pkg-proxy") {
|
|
8944
|
-
const pkgPath =
|
|
8945
|
-
if (await
|
|
8946
|
-
const pkg = await
|
|
9280
|
+
const pkgPath = path20.join(frontendDir, "package.json");
|
|
9281
|
+
if (await fs21.pathExists(pkgPath)) {
|
|
9282
|
+
const pkg = await fs21.readJson(pkgPath);
|
|
8947
9283
|
if (action.originalProxy == null) {
|
|
8948
9284
|
delete pkg.proxy;
|
|
8949
9285
|
} else {
|
|
8950
9286
|
pkg.proxy = action.originalProxy;
|
|
8951
9287
|
}
|
|
8952
|
-
await
|
|
9288
|
+
await fs21.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
8953
9289
|
}
|
|
8954
9290
|
}
|
|
8955
9291
|
}
|
|
@@ -8959,7 +9295,7 @@ async function restoreMockProxy(frontendDir) {
|
|
|
8959
9295
|
} catch {
|
|
8960
9296
|
}
|
|
8961
9297
|
}
|
|
8962
|
-
await
|
|
9298
|
+
await fs21.remove(lockPath);
|
|
8963
9299
|
return { restored: true };
|
|
8964
9300
|
}
|
|
8965
9301
|
function startMockServerBackground(serverJsPath, port) {
|
|
@@ -8972,23 +9308,23 @@ function startMockServerBackground(serverJsPath, port) {
|
|
|
8972
9308
|
return child.pid;
|
|
8973
9309
|
}
|
|
8974
9310
|
async function saveMockServerPid(frontendDir, pid) {
|
|
8975
|
-
const lockPath =
|
|
8976
|
-
if (await
|
|
8977
|
-
const lock = await
|
|
9311
|
+
const lockPath = path20.join(frontendDir, MOCK_LOCK_FILE);
|
|
9312
|
+
if (await fs21.pathExists(lockPath)) {
|
|
9313
|
+
const lock = await fs21.readJson(lockPath);
|
|
8978
9314
|
lock.mockServerPid = pid;
|
|
8979
|
-
await
|
|
9315
|
+
await fs21.writeJson(lockPath, lock, { spaces: 2 });
|
|
8980
9316
|
}
|
|
8981
9317
|
}
|
|
8982
9318
|
async function generateMockAssets(dsl, projectDir, opts = {}) {
|
|
8983
9319
|
const port = opts.port ?? 3001;
|
|
8984
|
-
const outputDir =
|
|
9320
|
+
const outputDir = path20.join(projectDir, opts.outputDir ?? "mock");
|
|
8985
9321
|
const result = { files: [] };
|
|
8986
|
-
await
|
|
9322
|
+
await fs21.ensureDir(outputDir);
|
|
8987
9323
|
const serverJs = generateMockServerJs(dsl, port);
|
|
8988
|
-
const serverPath =
|
|
8989
|
-
await
|
|
9324
|
+
const serverPath = path20.join(outputDir, "server.js");
|
|
9325
|
+
await fs21.writeFile(serverPath, serverJs, "utf-8");
|
|
8990
9326
|
result.files.push({
|
|
8991
|
-
path:
|
|
9327
|
+
path: path20.relative(projectDir, serverPath),
|
|
8992
9328
|
description: `Express mock server \u2014 run with: node mock/server.js`
|
|
8993
9329
|
});
|
|
8994
9330
|
const mockReadme = `# Mock Server
|
|
@@ -9018,51 +9354,51 @@ ${dsl.endpoints.map(
|
|
|
9018
9354
|
|
|
9019
9355
|
Append \`?simulate_error=1\` to any request to test error handling (not yet auto-wired \u2014 edit server.js manually).
|
|
9020
9356
|
`;
|
|
9021
|
-
const readmePath =
|
|
9022
|
-
await
|
|
9357
|
+
const readmePath = path20.join(outputDir, "README.md");
|
|
9358
|
+
await fs21.writeFile(readmePath, mockReadme, "utf-8");
|
|
9023
9359
|
result.files.push({
|
|
9024
|
-
path:
|
|
9360
|
+
path: path20.relative(projectDir, readmePath),
|
|
9025
9361
|
description: "Mock server usage guide"
|
|
9026
9362
|
});
|
|
9027
9363
|
if (opts.proxy) {
|
|
9028
9364
|
const { content, filename } = generateProxyConfig(dsl, port, projectDir);
|
|
9029
|
-
const proxyPath =
|
|
9030
|
-
await
|
|
9031
|
-
await
|
|
9365
|
+
const proxyPath = path20.join(projectDir, filename);
|
|
9366
|
+
await fs21.ensureDir(path20.dirname(proxyPath));
|
|
9367
|
+
await fs21.writeFile(proxyPath, content, "utf-8");
|
|
9032
9368
|
result.files.push({
|
|
9033
9369
|
path: filename,
|
|
9034
9370
|
description: "Proxy config snippet \u2014 copy instructions into your framework config"
|
|
9035
9371
|
});
|
|
9036
9372
|
}
|
|
9037
9373
|
if (opts.msw) {
|
|
9038
|
-
const mswDir =
|
|
9039
|
-
await
|
|
9374
|
+
const mswDir = path20.join(projectDir, "src", "mocks");
|
|
9375
|
+
await fs21.ensureDir(mswDir);
|
|
9040
9376
|
const handlersContent = generateMswHandlers(dsl);
|
|
9041
|
-
const handlersPath =
|
|
9042
|
-
await
|
|
9377
|
+
const handlersPath = path20.join(mswDir, "handlers.ts");
|
|
9378
|
+
await fs21.writeFile(handlersPath, handlersContent, "utf-8");
|
|
9043
9379
|
result.files.push({
|
|
9044
|
-
path:
|
|
9380
|
+
path: path20.relative(projectDir, handlersPath),
|
|
9045
9381
|
description: "MSW request handlers"
|
|
9046
9382
|
});
|
|
9047
9383
|
const browserContent = generateMswBrowser();
|
|
9048
|
-
const browserPath =
|
|
9049
|
-
await
|
|
9384
|
+
const browserPath = path20.join(mswDir, "browser.ts");
|
|
9385
|
+
await fs21.writeFile(browserPath, browserContent, "utf-8");
|
|
9050
9386
|
result.files.push({
|
|
9051
|
-
path:
|
|
9387
|
+
path: path20.relative(projectDir, browserPath),
|
|
9052
9388
|
description: "MSW browser worker setup"
|
|
9053
9389
|
});
|
|
9054
9390
|
}
|
|
9055
9391
|
return result;
|
|
9056
9392
|
}
|
|
9057
9393
|
async function findLatestDslFile(projectDir) {
|
|
9058
|
-
const specDir =
|
|
9059
|
-
if (!await
|
|
9394
|
+
const specDir = path20.join(projectDir, ".ai-spec");
|
|
9395
|
+
if (!await fs21.pathExists(specDir)) return null;
|
|
9060
9396
|
const allFiles = [];
|
|
9061
9397
|
async function scan(dir) {
|
|
9062
|
-
const entries = await
|
|
9398
|
+
const entries = await fs21.readdir(dir);
|
|
9063
9399
|
for (const entry of entries) {
|
|
9064
|
-
const abs =
|
|
9065
|
-
const stat4 = await
|
|
9400
|
+
const abs = path20.join(dir, entry);
|
|
9401
|
+
const stat4 = await fs21.stat(abs);
|
|
9066
9402
|
if (stat4.isDirectory()) {
|
|
9067
9403
|
await scan(abs);
|
|
9068
9404
|
} else if (entry.endsWith(".dsl.json")) {
|
|
@@ -9073,16 +9409,16 @@ async function findLatestDslFile(projectDir) {
|
|
|
9073
9409
|
await scan(specDir);
|
|
9074
9410
|
if (allFiles.length === 0) return null;
|
|
9075
9411
|
const withMtimes = await Promise.all(
|
|
9076
|
-
allFiles.map(async (f) => ({ f, mtime: (await
|
|
9412
|
+
allFiles.map(async (f) => ({ f, mtime: (await fs21.stat(f)).mtime }))
|
|
9077
9413
|
);
|
|
9078
9414
|
withMtimes.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
9079
9415
|
return withMtimes[0].f;
|
|
9080
9416
|
}
|
|
9081
9417
|
|
|
9082
9418
|
// core/spec-updater.ts
|
|
9083
|
-
import
|
|
9084
|
-
import * as
|
|
9085
|
-
import * as
|
|
9419
|
+
import chalk18 from "chalk";
|
|
9420
|
+
import * as path21 from "path";
|
|
9421
|
+
import * as fs22 from "fs-extra";
|
|
9086
9422
|
|
|
9087
9423
|
// prompts/update.prompt.ts
|
|
9088
9424
|
var specUpdateSystemPrompt = `You are a Senior Software Architect updating an existing Feature Spec based on a change request.
|
|
@@ -9228,8 +9564,8 @@ var SpecUpdater = class {
|
|
|
9228
9564
|
* Returns all .md spec files sorted newest-first.
|
|
9229
9565
|
*/
|
|
9230
9566
|
static async findLatestSpec(specsDir) {
|
|
9231
|
-
if (!await
|
|
9232
|
-
const files = await
|
|
9567
|
+
if (!await fs22.pathExists(specsDir)) return null;
|
|
9568
|
+
const files = await fs22.readdir(specsDir);
|
|
9233
9569
|
const pattern = /^feature-(.+)-v(\d+)\.md$/;
|
|
9234
9570
|
let latest = null;
|
|
9235
9571
|
for (const file of files) {
|
|
@@ -9237,8 +9573,8 @@ var SpecUpdater = class {
|
|
|
9237
9573
|
if (!m) continue;
|
|
9238
9574
|
const version = parseInt(m[2], 10);
|
|
9239
9575
|
if (!latest || version > latest.version) {
|
|
9240
|
-
const filePath =
|
|
9241
|
-
const content = await
|
|
9576
|
+
const filePath = path21.join(specsDir, file);
|
|
9577
|
+
const content = await fs22.readFile(filePath, "utf-8");
|
|
9242
9578
|
latest = { filePath, version, slug: m[1], content };
|
|
9243
9579
|
}
|
|
9244
9580
|
}
|
|
@@ -9249,16 +9585,16 @@ var SpecUpdater = class {
|
|
|
9249
9585
|
* Generates a new version of the spec, re-extracts the DSL, and identifies affected files.
|
|
9250
9586
|
*/
|
|
9251
9587
|
async update(changeRequest, existingSpecPath, projectDir, context, opts = {}) {
|
|
9252
|
-
const existingSpec = await
|
|
9588
|
+
const existingSpec = await fs22.readFile(existingSpecPath, "utf-8");
|
|
9253
9589
|
let existingDsl = null;
|
|
9254
9590
|
const dslFile = await findLatestDslFile(projectDir);
|
|
9255
9591
|
if (dslFile) {
|
|
9256
9592
|
try {
|
|
9257
|
-
existingDsl = await
|
|
9593
|
+
existingDsl = await fs22.readJson(dslFile);
|
|
9258
9594
|
} catch {
|
|
9259
9595
|
}
|
|
9260
9596
|
}
|
|
9261
|
-
console.log(
|
|
9597
|
+
console.log(chalk18.blue(" [1/3] Generating updated spec..."));
|
|
9262
9598
|
const updatePrompt = buildSpecUpdatePrompt(changeRequest, existingSpec, existingDsl, context);
|
|
9263
9599
|
let updatedSpecContent;
|
|
9264
9600
|
try {
|
|
@@ -9267,15 +9603,15 @@ var SpecUpdater = class {
|
|
|
9267
9603
|
} catch (err) {
|
|
9268
9604
|
throw new Error(`Spec update generation failed: ${err.message}`);
|
|
9269
9605
|
}
|
|
9270
|
-
const specBasename =
|
|
9606
|
+
const specBasename = path21.basename(existingSpecPath);
|
|
9271
9607
|
const slugMatch = specBasename.match(/^feature-(.+)-v\d+\.md$/);
|
|
9272
9608
|
const slug = slugMatch ? slugMatch[1] : "feature";
|
|
9273
|
-
const specsDir =
|
|
9609
|
+
const specsDir = path21.dirname(existingSpecPath);
|
|
9274
9610
|
const { filePath: newSpecPath, version: newVersion } = await nextVersionPath(specsDir, slug);
|
|
9275
|
-
await
|
|
9276
|
-
await
|
|
9277
|
-
console.log(
|
|
9278
|
-
console.log(
|
|
9611
|
+
await fs22.ensureDir(specsDir);
|
|
9612
|
+
await fs22.writeFile(newSpecPath, updatedSpecContent, "utf-8");
|
|
9613
|
+
console.log(chalk18.green(` \u2714 New spec written: ${path21.relative(projectDir, newSpecPath)}`));
|
|
9614
|
+
console.log(chalk18.blue(" [2/3] Updating DSL..."));
|
|
9279
9615
|
let updatedDsl = null;
|
|
9280
9616
|
let newDslPath = null;
|
|
9281
9617
|
if (existingDsl) {
|
|
@@ -9287,7 +9623,7 @@ var SpecUpdater = class {
|
|
|
9287
9623
|
updatedDsl = parsed;
|
|
9288
9624
|
}
|
|
9289
9625
|
} catch {
|
|
9290
|
-
console.log(
|
|
9626
|
+
console.log(chalk18.gray(" Targeted DSL update failed \u2014 falling back to full extraction."));
|
|
9291
9627
|
}
|
|
9292
9628
|
}
|
|
9293
9629
|
if (!updatedDsl) {
|
|
@@ -9296,15 +9632,15 @@ var SpecUpdater = class {
|
|
|
9296
9632
|
}
|
|
9297
9633
|
if (updatedDsl) {
|
|
9298
9634
|
const dslPath = newSpecPath.replace(/\.md$/, ".dsl.json");
|
|
9299
|
-
await
|
|
9635
|
+
await fs22.writeJson(dslPath, updatedDsl, { spaces: 2 });
|
|
9300
9636
|
newDslPath = dslPath;
|
|
9301
|
-
console.log(
|
|
9637
|
+
console.log(chalk18.green(` \u2714 DSL updated: ${path21.relative(projectDir, dslPath)}`));
|
|
9302
9638
|
} else {
|
|
9303
|
-
console.log(
|
|
9639
|
+
console.log(chalk18.yellow(" \u26A0 DSL update failed \u2014 continuing without DSL."));
|
|
9304
9640
|
}
|
|
9305
9641
|
let affectedFiles = [];
|
|
9306
9642
|
if (!opts.skipAffectedFiles && updatedDsl && existingDsl && context) {
|
|
9307
|
-
console.log(
|
|
9643
|
+
console.log(chalk18.blue(" [3/3] Identifying affected files..."));
|
|
9308
9644
|
const systemPrompt = getCodeGenSystemPrompt(opts.repoType);
|
|
9309
9645
|
const affectedPrompt = buildAffectedFilesPrompt(
|
|
9310
9646
|
changeRequest,
|
|
@@ -9315,9 +9651,9 @@ var SpecUpdater = class {
|
|
|
9315
9651
|
try {
|
|
9316
9652
|
const affectedRaw = await this.provider.generate(affectedPrompt, systemPrompt);
|
|
9317
9653
|
affectedFiles = parseAffectedFiles(affectedRaw);
|
|
9318
|
-
console.log(
|
|
9654
|
+
console.log(chalk18.green(` \u2714 ${affectedFiles.length} file(s) identified for update`));
|
|
9319
9655
|
} catch {
|
|
9320
|
-
console.log(
|
|
9656
|
+
console.log(chalk18.gray(" Could not identify affected files \u2014 use manual selection."));
|
|
9321
9657
|
}
|
|
9322
9658
|
}
|
|
9323
9659
|
return { newSpecPath, newVersion, newDslPath, affectedFiles, updatedDsl };
|
|
@@ -9325,8 +9661,8 @@ var SpecUpdater = class {
|
|
|
9325
9661
|
};
|
|
9326
9662
|
|
|
9327
9663
|
// core/openapi-exporter.ts
|
|
9328
|
-
import * as
|
|
9329
|
-
import * as
|
|
9664
|
+
import * as path22 from "path";
|
|
9665
|
+
import * as fs23 from "fs-extra";
|
|
9330
9666
|
function dslTypeToOASchema(typeDesc, fieldName = "") {
|
|
9331
9667
|
const t = typeDesc.toLowerCase();
|
|
9332
9668
|
if (t === "string" || t.includes("string")) {
|
|
@@ -9555,7 +9891,7 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9555
9891
|
const format = opts.format ?? "yaml";
|
|
9556
9892
|
const serverUrl = opts.serverUrl ?? "http://localhost:3000";
|
|
9557
9893
|
const defaultName = `openapi.${format}`;
|
|
9558
|
-
const outputPath = opts.outputPath ?
|
|
9894
|
+
const outputPath = opts.outputPath ? path22.isAbsolute(opts.outputPath) ? opts.outputPath : path22.join(projectDir, opts.outputPath) : path22.join(projectDir, defaultName);
|
|
9559
9895
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
9560
9896
|
let content;
|
|
9561
9897
|
if (format === "json") {
|
|
@@ -9563,8 +9899,8 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9563
9899
|
} else {
|
|
9564
9900
|
content = buildYamlDoc(doc);
|
|
9565
9901
|
}
|
|
9566
|
-
await
|
|
9567
|
-
await
|
|
9902
|
+
await fs23.ensureDir(path22.dirname(outputPath));
|
|
9903
|
+
await fs23.writeFile(outputPath, content, "utf-8");
|
|
9568
9904
|
return outputPath;
|
|
9569
9905
|
}
|
|
9570
9906
|
|
|
@@ -9572,9 +9908,9 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9572
9908
|
dotenv.config();
|
|
9573
9909
|
var CONFIG_FILE = ".ai-spec.json";
|
|
9574
9910
|
async function loadConfig(dir) {
|
|
9575
|
-
const p =
|
|
9576
|
-
if (await
|
|
9577
|
-
return
|
|
9911
|
+
const p = path23.join(dir, CONFIG_FILE);
|
|
9912
|
+
if (await fs24.pathExists(p)) {
|
|
9913
|
+
return fs24.readJson(p);
|
|
9578
9914
|
}
|
|
9579
9915
|
return {};
|
|
9580
9916
|
}
|
|
@@ -9599,20 +9935,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
9599
9935
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
9600
9936
|
});
|
|
9601
9937
|
await saveKey(providerName, newKey.trim());
|
|
9602
|
-
console.log(
|
|
9938
|
+
console.log(chalk19.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
9603
9939
|
return newKey.trim();
|
|
9604
9940
|
}
|
|
9605
9941
|
function printBanner(opts) {
|
|
9606
|
-
console.log(
|
|
9607
|
-
console.log(
|
|
9608
|
-
console.log(
|
|
9609
|
-
console.log(
|
|
9942
|
+
console.log(chalk19.blue("\n" + "\u2500".repeat(52)));
|
|
9943
|
+
console.log(chalk19.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
9944
|
+
console.log(chalk19.blue("\u2500".repeat(52)));
|
|
9945
|
+
console.log(chalk19.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
9610
9946
|
console.log(
|
|
9611
|
-
|
|
9947
|
+
chalk19.gray(
|
|
9612
9948
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
9613
9949
|
)
|
|
9614
9950
|
);
|
|
9615
|
-
console.log(
|
|
9951
|
+
console.log(chalk19.blue("\u2500".repeat(52) + "\n"));
|
|
9616
9952
|
}
|
|
9617
9953
|
var program = new Command();
|
|
9618
9954
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -9639,42 +9975,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9639
9975
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
9640
9976
|
const workspaceConfig = await workspaceLoader.load();
|
|
9641
9977
|
if (workspaceConfig) {
|
|
9642
|
-
console.log(
|
|
9978
|
+
console.log(chalk19.cyan(`
|
|
9643
9979
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
9644
|
-
console.log(
|
|
9980
|
+
console.log(chalk19.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
9645
9981
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9646
9982
|
if (opts.serve) {
|
|
9647
|
-
console.log(
|
|
9983
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9648
9984
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
9649
9985
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
9650
9986
|
if (!backendResult) {
|
|
9651
|
-
console.log(
|
|
9987
|
+
console.log(chalk19.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
9652
9988
|
} else {
|
|
9653
9989
|
const mockPort = 3001;
|
|
9654
9990
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
9655
|
-
const serverJsPath =
|
|
9656
|
-
console.log(
|
|
9991
|
+
const serverJsPath = path23.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9992
|
+
console.log(chalk19.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
9657
9993
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
9658
|
-
console.log(
|
|
9994
|
+
console.log(chalk19.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
9659
9995
|
if (frontendResult) {
|
|
9660
9996
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
9661
9997
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
9662
9998
|
if (proxyResult.applied) {
|
|
9663
|
-
console.log(
|
|
9664
|
-
console.log(
|
|
9999
|
+
console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
10000
|
+
console.log(chalk19.bold.cyan(`
|
|
9665
10001
|
Ready! Run your frontend dev server:`));
|
|
9666
|
-
console.log(
|
|
9667
|
-
console.log(
|
|
9668
|
-
console.log(
|
|
10002
|
+
console.log(chalk19.white(` cd ${frontendResult.repoAbsPath}`));
|
|
10003
|
+
console.log(chalk19.white(` ${proxyResult.devCommand}`));
|
|
10004
|
+
console.log(chalk19.gray(`
|
|
9669
10005
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
9670
10006
|
} else {
|
|
9671
|
-
console.log(
|
|
9672
|
-
if (proxyResult.note) console.log(
|
|
9673
|
-
console.log(
|
|
10007
|
+
console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
10008
|
+
if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
|
|
10009
|
+
console.log(chalk19.gray(` Mock server: http://localhost:${mockPort}`));
|
|
9674
10010
|
}
|
|
9675
10011
|
} else {
|
|
9676
|
-
console.log(
|
|
9677
|
-
console.log(
|
|
10012
|
+
console.log(chalk19.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
10013
|
+
console.log(chalk19.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
9678
10014
|
}
|
|
9679
10015
|
}
|
|
9680
10016
|
}
|
|
@@ -9694,23 +10030,32 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9694
10030
|
codegenProvider: codegenProviderName,
|
|
9695
10031
|
codegenModel: codegenModelName
|
|
9696
10032
|
});
|
|
9697
|
-
|
|
10033
|
+
const runId = generateRunId();
|
|
10034
|
+
console.log(chalk19.gray(` Run ID: ${runId}`));
|
|
10035
|
+
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10036
|
+
setActiveSnapshot(runSnapshot);
|
|
10037
|
+
const runLogger = new RunLogger(currentDir, runId, {
|
|
10038
|
+
provider: specProviderName,
|
|
10039
|
+
model: specModelName
|
|
10040
|
+
});
|
|
10041
|
+
setActiveLogger(runLogger);
|
|
10042
|
+
console.log(chalk19.blue("[1/6] Loading project context..."));
|
|
9698
10043
|
const loader = new ContextLoader(currentDir);
|
|
9699
10044
|
const context = await loader.loadProjectContext();
|
|
9700
10045
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
9701
|
-
console.log(
|
|
9702
|
-
console.log(
|
|
9703
|
-
console.log(
|
|
10046
|
+
console.log(chalk19.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10047
|
+
console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10048
|
+
console.log(chalk19.gray(` API files : ${context.apiStructure.length} files`));
|
|
9704
10049
|
if (context.schema) {
|
|
9705
|
-
console.log(
|
|
10050
|
+
console.log(chalk19.gray(` Prisma schema: found`));
|
|
9706
10051
|
}
|
|
9707
10052
|
if (context.constitution) {
|
|
9708
|
-
console.log(
|
|
10053
|
+
console.log(chalk19.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
9709
10054
|
if (context.constitution.length > 6e3) {
|
|
9710
|
-
console.log(
|
|
10055
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
9711
10056
|
}
|
|
9712
10057
|
} else {
|
|
9713
|
-
console.log(
|
|
10058
|
+
console.log(chalk19.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
9714
10059
|
try {
|
|
9715
10060
|
const constitutionGen = new ConstitutionGenerator(
|
|
9716
10061
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -9718,12 +10063,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9718
10063
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
9719
10064
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
9720
10065
|
context.constitution = constitutionContent;
|
|
9721
|
-
console.log(
|
|
10066
|
+
console.log(chalk19.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
9722
10067
|
} catch (err) {
|
|
9723
|
-
console.log(
|
|
10068
|
+
console.log(chalk19.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
9724
10069
|
}
|
|
9725
10070
|
}
|
|
9726
|
-
console.log(
|
|
10071
|
+
console.log(chalk19.blue(`
|
|
9727
10072
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
9728
10073
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
9729
10074
|
let initialSpec;
|
|
@@ -9732,28 +10077,28 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9732
10077
|
if (opts.skipTasks) {
|
|
9733
10078
|
const generator = new SpecGenerator(specProvider);
|
|
9734
10079
|
initialSpec = await generator.generateSpec(idea, context);
|
|
9735
|
-
console.log(
|
|
10080
|
+
console.log(chalk19.green(" \u2714 Spec generated."));
|
|
9736
10081
|
} else {
|
|
9737
10082
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
9738
10083
|
initialSpec = result.spec;
|
|
9739
10084
|
initialTasks = result.tasks;
|
|
9740
|
-
console.log(
|
|
10085
|
+
console.log(chalk19.green(` \u2714 Spec generated.`));
|
|
9741
10086
|
if (initialTasks.length > 0) {
|
|
9742
|
-
console.log(
|
|
10087
|
+
console.log(chalk19.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
9743
10088
|
} else {
|
|
9744
|
-
console.log(
|
|
10089
|
+
console.log(chalk19.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
9745
10090
|
}
|
|
9746
10091
|
}
|
|
9747
10092
|
} catch (err) {
|
|
9748
|
-
console.error(
|
|
10093
|
+
console.error(chalk19.red(" \u2718 Spec generation failed:"), err);
|
|
9749
10094
|
process.exit(1);
|
|
9750
10095
|
}
|
|
9751
10096
|
let finalSpec;
|
|
9752
10097
|
if (opts.fast) {
|
|
9753
|
-
console.log(
|
|
10098
|
+
console.log(chalk19.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
9754
10099
|
finalSpec = initialSpec;
|
|
9755
10100
|
} else {
|
|
9756
|
-
console.log(
|
|
10101
|
+
console.log(chalk19.blue("\n[3/6] Interactive spec refinement..."));
|
|
9757
10102
|
const refiner = new SpecRefiner(specProvider);
|
|
9758
10103
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
9759
10104
|
}
|
|
@@ -9762,48 +10107,48 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9762
10107
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
9763
10108
|
if (shouldRunAssessment) {
|
|
9764
10109
|
if (!opts.auto) {
|
|
9765
|
-
console.log(
|
|
10110
|
+
console.log(chalk19.blue("\n[3.4/6] Spec quality assessment..."));
|
|
9766
10111
|
}
|
|
9767
10112
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
9768
10113
|
if (assessment) {
|
|
9769
10114
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
9770
10115
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
9771
10116
|
if (opts.force) {
|
|
9772
|
-
console.log(
|
|
10117
|
+
console.log(chalk19.yellow(`
|
|
9773
10118
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
9774
10119
|
} else {
|
|
9775
|
-
console.log(
|
|
10120
|
+
console.log(chalk19.red(`
|
|
9776
10121
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
9777
10122
|
if (!opts.auto) {
|
|
9778
|
-
console.log(
|
|
10123
|
+
console.log(chalk19.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
9779
10124
|
} else {
|
|
9780
|
-
console.log(
|
|
10125
|
+
console.log(chalk19.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
9781
10126
|
}
|
|
9782
|
-
console.log(
|
|
10127
|
+
console.log(chalk19.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
9783
10128
|
process.exit(1);
|
|
9784
10129
|
}
|
|
9785
10130
|
}
|
|
9786
10131
|
} else if (!opts.auto) {
|
|
9787
|
-
console.log(
|
|
10132
|
+
console.log(chalk19.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
9788
10133
|
}
|
|
9789
10134
|
}
|
|
9790
10135
|
if (!opts.auto) {
|
|
9791
|
-
console.log(
|
|
10136
|
+
console.log(chalk19.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
9792
10137
|
const specLines = finalSpec.split("\n").length;
|
|
9793
10138
|
const specWords = finalSpec.split(/\s+/).length;
|
|
9794
10139
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
9795
|
-
console.log(
|
|
9796
|
-
if (taskCountHint) console.log(
|
|
9797
|
-
const previewSpecsDir =
|
|
10140
|
+
console.log(chalk19.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10141
|
+
if (taskCountHint) console.log(chalk19.gray(taskCountHint));
|
|
10142
|
+
const previewSpecsDir = path23.join(currentDir, "specs");
|
|
9798
10143
|
const slug = featureSlug;
|
|
9799
10144
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
9800
10145
|
if (prevVersion) {
|
|
9801
|
-
console.log(
|
|
10146
|
+
console.log(chalk19.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
9802
10147
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
9803
|
-
console.log(
|
|
10148
|
+
console.log(chalk19.cyan("\n \u2500\u2500 Changes vs previous version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9804
10149
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
9805
10150
|
printDiff(diff);
|
|
9806
|
-
console.log(
|
|
10151
|
+
console.log(chalk19.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9807
10152
|
}
|
|
9808
10153
|
const gate = await select3({
|
|
9809
10154
|
message: "Ready to proceed to code generation?",
|
|
@@ -9814,9 +10159,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9814
10159
|
]
|
|
9815
10160
|
});
|
|
9816
10161
|
if (gate === "view") {
|
|
9817
|
-
console.log(
|
|
10162
|
+
console.log(chalk19.cyan("\n" + "\u2500".repeat(52)));
|
|
9818
10163
|
console.log(finalSpec);
|
|
9819
|
-
console.log(
|
|
10164
|
+
console.log(chalk19.cyan("\u2500".repeat(52) + "\n"));
|
|
9820
10165
|
const confirm22 = await select3({
|
|
9821
10166
|
message: "Proceed to code generation?",
|
|
9822
10167
|
choices: [
|
|
@@ -9825,88 +10170,88 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9825
10170
|
]
|
|
9826
10171
|
});
|
|
9827
10172
|
if (confirm22 === "abort") {
|
|
9828
|
-
console.log(
|
|
10173
|
+
console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
|
|
9829
10174
|
process.exit(0);
|
|
9830
10175
|
}
|
|
9831
10176
|
} else if (gate === "abort") {
|
|
9832
|
-
console.log(
|
|
10177
|
+
console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
|
|
9833
10178
|
process.exit(0);
|
|
9834
10179
|
}
|
|
9835
|
-
console.log(
|
|
10180
|
+
console.log(chalk19.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
9836
10181
|
} else {
|
|
9837
|
-
console.log(
|
|
10182
|
+
console.log(chalk19.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
9838
10183
|
}
|
|
9839
10184
|
let extractedDsl = null;
|
|
9840
10185
|
if (opts.skipDsl) {
|
|
9841
|
-
console.log(
|
|
10186
|
+
console.log(chalk19.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
9842
10187
|
} else {
|
|
9843
|
-
console.log(
|
|
9844
|
-
console.log(
|
|
10188
|
+
console.log(chalk19.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10189
|
+
console.log(chalk19.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
9845
10190
|
try {
|
|
9846
10191
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
9847
|
-
if (isFrontend) console.log(
|
|
10192
|
+
if (isFrontend) console.log(chalk19.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
9848
10193
|
const dslExtractor = new DslExtractor(specProvider);
|
|
9849
10194
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
9850
10195
|
if (extractedDsl) {
|
|
9851
|
-
console.log(
|
|
10196
|
+
console.log(chalk19.green(" \u2714 DSL extracted and validated."));
|
|
9852
10197
|
} else {
|
|
9853
|
-
console.log(
|
|
10198
|
+
console.log(chalk19.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
9854
10199
|
}
|
|
9855
10200
|
} catch (err) {
|
|
9856
|
-
console.log(
|
|
10201
|
+
console.log(chalk19.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
9857
10202
|
}
|
|
9858
10203
|
}
|
|
9859
10204
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
9860
10205
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
9861
10206
|
let workingDir = currentDir;
|
|
9862
10207
|
if (!skipWorktree) {
|
|
9863
|
-
console.log(
|
|
10208
|
+
console.log(chalk19.blue("\n[4/6] Setting up git worktree..."));
|
|
9864
10209
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
9865
10210
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
9866
10211
|
if (worktreePath) workingDir = worktreePath;
|
|
9867
10212
|
} else {
|
|
9868
10213
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
9869
|
-
console.log(
|
|
10214
|
+
console.log(chalk19.gray(`[4/6] Skipping worktree${reason}.`));
|
|
9870
10215
|
}
|
|
9871
|
-
const specsDir =
|
|
9872
|
-
await
|
|
10216
|
+
const specsDir = path23.join(workingDir, "specs");
|
|
10217
|
+
await fs24.ensureDir(specsDir);
|
|
9873
10218
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
9874
|
-
await
|
|
9875
|
-
console.log(
|
|
9876
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10219
|
+
await fs24.writeFile(specFile, finalSpec, "utf-8");
|
|
10220
|
+
console.log(chalk19.green(`
|
|
10221
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + chalk19.gray(` (v${specVersion})`));
|
|
9877
10222
|
let savedDslFile = null;
|
|
9878
10223
|
if (extractedDsl) {
|
|
9879
10224
|
const dslExtractor = new DslExtractor(specProvider);
|
|
9880
10225
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
9881
|
-
console.log(
|
|
10226
|
+
console.log(chalk19.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
9882
10227
|
}
|
|
9883
10228
|
if (!opts.skipTasks) {
|
|
9884
10229
|
const taskGen = new TaskGenerator(specProvider);
|
|
9885
10230
|
let tasksToSave = initialTasks;
|
|
9886
10231
|
if (tasksToSave.length === 0) {
|
|
9887
|
-
console.log(
|
|
10232
|
+
console.log(chalk19.blue(`
|
|
9888
10233
|
Generating tasks (separate call)...`));
|
|
9889
10234
|
try {
|
|
9890
10235
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
9891
10236
|
} catch (err) {
|
|
9892
|
-
console.log(
|
|
10237
|
+
console.log(chalk19.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
9893
10238
|
}
|
|
9894
10239
|
}
|
|
9895
10240
|
if (tasksToSave.length > 0) {
|
|
9896
10241
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
9897
10242
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
9898
10243
|
printTasks(sorted);
|
|
9899
|
-
console.log(
|
|
10244
|
+
console.log(chalk19.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
9900
10245
|
} else {
|
|
9901
|
-
console.log(
|
|
10246
|
+
console.log(chalk19.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
9902
10247
|
}
|
|
9903
10248
|
}
|
|
9904
|
-
console.log(
|
|
10249
|
+
console.log(chalk19.blue(`
|
|
9905
10250
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
9906
10251
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
9907
10252
|
let generatedTestFiles = [];
|
|
9908
10253
|
if (opts.tdd && extractedDsl) {
|
|
9909
|
-
console.log(
|
|
10254
|
+
console.log(chalk19.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
9910
10255
|
const testGen = new TestGenerator(codegenProvider);
|
|
9911
10256
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
9912
10257
|
}
|
|
@@ -9918,22 +10263,22 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9918
10263
|
repoType: detectedRepoType
|
|
9919
10264
|
});
|
|
9920
10265
|
if (opts.tdd) {
|
|
9921
|
-
console.log(
|
|
10266
|
+
console.log(chalk19.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
9922
10267
|
} else if (opts.skipTests) {
|
|
9923
|
-
console.log(
|
|
10268
|
+
console.log(chalk19.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
9924
10269
|
} else if (!extractedDsl) {
|
|
9925
|
-
console.log(
|
|
10270
|
+
console.log(chalk19.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
9926
10271
|
} else {
|
|
9927
|
-
console.log(
|
|
10272
|
+
console.log(chalk19.blue(`
|
|
9928
10273
|
[7/9] Test skeleton generation...`));
|
|
9929
10274
|
const testGen = new TestGenerator(codegenProvider);
|
|
9930
10275
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
9931
10276
|
}
|
|
9932
10277
|
if (opts.skipErrorFeedback) {
|
|
9933
|
-
console.log(
|
|
10278
|
+
console.log(chalk19.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
9934
10279
|
} else {
|
|
9935
10280
|
if (opts.tdd) {
|
|
9936
|
-
console.log(
|
|
10281
|
+
console.log(chalk19.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
9937
10282
|
}
|
|
9938
10283
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
9939
10284
|
maxCycles: opts.tdd ? 3 : 2
|
|
@@ -9942,9 +10287,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9942
10287
|
}
|
|
9943
10288
|
let reviewResult = "";
|
|
9944
10289
|
if (!opts.skipReview) {
|
|
9945
|
-
console.log(
|
|
10290
|
+
console.log(chalk19.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
9946
10291
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
9947
|
-
const savedSpec = await
|
|
10292
|
+
const savedSpec = await fs24.readFile(specFile, "utf-8");
|
|
9948
10293
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
9949
10294
|
reviewResult = await reviewer.reviewFiles(savedSpec, generatedFiles, workingDir, specFile);
|
|
9950
10295
|
} else {
|
|
@@ -9958,15 +10303,20 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9958
10303
|
}
|
|
9959
10304
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
9960
10305
|
}
|
|
9961
|
-
|
|
9962
|
-
console.log(
|
|
9963
|
-
|
|
10306
|
+
runLogger.finish();
|
|
10307
|
+
console.log(chalk19.bold.green("\n\u2714 All done!"));
|
|
10308
|
+
console.log(chalk19.gray(` Spec : ${specFile}`));
|
|
10309
|
+
if (savedDslFile) console.log(chalk19.gray(` DSL : ${savedDslFile}`));
|
|
9964
10310
|
if (generatedTestFiles.length > 0) {
|
|
9965
|
-
console.log(
|
|
10311
|
+
console.log(chalk19.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
9966
10312
|
}
|
|
9967
|
-
console.log(
|
|
10313
|
+
console.log(chalk19.gray(` Working dir : ${workingDir}`));
|
|
9968
10314
|
if (workingDir !== currentDir) {
|
|
9969
|
-
console.log(
|
|
10315
|
+
console.log(chalk19.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10316
|
+
}
|
|
10317
|
+
runLogger.printSummary();
|
|
10318
|
+
if (runSnapshot.fileCount > 0) {
|
|
10319
|
+
console.log(chalk19.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
9970
10320
|
}
|
|
9971
10321
|
});
|
|
9972
10322
|
program.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
|
|
@@ -9983,24 +10333,24 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
9983
10333
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
9984
10334
|
let specContent = "";
|
|
9985
10335
|
let resolvedSpecFile;
|
|
9986
|
-
if (specFile && await
|
|
9987
|
-
specContent = await
|
|
10336
|
+
if (specFile && await fs24.pathExists(specFile)) {
|
|
10337
|
+
specContent = await fs24.readFile(specFile, "utf-8");
|
|
9988
10338
|
resolvedSpecFile = specFile;
|
|
9989
|
-
console.log(
|
|
10339
|
+
console.log(chalk19.gray(`Using spec: ${specFile}`));
|
|
9990
10340
|
} else {
|
|
9991
|
-
const specsDir =
|
|
9992
|
-
if (await
|
|
9993
|
-
const files = (await
|
|
10341
|
+
const specsDir = path23.join(currentDir, "specs");
|
|
10342
|
+
if (await fs24.pathExists(specsDir)) {
|
|
10343
|
+
const files = (await fs24.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
9994
10344
|
if (files.length > 0) {
|
|
9995
|
-
const latest =
|
|
9996
|
-
specContent = await
|
|
10345
|
+
const latest = path23.join(specsDir, files[0]);
|
|
10346
|
+
specContent = await fs24.readFile(latest, "utf-8");
|
|
9997
10347
|
resolvedSpecFile = latest;
|
|
9998
|
-
console.log(
|
|
10348
|
+
console.log(chalk19.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
9999
10349
|
}
|
|
10000
10350
|
}
|
|
10001
10351
|
}
|
|
10002
10352
|
if (!specContent) {
|
|
10003
|
-
console.log(
|
|
10353
|
+
console.log(chalk19.yellow("No spec file found. Running review without spec context."));
|
|
10004
10354
|
}
|
|
10005
10355
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10006
10356
|
await reviewer.printScoreTrend();
|
|
@@ -10027,15 +10377,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10027
10377
|
auto: opts.auto
|
|
10028
10378
|
});
|
|
10029
10379
|
if (result.written) {
|
|
10030
|
-
console.log(
|
|
10031
|
-
console.log(
|
|
10032
|
-
console.log(
|
|
10380
|
+
console.log(chalk19.blue("\n Summary:"));
|
|
10381
|
+
console.log(chalk19.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
10382
|
+
console.log(chalk19.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10033
10383
|
if (result.backupPath) {
|
|
10034
|
-
console.log(
|
|
10384
|
+
console.log(chalk19.gray(` Backup: ${path23.basename(result.backupPath)}`));
|
|
10035
10385
|
}
|
|
10036
10386
|
}
|
|
10037
10387
|
} catch (err) {
|
|
10038
|
-
console.error(
|
|
10388
|
+
console.error(chalk19.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10039
10389
|
process.exit(1);
|
|
10040
10390
|
}
|
|
10041
10391
|
return;
|
|
@@ -10043,75 +10393,75 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10043
10393
|
if (opts.global) {
|
|
10044
10394
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10045
10395
|
if (existing && !opts.force) {
|
|
10046
|
-
console.log(
|
|
10396
|
+
console.log(chalk19.yellow(`
|
|
10047
10397
|
Global constitution already exists at: ${existing.source}`));
|
|
10048
|
-
console.log(
|
|
10398
|
+
console.log(chalk19.gray(" Use --force to overwrite it."));
|
|
10049
10399
|
return;
|
|
10050
10400
|
}
|
|
10051
|
-
console.log(
|
|
10052
|
-
console.log(
|
|
10053
|
-
console.log(
|
|
10401
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10402
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
10403
|
+
console.log(chalk19.gray(" Scanning repos in workspace..."));
|
|
10054
10404
|
const loader = new ContextLoader(currentDir);
|
|
10055
10405
|
const ctx = await loader.loadProjectContext();
|
|
10056
10406
|
const summary = [
|
|
10057
10407
|
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
10058
10408
|
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
10059
10409
|
].join("\n");
|
|
10060
|
-
const prompt = buildGlobalConstitutionPrompt([{ name:
|
|
10410
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path23.basename(currentDir), summary }]);
|
|
10061
10411
|
let globalConstitution;
|
|
10062
10412
|
try {
|
|
10063
10413
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10064
10414
|
} catch (err) {
|
|
10065
|
-
console.error(
|
|
10415
|
+
console.error(chalk19.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10066
10416
|
process.exit(1);
|
|
10067
10417
|
}
|
|
10068
10418
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10069
|
-
console.log(
|
|
10419
|
+
console.log(chalk19.green(`
|
|
10070
10420
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10071
|
-
console.log(
|
|
10072
|
-
console.log(
|
|
10073
|
-
console.log(
|
|
10074
|
-
console.log(
|
|
10421
|
+
console.log(chalk19.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
10422
|
+
console.log(chalk19.gray(" Project-level rules always override global rules.\n"));
|
|
10423
|
+
console.log(chalk19.bold(" Preview:"));
|
|
10424
|
+
console.log(chalk19.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10075
10425
|
if (globalConstitution.split("\n").length > 12) {
|
|
10076
|
-
console.log(
|
|
10426
|
+
console.log(chalk19.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10077
10427
|
}
|
|
10078
10428
|
return;
|
|
10079
10429
|
}
|
|
10080
|
-
const constitutionPath =
|
|
10081
|
-
if (!opts.force && await
|
|
10082
|
-
console.log(
|
|
10430
|
+
const constitutionPath = path23.join(currentDir, CONSTITUTION_FILE);
|
|
10431
|
+
if (!opts.force && await fs24.pathExists(constitutionPath)) {
|
|
10432
|
+
console.log(chalk19.yellow(`
|
|
10083
10433
|
${CONSTITUTION_FILE} already exists.`));
|
|
10084
|
-
console.log(
|
|
10085
|
-
console.log(
|
|
10434
|
+
console.log(chalk19.gray(" Use --force to overwrite it."));
|
|
10435
|
+
console.log(chalk19.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10086
10436
|
return;
|
|
10087
10437
|
}
|
|
10088
|
-
console.log(
|
|
10089
|
-
console.log(
|
|
10090
|
-
console.log(
|
|
10438
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10439
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
10440
|
+
console.log(chalk19.gray(" Analyzing codebase..."));
|
|
10091
10441
|
const generator = new ConstitutionGenerator(provider);
|
|
10092
10442
|
let constitution;
|
|
10093
10443
|
try {
|
|
10094
10444
|
constitution = await generator.generate(currentDir);
|
|
10095
10445
|
} catch (err) {
|
|
10096
|
-
console.error(
|
|
10446
|
+
console.error(chalk19.red(" \u2718 Failed to generate constitution:"), err);
|
|
10097
10447
|
process.exit(1);
|
|
10098
10448
|
}
|
|
10099
10449
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10100
|
-
const globalResult = await loadGlobalConstitution([
|
|
10450
|
+
const globalResult = await loadGlobalConstitution([path23.dirname(currentDir)]);
|
|
10101
10451
|
if (globalResult) {
|
|
10102
|
-
console.log(
|
|
10452
|
+
console.log(chalk19.cyan(`
|
|
10103
10453
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10104
|
-
console.log(
|
|
10105
|
-
console.log(
|
|
10454
|
+
console.log(chalk19.gray(" It will be merged with this project constitution at runtime."));
|
|
10455
|
+
console.log(chalk19.gray(" Project rules take priority over global rules."));
|
|
10106
10456
|
}
|
|
10107
|
-
console.log(
|
|
10457
|
+
console.log(chalk19.green(`
|
|
10108
10458
|
\u2714 Constitution saved: ${saved}`));
|
|
10109
|
-
console.log(
|
|
10110
|
-
console.log(
|
|
10111
|
-
console.log(
|
|
10112
|
-
console.log(
|
|
10459
|
+
console.log(chalk19.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
10460
|
+
console.log(chalk19.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
10461
|
+
console.log(chalk19.bold(" Preview:"));
|
|
10462
|
+
console.log(chalk19.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10113
10463
|
if (constitution.split("\n").length > 15) {
|
|
10114
|
-
console.log(
|
|
10464
|
+
console.log(chalk19.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10115
10465
|
}
|
|
10116
10466
|
});
|
|
10117
10467
|
program.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option(
|
|
@@ -10119,44 +10469,44 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10119
10469
|
"Default code generation mode (claude-code|api|plan)"
|
|
10120
10470
|
).option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
10121
10471
|
const currentDir = process.cwd();
|
|
10122
|
-
const configPath =
|
|
10472
|
+
const configPath = path23.join(currentDir, CONFIG_FILE);
|
|
10123
10473
|
if (opts.clearKeys) {
|
|
10124
10474
|
await clearAllKeys();
|
|
10125
|
-
console.log(
|
|
10475
|
+
console.log(chalk19.green(`\u2714 All saved API keys cleared.`));
|
|
10126
10476
|
return;
|
|
10127
10477
|
}
|
|
10128
10478
|
if (opts.clearKey) {
|
|
10129
10479
|
await clearKey(opts.clearKey);
|
|
10130
|
-
console.log(
|
|
10480
|
+
console.log(chalk19.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10131
10481
|
return;
|
|
10132
10482
|
}
|
|
10133
10483
|
if (opts.listKeys) {
|
|
10134
|
-
const store = await
|
|
10484
|
+
const store = await fs24.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10135
10485
|
const providers = Object.keys(store);
|
|
10136
10486
|
if (providers.length === 0) {
|
|
10137
|
-
console.log(
|
|
10487
|
+
console.log(chalk19.gray("No saved API keys."));
|
|
10138
10488
|
} else {
|
|
10139
|
-
console.log(
|
|
10489
|
+
console.log(chalk19.bold("Saved API keys:"));
|
|
10140
10490
|
for (const p of providers) {
|
|
10141
10491
|
const k2 = store[p];
|
|
10142
|
-
console.log(
|
|
10492
|
+
console.log(chalk19.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10143
10493
|
}
|
|
10144
|
-
console.log(
|
|
10494
|
+
console.log(chalk19.gray(`
|
|
10145
10495
|
File: ${KEY_STORE_FILE}`));
|
|
10146
10496
|
}
|
|
10147
10497
|
return;
|
|
10148
10498
|
}
|
|
10149
10499
|
if (opts.reset) {
|
|
10150
|
-
await
|
|
10151
|
-
console.log(
|
|
10500
|
+
await fs24.writeJson(configPath, {}, { spaces: 2 });
|
|
10501
|
+
console.log(chalk19.green(`\u2714 Config reset: ${configPath}`));
|
|
10152
10502
|
return;
|
|
10153
10503
|
}
|
|
10154
10504
|
const existing = await loadConfig(currentDir);
|
|
10155
10505
|
if (opts.show) {
|
|
10156
10506
|
if (Object.keys(existing).length === 0) {
|
|
10157
|
-
console.log(
|
|
10507
|
+
console.log(chalk19.gray("No config file found. Using built-in defaults."));
|
|
10158
10508
|
} else {
|
|
10159
|
-
console.log(
|
|
10509
|
+
console.log(chalk19.bold(`${configPath}:`));
|
|
10160
10510
|
console.log(JSON.stringify(existing, null, 2));
|
|
10161
10511
|
}
|
|
10162
10512
|
return;
|
|
@@ -10170,27 +10520,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10170
10520
|
if (opts.minSpecScore !== void 0) {
|
|
10171
10521
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10172
10522
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10173
|
-
console.error(
|
|
10523
|
+
console.error(chalk19.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10174
10524
|
process.exit(1);
|
|
10175
10525
|
}
|
|
10176
10526
|
updated.minSpecScore = score;
|
|
10177
10527
|
}
|
|
10178
|
-
await
|
|
10179
|
-
console.log(
|
|
10528
|
+
await fs24.writeJson(configPath, updated, { spaces: 2 });
|
|
10529
|
+
console.log(chalk19.green(`\u2714 Config saved to ${configPath}`));
|
|
10180
10530
|
console.log(JSON.stringify(updated, null, 2));
|
|
10181
10531
|
});
|
|
10182
10532
|
program.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
|
|
10183
10533
|
const currentDir = process.cwd();
|
|
10184
|
-
const configPath =
|
|
10534
|
+
const configPath = path23.join(currentDir, CONFIG_FILE);
|
|
10185
10535
|
if (opts.list) {
|
|
10186
|
-
console.log(
|
|
10536
|
+
console.log(chalk19.bold("\nAvailable providers & models:\n"));
|
|
10187
10537
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10188
10538
|
console.log(
|
|
10189
|
-
` ${
|
|
10539
|
+
` ${chalk19.bold.cyan(key.padEnd(10))} ${chalk19.white(meta.displayName)}`
|
|
10190
10540
|
);
|
|
10191
|
-
console.log(
|
|
10541
|
+
console.log(chalk19.gray(` ${meta.description}`));
|
|
10192
10542
|
console.log(
|
|
10193
|
-
|
|
10543
|
+
chalk19.gray(
|
|
10194
10544
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10195
10545
|
)
|
|
10196
10546
|
);
|
|
@@ -10199,10 +10549,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10199
10549
|
return;
|
|
10200
10550
|
}
|
|
10201
10551
|
const existing = await loadConfig(currentDir);
|
|
10202
|
-
console.log(
|
|
10552
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10203
10553
|
if (Object.keys(existing).length > 0) {
|
|
10204
10554
|
console.log(
|
|
10205
|
-
|
|
10555
|
+
chalk19.gray(
|
|
10206
10556
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10207
10557
|
)
|
|
10208
10558
|
);
|
|
@@ -10220,7 +10570,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10220
10570
|
const providerKey = await select3({
|
|
10221
10571
|
message: `${label} \u2014 select provider:`,
|
|
10222
10572
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10223
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
10573
|
+
name: `${meta2.displayName.padEnd(22)} ${chalk19.gray(meta2.description)}`,
|
|
10224
10574
|
value: key,
|
|
10225
10575
|
short: meta2.displayName
|
|
10226
10576
|
}))
|
|
@@ -10228,7 +10578,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10228
10578
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10229
10579
|
const modelChoices = [
|
|
10230
10580
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10231
|
-
{ name:
|
|
10581
|
+
{ name: chalk19.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10232
10582
|
];
|
|
10233
10583
|
let chosenModel = await select3({
|
|
10234
10584
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10262,37 +10612,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10262
10612
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10263
10613
|
updated.codegen = "api";
|
|
10264
10614
|
console.log(
|
|
10265
|
-
|
|
10615
|
+
chalk19.yellow(
|
|
10266
10616
|
`
|
|
10267
10617
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10268
10618
|
)
|
|
10269
10619
|
);
|
|
10270
|
-
console.log(
|
|
10620
|
+
console.log(chalk19.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10271
10621
|
}
|
|
10272
10622
|
}
|
|
10273
10623
|
}
|
|
10274
|
-
console.log(
|
|
10275
|
-
console.log(
|
|
10624
|
+
console.log(chalk19.blue("\n Preview:"));
|
|
10625
|
+
console.log(chalk19.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10276
10626
|
if (updated.codegenProvider) {
|
|
10277
10627
|
console.log(
|
|
10278
|
-
|
|
10628
|
+
chalk19.gray(
|
|
10279
10629
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10280
10630
|
)
|
|
10281
10631
|
);
|
|
10282
10632
|
}
|
|
10283
10633
|
const ok = await confirm2({ message: "Save to .ai-spec.json?", default: true });
|
|
10284
10634
|
if (!ok) {
|
|
10285
|
-
console.log(
|
|
10635
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
10286
10636
|
return;
|
|
10287
10637
|
}
|
|
10288
|
-
await
|
|
10289
|
-
console.log(
|
|
10638
|
+
await fs24.writeJson(configPath, updated, { spaces: 2 });
|
|
10639
|
+
console.log(chalk19.green(`
|
|
10290
10640
|
\u2714 Saved to ${configPath}`));
|
|
10291
10641
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10292
10642
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10293
10643
|
if (envKey && !process.env[envKey]) {
|
|
10294
10644
|
console.log(
|
|
10295
|
-
|
|
10645
|
+
chalk19.yellow(
|
|
10296
10646
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10297
10647
|
)
|
|
10298
10648
|
);
|
|
@@ -10311,29 +10661,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10311
10661
|
cliOpts,
|
|
10312
10662
|
contractContextSection
|
|
10313
10663
|
} = opts;
|
|
10314
|
-
console.log(
|
|
10664
|
+
console.log(chalk19.blue(`
|
|
10315
10665
|
[${repoName}] Loading project context...`));
|
|
10316
10666
|
const loader = new ContextLoader(repoAbsPath);
|
|
10317
10667
|
let context = await loader.loadProjectContext();
|
|
10318
10668
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10319
|
-
console.log(
|
|
10320
|
-
console.log(
|
|
10669
|
+
console.log(chalk19.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10670
|
+
console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10321
10671
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10322
|
-
console.log(
|
|
10672
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10323
10673
|
}
|
|
10324
10674
|
if (!context.constitution) {
|
|
10325
|
-
console.log(
|
|
10675
|
+
console.log(chalk19.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10326
10676
|
try {
|
|
10327
10677
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10328
10678
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10329
10679
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10330
10680
|
context.constitution = constitutionContent;
|
|
10331
|
-
console.log(
|
|
10681
|
+
console.log(chalk19.green(` Constitution: generated`));
|
|
10332
10682
|
} catch (err) {
|
|
10333
|
-
console.log(
|
|
10683
|
+
console.log(chalk19.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10334
10684
|
}
|
|
10335
10685
|
} else {
|
|
10336
|
-
console.log(
|
|
10686
|
+
console.log(chalk19.green(` Constitution: found`));
|
|
10337
10687
|
}
|
|
10338
10688
|
let fullIdea = idea;
|
|
10339
10689
|
if (contractContextSection) {
|
|
@@ -10341,58 +10691,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10341
10691
|
|
|
10342
10692
|
${contractContextSection}`;
|
|
10343
10693
|
}
|
|
10344
|
-
console.log(
|
|
10694
|
+
console.log(chalk19.blue(` [${repoName}] Generating spec...`));
|
|
10345
10695
|
let finalSpec;
|
|
10346
10696
|
try {
|
|
10347
10697
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10348
10698
|
finalSpec = result.spec;
|
|
10349
|
-
console.log(
|
|
10699
|
+
console.log(chalk19.green(` Spec generated.`));
|
|
10350
10700
|
} catch (err) {
|
|
10351
|
-
console.error(
|
|
10701
|
+
console.error(chalk19.red(` Spec generation failed: ${err.message}`));
|
|
10352
10702
|
return { dsl: null, specFile: null };
|
|
10353
10703
|
}
|
|
10354
10704
|
let extractedDsl = null;
|
|
10355
10705
|
if (!cliOpts.skipDsl) {
|
|
10356
|
-
console.log(
|
|
10706
|
+
console.log(chalk19.blue(` [${repoName}] Extracting DSL...`));
|
|
10357
10707
|
try {
|
|
10358
10708
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10359
10709
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10360
10710
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10361
10711
|
if (extractedDsl) {
|
|
10362
|
-
console.log(
|
|
10712
|
+
console.log(chalk19.green(` DSL extracted.`));
|
|
10363
10713
|
}
|
|
10364
10714
|
} catch (err) {
|
|
10365
|
-
console.log(
|
|
10715
|
+
console.log(chalk19.yellow(` DSL extraction failed: ${err.message}`));
|
|
10366
10716
|
}
|
|
10367
10717
|
}
|
|
10368
10718
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10369
10719
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10370
10720
|
let workingDir = repoAbsPath;
|
|
10371
10721
|
if (!skipWorktreeForRepo) {
|
|
10372
|
-
console.log(
|
|
10722
|
+
console.log(chalk19.blue(` [${repoName}] Setting up git worktree...`));
|
|
10373
10723
|
try {
|
|
10374
10724
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10375
10725
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10376
10726
|
if (worktreePath) workingDir = worktreePath;
|
|
10377
10727
|
} catch (err) {
|
|
10378
|
-
console.log(
|
|
10728
|
+
console.log(chalk19.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10379
10729
|
}
|
|
10380
10730
|
} else {
|
|
10381
|
-
console.log(
|
|
10731
|
+
console.log(chalk19.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10382
10732
|
}
|
|
10383
|
-
const specsDir =
|
|
10384
|
-
await
|
|
10733
|
+
const specsDir = path23.join(workingDir, "specs");
|
|
10734
|
+
await fs24.ensureDir(specsDir);
|
|
10385
10735
|
const featureSlug = slugify(idea);
|
|
10386
10736
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10387
|
-
await
|
|
10388
|
-
console.log(
|
|
10737
|
+
await fs24.writeFile(specFile, finalSpec, "utf-8");
|
|
10738
|
+
console.log(chalk19.green(` Spec saved: ${path23.relative(repoAbsPath, specFile)}`));
|
|
10389
10739
|
let savedDslFile = null;
|
|
10390
10740
|
if (extractedDsl) {
|
|
10391
10741
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10392
10742
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10393
|
-
console.log(
|
|
10743
|
+
console.log(chalk19.green(` DSL saved: ${path23.relative(repoAbsPath, savedDslFile)}`));
|
|
10394
10744
|
}
|
|
10395
|
-
console.log(
|
|
10745
|
+
console.log(chalk19.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10396
10746
|
try {
|
|
10397
10747
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10398
10748
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10400,36 +10750,43 @@ ${contractContextSection}`;
|
|
|
10400
10750
|
dslFilePath: savedDslFile ?? void 0,
|
|
10401
10751
|
repoType: detectedRepoType
|
|
10402
10752
|
});
|
|
10403
|
-
console.log(
|
|
10753
|
+
console.log(chalk19.green(` Code generation complete.`));
|
|
10404
10754
|
} catch (err) {
|
|
10405
|
-
console.log(
|
|
10755
|
+
console.log(chalk19.yellow(` Code generation failed: ${err.message}`));
|
|
10406
10756
|
}
|
|
10407
10757
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10408
|
-
console.log(
|
|
10758
|
+
console.log(chalk19.blue(` [${repoName}] Generating test skeletons...`));
|
|
10409
10759
|
try {
|
|
10410
10760
|
const testGen = new TestGenerator(codegenProvider);
|
|
10411
10761
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10412
|
-
console.log(
|
|
10762
|
+
console.log(chalk19.green(` ${testFiles.length} test file(s) generated.`));
|
|
10413
10763
|
} catch (err) {
|
|
10414
|
-
console.log(
|
|
10764
|
+
console.log(chalk19.yellow(` Test generation failed: ${err.message}`));
|
|
10415
10765
|
}
|
|
10416
10766
|
}
|
|
10417
10767
|
if (!cliOpts.skipErrorFeedback) {
|
|
10418
10768
|
try {
|
|
10419
10769
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10420
10770
|
} catch (err) {
|
|
10421
|
-
console.log(
|
|
10771
|
+
console.log(chalk19.yellow(` Error feedback failed: ${err.message}`));
|
|
10422
10772
|
}
|
|
10423
10773
|
}
|
|
10424
10774
|
if (!cliOpts.skipReview) {
|
|
10425
|
-
console.log(
|
|
10775
|
+
console.log(chalk19.blue(` [${repoName}] Running code review...`));
|
|
10426
10776
|
try {
|
|
10427
10777
|
const reviewer = new CodeReviewer(specProvider);
|
|
10428
|
-
const
|
|
10778
|
+
const originalDir = process.cwd();
|
|
10779
|
+
let reviewResult;
|
|
10780
|
+
try {
|
|
10781
|
+
process.chdir(workingDir);
|
|
10782
|
+
reviewResult = await reviewer.reviewCode(finalSpec);
|
|
10783
|
+
} finally {
|
|
10784
|
+
process.chdir(originalDir);
|
|
10785
|
+
}
|
|
10429
10786
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10430
|
-
console.log(
|
|
10787
|
+
console.log(chalk19.green(` Code review complete.`));
|
|
10431
10788
|
} catch (err) {
|
|
10432
|
-
console.log(
|
|
10789
|
+
console.log(chalk19.yellow(` Code review failed: ${err.message}`));
|
|
10433
10790
|
}
|
|
10434
10791
|
}
|
|
10435
10792
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10452,7 +10809,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10452
10809
|
codegenModel: codegenModelName
|
|
10453
10810
|
});
|
|
10454
10811
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10455
|
-
console.log(
|
|
10812
|
+
console.log(chalk19.blue("\n[W1] Loading per-repo contexts..."));
|
|
10456
10813
|
const contexts = /* @__PURE__ */ new Map();
|
|
10457
10814
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10458
10815
|
for (const repo of workspace.repos) {
|
|
@@ -10464,27 +10821,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10464
10821
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10465
10822
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10466
10823
|
frontendContexts.set(repo.name, fctx);
|
|
10467
|
-
console.log(
|
|
10824
|
+
console.log(chalk19.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10468
10825
|
} else {
|
|
10469
|
-
console.log(
|
|
10826
|
+
console.log(chalk19.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10470
10827
|
}
|
|
10471
10828
|
} catch (err) {
|
|
10472
|
-
console.log(
|
|
10829
|
+
console.log(chalk19.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10473
10830
|
}
|
|
10474
10831
|
}
|
|
10475
|
-
console.log(
|
|
10832
|
+
console.log(chalk19.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10476
10833
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10477
10834
|
let decomposition;
|
|
10478
10835
|
try {
|
|
10479
10836
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10480
|
-
console.log(
|
|
10481
|
-
console.log(
|
|
10837
|
+
console.log(chalk19.green(` Summary: ${decomposition.summary}`));
|
|
10838
|
+
console.log(chalk19.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10482
10839
|
if (decomposition.coordinationNotes) {
|
|
10483
|
-
console.log(
|
|
10840
|
+
console.log(chalk19.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10484
10841
|
}
|
|
10485
10842
|
} catch (err) {
|
|
10486
|
-
console.error(
|
|
10487
|
-
console.log(
|
|
10843
|
+
console.error(chalk19.red(` Decomposition failed: ${err.message}`));
|
|
10844
|
+
console.log(chalk19.yellow(" Falling back to running all repos independently."));
|
|
10488
10845
|
decomposition = {
|
|
10489
10846
|
originalRequirement: idea,
|
|
10490
10847
|
summary: idea,
|
|
@@ -10500,11 +10857,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10500
10857
|
};
|
|
10501
10858
|
}
|
|
10502
10859
|
if (!opts.auto) {
|
|
10503
|
-
console.log(
|
|
10504
|
-
console.log(
|
|
10860
|
+
console.log(chalk19.cyan("\n[W3] Decomposition Preview:"));
|
|
10861
|
+
console.log(chalk19.cyan("\u2500".repeat(52)));
|
|
10505
10862
|
for (const r of decomposition.repos) {
|
|
10506
|
-
console.log(
|
|
10507
|
-
console.log(
|
|
10863
|
+
console.log(chalk19.bold(` ${r.repoName} (${r.role})`));
|
|
10864
|
+
console.log(chalk19.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
10508
10865
|
if (r.uxDecisions) {
|
|
10509
10866
|
const ux = r.uxDecisions;
|
|
10510
10867
|
const uxSummary = [
|
|
@@ -10513,13 +10870,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10513
10870
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
10514
10871
|
ux.errorRollback ? "rollback" : ""
|
|
10515
10872
|
].filter(Boolean).join(", ");
|
|
10516
|
-
if (uxSummary) console.log(
|
|
10873
|
+
if (uxSummary) console.log(chalk19.cyan(` UX: ${uxSummary}`));
|
|
10517
10874
|
}
|
|
10518
10875
|
if (r.dependsOnRepos.length > 0) {
|
|
10519
|
-
console.log(
|
|
10876
|
+
console.log(chalk19.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
10520
10877
|
}
|
|
10521
10878
|
}
|
|
10522
|
-
console.log(
|
|
10879
|
+
console.log(chalk19.cyan("\u2500".repeat(52)));
|
|
10523
10880
|
const gate = await select3({
|
|
10524
10881
|
message: "Proceed with multi-repo pipeline?",
|
|
10525
10882
|
choices: [
|
|
@@ -10528,24 +10885,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10528
10885
|
]
|
|
10529
10886
|
});
|
|
10530
10887
|
if (gate === "abort") {
|
|
10531
|
-
console.log(
|
|
10888
|
+
console.log(chalk19.yellow(" Aborted."));
|
|
10532
10889
|
process.exit(0);
|
|
10533
10890
|
}
|
|
10534
10891
|
}
|
|
10535
10892
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
10536
10893
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
10537
|
-
console.log(
|
|
10894
|
+
console.log(chalk19.blue(`
|
|
10538
10895
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
10539
10896
|
const results = [];
|
|
10540
10897
|
for (const repoReq of sortedRepoRequirements) {
|
|
10541
10898
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
10542
10899
|
if (!repoConfig) {
|
|
10543
|
-
console.log(
|
|
10900
|
+
console.log(chalk19.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
10544
10901
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
10545
10902
|
continue;
|
|
10546
10903
|
}
|
|
10547
10904
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
10548
|
-
console.log(
|
|
10905
|
+
console.log(chalk19.bold.blue(`
|
|
10549
10906
|
\u2500\u2500 ${repoReq.repoName} (${repoReq.role}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
10550
10907
|
let contractContextSection;
|
|
10551
10908
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -10553,7 +10910,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10553
10910
|
for (const depName of repoReq.dependsOnRepos) {
|
|
10554
10911
|
const depDsl = contractDsls.get(depName);
|
|
10555
10912
|
if (depDsl) {
|
|
10556
|
-
console.log(
|
|
10913
|
+
console.log(chalk19.gray(` Using API contract from: ${depName}`));
|
|
10557
10914
|
const contract = buildFrontendApiContract(depDsl);
|
|
10558
10915
|
contractParts.push(buildContractContextSection(contract));
|
|
10559
10916
|
}
|
|
@@ -10573,7 +10930,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10573
10930
|
frontendContext: frontendCtx
|
|
10574
10931
|
});
|
|
10575
10932
|
contractContextSection = void 0;
|
|
10576
|
-
console.log(
|
|
10933
|
+
console.log(chalk19.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
10577
10934
|
}
|
|
10578
10935
|
try {
|
|
10579
10936
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -10590,22 +10947,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10590
10947
|
});
|
|
10591
10948
|
if (repoReq.isContractProvider && dsl) {
|
|
10592
10949
|
contractDsls.set(repoReq.repoName, dsl);
|
|
10593
|
-
console.log(
|
|
10950
|
+
console.log(chalk19.green(` Contract stored for downstream repos.`));
|
|
10594
10951
|
}
|
|
10595
10952
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
10596
|
-
console.log(
|
|
10953
|
+
console.log(chalk19.green(` \u2714 ${repoReq.repoName} complete`));
|
|
10597
10954
|
} catch (err) {
|
|
10598
|
-
console.error(
|
|
10955
|
+
console.error(chalk19.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
10599
10956
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
10600
10957
|
}
|
|
10601
10958
|
}
|
|
10602
|
-
console.log(
|
|
10603
|
-
console.log(
|
|
10604
|
-
console.log(
|
|
10959
|
+
console.log(chalk19.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
10960
|
+
console.log(chalk19.gray(` Workspace: ${workspace.name}`));
|
|
10961
|
+
console.log(chalk19.gray(` Requirement: ${idea}`));
|
|
10605
10962
|
console.log();
|
|
10606
10963
|
for (const r of results) {
|
|
10607
|
-
const icon = r.status === "success" ?
|
|
10608
|
-
const specInfo = r.specFile ?
|
|
10964
|
+
const icon = r.status === "success" ? chalk19.green("\u2714") : r.status === "failed" ? chalk19.red("\u2718") : chalk19.gray("\u2212");
|
|
10965
|
+
const specInfo = r.specFile ? chalk19.gray(` \u2192 ${r.specFile}`) : "";
|
|
10609
10966
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
10610
10967
|
}
|
|
10611
10968
|
return results;
|
|
@@ -10613,18 +10970,18 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10613
10970
|
var workspaceCmd = program.command("workspace").description("Manage multi-repo workspace configuration");
|
|
10614
10971
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
10615
10972
|
const currentDir = process.cwd();
|
|
10616
|
-
const configPath =
|
|
10617
|
-
if (await
|
|
10973
|
+
const configPath = path23.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
10974
|
+
if (await fs24.pathExists(configPath)) {
|
|
10618
10975
|
const overwrite = await confirm2({
|
|
10619
10976
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
10620
10977
|
default: false
|
|
10621
10978
|
});
|
|
10622
10979
|
if (!overwrite) {
|
|
10623
|
-
console.log(
|
|
10980
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
10624
10981
|
return;
|
|
10625
10982
|
}
|
|
10626
10983
|
}
|
|
10627
|
-
console.log(
|
|
10984
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10628
10985
|
const workspaceName = await input({
|
|
10629
10986
|
message: "Workspace name:",
|
|
10630
10987
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -10638,11 +10995,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10638
10995
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10639
10996
|
const detected = await workspaceLoader.autoDetect();
|
|
10640
10997
|
if (detected.length === 0) {
|
|
10641
|
-
console.log(
|
|
10998
|
+
console.log(chalk19.yellow(" No recognizable repos found in sibling directories."));
|
|
10642
10999
|
} else {
|
|
10643
|
-
console.log(
|
|
11000
|
+
console.log(chalk19.cyan("\n Detected repos:"));
|
|
10644
11001
|
for (const r of detected) {
|
|
10645
|
-
console.log(
|
|
11002
|
+
console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
10646
11003
|
}
|
|
10647
11004
|
const keepAll = await confirm2({
|
|
10648
11005
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -10659,7 +11016,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10659
11016
|
if (keep) repos.push(r);
|
|
10660
11017
|
}
|
|
10661
11018
|
}
|
|
10662
|
-
console.log(
|
|
11019
|
+
console.log(chalk19.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
10663
11020
|
}
|
|
10664
11021
|
}
|
|
10665
11022
|
const repoTypeChoices = [
|
|
@@ -10681,7 +11038,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10681
11038
|
default: repos.length === 0
|
|
10682
11039
|
});
|
|
10683
11040
|
while (addMore) {
|
|
10684
|
-
console.log(
|
|
11041
|
+
console.log(chalk19.cyan(`
|
|
10685
11042
|
Adding repo #${repos.length + 1}`));
|
|
10686
11043
|
const repoName = await input({
|
|
10687
11044
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -10695,16 +11052,16 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10695
11052
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
10696
11053
|
default: `./${repoName}`
|
|
10697
11054
|
});
|
|
10698
|
-
const absPath =
|
|
11055
|
+
const absPath = path23.resolve(currentDir, repoPath);
|
|
10699
11056
|
let detectedType = "unknown";
|
|
10700
11057
|
let detectedRole = "shared";
|
|
10701
|
-
if (await
|
|
11058
|
+
if (await fs24.pathExists(absPath)) {
|
|
10702
11059
|
const { type, role } = await detectRepoType(absPath);
|
|
10703
11060
|
detectedType = type;
|
|
10704
11061
|
detectedRole = role;
|
|
10705
|
-
console.log(
|
|
11062
|
+
console.log(chalk19.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
10706
11063
|
} else {
|
|
10707
|
-
console.log(
|
|
11064
|
+
console.log(chalk19.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
10708
11065
|
}
|
|
10709
11066
|
const repoType = await select3({
|
|
10710
11067
|
message: `Repo type for "${repoName}":`,
|
|
@@ -10727,53 +11084,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10727
11084
|
type: repoType,
|
|
10728
11085
|
role: repoRole
|
|
10729
11086
|
});
|
|
10730
|
-
console.log(
|
|
11087
|
+
console.log(chalk19.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
10731
11088
|
addMore = await confirm2({
|
|
10732
11089
|
message: "Add another repo?",
|
|
10733
11090
|
default: false
|
|
10734
11091
|
});
|
|
10735
11092
|
}
|
|
10736
11093
|
const workspaceConfig = { name: workspaceName, repos };
|
|
10737
|
-
console.log(
|
|
10738
|
-
console.log(
|
|
11094
|
+
console.log(chalk19.cyan("\n Workspace summary:"));
|
|
11095
|
+
console.log(chalk19.gray(` Name: ${workspaceName}`));
|
|
10739
11096
|
for (const r of repos) {
|
|
10740
|
-
console.log(
|
|
11097
|
+
console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
10741
11098
|
}
|
|
10742
11099
|
const ok = await confirm2({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
10743
11100
|
if (!ok) {
|
|
10744
|
-
console.log(
|
|
11101
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
10745
11102
|
return;
|
|
10746
11103
|
}
|
|
10747
11104
|
const loader = new WorkspaceLoader(currentDir);
|
|
10748
11105
|
const saved = await loader.save(workspaceConfig);
|
|
10749
|
-
console.log(
|
|
11106
|
+
console.log(chalk19.green(`
|
|
10750
11107
|
\u2714 Workspace saved: ${saved}`));
|
|
10751
|
-
console.log(
|
|
11108
|
+
console.log(chalk19.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
10752
11109
|
});
|
|
10753
11110
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
10754
11111
|
const currentDir = process.cwd();
|
|
10755
11112
|
const loader = new WorkspaceLoader(currentDir);
|
|
10756
11113
|
const config2 = await loader.load();
|
|
10757
11114
|
if (!config2) {
|
|
10758
|
-
console.log(
|
|
10759
|
-
console.log(
|
|
11115
|
+
console.log(chalk19.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11116
|
+
console.log(chalk19.gray(" Run `ai-spec workspace init` to create one."));
|
|
10760
11117
|
return;
|
|
10761
11118
|
}
|
|
10762
|
-
console.log(
|
|
11119
|
+
console.log(chalk19.bold(`
|
|
10763
11120
|
Workspace: ${config2.name}`));
|
|
10764
|
-
console.log(
|
|
10765
|
-
console.log(
|
|
11121
|
+
console.log(chalk19.gray(` Config: ${path23.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11122
|
+
console.log(chalk19.gray(` Repos (${config2.repos.length}):
|
|
10766
11123
|
`));
|
|
10767
11124
|
for (const repo of config2.repos) {
|
|
10768
11125
|
const absPath = loader.resolveAbsPath(repo);
|
|
10769
|
-
const exists = await
|
|
10770
|
-
const status = exists ?
|
|
11126
|
+
const exists = await fs24.pathExists(absPath);
|
|
11127
|
+
const status = exists ? chalk19.green("found") : chalk19.red("not found");
|
|
10771
11128
|
console.log(
|
|
10772
|
-
` ${
|
|
11129
|
+
` ${chalk19.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
10773
11130
|
);
|
|
10774
|
-
console.log(
|
|
11131
|
+
console.log(chalk19.gray(` path: ${absPath}`));
|
|
10775
11132
|
if (repo.constitution) {
|
|
10776
|
-
console.log(
|
|
11133
|
+
console.log(chalk19.green(` constitution: found`));
|
|
10777
11134
|
}
|
|
10778
11135
|
}
|
|
10779
11136
|
});
|
|
@@ -10790,24 +11147,24 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
10790
11147
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
10791
11148
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
10792
11149
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
10793
|
-
console.log(
|
|
10794
|
-
console.log(
|
|
11150
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11151
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
10795
11152
|
let specPath = opts.spec ?? null;
|
|
10796
11153
|
if (!specPath) {
|
|
10797
|
-
const specsDir =
|
|
11154
|
+
const specsDir = path23.join(currentDir, "specs");
|
|
10798
11155
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
10799
11156
|
if (!latest) {
|
|
10800
|
-
console.error(
|
|
11157
|
+
console.error(chalk19.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
10801
11158
|
process.exit(1);
|
|
10802
11159
|
}
|
|
10803
11160
|
specPath = latest.filePath;
|
|
10804
|
-
console.log(
|
|
11161
|
+
console.log(chalk19.gray(` Using spec: ${path23.relative(currentDir, specPath)} (v${latest.version})`));
|
|
10805
11162
|
}
|
|
10806
|
-
console.log(
|
|
11163
|
+
console.log(chalk19.gray(" Loading project context..."));
|
|
10807
11164
|
const loader = new ContextLoader(currentDir);
|
|
10808
11165
|
const context = await loader.loadProjectContext();
|
|
10809
11166
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10810
|
-
console.log(
|
|
11167
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10811
11168
|
}
|
|
10812
11169
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
10813
11170
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -10819,19 +11176,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
10819
11176
|
repoType
|
|
10820
11177
|
});
|
|
10821
11178
|
} catch (err) {
|
|
10822
|
-
console.error(
|
|
11179
|
+
console.error(chalk19.red(` Update failed: ${err.message}`));
|
|
10823
11180
|
process.exit(1);
|
|
10824
11181
|
}
|
|
10825
|
-
console.log(
|
|
10826
|
-
\u2714 Spec updated \u2192 v${result.newVersion}: ${
|
|
11182
|
+
console.log(chalk19.green(`
|
|
11183
|
+
\u2714 Spec updated \u2192 v${result.newVersion}: ${path23.relative(currentDir, result.newSpecPath)}`));
|
|
10827
11184
|
if (result.newDslPath) {
|
|
10828
|
-
console.log(
|
|
11185
|
+
console.log(chalk19.green(` \u2714 DSL updated: ${path23.relative(currentDir, result.newDslPath)}`));
|
|
10829
11186
|
}
|
|
10830
11187
|
if (result.affectedFiles.length > 0) {
|
|
10831
|
-
console.log(
|
|
11188
|
+
console.log(chalk19.cyan("\n Affected files:"));
|
|
10832
11189
|
for (const f of result.affectedFiles) {
|
|
10833
|
-
const icon = f.action === "create" ?
|
|
10834
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11190
|
+
const icon = f.action === "create" ? chalk19.green("+") : chalk19.yellow("~");
|
|
11191
|
+
console.log(` ${icon} ${f.file}: ${chalk19.gray(f.description)}`);
|
|
10835
11192
|
}
|
|
10836
11193
|
}
|
|
10837
11194
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -10839,9 +11196,9 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
10839
11196
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
10840
11197
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
10841
11198
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10842
|
-
console.log(
|
|
11199
|
+
console.log(chalk19.blue("\n Regenerating affected files..."));
|
|
10843
11200
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
10844
|
-
const specContent = await
|
|
11201
|
+
const specContent = await fs24.readFile(result.newSpecPath, "utf-8");
|
|
10845
11202
|
const constitutionSection = context.constitution ? `
|
|
10846
11203
|
=== Project Constitution (MUST follow) ===
|
|
10847
11204
|
${context.constitution}
|
|
@@ -10851,10 +11208,10 @@ ${context.constitution}
|
|
|
10851
11208
|
${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
10852
11209
|
` : "";
|
|
10853
11210
|
for (const affected of result.affectedFiles) {
|
|
10854
|
-
const fullPath =
|
|
11211
|
+
const fullPath = path23.join(currentDir, affected.file);
|
|
10855
11212
|
let existing = "";
|
|
10856
11213
|
try {
|
|
10857
|
-
existing = await
|
|
11214
|
+
existing = await fs24.readFile(fullPath, "utf-8");
|
|
10858
11215
|
} catch {
|
|
10859
11216
|
}
|
|
10860
11217
|
const codePrompt = `Apply this change to the file.
|
|
@@ -10868,24 +11225,24 @@ ${specContent}
|
|
|
10868
11225
|
${constitutionSection}${dslSection}
|
|
10869
11226
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
10870
11227
|
${existing || "Create from scratch."}`;
|
|
10871
|
-
process.stdout.write(` ${existing ?
|
|
11228
|
+
process.stdout.write(` ${existing ? chalk19.yellow("~") : chalk19.green("+")} ${affected.file}... `);
|
|
10872
11229
|
try {
|
|
10873
11230
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
10874
11231
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
10875
11232
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
10876
|
-
await
|
|
10877
|
-
await
|
|
10878
|
-
console.log(
|
|
11233
|
+
await fs24.ensureDir(path23.dirname(fullPath));
|
|
11234
|
+
await fs24.writeFile(fullPath, content, "utf-8");
|
|
11235
|
+
console.log(chalk19.green("\u2714"));
|
|
10879
11236
|
} catch (err) {
|
|
10880
|
-
console.log(
|
|
11237
|
+
console.log(chalk19.red(`\u2718 ${err.message}`));
|
|
10881
11238
|
}
|
|
10882
11239
|
}
|
|
10883
11240
|
}
|
|
10884
11241
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
10885
|
-
console.log(
|
|
10886
|
-
console.log(
|
|
10887
|
-
console.log(
|
|
10888
|
-
console.log(
|
|
11242
|
+
console.log(chalk19.blue("\n Next steps:"));
|
|
11243
|
+
console.log(chalk19.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11244
|
+
console.log(chalk19.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11245
|
+
console.log(chalk19.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
10889
11246
|
}
|
|
10890
11247
|
});
|
|
10891
11248
|
program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)").option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)").option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml").option("--output <path>", "Output file path (default: openapi.yaml)").option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").action(async (opts) => {
|
|
@@ -10894,19 +11251,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
10894
11251
|
if (!dslPath) {
|
|
10895
11252
|
dslPath = await findLatestDslFile(currentDir);
|
|
10896
11253
|
if (!dslPath) {
|
|
10897
|
-
console.error(
|
|
11254
|
+
console.error(chalk19.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
10898
11255
|
process.exit(1);
|
|
10899
11256
|
}
|
|
10900
|
-
console.log(
|
|
11257
|
+
console.log(chalk19.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
|
|
10901
11258
|
}
|
|
10902
11259
|
let dsl;
|
|
10903
11260
|
try {
|
|
10904
|
-
dsl = await
|
|
11261
|
+
dsl = await fs24.readJson(dslPath);
|
|
10905
11262
|
} catch (err) {
|
|
10906
|
-
console.error(
|
|
11263
|
+
console.error(chalk19.red(` Failed to read DSL: ${err.message}`));
|
|
10907
11264
|
process.exit(1);
|
|
10908
11265
|
}
|
|
10909
|
-
console.log(
|
|
11266
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10910
11267
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
10911
11268
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
10912
11269
|
try {
|
|
@@ -10915,31 +11272,31 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
10915
11272
|
serverUrl,
|
|
10916
11273
|
outputPath: opts.output
|
|
10917
11274
|
});
|
|
10918
|
-
const rel =
|
|
10919
|
-
console.log(
|
|
10920
|
-
console.log(
|
|
10921
|
-
console.log(
|
|
10922
|
-
console.log(
|
|
10923
|
-
console.log(
|
|
10924
|
-
console.log(
|
|
10925
|
-
console.log(
|
|
10926
|
-
console.log(
|
|
11275
|
+
const rel = path23.relative(currentDir, outputPath);
|
|
11276
|
+
console.log(chalk19.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11277
|
+
console.log(chalk19.gray(` Feature : ${dsl.feature.title}`));
|
|
11278
|
+
console.log(chalk19.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11279
|
+
console.log(chalk19.gray(` Models : ${dsl.models.length}`));
|
|
11280
|
+
console.log(chalk19.gray(` Server : ${serverUrl}`));
|
|
11281
|
+
console.log(chalk19.blue("\n Next steps:"));
|
|
11282
|
+
console.log(chalk19.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11283
|
+
console.log(chalk19.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
10927
11284
|
} catch (err) {
|
|
10928
|
-
console.error(
|
|
11285
|
+
console.error(chalk19.red(` Export failed: ${err.message}`));
|
|
10929
11286
|
process.exit(1);
|
|
10930
11287
|
}
|
|
10931
11288
|
});
|
|
10932
11289
|
program.command("mock").description("Generate a standalone mock server + proxy config from the latest DSL").option("--port <n>", "Mock server port (default: 3001)", "3001").option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/").option("--proxy", "Also generate frontend proxy config snippet").option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)").option("--workspace", "Generate mock assets for all backend repos in the workspace").option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)").option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)").option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)").action(async (opts) => {
|
|
10933
11290
|
const currentDir = process.cwd();
|
|
10934
11291
|
const port = parseInt(opts.port, 10) || 3001;
|
|
10935
|
-
console.log(
|
|
11292
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 ai-spec mock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10936
11293
|
if (opts.restore) {
|
|
10937
|
-
const frontendDir = opts.frontend ?
|
|
11294
|
+
const frontendDir = opts.frontend ? path23.resolve(opts.frontend) : currentDir;
|
|
10938
11295
|
const r = await restoreMockProxy(frontendDir);
|
|
10939
11296
|
if (r.restored) {
|
|
10940
|
-
console.log(
|
|
11297
|
+
console.log(chalk19.green(" \u2714 Proxy restored and mock server stopped."));
|
|
10941
11298
|
} else {
|
|
10942
|
-
console.log(
|
|
11299
|
+
console.log(chalk19.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
10943
11300
|
}
|
|
10944
11301
|
return;
|
|
10945
11302
|
}
|
|
@@ -10947,32 +11304,32 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
10947
11304
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10948
11305
|
const workspaceConfig = await workspaceLoader.load();
|
|
10949
11306
|
if (!workspaceConfig) {
|
|
10950
|
-
console.error(
|
|
11307
|
+
console.error(chalk19.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
10951
11308
|
process.exit(1);
|
|
10952
11309
|
}
|
|
10953
11310
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
10954
11311
|
if (backendRepos.length === 0) {
|
|
10955
|
-
console.log(
|
|
11312
|
+
console.log(chalk19.yellow(" No backend repos found in workspace."));
|
|
10956
11313
|
return;
|
|
10957
11314
|
}
|
|
10958
11315
|
for (const repo of backendRepos) {
|
|
10959
11316
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
10960
|
-
console.log(
|
|
11317
|
+
console.log(chalk19.cyan(`
|
|
10961
11318
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
10962
11319
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
10963
11320
|
if (!dslFile) {
|
|
10964
|
-
console.log(
|
|
11321
|
+
console.log(chalk19.yellow(` No DSL file found \u2014 skipping.`));
|
|
10965
11322
|
continue;
|
|
10966
11323
|
}
|
|
10967
|
-
const dsl2 = await
|
|
11324
|
+
const dsl2 = await fs24.readJson(dslFile);
|
|
10968
11325
|
const result2 = await generateMockAssets(dsl2, repoAbsPath, {
|
|
10969
11326
|
port,
|
|
10970
11327
|
msw: opts.msw,
|
|
10971
11328
|
proxy: opts.proxy
|
|
10972
11329
|
});
|
|
10973
11330
|
for (const f of result2.files) {
|
|
10974
|
-
console.log(
|
|
10975
|
-
console.log(
|
|
11331
|
+
console.log(chalk19.green(` \u2714 ${f.path}`));
|
|
11332
|
+
console.log(chalk19.gray(` ${f.description}`));
|
|
10976
11333
|
}
|
|
10977
11334
|
}
|
|
10978
11335
|
return;
|
|
@@ -10982,19 +11339,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
10982
11339
|
dslPath = await findLatestDslFile(currentDir);
|
|
10983
11340
|
if (!dslPath) {
|
|
10984
11341
|
console.error(
|
|
10985
|
-
|
|
11342
|
+
chalk19.red(
|
|
10986
11343
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
10987
11344
|
)
|
|
10988
11345
|
);
|
|
10989
11346
|
process.exit(1);
|
|
10990
11347
|
}
|
|
10991
|
-
console.log(
|
|
11348
|
+
console.log(chalk19.gray(` Using DSL: ${path23.relative(currentDir, dslPath)}`));
|
|
10992
11349
|
}
|
|
10993
11350
|
let dsl;
|
|
10994
11351
|
try {
|
|
10995
|
-
dsl = await
|
|
11352
|
+
dsl = await fs24.readJson(dslPath);
|
|
10996
11353
|
} catch (err) {
|
|
10997
|
-
console.error(
|
|
11354
|
+
console.error(chalk19.red(` Failed to read DSL file: ${err.message}`));
|
|
10998
11355
|
process.exit(1);
|
|
10999
11356
|
}
|
|
11000
11357
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11002,58 +11359,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11002
11359
|
msw: opts.msw,
|
|
11003
11360
|
proxy: opts.proxy
|
|
11004
11361
|
});
|
|
11005
|
-
console.log(
|
|
11362
|
+
console.log(chalk19.green(`
|
|
11006
11363
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11007
11364
|
for (const f of result.files) {
|
|
11008
|
-
console.log(
|
|
11009
|
-
console.log(
|
|
11365
|
+
console.log(chalk19.green(` ${f.path}`));
|
|
11366
|
+
console.log(chalk19.gray(` ${f.description}`));
|
|
11010
11367
|
}
|
|
11011
11368
|
if (opts.serve) {
|
|
11012
|
-
const serverJsPath =
|
|
11013
|
-
if (!await
|
|
11014
|
-
console.error(
|
|
11369
|
+
const serverJsPath = path23.join(currentDir, "mock", "server.js");
|
|
11370
|
+
if (!await fs24.pathExists(serverJsPath)) {
|
|
11371
|
+
console.error(chalk19.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11015
11372
|
process.exit(1);
|
|
11016
11373
|
}
|
|
11017
11374
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11018
|
-
console.log(
|
|
11375
|
+
console.log(chalk19.green(`
|
|
11019
11376
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11020
11377
|
if (opts.frontend) {
|
|
11021
|
-
const frontendDir =
|
|
11378
|
+
const frontendDir = path23.resolve(opts.frontend);
|
|
11022
11379
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11023
11380
|
await saveMockServerPid(frontendDir, pid);
|
|
11024
11381
|
if (proxyResult.applied) {
|
|
11025
|
-
console.log(
|
|
11026
|
-
console.log(
|
|
11382
|
+
console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11383
|
+
console.log(chalk19.bold.cyan(`
|
|
11027
11384
|
Ready! Open a new terminal and run:`));
|
|
11028
|
-
console.log(
|
|
11029
|
-
console.log(
|
|
11030
|
-
console.log(
|
|
11385
|
+
console.log(chalk19.white(` cd ${frontendDir}`));
|
|
11386
|
+
console.log(chalk19.white(` ${proxyResult.devCommand}`));
|
|
11387
|
+
console.log(chalk19.gray(`
|
|
11031
11388
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11032
11389
|
} else {
|
|
11033
|
-
console.log(
|
|
11034
|
-
if (proxyResult.note) console.log(
|
|
11390
|
+
console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11391
|
+
if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
|
|
11035
11392
|
}
|
|
11036
11393
|
} else {
|
|
11037
|
-
console.log(
|
|
11038
|
-
console.log(
|
|
11394
|
+
console.log(chalk19.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11395
|
+
console.log(chalk19.gray(` Mock server: http://localhost:${port}`));
|
|
11039
11396
|
}
|
|
11040
11397
|
return;
|
|
11041
11398
|
}
|
|
11042
|
-
console.log(
|
|
11043
|
-
console.log(
|
|
11044
|
-
console.log(
|
|
11045
|
-
console.log(
|
|
11046
|
-
console.log(
|
|
11047
|
-
console.log(
|
|
11048
|
-
console.log(
|
|
11049
|
-
console.log(
|
|
11399
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11400
|
+
console.log(chalk19.white(` 1. Install express (if not already):`));
|
|
11401
|
+
console.log(chalk19.gray(` npm install --save-dev express`));
|
|
11402
|
+
console.log(chalk19.white(` 2. Start mock server:`));
|
|
11403
|
+
console.log(chalk19.gray(` node mock/server.js`));
|
|
11404
|
+
console.log(chalk19.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
11405
|
+
console.log(chalk19.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
11406
|
+
console.log(chalk19.gray(` http://localhost:${port}`));
|
|
11050
11407
|
if (opts.proxy) {
|
|
11051
|
-
console.log(
|
|
11408
|
+
console.log(chalk19.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11052
11409
|
}
|
|
11053
11410
|
if (opts.msw) {
|
|
11054
|
-
console.log(
|
|
11055
|
-
console.log(
|
|
11056
|
-
console.log(
|
|
11411
|
+
console.log(chalk19.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
11412
|
+
console.log(chalk19.gray(` import { worker } from './mocks/browser';`));
|
|
11413
|
+
console.log(chalk19.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11057
11414
|
}
|
|
11058
11415
|
});
|
|
11059
11416
|
program.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
@@ -11069,14 +11426,27 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11069
11426
|
}
|
|
11070
11427
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11071
11428
|
if (result.appended) {
|
|
11072
|
-
console.log(
|
|
11429
|
+
console.log(chalk19.green(`
|
|
11073
11430
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11074
|
-
console.log(
|
|
11431
|
+
console.log(chalk19.gray(` File: .ai-spec-constitution.md`));
|
|
11075
11432
|
} else {
|
|
11076
|
-
console.log(
|
|
11433
|
+
console.log(chalk19.yellow(`
|
|
11077
11434
|
\u26A0 Not appended: ${result.reason}`));
|
|
11078
11435
|
}
|
|
11079
11436
|
});
|
|
11437
|
+
program.command("restore").description("Restore files modified by a previous run").argument("<runId>", "Run ID shown at the end of a create / generate run").action(async (runId) => {
|
|
11438
|
+
const currentDir = process.cwd();
|
|
11439
|
+
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11440
|
+
console.log(chalk19.blue(`Restoring run: ${runId}...`));
|
|
11441
|
+
const restored = await snapshot.restore();
|
|
11442
|
+
if (restored.length === 0) {
|
|
11443
|
+
console.log(chalk19.yellow(" No backup found for this run ID."));
|
|
11444
|
+
} else {
|
|
11445
|
+
restored.forEach((f) => console.log(chalk19.green(` \u2714 restored: ${f}`)));
|
|
11446
|
+
console.log(chalk19.bold.green(`
|
|
11447
|
+
\u2714 ${restored.length} file(s) restored.`));
|
|
11448
|
+
}
|
|
11449
|
+
});
|
|
11080
11450
|
if (process.argv.length <= 2) {
|
|
11081
11451
|
(async () => {
|
|
11082
11452
|
const currentDir = process.cwd();
|