ai-spec-dev 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/RELEASE_LOG.md +33 -0
- package/cli/index.ts +24 -1
- package/core/prompt-hasher.ts +42 -0
- package/core/run-logger.ts +21 -0
- package/core/self-evaluator.ts +172 -0
- package/dist/cli/index.js +444 -323
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +444 -323
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/purpose.md +189 -2
package/dist/cli/index.mjs
CHANGED
|
@@ -469,7 +469,7 @@ var init_workspace_loader = __esm({
|
|
|
469
469
|
import { Command } from "commander";
|
|
470
470
|
import * as path22 from "path";
|
|
471
471
|
import * as fs23 from "fs-extra";
|
|
472
|
-
import
|
|
472
|
+
import chalk19 from "chalk";
|
|
473
473
|
import * as dotenv from "dotenv";
|
|
474
474
|
import { input, confirm as confirm2, select as select3 } from "@inquirer/prompts";
|
|
475
475
|
|
|
@@ -6106,6 +6106,16 @@ var RunLogger = class {
|
|
|
6106
6106
|
this.log.errors.push(`[${event}] ${error}`);
|
|
6107
6107
|
this.flush();
|
|
6108
6108
|
}
|
|
6109
|
+
/** Record the prompt hash for this run (call once at run start). */
|
|
6110
|
+
setPromptHash(hash) {
|
|
6111
|
+
this.log.promptHash = hash;
|
|
6112
|
+
this.flush();
|
|
6113
|
+
}
|
|
6114
|
+
/** Record the harness self-eval score (call once at run end). */
|
|
6115
|
+
setHarnessScore(score) {
|
|
6116
|
+
this.log.harnessScore = score;
|
|
6117
|
+
this.flush();
|
|
6118
|
+
}
|
|
6109
6119
|
fileWritten(filePath) {
|
|
6110
6120
|
if (!this.log.filesWritten.includes(filePath)) {
|
|
6111
6121
|
this.log.filesWritten.push(filePath);
|
|
@@ -9890,6 +9900,103 @@ async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
|
9890
9900
|
return outputPath;
|
|
9891
9901
|
}
|
|
9892
9902
|
|
|
9903
|
+
// core/prompt-hasher.ts
|
|
9904
|
+
init_codegen_prompt();
|
|
9905
|
+
init_codegen_prompt();
|
|
9906
|
+
import { createHash } from "crypto";
|
|
9907
|
+
function computePromptHash() {
|
|
9908
|
+
const segments = [
|
|
9909
|
+
codeGenSystemPrompt,
|
|
9910
|
+
dslSystemPrompt,
|
|
9911
|
+
specPrompt,
|
|
9912
|
+
reviewArchitectureSystemPrompt,
|
|
9913
|
+
reviewImplementationSystemPrompt,
|
|
9914
|
+
reviewImpactComplexitySystemPrompt
|
|
9915
|
+
];
|
|
9916
|
+
return createHash("sha256").update(segments.join("\0")).digest("hex").slice(0, 8);
|
|
9917
|
+
}
|
|
9918
|
+
|
|
9919
|
+
// core/self-evaluator.ts
|
|
9920
|
+
import chalk18 from "chalk";
|
|
9921
|
+
var ENDPOINT_LAYER_PATTERNS = [
|
|
9922
|
+
/src\/api/,
|
|
9923
|
+
/src\/routes?/,
|
|
9924
|
+
/src\/controller/,
|
|
9925
|
+
/src\/handler/,
|
|
9926
|
+
/src\/endpoints?/
|
|
9927
|
+
];
|
|
9928
|
+
var MODEL_LAYER_PATTERNS = [
|
|
9929
|
+
/src\/model/,
|
|
9930
|
+
/src\/schema/,
|
|
9931
|
+
/src\/entit/,
|
|
9932
|
+
/src\/db/,
|
|
9933
|
+
/prisma/,
|
|
9934
|
+
/src\/data/,
|
|
9935
|
+
/src\/domain/
|
|
9936
|
+
];
|
|
9937
|
+
function extractReviewScore(reviewText) {
|
|
9938
|
+
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
9939
|
+
return match ? parseFloat(match[1]) : null;
|
|
9940
|
+
}
|
|
9941
|
+
function runSelfEval(opts) {
|
|
9942
|
+
const { dsl, generatedFiles, compilePassed, reviewText, promptHash, logger } = opts;
|
|
9943
|
+
const endpointsTotal = dsl?.endpoints?.length ?? 0;
|
|
9944
|
+
const modelsTotal = dsl?.models?.length ?? 0;
|
|
9945
|
+
const endpointLayerCovered = generatedFiles.some(
|
|
9946
|
+
(f) => ENDPOINT_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9947
|
+
);
|
|
9948
|
+
const modelLayerCovered = generatedFiles.some(
|
|
9949
|
+
(f) => MODEL_LAYER_PATTERNS.some((p) => p.test(f))
|
|
9950
|
+
);
|
|
9951
|
+
let dslCoverageScore = 10;
|
|
9952
|
+
if (generatedFiles.length === 0) {
|
|
9953
|
+
dslCoverageScore = 0;
|
|
9954
|
+
} else {
|
|
9955
|
+
if (endpointsTotal > 0 && !endpointLayerCovered) dslCoverageScore -= 4;
|
|
9956
|
+
if (modelsTotal > 0 && !modelLayerCovered) dslCoverageScore -= 3;
|
|
9957
|
+
}
|
|
9958
|
+
const compileScore = compilePassed ? 10 : 5;
|
|
9959
|
+
const reviewScore = reviewText ? extractReviewScore(reviewText) : null;
|
|
9960
|
+
const harnessScore = reviewScore !== null ? Math.round((dslCoverageScore * 0.4 + compileScore * 0.3 + reviewScore * 0.3) * 10) / 10 : Math.round((dslCoverageScore * 0.55 + compileScore * 0.45) * 10) / 10;
|
|
9961
|
+
const result = {
|
|
9962
|
+
dslCoverageScore,
|
|
9963
|
+
compileScore,
|
|
9964
|
+
reviewScore,
|
|
9965
|
+
harnessScore,
|
|
9966
|
+
promptHash,
|
|
9967
|
+
detail: {
|
|
9968
|
+
endpointsTotal,
|
|
9969
|
+
endpointLayerCovered,
|
|
9970
|
+
modelsTotal,
|
|
9971
|
+
modelLayerCovered,
|
|
9972
|
+
filesWritten: generatedFiles.length
|
|
9973
|
+
}
|
|
9974
|
+
};
|
|
9975
|
+
logger.setHarnessScore(harnessScore);
|
|
9976
|
+
logger.stageEnd("self_eval", {
|
|
9977
|
+
harnessScore,
|
|
9978
|
+
dslCoverageScore,
|
|
9979
|
+
compileScore,
|
|
9980
|
+
reviewScore: reviewScore ?? void 0,
|
|
9981
|
+
promptHash
|
|
9982
|
+
});
|
|
9983
|
+
return result;
|
|
9984
|
+
}
|
|
9985
|
+
function printSelfEval(result) {
|
|
9986
|
+
const scoreColor = result.harnessScore >= 8 ? chalk18.green : result.harnessScore >= 6 ? chalk18.yellow : chalk18.red;
|
|
9987
|
+
const filled = Math.round(result.harnessScore);
|
|
9988
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
9989
|
+
const compileTag = result.compileScore === 10 ? chalk18.green("pass") : chalk18.yellow("partial");
|
|
9990
|
+
const reviewTag = result.reviewScore !== null ? `Review: ${result.reviewScore}/10` : chalk18.gray("Review: skipped");
|
|
9991
|
+
console.log(chalk18.cyan("\n\u2500\u2500\u2500 Harness Self-Eval \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"));
|
|
9992
|
+
console.log(` Score : ${scoreColor(`[${bar}] ${result.harnessScore}/10`)}`);
|
|
9993
|
+
console.log(
|
|
9994
|
+
` DSL : ${scoreColor(result.dslCoverageScore + "/10")} Compile: ${compileTag} ${reviewTag}`
|
|
9995
|
+
);
|
|
9996
|
+
console.log(chalk18.gray(` Prompt : ${result.promptHash}`));
|
|
9997
|
+
console.log(chalk18.gray("\u2500".repeat(49)));
|
|
9998
|
+
}
|
|
9999
|
+
|
|
9893
10000
|
// cli/index.ts
|
|
9894
10001
|
dotenv.config();
|
|
9895
10002
|
var CONFIG_FILE = ".ai-spec.json";
|
|
@@ -9921,20 +10028,20 @@ async function resolveApiKey(providerName, cliKey) {
|
|
|
9921
10028
|
validate: (v2) => v2.trim().length > 0 || "API key cannot be empty"
|
|
9922
10029
|
});
|
|
9923
10030
|
await saveKey(providerName, newKey.trim());
|
|
9924
|
-
console.log(
|
|
10031
|
+
console.log(chalk19.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
9925
10032
|
return newKey.trim();
|
|
9926
10033
|
}
|
|
9927
10034
|
function printBanner(opts) {
|
|
9928
|
-
console.log(
|
|
9929
|
-
console.log(
|
|
9930
|
-
console.log(
|
|
9931
|
-
console.log(
|
|
10035
|
+
console.log(chalk19.blue("\n" + "\u2500".repeat(52)));
|
|
10036
|
+
console.log(chalk19.bold(" ai-spec \u2014 AI-driven Development Orchestrator"));
|
|
10037
|
+
console.log(chalk19.blue("\u2500".repeat(52)));
|
|
10038
|
+
console.log(chalk19.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
9932
10039
|
console.log(
|
|
9933
|
-
|
|
10040
|
+
chalk19.gray(
|
|
9934
10041
|
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
9935
10042
|
)
|
|
9936
10043
|
);
|
|
9937
|
-
console.log(
|
|
10044
|
+
console.log(chalk19.blue("\u2500".repeat(52) + "\n"));
|
|
9938
10045
|
}
|
|
9939
10046
|
var program = new Command();
|
|
9940
10047
|
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version("0.14.1");
|
|
@@ -9961,42 +10068,42 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
9961
10068
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
9962
10069
|
const workspaceConfig = await workspaceLoader.load();
|
|
9963
10070
|
if (workspaceConfig) {
|
|
9964
|
-
console.log(
|
|
10071
|
+
console.log(chalk19.cyan(`
|
|
9965
10072
|
[Workspace] Detected workspace: ${workspaceConfig.name}`));
|
|
9966
|
-
console.log(
|
|
10073
|
+
console.log(chalk19.gray(` Repos: ${workspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
9967
10074
|
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
9968
10075
|
if (opts.serve) {
|
|
9969
|
-
console.log(
|
|
10076
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Auto-serve: starting mock server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
9970
10077
|
const backendResult = pipelineResults.find((r) => r.role === "backend" && r.status === "success" && r.dsl);
|
|
9971
10078
|
const frontendResult = pipelineResults.find((r) => (r.role === "frontend" || r.role === "mobile") && r.status === "success");
|
|
9972
10079
|
if (!backendResult) {
|
|
9973
|
-
console.log(
|
|
10080
|
+
console.log(chalk19.yellow(" No successful backend with DSL found \u2014 skipping auto-serve."));
|
|
9974
10081
|
} else {
|
|
9975
10082
|
const mockPort = 3001;
|
|
9976
10083
|
const mockResult = await generateMockAssets(backendResult.dsl, backendResult.repoAbsPath, { port: mockPort });
|
|
9977
10084
|
const serverJsPath = path22.join(backendResult.repoAbsPath, "mock", "server.js");
|
|
9978
|
-
console.log(
|
|
10085
|
+
console.log(chalk19.green(` \u2714 Mock assets generated (${mockResult.files.length} file(s))`));
|
|
9979
10086
|
const pid = startMockServerBackground(serverJsPath, mockPort);
|
|
9980
|
-
console.log(
|
|
10087
|
+
console.log(chalk19.green(` \u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${mockPort}`));
|
|
9981
10088
|
if (frontendResult) {
|
|
9982
10089
|
const proxyResult = await applyMockProxy(frontendResult.repoAbsPath, mockPort, backendResult.dsl.endpoints);
|
|
9983
10090
|
await saveMockServerPid(frontendResult.repoAbsPath, pid);
|
|
9984
10091
|
if (proxyResult.applied) {
|
|
9985
|
-
console.log(
|
|
9986
|
-
console.log(
|
|
10092
|
+
console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
10093
|
+
console.log(chalk19.bold.cyan(`
|
|
9987
10094
|
Ready! Run your frontend dev server:`));
|
|
9988
|
-
console.log(
|
|
9989
|
-
console.log(
|
|
9990
|
-
console.log(
|
|
10095
|
+
console.log(chalk19.white(` cd ${frontendResult.repoAbsPath}`));
|
|
10096
|
+
console.log(chalk19.white(` ${proxyResult.devCommand}`));
|
|
10097
|
+
console.log(chalk19.gray(`
|
|
9991
10098
|
When done, restore: ai-spec mock --restore --frontend ${frontendResult.repoAbsPath}`));
|
|
9992
10099
|
} else {
|
|
9993
|
-
console.log(
|
|
9994
|
-
if (proxyResult.note) console.log(
|
|
9995
|
-
console.log(
|
|
10100
|
+
console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
10101
|
+
if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
|
|
10102
|
+
console.log(chalk19.gray(` Mock server: http://localhost:${mockPort}`));
|
|
9996
10103
|
}
|
|
9997
10104
|
} else {
|
|
9998
|
-
console.log(
|
|
9999
|
-
console.log(
|
|
10105
|
+
console.log(chalk19.gray(` No frontend repo found \u2014 mock server is running at http://localhost:${mockPort}`));
|
|
10106
|
+
console.log(chalk19.gray(` Configure your frontend proxy manually to point to http://localhost:${mockPort}`));
|
|
10000
10107
|
}
|
|
10001
10108
|
}
|
|
10002
10109
|
}
|
|
@@ -10017,7 +10124,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10017
10124
|
codegenModel: codegenModelName
|
|
10018
10125
|
});
|
|
10019
10126
|
const runId = generateRunId();
|
|
10020
|
-
console.log(
|
|
10127
|
+
console.log(chalk19.gray(` Run ID: ${runId}`));
|
|
10021
10128
|
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
10022
10129
|
setActiveSnapshot(runSnapshot);
|
|
10023
10130
|
const runLogger = new RunLogger(currentDir, runId, {
|
|
@@ -10025,25 +10132,27 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10025
10132
|
model: specModelName
|
|
10026
10133
|
});
|
|
10027
10134
|
setActiveLogger(runLogger);
|
|
10028
|
-
|
|
10135
|
+
const promptHash = computePromptHash();
|
|
10136
|
+
runLogger.setPromptHash(promptHash);
|
|
10137
|
+
console.log(chalk19.blue("[1/6] Loading project context..."));
|
|
10029
10138
|
runLogger.stageStart("context_load");
|
|
10030
10139
|
const loader = new ContextLoader(currentDir);
|
|
10031
10140
|
const context = await loader.loadProjectContext();
|
|
10032
10141
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10033
10142
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10034
|
-
console.log(
|
|
10035
|
-
console.log(
|
|
10036
|
-
console.log(
|
|
10143
|
+
console.log(chalk19.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10144
|
+
console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10145
|
+
console.log(chalk19.gray(` API files : ${context.apiStructure.length} files`));
|
|
10037
10146
|
if (context.schema) {
|
|
10038
|
-
console.log(
|
|
10147
|
+
console.log(chalk19.gray(` Prisma schema: found`));
|
|
10039
10148
|
}
|
|
10040
10149
|
if (context.constitution) {
|
|
10041
|
-
console.log(
|
|
10150
|
+
console.log(chalk19.green(` Constitution : found (.ai-spec-constitution.md)`));
|
|
10042
10151
|
if (context.constitution.length > 6e3) {
|
|
10043
|
-
console.log(
|
|
10152
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10044
10153
|
}
|
|
10045
10154
|
} else {
|
|
10046
|
-
console.log(
|
|
10155
|
+
console.log(chalk19.yellow(" Constitution : not found \u2014 auto-generating..."));
|
|
10047
10156
|
try {
|
|
10048
10157
|
const constitutionGen = new ConstitutionGenerator(
|
|
10049
10158
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -10051,12 +10160,12 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10051
10160
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
10052
10161
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
10053
10162
|
context.constitution = constitutionContent;
|
|
10054
|
-
console.log(
|
|
10163
|
+
console.log(chalk19.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
|
|
10055
10164
|
} catch (err) {
|
|
10056
|
-
console.log(
|
|
10165
|
+
console.log(chalk19.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
|
|
10057
10166
|
}
|
|
10058
10167
|
}
|
|
10059
|
-
console.log(
|
|
10168
|
+
console.log(chalk19.blue(`
|
|
10060
10169
|
[2/6] Generating spec with ${specProviderName}/${specModelName}...`));
|
|
10061
10170
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10062
10171
|
let initialSpec;
|
|
@@ -10066,30 +10175,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10066
10175
|
if (opts.skipTasks) {
|
|
10067
10176
|
const generator = new SpecGenerator(specProvider);
|
|
10068
10177
|
initialSpec = await generator.generateSpec(idea, context);
|
|
10069
|
-
console.log(
|
|
10178
|
+
console.log(chalk19.green(" \u2714 Spec generated."));
|
|
10070
10179
|
} else {
|
|
10071
10180
|
const result = await generateSpecWithTasks(specProvider, idea, context);
|
|
10072
10181
|
initialSpec = result.spec;
|
|
10073
10182
|
initialTasks = result.tasks;
|
|
10074
|
-
console.log(
|
|
10183
|
+
console.log(chalk19.green(` \u2714 Spec generated.`));
|
|
10075
10184
|
if (initialTasks.length > 0) {
|
|
10076
|
-
console.log(
|
|
10185
|
+
console.log(chalk19.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
10077
10186
|
} else {
|
|
10078
|
-
console.log(
|
|
10187
|
+
console.log(chalk19.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10079
10188
|
}
|
|
10080
10189
|
}
|
|
10081
10190
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10082
10191
|
} catch (err) {
|
|
10083
10192
|
runLogger.stageFail("spec_gen", err.message);
|
|
10084
|
-
console.error(
|
|
10193
|
+
console.error(chalk19.red(" \u2718 Spec generation failed:"), err);
|
|
10085
10194
|
process.exit(1);
|
|
10086
10195
|
}
|
|
10087
10196
|
let finalSpec;
|
|
10088
10197
|
if (opts.fast) {
|
|
10089
|
-
console.log(
|
|
10198
|
+
console.log(chalk19.gray("\n[3/6] Skipping refinement (--fast)."));
|
|
10090
10199
|
finalSpec = initialSpec;
|
|
10091
10200
|
} else {
|
|
10092
|
-
console.log(
|
|
10201
|
+
console.log(chalk19.blue("\n[3/6] Interactive spec refinement..."));
|
|
10093
10202
|
runLogger.stageStart("spec_refine");
|
|
10094
10203
|
const refiner = new SpecRefiner(specProvider);
|
|
10095
10204
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
@@ -10100,7 +10209,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10100
10209
|
const shouldRunAssessment = !opts.skipAssessment && (!opts.auto || minScore > 0);
|
|
10101
10210
|
if (shouldRunAssessment) {
|
|
10102
10211
|
if (!opts.auto) {
|
|
10103
|
-
console.log(
|
|
10212
|
+
console.log(chalk19.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10104
10213
|
}
|
|
10105
10214
|
runLogger.stageStart("spec_assess");
|
|
10106
10215
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
@@ -10109,45 +10218,45 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10109
10218
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10110
10219
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10111
10220
|
if (opts.force) {
|
|
10112
|
-
console.log(
|
|
10221
|
+
console.log(chalk19.yellow(`
|
|
10113
10222
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10114
10223
|
} else {
|
|
10115
10224
|
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10116
|
-
console.log(
|
|
10225
|
+
console.log(chalk19.red(`
|
|
10117
10226
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10118
10227
|
if (!opts.auto) {
|
|
10119
|
-
console.log(
|
|
10228
|
+
console.log(chalk19.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
10120
10229
|
} else {
|
|
10121
|
-
console.log(
|
|
10230
|
+
console.log(chalk19.gray(` Auto mode: gate enforced. Fix the spec or lower minSpecScore, or use --force to bypass.`));
|
|
10122
10231
|
}
|
|
10123
|
-
console.log(
|
|
10232
|
+
console.log(chalk19.gray(` Gate threshold set in .ai-spec.json \u2192 "minSpecScore": ${minScore}`));
|
|
10124
10233
|
process.exit(1);
|
|
10125
10234
|
}
|
|
10126
10235
|
}
|
|
10127
10236
|
} else {
|
|
10128
10237
|
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10129
10238
|
if (!opts.auto) {
|
|
10130
|
-
console.log(
|
|
10239
|
+
console.log(chalk19.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10131
10240
|
}
|
|
10132
10241
|
}
|
|
10133
10242
|
}
|
|
10134
10243
|
if (!opts.auto) {
|
|
10135
|
-
console.log(
|
|
10244
|
+
console.log(chalk19.blue("\n[3.5/6] Approval Gate \u2014 review before code generation"));
|
|
10136
10245
|
const specLines = finalSpec.split("\n").length;
|
|
10137
10246
|
const specWords = finalSpec.split(/\s+/).length;
|
|
10138
10247
|
const taskCountHint = initialTasks.length > 0 ? ` Tasks generated : ${initialTasks.length}` : "";
|
|
10139
|
-
console.log(
|
|
10140
|
-
if (taskCountHint) console.log(
|
|
10248
|
+
console.log(chalk19.gray(` Spec length : ${specLines} lines / ${specWords} words`));
|
|
10249
|
+
if (taskCountHint) console.log(chalk19.gray(taskCountHint));
|
|
10141
10250
|
const previewSpecsDir = path22.join(currentDir, "specs");
|
|
10142
10251
|
const slug = featureSlug;
|
|
10143
10252
|
const prevVersion = await findLatestVersion(previewSpecsDir, slug);
|
|
10144
10253
|
if (prevVersion) {
|
|
10145
|
-
console.log(
|
|
10254
|
+
console.log(chalk19.gray(` Previous version: v${prevVersion.version} (${prevVersion.filePath})`));
|
|
10146
10255
|
const diff = computeDiff(prevVersion.content, finalSpec);
|
|
10147
|
-
console.log(
|
|
10256
|
+
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"));
|
|
10148
10257
|
printDiffSummary(diff, `v${prevVersion.version} \u2192 v${prevVersion.version + 1}`);
|
|
10149
10258
|
printDiff(diff);
|
|
10150
|
-
console.log(
|
|
10259
|
+
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"));
|
|
10151
10260
|
}
|
|
10152
10261
|
const gate = await select3({
|
|
10153
10262
|
message: "Ready to proceed to code generation?",
|
|
@@ -10158,9 +10267,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10158
10267
|
]
|
|
10159
10268
|
});
|
|
10160
10269
|
if (gate === "view") {
|
|
10161
|
-
console.log(
|
|
10270
|
+
console.log(chalk19.cyan("\n" + "\u2500".repeat(52)));
|
|
10162
10271
|
console.log(finalSpec);
|
|
10163
|
-
console.log(
|
|
10272
|
+
console.log(chalk19.cyan("\u2500".repeat(52) + "\n"));
|
|
10164
10273
|
const confirm22 = await select3({
|
|
10165
10274
|
message: "Proceed to code generation?",
|
|
10166
10275
|
choices: [
|
|
@@ -10169,92 +10278,92 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10169
10278
|
]
|
|
10170
10279
|
});
|
|
10171
10280
|
if (confirm22 === "abort") {
|
|
10172
|
-
console.log(
|
|
10281
|
+
console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
|
|
10173
10282
|
process.exit(0);
|
|
10174
10283
|
}
|
|
10175
10284
|
} else if (gate === "abort") {
|
|
10176
|
-
console.log(
|
|
10285
|
+
console.log(chalk19.yellow(" Aborted. Spec was NOT saved."));
|
|
10177
10286
|
process.exit(0);
|
|
10178
10287
|
}
|
|
10179
|
-
console.log(
|
|
10288
|
+
console.log(chalk19.green(" \u2714 Approved \u2014 continuing to code generation."));
|
|
10180
10289
|
} else {
|
|
10181
|
-
console.log(
|
|
10290
|
+
console.log(chalk19.gray("[3.5/6] Approval Gate: skipped (--auto)."));
|
|
10182
10291
|
}
|
|
10183
10292
|
let extractedDsl = null;
|
|
10184
10293
|
if (opts.skipDsl) {
|
|
10185
|
-
console.log(
|
|
10294
|
+
console.log(chalk19.gray("\n[DSL] Skipped (--skip-dsl)."));
|
|
10186
10295
|
} else {
|
|
10187
|
-
console.log(
|
|
10188
|
-
console.log(
|
|
10296
|
+
console.log(chalk19.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10297
|
+
console.log(chalk19.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10189
10298
|
runLogger.stageStart("dsl_extract");
|
|
10190
10299
|
try {
|
|
10191
10300
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10192
|
-
if (isFrontend) console.log(
|
|
10301
|
+
if (isFrontend) console.log(chalk19.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10193
10302
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10194
10303
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10195
10304
|
if (extractedDsl) {
|
|
10196
10305
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10197
|
-
console.log(
|
|
10306
|
+
console.log(chalk19.green(" \u2714 DSL extracted and validated."));
|
|
10198
10307
|
} else {
|
|
10199
10308
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10200
|
-
console.log(
|
|
10309
|
+
console.log(chalk19.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10201
10310
|
}
|
|
10202
10311
|
} catch (err) {
|
|
10203
10312
|
runLogger.stageFail("dsl_extract", err.message);
|
|
10204
|
-
console.log(
|
|
10313
|
+
console.log(chalk19.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10205
10314
|
}
|
|
10206
10315
|
}
|
|
10207
10316
|
const isFrontendProject2 = isFrontendDeps(context.dependencies ?? []);
|
|
10208
10317
|
const skipWorktree = opts.worktree ? false : opts.skipWorktree || isFrontendProject2;
|
|
10209
10318
|
let workingDir = currentDir;
|
|
10210
10319
|
if (!skipWorktree) {
|
|
10211
|
-
console.log(
|
|
10320
|
+
console.log(chalk19.blue("\n[4/6] Setting up git worktree..."));
|
|
10212
10321
|
const worktreeManager = new GitWorktreeManager(currentDir);
|
|
10213
10322
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10214
10323
|
if (worktreePath) workingDir = worktreePath;
|
|
10215
10324
|
} else {
|
|
10216
10325
|
const reason = opts.worktree ? "" : isFrontendProject2 ? " (frontend project \u2014 use --worktree to override)" : " (--skip-worktree)";
|
|
10217
|
-
console.log(
|
|
10326
|
+
console.log(chalk19.gray(`[4/6] Skipping worktree${reason}.`));
|
|
10218
10327
|
}
|
|
10219
10328
|
const specsDir = path22.join(workingDir, "specs");
|
|
10220
10329
|
await fs23.ensureDir(specsDir);
|
|
10221
10330
|
const { filePath: specFile, version: specVersion } = await nextVersionPath(specsDir, featureSlug);
|
|
10222
10331
|
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10223
|
-
console.log(
|
|
10224
|
-
[5/6] \u2714 Spec saved: ${specFile}`) +
|
|
10332
|
+
console.log(chalk19.green(`
|
|
10333
|
+
[5/6] \u2714 Spec saved: ${specFile}`) + chalk19.gray(` (v${specVersion})`));
|
|
10225
10334
|
let savedDslFile = null;
|
|
10226
10335
|
if (extractedDsl) {
|
|
10227
10336
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10228
10337
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
10229
|
-
console.log(
|
|
10338
|
+
console.log(chalk19.green(` \u2714 DSL saved : ${savedDslFile}`));
|
|
10230
10339
|
}
|
|
10231
10340
|
if (!opts.skipTasks) {
|
|
10232
10341
|
const taskGen = new TaskGenerator(specProvider);
|
|
10233
10342
|
let tasksToSave = initialTasks;
|
|
10234
10343
|
if (tasksToSave.length === 0) {
|
|
10235
|
-
console.log(
|
|
10344
|
+
console.log(chalk19.blue(`
|
|
10236
10345
|
Generating tasks (separate call)...`));
|
|
10237
10346
|
try {
|
|
10238
10347
|
tasksToSave = await taskGen.generateTasks(finalSpec, context);
|
|
10239
10348
|
} catch (err) {
|
|
10240
|
-
console.log(
|
|
10349
|
+
console.log(chalk19.yellow(` \u26A0 Task generation failed: ${err.message}`));
|
|
10241
10350
|
}
|
|
10242
10351
|
}
|
|
10243
10352
|
if (tasksToSave.length > 0) {
|
|
10244
10353
|
const sorted = taskGen.sortByLayer(tasksToSave);
|
|
10245
10354
|
const tasksFile = await taskGen.saveTasks(sorted, specFile);
|
|
10246
10355
|
printTasks(sorted);
|
|
10247
|
-
console.log(
|
|
10356
|
+
console.log(chalk19.green(` \u2714 Tasks saved: ${tasksFile}`));
|
|
10248
10357
|
} else {
|
|
10249
|
-
console.log(
|
|
10358
|
+
console.log(chalk19.yellow(" \u26A0 No tasks generated \u2014 code generation will use fallback file planning."));
|
|
10250
10359
|
}
|
|
10251
10360
|
}
|
|
10252
|
-
console.log(
|
|
10361
|
+
console.log(chalk19.blue(`
|
|
10253
10362
|
[6/6] Code generation (mode: ${codegenMode})...`));
|
|
10254
10363
|
const codegenProvider = codegenProviderName === specProviderName && codegenApiKey === specApiKey ? specProvider : createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
10255
10364
|
let generatedTestFiles = [];
|
|
10256
10365
|
if (opts.tdd && extractedDsl) {
|
|
10257
|
-
console.log(
|
|
10366
|
+
console.log(chalk19.cyan("\n[TDD] Generating pre-implementation tests (will fail until code is written)..."));
|
|
10258
10367
|
const testGen = new TestGenerator(codegenProvider);
|
|
10259
10368
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10260
10369
|
}
|
|
@@ -10268,27 +10377,29 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10268
10377
|
});
|
|
10269
10378
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10270
10379
|
if (opts.tdd) {
|
|
10271
|
-
console.log(
|
|
10380
|
+
console.log(chalk19.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10272
10381
|
} else if (opts.skipTests) {
|
|
10273
|
-
console.log(
|
|
10382
|
+
console.log(chalk19.gray("\n[7/9] Skipping test generation (--skip-tests)."));
|
|
10274
10383
|
} else if (!extractedDsl) {
|
|
10275
|
-
console.log(
|
|
10384
|
+
console.log(chalk19.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
10276
10385
|
} else {
|
|
10277
|
-
console.log(
|
|
10386
|
+
console.log(chalk19.blue(`
|
|
10278
10387
|
[7/9] Test skeleton generation...`));
|
|
10279
10388
|
runLogger.stageStart("test_gen");
|
|
10280
10389
|
const testGen = new TestGenerator(codegenProvider);
|
|
10281
10390
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10282
10391
|
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10283
10392
|
}
|
|
10393
|
+
let compilePassed = false;
|
|
10284
10394
|
if (opts.skipErrorFeedback) {
|
|
10285
|
-
console.log(
|
|
10395
|
+
console.log(chalk19.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
10396
|
+
compilePassed = true;
|
|
10286
10397
|
} else {
|
|
10287
10398
|
if (opts.tdd) {
|
|
10288
|
-
console.log(
|
|
10399
|
+
console.log(chalk19.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10289
10400
|
}
|
|
10290
10401
|
runLogger.stageStart("error_feedback");
|
|
10291
|
-
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10402
|
+
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10292
10403
|
maxCycles: opts.tdd ? 3 : 2
|
|
10293
10404
|
// TDD gets one extra cycle
|
|
10294
10405
|
});
|
|
@@ -10296,7 +10407,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10296
10407
|
}
|
|
10297
10408
|
let reviewResult = "";
|
|
10298
10409
|
if (!opts.skipReview) {
|
|
10299
|
-
console.log(
|
|
10410
|
+
console.log(chalk19.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10300
10411
|
runLogger.stageStart("review");
|
|
10301
10412
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10302
10413
|
const savedSpec = await fs23.readFile(specFile, "utf-8");
|
|
@@ -10314,20 +10425,30 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10314
10425
|
runLogger.stageEnd("review");
|
|
10315
10426
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10316
10427
|
}
|
|
10428
|
+
runLogger.stageStart("self_eval");
|
|
10429
|
+
const selfEvalResult = runSelfEval({
|
|
10430
|
+
dsl: extractedDsl,
|
|
10431
|
+
generatedFiles,
|
|
10432
|
+
compilePassed,
|
|
10433
|
+
reviewText: reviewResult,
|
|
10434
|
+
promptHash,
|
|
10435
|
+
logger: runLogger
|
|
10436
|
+
});
|
|
10437
|
+
printSelfEval(selfEvalResult);
|
|
10317
10438
|
runLogger.finish();
|
|
10318
|
-
console.log(
|
|
10319
|
-
console.log(
|
|
10320
|
-
if (savedDslFile) console.log(
|
|
10439
|
+
console.log(chalk19.bold.green("\n\u2714 All done!"));
|
|
10440
|
+
console.log(chalk19.gray(` Spec : ${specFile}`));
|
|
10441
|
+
if (savedDslFile) console.log(chalk19.gray(` DSL : ${savedDslFile}`));
|
|
10321
10442
|
if (generatedTestFiles.length > 0) {
|
|
10322
|
-
console.log(
|
|
10443
|
+
console.log(chalk19.gray(` Tests : ${generatedTestFiles.length} skeleton file(s) generated`));
|
|
10323
10444
|
}
|
|
10324
|
-
console.log(
|
|
10445
|
+
console.log(chalk19.gray(` Working dir : ${workingDir}`));
|
|
10325
10446
|
if (workingDir !== currentDir) {
|
|
10326
|
-
console.log(
|
|
10447
|
+
console.log(chalk19.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
10327
10448
|
}
|
|
10328
10449
|
runLogger.printSummary();
|
|
10329
10450
|
if (runSnapshot.fileCount > 0) {
|
|
10330
|
-
console.log(
|
|
10451
|
+
console.log(chalk19.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
10331
10452
|
}
|
|
10332
10453
|
});
|
|
10333
10454
|
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(
|
|
@@ -10347,7 +10468,7 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10347
10468
|
if (specFile && await fs23.pathExists(specFile)) {
|
|
10348
10469
|
specContent = await fs23.readFile(specFile, "utf-8");
|
|
10349
10470
|
resolvedSpecFile = specFile;
|
|
10350
|
-
console.log(
|
|
10471
|
+
console.log(chalk19.gray(`Using spec: ${specFile}`));
|
|
10351
10472
|
} else {
|
|
10352
10473
|
const specsDir = path22.join(currentDir, "specs");
|
|
10353
10474
|
if (await fs23.pathExists(specsDir)) {
|
|
@@ -10356,12 +10477,12 @@ program.command("review").description("Run AI code review on current git diff ag
|
|
|
10356
10477
|
const latest = path22.join(specsDir, files[0]);
|
|
10357
10478
|
specContent = await fs23.readFile(latest, "utf-8");
|
|
10358
10479
|
resolvedSpecFile = latest;
|
|
10359
|
-
console.log(
|
|
10480
|
+
console.log(chalk19.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
10360
10481
|
}
|
|
10361
10482
|
}
|
|
10362
10483
|
}
|
|
10363
10484
|
if (!specContent) {
|
|
10364
|
-
console.log(
|
|
10485
|
+
console.log(chalk19.yellow("No spec file found. Running review without spec context."));
|
|
10365
10486
|
}
|
|
10366
10487
|
await reviewer.reviewCode(specContent, resolvedSpecFile);
|
|
10367
10488
|
await reviewer.printScoreTrend();
|
|
@@ -10388,15 +10509,15 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10388
10509
|
auto: opts.auto
|
|
10389
10510
|
});
|
|
10390
10511
|
if (result.written) {
|
|
10391
|
-
console.log(
|
|
10392
|
-
console.log(
|
|
10393
|
-
console.log(
|
|
10512
|
+
console.log(chalk19.blue("\n Summary:"));
|
|
10513
|
+
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)})`));
|
|
10514
|
+
console.log(chalk19.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
10394
10515
|
if (result.backupPath) {
|
|
10395
|
-
console.log(
|
|
10516
|
+
console.log(chalk19.gray(` Backup: ${path22.basename(result.backupPath)}`));
|
|
10396
10517
|
}
|
|
10397
10518
|
}
|
|
10398
10519
|
} catch (err) {
|
|
10399
|
-
console.error(
|
|
10520
|
+
console.error(chalk19.red(` \u2718 Consolidation failed: ${err.message}`));
|
|
10400
10521
|
process.exit(1);
|
|
10401
10522
|
}
|
|
10402
10523
|
return;
|
|
@@ -10404,14 +10525,14 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10404
10525
|
if (opts.global) {
|
|
10405
10526
|
const existing = await loadGlobalConstitution([currentDir]);
|
|
10406
10527
|
if (existing && !opts.force) {
|
|
10407
|
-
console.log(
|
|
10528
|
+
console.log(chalk19.yellow(`
|
|
10408
10529
|
Global constitution already exists at: ${existing.source}`));
|
|
10409
|
-
console.log(
|
|
10530
|
+
console.log(chalk19.gray(" Use --force to overwrite it."));
|
|
10410
10531
|
return;
|
|
10411
10532
|
}
|
|
10412
|
-
console.log(
|
|
10413
|
-
console.log(
|
|
10414
|
-
console.log(
|
|
10533
|
+
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"));
|
|
10534
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
10535
|
+
console.log(chalk19.gray(" Scanning repos in workspace..."));
|
|
10415
10536
|
const loader = new ContextLoader(currentDir);
|
|
10416
10537
|
const ctx = await loader.loadProjectContext();
|
|
10417
10538
|
const summary = [
|
|
@@ -10423,56 +10544,56 @@ program.command("init").description(`Analyze codebase and generate Project Const
|
|
|
10423
10544
|
try {
|
|
10424
10545
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
10425
10546
|
} catch (err) {
|
|
10426
|
-
console.error(
|
|
10547
|
+
console.error(chalk19.red(" \u2718 Failed to generate global constitution:"), err);
|
|
10427
10548
|
process.exit(1);
|
|
10428
10549
|
}
|
|
10429
10550
|
const saved2 = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
10430
|
-
console.log(
|
|
10551
|
+
console.log(chalk19.green(`
|
|
10431
10552
|
\u2714 Global constitution saved: ${saved2}`));
|
|
10432
|
-
console.log(
|
|
10433
|
-
console.log(
|
|
10434
|
-
console.log(
|
|
10435
|
-
console.log(
|
|
10553
|
+
console.log(chalk19.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
10554
|
+
console.log(chalk19.gray(" Project-level rules always override global rules.\n"));
|
|
10555
|
+
console.log(chalk19.bold(" Preview:"));
|
|
10556
|
+
console.log(chalk19.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
10436
10557
|
if (globalConstitution.split("\n").length > 12) {
|
|
10437
|
-
console.log(
|
|
10558
|
+
console.log(chalk19.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
10438
10559
|
}
|
|
10439
10560
|
return;
|
|
10440
10561
|
}
|
|
10441
10562
|
const constitutionPath = path22.join(currentDir, CONSTITUTION_FILE);
|
|
10442
10563
|
if (!opts.force && await fs23.pathExists(constitutionPath)) {
|
|
10443
|
-
console.log(
|
|
10564
|
+
console.log(chalk19.yellow(`
|
|
10444
10565
|
${CONSTITUTION_FILE} already exists.`));
|
|
10445
|
-
console.log(
|
|
10446
|
-
console.log(
|
|
10566
|
+
console.log(chalk19.gray(" Use --force to overwrite it."));
|
|
10567
|
+
console.log(chalk19.gray(` Or edit it directly: ${constitutionPath}`));
|
|
10447
10568
|
return;
|
|
10448
10569
|
}
|
|
10449
|
-
console.log(
|
|
10450
|
-
console.log(
|
|
10451
|
-
console.log(
|
|
10570
|
+
console.log(chalk19.blue("\n\u2500\u2500\u2500 Generating Project Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10571
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
10572
|
+
console.log(chalk19.gray(" Analyzing codebase..."));
|
|
10452
10573
|
const generator = new ConstitutionGenerator(provider);
|
|
10453
10574
|
let constitution;
|
|
10454
10575
|
try {
|
|
10455
10576
|
constitution = await generator.generate(currentDir);
|
|
10456
10577
|
} catch (err) {
|
|
10457
|
-
console.error(
|
|
10578
|
+
console.error(chalk19.red(" \u2718 Failed to generate constitution:"), err);
|
|
10458
10579
|
process.exit(1);
|
|
10459
10580
|
}
|
|
10460
10581
|
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
10461
10582
|
const globalResult = await loadGlobalConstitution([path22.dirname(currentDir)]);
|
|
10462
10583
|
if (globalResult) {
|
|
10463
|
-
console.log(
|
|
10584
|
+
console.log(chalk19.cyan(`
|
|
10464
10585
|
\u2139 Global constitution detected: ${globalResult.source}`));
|
|
10465
|
-
console.log(
|
|
10466
|
-
console.log(
|
|
10586
|
+
console.log(chalk19.gray(" It will be merged with this project constitution at runtime."));
|
|
10587
|
+
console.log(chalk19.gray(" Project rules take priority over global rules."));
|
|
10467
10588
|
}
|
|
10468
|
-
console.log(
|
|
10589
|
+
console.log(chalk19.green(`
|
|
10469
10590
|
\u2714 Constitution saved: ${saved}`));
|
|
10470
|
-
console.log(
|
|
10471
|
-
console.log(
|
|
10472
|
-
console.log(
|
|
10473
|
-
console.log(
|
|
10591
|
+
console.log(chalk19.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
10592
|
+
console.log(chalk19.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
10593
|
+
console.log(chalk19.bold(" Preview:"));
|
|
10594
|
+
console.log(chalk19.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
10474
10595
|
if (constitution.split("\n").length > 15) {
|
|
10475
|
-
console.log(
|
|
10596
|
+
console.log(chalk19.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
10476
10597
|
}
|
|
10477
10598
|
});
|
|
10478
10599
|
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(
|
|
@@ -10483,41 +10604,41 @@ program.command("config").description(`Set default configuration for this projec
|
|
|
10483
10604
|
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10484
10605
|
if (opts.clearKeys) {
|
|
10485
10606
|
await clearAllKeys();
|
|
10486
|
-
console.log(
|
|
10607
|
+
console.log(chalk19.green(`\u2714 All saved API keys cleared.`));
|
|
10487
10608
|
return;
|
|
10488
10609
|
}
|
|
10489
10610
|
if (opts.clearKey) {
|
|
10490
10611
|
await clearKey(opts.clearKey);
|
|
10491
|
-
console.log(
|
|
10612
|
+
console.log(chalk19.green(`\u2714 Saved key for "${opts.clearKey}" removed.`));
|
|
10492
10613
|
return;
|
|
10493
10614
|
}
|
|
10494
10615
|
if (opts.listKeys) {
|
|
10495
10616
|
const store = await fs23.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
10496
10617
|
const providers = Object.keys(store);
|
|
10497
10618
|
if (providers.length === 0) {
|
|
10498
|
-
console.log(
|
|
10619
|
+
console.log(chalk19.gray("No saved API keys."));
|
|
10499
10620
|
} else {
|
|
10500
|
-
console.log(
|
|
10621
|
+
console.log(chalk19.bold("Saved API keys:"));
|
|
10501
10622
|
for (const p of providers) {
|
|
10502
10623
|
const k2 = store[p];
|
|
10503
|
-
console.log(
|
|
10624
|
+
console.log(chalk19.gray(` ${p}: ${k2.slice(0, 6)}...${k2.slice(-4)}`));
|
|
10504
10625
|
}
|
|
10505
|
-
console.log(
|
|
10626
|
+
console.log(chalk19.gray(`
|
|
10506
10627
|
File: ${KEY_STORE_FILE}`));
|
|
10507
10628
|
}
|
|
10508
10629
|
return;
|
|
10509
10630
|
}
|
|
10510
10631
|
if (opts.reset) {
|
|
10511
10632
|
await fs23.writeJson(configPath, {}, { spaces: 2 });
|
|
10512
|
-
console.log(
|
|
10633
|
+
console.log(chalk19.green(`\u2714 Config reset: ${configPath}`));
|
|
10513
10634
|
return;
|
|
10514
10635
|
}
|
|
10515
10636
|
const existing = await loadConfig(currentDir);
|
|
10516
10637
|
if (opts.show) {
|
|
10517
10638
|
if (Object.keys(existing).length === 0) {
|
|
10518
|
-
console.log(
|
|
10639
|
+
console.log(chalk19.gray("No config file found. Using built-in defaults."));
|
|
10519
10640
|
} else {
|
|
10520
|
-
console.log(
|
|
10641
|
+
console.log(chalk19.bold(`${configPath}:`));
|
|
10521
10642
|
console.log(JSON.stringify(existing, null, 2));
|
|
10522
10643
|
}
|
|
10523
10644
|
return;
|
|
@@ -10531,27 +10652,27 @@ File: ${KEY_STORE_FILE}`));
|
|
|
10531
10652
|
if (opts.minSpecScore !== void 0) {
|
|
10532
10653
|
const score = parseInt(opts.minSpecScore, 10);
|
|
10533
10654
|
if (isNaN(score) || score < 0 || score > 10) {
|
|
10534
|
-
console.error(
|
|
10655
|
+
console.error(chalk19.red(" --min-spec-score must be a number between 0 and 10"));
|
|
10535
10656
|
process.exit(1);
|
|
10536
10657
|
}
|
|
10537
10658
|
updated.minSpecScore = score;
|
|
10538
10659
|
}
|
|
10539
10660
|
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10540
|
-
console.log(
|
|
10661
|
+
console.log(chalk19.green(`\u2714 Config saved to ${configPath}`));
|
|
10541
10662
|
console.log(JSON.stringify(updated, null, 2));
|
|
10542
10663
|
});
|
|
10543
10664
|
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) => {
|
|
10544
10665
|
const currentDir = process.cwd();
|
|
10545
10666
|
const configPath = path22.join(currentDir, CONFIG_FILE);
|
|
10546
10667
|
if (opts.list) {
|
|
10547
|
-
console.log(
|
|
10668
|
+
console.log(chalk19.bold("\nAvailable providers & models:\n"));
|
|
10548
10669
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
10549
10670
|
console.log(
|
|
10550
|
-
` ${
|
|
10671
|
+
` ${chalk19.bold.cyan(key.padEnd(10))} ${chalk19.white(meta.displayName)}`
|
|
10551
10672
|
);
|
|
10552
|
-
console.log(
|
|
10673
|
+
console.log(chalk19.gray(` ${meta.description}`));
|
|
10553
10674
|
console.log(
|
|
10554
|
-
|
|
10675
|
+
chalk19.gray(
|
|
10555
10676
|
` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
|
|
10556
10677
|
)
|
|
10557
10678
|
);
|
|
@@ -10560,10 +10681,10 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10560
10681
|
return;
|
|
10561
10682
|
}
|
|
10562
10683
|
const existing = await loadConfig(currentDir);
|
|
10563
|
-
console.log(
|
|
10684
|
+
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"));
|
|
10564
10685
|
if (Object.keys(existing).length > 0) {
|
|
10565
10686
|
console.log(
|
|
10566
|
-
|
|
10687
|
+
chalk19.gray(
|
|
10567
10688
|
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` + (existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
10568
10689
|
)
|
|
10569
10690
|
);
|
|
@@ -10581,7 +10702,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10581
10702
|
const providerKey = await select3({
|
|
10582
10703
|
message: `${label} \u2014 select provider:`,
|
|
10583
10704
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
10584
|
-
name: `${meta2.displayName.padEnd(22)} ${
|
|
10705
|
+
name: `${meta2.displayName.padEnd(22)} ${chalk19.gray(meta2.description)}`,
|
|
10585
10706
|
value: key,
|
|
10586
10707
|
short: meta2.displayName
|
|
10587
10708
|
}))
|
|
@@ -10589,7 +10710,7 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10589
10710
|
const meta = PROVIDER_CATALOG[providerKey];
|
|
10590
10711
|
const modelChoices = [
|
|
10591
10712
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
10592
|
-
{ name:
|
|
10713
|
+
{ name: chalk19.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
10593
10714
|
];
|
|
10594
10715
|
let chosenModel = await select3({
|
|
10595
10716
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
@@ -10623,37 +10744,37 @@ program.command("model").description("Interactively switch the active AI provide
|
|
|
10623
10744
|
if (!updated.codegen || updated.codegen === "claude-code") {
|
|
10624
10745
|
updated.codegen = "api";
|
|
10625
10746
|
console.log(
|
|
10626
|
-
|
|
10747
|
+
chalk19.yellow(
|
|
10627
10748
|
`
|
|
10628
10749
|
\u26A0 provider "${effectiveCodegenProvider}" \u4E0D\u652F\u6301 "claude-code" \u6A21\u5F0F\u3002`
|
|
10629
10750
|
)
|
|
10630
10751
|
);
|
|
10631
|
-
console.log(
|
|
10752
|
+
console.log(chalk19.gray(` \u5DF2\u81EA\u52A8\u5C06 codegen \u6A21\u5F0F\u8BBE\u4E3A "api"\u3002`));
|
|
10632
10753
|
}
|
|
10633
10754
|
}
|
|
10634
10755
|
}
|
|
10635
|
-
console.log(
|
|
10636
|
-
console.log(
|
|
10756
|
+
console.log(chalk19.blue("\n Preview:"));
|
|
10757
|
+
console.log(chalk19.gray(` spec \u2192 ${updated.provider}/${updated.model}`));
|
|
10637
10758
|
if (updated.codegenProvider) {
|
|
10638
10759
|
console.log(
|
|
10639
|
-
|
|
10760
|
+
chalk19.gray(
|
|
10640
10761
|
` codegen \u2192 ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
|
|
10641
10762
|
)
|
|
10642
10763
|
);
|
|
10643
10764
|
}
|
|
10644
10765
|
const ok = await confirm2({ message: "Save to .ai-spec.json?", default: true });
|
|
10645
10766
|
if (!ok) {
|
|
10646
|
-
console.log(
|
|
10767
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
10647
10768
|
return;
|
|
10648
10769
|
}
|
|
10649
10770
|
await fs23.writeJson(configPath, updated, { spaces: 2 });
|
|
10650
|
-
console.log(
|
|
10771
|
+
console.log(chalk19.green(`
|
|
10651
10772
|
\u2714 Saved to ${configPath}`));
|
|
10652
10773
|
const providerToCheck = updated.provider ?? "gemini";
|
|
10653
10774
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
10654
10775
|
if (envKey && !process.env[envKey]) {
|
|
10655
10776
|
console.log(
|
|
10656
|
-
|
|
10777
|
+
chalk19.yellow(
|
|
10657
10778
|
` \u26A0 Remember to set ${envKey} in your environment or .env file.`
|
|
10658
10779
|
)
|
|
10659
10780
|
);
|
|
@@ -10672,29 +10793,29 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10672
10793
|
cliOpts,
|
|
10673
10794
|
contractContextSection
|
|
10674
10795
|
} = opts;
|
|
10675
|
-
console.log(
|
|
10796
|
+
console.log(chalk19.blue(`
|
|
10676
10797
|
[${repoName}] Loading project context...`));
|
|
10677
10798
|
const loader = new ContextLoader(repoAbsPath);
|
|
10678
10799
|
let context = await loader.loadProjectContext();
|
|
10679
10800
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
10680
|
-
console.log(
|
|
10681
|
-
console.log(
|
|
10801
|
+
console.log(chalk19.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10802
|
+
console.log(chalk19.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10682
10803
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
10683
|
-
console.log(
|
|
10804
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
10684
10805
|
}
|
|
10685
10806
|
if (!context.constitution) {
|
|
10686
|
-
console.log(
|
|
10807
|
+
console.log(chalk19.yellow(` Constitution: not found \u2014 auto-generating...`));
|
|
10687
10808
|
try {
|
|
10688
10809
|
const constitutionGen = new ConstitutionGenerator(specProvider);
|
|
10689
10810
|
const constitutionContent = await constitutionGen.generate(repoAbsPath);
|
|
10690
10811
|
await constitutionGen.saveConstitution(repoAbsPath, constitutionContent);
|
|
10691
10812
|
context.constitution = constitutionContent;
|
|
10692
|
-
console.log(
|
|
10813
|
+
console.log(chalk19.green(` Constitution: generated`));
|
|
10693
10814
|
} catch (err) {
|
|
10694
|
-
console.log(
|
|
10815
|
+
console.log(chalk19.yellow(` Constitution: auto-generation failed (${err.message}), continuing.`));
|
|
10695
10816
|
}
|
|
10696
10817
|
} else {
|
|
10697
|
-
console.log(
|
|
10818
|
+
console.log(chalk19.green(` Constitution: found`));
|
|
10698
10819
|
}
|
|
10699
10820
|
let fullIdea = idea;
|
|
10700
10821
|
if (contractContextSection) {
|
|
@@ -10702,58 +10823,58 @@ async function runSingleRepoPipelineInWorkspace(opts) {
|
|
|
10702
10823
|
|
|
10703
10824
|
${contractContextSection}`;
|
|
10704
10825
|
}
|
|
10705
|
-
console.log(
|
|
10826
|
+
console.log(chalk19.blue(` [${repoName}] Generating spec...`));
|
|
10706
10827
|
let finalSpec;
|
|
10707
10828
|
try {
|
|
10708
10829
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
10709
10830
|
finalSpec = result.spec;
|
|
10710
|
-
console.log(
|
|
10831
|
+
console.log(chalk19.green(` Spec generated.`));
|
|
10711
10832
|
} catch (err) {
|
|
10712
|
-
console.error(
|
|
10833
|
+
console.error(chalk19.red(` Spec generation failed: ${err.message}`));
|
|
10713
10834
|
return { dsl: null, specFile: null };
|
|
10714
10835
|
}
|
|
10715
10836
|
let extractedDsl = null;
|
|
10716
10837
|
if (!cliOpts.skipDsl) {
|
|
10717
|
-
console.log(
|
|
10838
|
+
console.log(chalk19.blue(` [${repoName}] Extracting DSL...`));
|
|
10718
10839
|
try {
|
|
10719
10840
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10720
10841
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
10721
10842
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
10722
10843
|
if (extractedDsl) {
|
|
10723
|
-
console.log(
|
|
10844
|
+
console.log(chalk19.green(` DSL extracted.`));
|
|
10724
10845
|
}
|
|
10725
10846
|
} catch (err) {
|
|
10726
|
-
console.log(
|
|
10847
|
+
console.log(chalk19.yellow(` DSL extraction failed: ${err.message}`));
|
|
10727
10848
|
}
|
|
10728
10849
|
}
|
|
10729
10850
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
10730
10851
|
const skipWorktreeForRepo = cliOpts.worktree ? false : cliOpts.skipWorktree || isFrontendRepo;
|
|
10731
10852
|
let workingDir = repoAbsPath;
|
|
10732
10853
|
if (!skipWorktreeForRepo) {
|
|
10733
|
-
console.log(
|
|
10854
|
+
console.log(chalk19.blue(` [${repoName}] Setting up git worktree...`));
|
|
10734
10855
|
try {
|
|
10735
10856
|
const worktreeManager = new GitWorktreeManager(repoAbsPath);
|
|
10736
10857
|
const worktreePath = await worktreeManager.createWorktree(idea);
|
|
10737
10858
|
if (worktreePath) workingDir = worktreePath;
|
|
10738
10859
|
} catch (err) {
|
|
10739
|
-
console.log(
|
|
10860
|
+
console.log(chalk19.yellow(` Worktree setup failed: ${err.message}. Using main branch.`));
|
|
10740
10861
|
}
|
|
10741
10862
|
} else {
|
|
10742
|
-
console.log(
|
|
10863
|
+
console.log(chalk19.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
10743
10864
|
}
|
|
10744
10865
|
const specsDir = path22.join(workingDir, "specs");
|
|
10745
10866
|
await fs23.ensureDir(specsDir);
|
|
10746
10867
|
const featureSlug = slugify(idea);
|
|
10747
10868
|
const { filePath: specFile } = await nextVersionPath(specsDir, featureSlug);
|
|
10748
10869
|
await fs23.writeFile(specFile, finalSpec, "utf-8");
|
|
10749
|
-
console.log(
|
|
10870
|
+
console.log(chalk19.green(` Spec saved: ${path22.relative(repoAbsPath, specFile)}`));
|
|
10750
10871
|
let savedDslFile = null;
|
|
10751
10872
|
if (extractedDsl) {
|
|
10752
10873
|
const dslExtractorForSave = new DslExtractor(specProvider);
|
|
10753
10874
|
savedDslFile = await dslExtractorForSave.saveDsl(extractedDsl, specFile);
|
|
10754
|
-
console.log(
|
|
10875
|
+
console.log(chalk19.green(` DSL saved: ${path22.relative(repoAbsPath, savedDslFile)}`));
|
|
10755
10876
|
}
|
|
10756
|
-
console.log(
|
|
10877
|
+
console.log(chalk19.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
10757
10878
|
try {
|
|
10758
10879
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10759
10880
|
await codegen.generateCode(specFile, workingDir, context, {
|
|
@@ -10761,29 +10882,29 @@ ${contractContextSection}`;
|
|
|
10761
10882
|
dslFilePath: savedDslFile ?? void 0,
|
|
10762
10883
|
repoType: detectedRepoType
|
|
10763
10884
|
});
|
|
10764
|
-
console.log(
|
|
10885
|
+
console.log(chalk19.green(` Code generation complete.`));
|
|
10765
10886
|
} catch (err) {
|
|
10766
|
-
console.log(
|
|
10887
|
+
console.log(chalk19.yellow(` Code generation failed: ${err.message}`));
|
|
10767
10888
|
}
|
|
10768
10889
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
10769
|
-
console.log(
|
|
10890
|
+
console.log(chalk19.blue(` [${repoName}] Generating test skeletons...`));
|
|
10770
10891
|
try {
|
|
10771
10892
|
const testGen = new TestGenerator(codegenProvider);
|
|
10772
10893
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10773
|
-
console.log(
|
|
10894
|
+
console.log(chalk19.green(` ${testFiles.length} test file(s) generated.`));
|
|
10774
10895
|
} catch (err) {
|
|
10775
|
-
console.log(
|
|
10896
|
+
console.log(chalk19.yellow(` Test generation failed: ${err.message}`));
|
|
10776
10897
|
}
|
|
10777
10898
|
}
|
|
10778
10899
|
if (!cliOpts.skipErrorFeedback) {
|
|
10779
10900
|
try {
|
|
10780
10901
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
10781
10902
|
} catch (err) {
|
|
10782
|
-
console.log(
|
|
10903
|
+
console.log(chalk19.yellow(` Error feedback failed: ${err.message}`));
|
|
10783
10904
|
}
|
|
10784
10905
|
}
|
|
10785
10906
|
if (!cliOpts.skipReview) {
|
|
10786
|
-
console.log(
|
|
10907
|
+
console.log(chalk19.blue(` [${repoName}] Running code review...`));
|
|
10787
10908
|
try {
|
|
10788
10909
|
const reviewer = new CodeReviewer(specProvider);
|
|
10789
10910
|
const originalDir = process.cwd();
|
|
@@ -10795,9 +10916,9 @@ ${contractContextSection}`;
|
|
|
10795
10916
|
process.chdir(originalDir);
|
|
10796
10917
|
}
|
|
10797
10918
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
10798
|
-
console.log(
|
|
10919
|
+
console.log(chalk19.green(` Code review complete.`));
|
|
10799
10920
|
} catch (err) {
|
|
10800
|
-
console.log(
|
|
10921
|
+
console.log(chalk19.yellow(` Code review failed: ${err.message}`));
|
|
10801
10922
|
}
|
|
10802
10923
|
}
|
|
10803
10924
|
return { dsl: extractedDsl, specFile };
|
|
@@ -10820,7 +10941,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10820
10941
|
codegenModel: codegenModelName
|
|
10821
10942
|
});
|
|
10822
10943
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
10823
|
-
console.log(
|
|
10944
|
+
console.log(chalk19.blue("\n[W1] Loading per-repo contexts..."));
|
|
10824
10945
|
const contexts = /* @__PURE__ */ new Map();
|
|
10825
10946
|
const frontendContexts = /* @__PURE__ */ new Map();
|
|
10826
10947
|
for (const repo of workspace.repos) {
|
|
@@ -10832,27 +10953,27 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10832
10953
|
if (repo.role === "frontend" || repo.role === "mobile") {
|
|
10833
10954
|
const fctx = await loadFrontendContext(repoAbsPath);
|
|
10834
10955
|
frontendContexts.set(repo.name, fctx);
|
|
10835
|
-
console.log(
|
|
10956
|
+
console.log(chalk19.gray(` ${repo.name}: ${fctx.framework} / ${fctx.httpClient} / hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
10836
10957
|
} else {
|
|
10837
|
-
console.log(
|
|
10958
|
+
console.log(chalk19.gray(` ${repo.name}: ${ctx.techStack.join(", ") || "unknown"} (${ctx.dependencies.length} deps)`));
|
|
10838
10959
|
}
|
|
10839
10960
|
} catch (err) {
|
|
10840
|
-
console.log(
|
|
10961
|
+
console.log(chalk19.yellow(` ${repo.name}: context load failed \u2014 ${err.message}`));
|
|
10841
10962
|
}
|
|
10842
10963
|
}
|
|
10843
|
-
console.log(
|
|
10964
|
+
console.log(chalk19.blue("\n[W2] Decomposing requirement across repos..."));
|
|
10844
10965
|
const decomposer = new RequirementDecomposer(specProvider);
|
|
10845
10966
|
let decomposition;
|
|
10846
10967
|
try {
|
|
10847
10968
|
decomposition = await decomposer.decompose(idea, workspace, contexts, frontendContexts);
|
|
10848
|
-
console.log(
|
|
10849
|
-
console.log(
|
|
10969
|
+
console.log(chalk19.green(` Summary: ${decomposition.summary}`));
|
|
10970
|
+
console.log(chalk19.gray(` Repos affected: ${decomposition.repos.map((r) => r.repoName).join(", ")}`));
|
|
10850
10971
|
if (decomposition.coordinationNotes) {
|
|
10851
|
-
console.log(
|
|
10972
|
+
console.log(chalk19.gray(` Coordination: ${decomposition.coordinationNotes}`));
|
|
10852
10973
|
}
|
|
10853
10974
|
} catch (err) {
|
|
10854
|
-
console.error(
|
|
10855
|
-
console.log(
|
|
10975
|
+
console.error(chalk19.red(` Decomposition failed: ${err.message}`));
|
|
10976
|
+
console.log(chalk19.yellow(" Falling back to running all repos independently."));
|
|
10856
10977
|
decomposition = {
|
|
10857
10978
|
originalRequirement: idea,
|
|
10858
10979
|
summary: idea,
|
|
@@ -10868,11 +10989,11 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10868
10989
|
};
|
|
10869
10990
|
}
|
|
10870
10991
|
if (!opts.auto) {
|
|
10871
|
-
console.log(
|
|
10872
|
-
console.log(
|
|
10992
|
+
console.log(chalk19.cyan("\n[W3] Decomposition Preview:"));
|
|
10993
|
+
console.log(chalk19.cyan("\u2500".repeat(52)));
|
|
10873
10994
|
for (const r of decomposition.repos) {
|
|
10874
|
-
console.log(
|
|
10875
|
-
console.log(
|
|
10995
|
+
console.log(chalk19.bold(` ${r.repoName} (${r.role})`));
|
|
10996
|
+
console.log(chalk19.gray(` ${r.specIdea.slice(0, 150)}${r.specIdea.length > 150 ? "..." : ""}`));
|
|
10876
10997
|
if (r.uxDecisions) {
|
|
10877
10998
|
const ux = r.uxDecisions;
|
|
10878
10999
|
const uxSummary = [
|
|
@@ -10881,13 +11002,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10881
11002
|
ux.optimisticUpdate ? "optimistic-update" : "",
|
|
10882
11003
|
ux.errorRollback ? "rollback" : ""
|
|
10883
11004
|
].filter(Boolean).join(", ");
|
|
10884
|
-
if (uxSummary) console.log(
|
|
11005
|
+
if (uxSummary) console.log(chalk19.cyan(` UX: ${uxSummary}`));
|
|
10885
11006
|
}
|
|
10886
11007
|
if (r.dependsOnRepos.length > 0) {
|
|
10887
|
-
console.log(
|
|
11008
|
+
console.log(chalk19.gray(` Depends on: ${r.dependsOnRepos.join(", ")}`));
|
|
10888
11009
|
}
|
|
10889
11010
|
}
|
|
10890
|
-
console.log(
|
|
11011
|
+
console.log(chalk19.cyan("\u2500".repeat(52)));
|
|
10891
11012
|
const gate = await select3({
|
|
10892
11013
|
message: "Proceed with multi-repo pipeline?",
|
|
10893
11014
|
choices: [
|
|
@@ -10896,24 +11017,24 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10896
11017
|
]
|
|
10897
11018
|
});
|
|
10898
11019
|
if (gate === "abort") {
|
|
10899
|
-
console.log(
|
|
11020
|
+
console.log(chalk19.yellow(" Aborted."));
|
|
10900
11021
|
process.exit(0);
|
|
10901
11022
|
}
|
|
10902
11023
|
}
|
|
10903
11024
|
const sortedRepoRequirements = RequirementDecomposer.sortByDependency(decomposition.repos);
|
|
10904
11025
|
const contractDsls = /* @__PURE__ */ new Map();
|
|
10905
|
-
console.log(
|
|
11026
|
+
console.log(chalk19.blue(`
|
|
10906
11027
|
[W4] Running pipeline for ${sortedRepoRequirements.length} repo(s)...`));
|
|
10907
11028
|
const results = [];
|
|
10908
11029
|
for (const repoReq of sortedRepoRequirements) {
|
|
10909
11030
|
const repoConfig = workspace.repos.find((r) => r.name === repoReq.repoName);
|
|
10910
11031
|
if (!repoConfig) {
|
|
10911
|
-
console.log(
|
|
11032
|
+
console.log(chalk19.yellow(` Skipping ${repoReq.repoName} \u2014 not found in workspace config.`));
|
|
10912
11033
|
results.push({ repoName: repoReq.repoName, status: "skipped", specFile: null, dsl: null, repoAbsPath: "", role: repoReq.role });
|
|
10913
11034
|
continue;
|
|
10914
11035
|
}
|
|
10915
11036
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repoConfig);
|
|
10916
|
-
console.log(
|
|
11037
|
+
console.log(chalk19.bold.blue(`
|
|
10917
11038
|
\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`));
|
|
10918
11039
|
let contractContextSection;
|
|
10919
11040
|
if (repoReq.dependsOnRepos.length > 0) {
|
|
@@ -10921,7 +11042,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10921
11042
|
for (const depName of repoReq.dependsOnRepos) {
|
|
10922
11043
|
const depDsl = contractDsls.get(depName);
|
|
10923
11044
|
if (depDsl) {
|
|
10924
|
-
console.log(
|
|
11045
|
+
console.log(chalk19.gray(` Using API contract from: ${depName}`));
|
|
10925
11046
|
const contract = buildFrontendApiContract(depDsl);
|
|
10926
11047
|
contractParts.push(buildContractContextSection(contract));
|
|
10927
11048
|
}
|
|
@@ -10941,7 +11062,7 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10941
11062
|
frontendContext: frontendCtx
|
|
10942
11063
|
});
|
|
10943
11064
|
contractContextSection = void 0;
|
|
10944
|
-
console.log(
|
|
11065
|
+
console.log(chalk19.gray(` Frontend context: ${frontendCtx.framework} / ${frontendCtx.httpClient} / ${frontendCtx.uiLibrary}`));
|
|
10945
11066
|
}
|
|
10946
11067
|
try {
|
|
10947
11068
|
const { dsl, specFile } = await runSingleRepoPipelineInWorkspace({
|
|
@@ -10958,22 +11079,22 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
10958
11079
|
});
|
|
10959
11080
|
if (repoReq.isContractProvider && dsl) {
|
|
10960
11081
|
contractDsls.set(repoReq.repoName, dsl);
|
|
10961
|
-
console.log(
|
|
11082
|
+
console.log(chalk19.green(` Contract stored for downstream repos.`));
|
|
10962
11083
|
}
|
|
10963
11084
|
results.push({ repoName: repoReq.repoName, status: "success", specFile, dsl, repoAbsPath, role: repoReq.role });
|
|
10964
|
-
console.log(
|
|
11085
|
+
console.log(chalk19.green(` \u2714 ${repoReq.repoName} complete`));
|
|
10965
11086
|
} catch (err) {
|
|
10966
|
-
console.error(
|
|
11087
|
+
console.error(chalk19.red(` \u2718 ${repoReq.repoName} failed: ${err.message}`));
|
|
10967
11088
|
results.push({ repoName: repoReq.repoName, status: "failed", specFile: null, dsl: null, repoAbsPath, role: repoReq.role });
|
|
10968
11089
|
}
|
|
10969
11090
|
}
|
|
10970
|
-
console.log(
|
|
10971
|
-
console.log(
|
|
10972
|
-
console.log(
|
|
11091
|
+
console.log(chalk19.bold.green("\n\u2714 Multi-repo pipeline complete!"));
|
|
11092
|
+
console.log(chalk19.gray(` Workspace: ${workspace.name}`));
|
|
11093
|
+
console.log(chalk19.gray(` Requirement: ${idea}`));
|
|
10973
11094
|
console.log();
|
|
10974
11095
|
for (const r of results) {
|
|
10975
|
-
const icon = r.status === "success" ?
|
|
10976
|
-
const specInfo = r.specFile ?
|
|
11096
|
+
const icon = r.status === "success" ? chalk19.green("\u2714") : r.status === "failed" ? chalk19.red("\u2718") : chalk19.gray("\u2212");
|
|
11097
|
+
const specInfo = r.specFile ? chalk19.gray(` \u2192 ${r.specFile}`) : "";
|
|
10977
11098
|
console.log(` ${icon} ${r.repoName} (${r.status})${specInfo}`);
|
|
10978
11099
|
}
|
|
10979
11100
|
return results;
|
|
@@ -10988,11 +11109,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
10988
11109
|
default: false
|
|
10989
11110
|
});
|
|
10990
11111
|
if (!overwrite) {
|
|
10991
|
-
console.log(
|
|
11112
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
10992
11113
|
return;
|
|
10993
11114
|
}
|
|
10994
11115
|
}
|
|
10995
|
-
console.log(
|
|
11116
|
+
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"));
|
|
10996
11117
|
const workspaceName = await input({
|
|
10997
11118
|
message: "Workspace name:",
|
|
10998
11119
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
@@ -11006,11 +11127,11 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11006
11127
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11007
11128
|
const detected = await workspaceLoader.autoDetect();
|
|
11008
11129
|
if (detected.length === 0) {
|
|
11009
|
-
console.log(
|
|
11130
|
+
console.log(chalk19.yellow(" No recognizable repos found in sibling directories."));
|
|
11010
11131
|
} else {
|
|
11011
|
-
console.log(
|
|
11132
|
+
console.log(chalk19.cyan("\n Detected repos:"));
|
|
11012
11133
|
for (const r of detected) {
|
|
11013
|
-
console.log(
|
|
11134
|
+
console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11014
11135
|
}
|
|
11015
11136
|
const keepAll = await confirm2({
|
|
11016
11137
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
@@ -11027,7 +11148,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11027
11148
|
if (keep) repos.push(r);
|
|
11028
11149
|
}
|
|
11029
11150
|
}
|
|
11030
|
-
console.log(
|
|
11151
|
+
console.log(chalk19.green(` \u2714 ${repos.length} repo(s) added from auto-scan.`));
|
|
11031
11152
|
}
|
|
11032
11153
|
}
|
|
11033
11154
|
const repoTypeChoices = [
|
|
@@ -11049,7 +11170,7 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11049
11170
|
default: repos.length === 0
|
|
11050
11171
|
});
|
|
11051
11172
|
while (addMore) {
|
|
11052
|
-
console.log(
|
|
11173
|
+
console.log(chalk19.cyan(`
|
|
11053
11174
|
Adding repo #${repos.length + 1}`));
|
|
11054
11175
|
const repoName = await input({
|
|
11055
11176
|
message: "Repo name (e.g. api, web, app):",
|
|
@@ -11070,9 +11191,9 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11070
11191
|
const { type, role } = await detectRepoType(absPath);
|
|
11071
11192
|
detectedType = type;
|
|
11072
11193
|
detectedRole = role;
|
|
11073
|
-
console.log(
|
|
11194
|
+
console.log(chalk19.gray(` Auto-detected: type=${type}, role=${role}`));
|
|
11074
11195
|
} else {
|
|
11075
|
-
console.log(
|
|
11196
|
+
console.log(chalk19.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
11076
11197
|
}
|
|
11077
11198
|
const repoType = await select3({
|
|
11078
11199
|
message: `Repo type for "${repoName}":`,
|
|
@@ -11095,53 +11216,53 @@ workspaceCmd.command("init").description(`Interactive workspace setup \u2014 cre
|
|
|
11095
11216
|
type: repoType,
|
|
11096
11217
|
role: repoRole
|
|
11097
11218
|
});
|
|
11098
|
-
console.log(
|
|
11219
|
+
console.log(chalk19.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
11099
11220
|
addMore = await confirm2({
|
|
11100
11221
|
message: "Add another repo?",
|
|
11101
11222
|
default: false
|
|
11102
11223
|
});
|
|
11103
11224
|
}
|
|
11104
11225
|
const workspaceConfig = { name: workspaceName, repos };
|
|
11105
|
-
console.log(
|
|
11106
|
-
console.log(
|
|
11226
|
+
console.log(chalk19.cyan("\n Workspace summary:"));
|
|
11227
|
+
console.log(chalk19.gray(` Name: ${workspaceName}`));
|
|
11107
11228
|
for (const r of repos) {
|
|
11108
|
-
console.log(
|
|
11229
|
+
console.log(chalk19.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
11109
11230
|
}
|
|
11110
11231
|
const ok = await confirm2({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
11111
11232
|
if (!ok) {
|
|
11112
|
-
console.log(
|
|
11233
|
+
console.log(chalk19.gray(" Cancelled."));
|
|
11113
11234
|
return;
|
|
11114
11235
|
}
|
|
11115
11236
|
const loader = new WorkspaceLoader(currentDir);
|
|
11116
11237
|
const saved = await loader.save(workspaceConfig);
|
|
11117
|
-
console.log(
|
|
11238
|
+
console.log(chalk19.green(`
|
|
11118
11239
|
\u2714 Workspace saved: ${saved}`));
|
|
11119
|
-
console.log(
|
|
11240
|
+
console.log(chalk19.gray(` Run \`ai-spec create "your feature"\` \u2014 workspace mode will activate automatically.`));
|
|
11120
11241
|
});
|
|
11121
11242
|
workspaceCmd.command("status").description("Show current workspace configuration").action(async () => {
|
|
11122
11243
|
const currentDir = process.cwd();
|
|
11123
11244
|
const loader = new WorkspaceLoader(currentDir);
|
|
11124
11245
|
const config2 = await loader.load();
|
|
11125
11246
|
if (!config2) {
|
|
11126
|
-
console.log(
|
|
11127
|
-
console.log(
|
|
11247
|
+
console.log(chalk19.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
|
|
11248
|
+
console.log(chalk19.gray(" Run `ai-spec workspace init` to create one."));
|
|
11128
11249
|
return;
|
|
11129
11250
|
}
|
|
11130
|
-
console.log(
|
|
11251
|
+
console.log(chalk19.bold(`
|
|
11131
11252
|
Workspace: ${config2.name}`));
|
|
11132
|
-
console.log(
|
|
11133
|
-
console.log(
|
|
11253
|
+
console.log(chalk19.gray(` Config: ${path22.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
|
|
11254
|
+
console.log(chalk19.gray(` Repos (${config2.repos.length}):
|
|
11134
11255
|
`));
|
|
11135
11256
|
for (const repo of config2.repos) {
|
|
11136
11257
|
const absPath = loader.resolveAbsPath(repo);
|
|
11137
11258
|
const exists = await fs23.pathExists(absPath);
|
|
11138
|
-
const status = exists ?
|
|
11259
|
+
const status = exists ? chalk19.green("found") : chalk19.red("not found");
|
|
11139
11260
|
console.log(
|
|
11140
|
-
` ${
|
|
11261
|
+
` ${chalk19.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
|
|
11141
11262
|
);
|
|
11142
|
-
console.log(
|
|
11263
|
+
console.log(chalk19.gray(` path: ${absPath}`));
|
|
11143
11264
|
if (repo.constitution) {
|
|
11144
|
-
console.log(
|
|
11265
|
+
console.log(chalk19.green(` constitution: found`));
|
|
11145
11266
|
}
|
|
11146
11267
|
}
|
|
11147
11268
|
});
|
|
@@ -11158,30 +11279,30 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11158
11279
|
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
11159
11280
|
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
11160
11281
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11161
|
-
console.log(
|
|
11162
|
-
console.log(
|
|
11282
|
+
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"));
|
|
11283
|
+
console.log(chalk19.gray(` Provider: ${providerName}/${modelName}`));
|
|
11163
11284
|
const updateRunId = generateRunId();
|
|
11164
11285
|
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11165
11286
|
setActiveSnapshot(updateSnapshot);
|
|
11166
11287
|
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11167
11288
|
setActiveLogger(updateLogger);
|
|
11168
|
-
console.log(
|
|
11289
|
+
console.log(chalk19.gray(` Run ID: ${updateRunId}`));
|
|
11169
11290
|
let specPath = opts.spec ?? null;
|
|
11170
11291
|
if (!specPath) {
|
|
11171
11292
|
const specsDir = path22.join(currentDir, "specs");
|
|
11172
11293
|
const latest = await SpecUpdater.findLatestSpec(specsDir);
|
|
11173
11294
|
if (!latest) {
|
|
11174
|
-
console.error(
|
|
11295
|
+
console.error(chalk19.red(" No spec files found in specs/. Run `ai-spec create` first or use --spec <path>."));
|
|
11175
11296
|
process.exit(1);
|
|
11176
11297
|
}
|
|
11177
11298
|
specPath = latest.filePath;
|
|
11178
|
-
console.log(
|
|
11299
|
+
console.log(chalk19.gray(` Using spec: ${path22.relative(currentDir, specPath)} (v${latest.version})`));
|
|
11179
11300
|
}
|
|
11180
|
-
console.log(
|
|
11301
|
+
console.log(chalk19.gray(" Loading project context..."));
|
|
11181
11302
|
const loader = new ContextLoader(currentDir);
|
|
11182
11303
|
const context = await loader.loadProjectContext();
|
|
11183
11304
|
if (context.constitution && context.constitution.length > 6e3) {
|
|
11184
|
-
console.log(
|
|
11305
|
+
console.log(chalk19.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11185
11306
|
}
|
|
11186
11307
|
const { detectRepoType: _detectRepoType } = await Promise.resolve().then(() => (init_workspace_loader(), workspace_loader_exports));
|
|
11187
11308
|
const { type: repoType } = await _detectRepoType(currentDir);
|
|
@@ -11193,19 +11314,19 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11193
11314
|
repoType
|
|
11194
11315
|
});
|
|
11195
11316
|
} catch (err) {
|
|
11196
|
-
console.error(
|
|
11317
|
+
console.error(chalk19.red(` Update failed: ${err.message}`));
|
|
11197
11318
|
process.exit(1);
|
|
11198
11319
|
}
|
|
11199
|
-
console.log(
|
|
11320
|
+
console.log(chalk19.green(`
|
|
11200
11321
|
\u2714 Spec updated \u2192 v${result.newVersion}: ${path22.relative(currentDir, result.newSpecPath)}`));
|
|
11201
11322
|
if (result.newDslPath) {
|
|
11202
|
-
console.log(
|
|
11323
|
+
console.log(chalk19.green(` \u2714 DSL updated: ${path22.relative(currentDir, result.newDslPath)}`));
|
|
11203
11324
|
}
|
|
11204
11325
|
if (result.affectedFiles.length > 0) {
|
|
11205
|
-
console.log(
|
|
11326
|
+
console.log(chalk19.cyan("\n Affected files:"));
|
|
11206
11327
|
for (const f of result.affectedFiles) {
|
|
11207
|
-
const icon = f.action === "create" ?
|
|
11208
|
-
console.log(` ${icon} ${f.file}: ${
|
|
11328
|
+
const icon = f.action === "create" ? chalk19.green("+") : chalk19.yellow("~");
|
|
11329
|
+
console.log(` ${icon} ${f.file}: ${chalk19.gray(f.description)}`);
|
|
11209
11330
|
}
|
|
11210
11331
|
}
|
|
11211
11332
|
if (opts.codegen && result.affectedFiles.length > 0) {
|
|
@@ -11213,7 +11334,7 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11213
11334
|
const codegenModelName = opts.codegenModel || config2.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
11214
11335
|
const codegenApiKey = opts.codegenKey ?? (codegenProviderName === providerName ? apiKey : await resolveApiKey(codegenProviderName, opts.codegenKey));
|
|
11215
11336
|
const codegenProvider = createProvider(codegenProviderName, codegenApiKey, codegenModelName);
|
|
11216
|
-
console.log(
|
|
11337
|
+
console.log(chalk19.blue("\n Regenerating affected files..."));
|
|
11217
11338
|
const codeGenerator = new CodeGenerator(codegenProvider, "api");
|
|
11218
11339
|
const specContent = await fs23.readFile(result.newSpecPath, "utf-8");
|
|
11219
11340
|
const constitutionSection = context.constitution ? `
|
|
@@ -11243,7 +11364,7 @@ ${specContent}
|
|
|
11243
11364
|
${constitutionSection}${dslSection}
|
|
11244
11365
|
=== ${existing ? "Current File (return the FULL updated content)" : "New File"} ===
|
|
11245
11366
|
${existing || "Create from scratch."}`;
|
|
11246
|
-
process.stdout.write(` ${existing ?
|
|
11367
|
+
process.stdout.write(` ${existing ? chalk19.yellow("~") : chalk19.green("+")} ${affected.file}... `);
|
|
11247
11368
|
try {
|
|
11248
11369
|
const { getCodeGenSystemPrompt: _getPrompt } = await Promise.resolve().then(() => (init_codegen_prompt(), codegen_prompt_exports));
|
|
11249
11370
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
@@ -11252,10 +11373,10 @@ ${existing || "Create from scratch."}`;
|
|
|
11252
11373
|
await updateSnapshot.snapshotFile(fullPath);
|
|
11253
11374
|
await fs23.writeFile(fullPath, content, "utf-8");
|
|
11254
11375
|
updateLogger.fileWritten(affected.file);
|
|
11255
|
-
console.log(
|
|
11376
|
+
console.log(chalk19.green("\u2714"));
|
|
11256
11377
|
} catch (err) {
|
|
11257
11378
|
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11258
|
-
console.log(
|
|
11379
|
+
console.log(chalk19.red(`\u2718 ${err.message}`));
|
|
11259
11380
|
}
|
|
11260
11381
|
}
|
|
11261
11382
|
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
@@ -11271,13 +11392,13 @@ ${existing || "Create from scratch."}`;
|
|
|
11271
11392
|
updateLogger.finish();
|
|
11272
11393
|
updateLogger.printSummary();
|
|
11273
11394
|
if (updateSnapshot.fileCount > 0) {
|
|
11274
|
-
console.log(
|
|
11395
|
+
console.log(chalk19.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11275
11396
|
}
|
|
11276
11397
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11277
|
-
console.log(
|
|
11278
|
-
console.log(
|
|
11279
|
-
console.log(
|
|
11280
|
-
console.log(
|
|
11398
|
+
console.log(chalk19.blue("\n Next steps:"));
|
|
11399
|
+
console.log(chalk19.gray(` \u2022 Re-run with --codegen to regenerate affected files automatically`));
|
|
11400
|
+
console.log(chalk19.gray(` \u2022 Or update files manually based on the affected files list above`));
|
|
11401
|
+
console.log(chalk19.gray(` \u2022 Run \`ai-spec mock\` to refresh the mock server with the new DSL`));
|
|
11281
11402
|
}
|
|
11282
11403
|
});
|
|
11283
11404
|
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) => {
|
|
@@ -11286,19 +11407,19 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11286
11407
|
if (!dslPath) {
|
|
11287
11408
|
dslPath = await findLatestDslFile(currentDir);
|
|
11288
11409
|
if (!dslPath) {
|
|
11289
|
-
console.error(
|
|
11410
|
+
console.error(chalk19.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
11290
11411
|
process.exit(1);
|
|
11291
11412
|
}
|
|
11292
|
-
console.log(
|
|
11413
|
+
console.log(chalk19.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11293
11414
|
}
|
|
11294
11415
|
let dsl;
|
|
11295
11416
|
try {
|
|
11296
11417
|
dsl = await fs23.readJson(dslPath);
|
|
11297
11418
|
} catch (err) {
|
|
11298
|
-
console.error(
|
|
11419
|
+
console.error(chalk19.red(` Failed to read DSL: ${err.message}`));
|
|
11299
11420
|
process.exit(1);
|
|
11300
11421
|
}
|
|
11301
|
-
console.log(
|
|
11422
|
+
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"));
|
|
11302
11423
|
const format = opts.format === "json" ? "json" : "yaml";
|
|
11303
11424
|
const serverUrl = opts.server || "http://localhost:3000";
|
|
11304
11425
|
try {
|
|
@@ -11308,30 +11429,30 @@ program.command("export").description("Export the latest DSL to OpenAPI 3.1.0 (Y
|
|
|
11308
11429
|
outputPath: opts.output
|
|
11309
11430
|
});
|
|
11310
11431
|
const rel = path22.relative(currentDir, outputPath);
|
|
11311
|
-
console.log(
|
|
11312
|
-
console.log(
|
|
11313
|
-
console.log(
|
|
11314
|
-
console.log(
|
|
11315
|
-
console.log(
|
|
11316
|
-
console.log(
|
|
11317
|
-
console.log(
|
|
11318
|
-
console.log(
|
|
11432
|
+
console.log(chalk19.green(` \u2714 OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
11433
|
+
console.log(chalk19.gray(` Feature : ${dsl.feature.title}`));
|
|
11434
|
+
console.log(chalk19.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
11435
|
+
console.log(chalk19.gray(` Models : ${dsl.models.length}`));
|
|
11436
|
+
console.log(chalk19.gray(` Server : ${serverUrl}`));
|
|
11437
|
+
console.log(chalk19.blue("\n Next steps:"));
|
|
11438
|
+
console.log(chalk19.gray(` \u2022 Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
11439
|
+
console.log(chalk19.gray(` \u2022 Use openapi-generator to generate client SDKs`));
|
|
11319
11440
|
} catch (err) {
|
|
11320
|
-
console.error(
|
|
11441
|
+
console.error(chalk19.red(` Export failed: ${err.message}`));
|
|
11321
11442
|
process.exit(1);
|
|
11322
11443
|
}
|
|
11323
11444
|
});
|
|
11324
11445
|
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) => {
|
|
11325
11446
|
const currentDir = process.cwd();
|
|
11326
11447
|
const port = parseInt(opts.port, 10) || 3001;
|
|
11327
|
-
console.log(
|
|
11448
|
+
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"));
|
|
11328
11449
|
if (opts.restore) {
|
|
11329
11450
|
const frontendDir = opts.frontend ? path22.resolve(opts.frontend) : currentDir;
|
|
11330
11451
|
const r = await restoreMockProxy(frontendDir);
|
|
11331
11452
|
if (r.restored) {
|
|
11332
|
-
console.log(
|
|
11453
|
+
console.log(chalk19.green(" \u2714 Proxy restored and mock server stopped."));
|
|
11333
11454
|
} else {
|
|
11334
|
-
console.log(
|
|
11455
|
+
console.log(chalk19.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
11335
11456
|
}
|
|
11336
11457
|
return;
|
|
11337
11458
|
}
|
|
@@ -11339,21 +11460,21 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11339
11460
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11340
11461
|
const workspaceConfig = await workspaceLoader.load();
|
|
11341
11462
|
if (!workspaceConfig) {
|
|
11342
|
-
console.error(
|
|
11463
|
+
console.error(chalk19.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
11343
11464
|
process.exit(1);
|
|
11344
11465
|
}
|
|
11345
11466
|
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
11346
11467
|
if (backendRepos.length === 0) {
|
|
11347
|
-
console.log(
|
|
11468
|
+
console.log(chalk19.yellow(" No backend repos found in workspace."));
|
|
11348
11469
|
return;
|
|
11349
11470
|
}
|
|
11350
11471
|
for (const repo of backendRepos) {
|
|
11351
11472
|
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
11352
|
-
console.log(
|
|
11473
|
+
console.log(chalk19.cyan(`
|
|
11353
11474
|
Repo: ${repo.name} (${repoAbsPath})`));
|
|
11354
11475
|
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
11355
11476
|
if (!dslFile) {
|
|
11356
|
-
console.log(
|
|
11477
|
+
console.log(chalk19.yellow(` No DSL file found \u2014 skipping.`));
|
|
11357
11478
|
continue;
|
|
11358
11479
|
}
|
|
11359
11480
|
const dsl2 = await fs23.readJson(dslFile);
|
|
@@ -11363,8 +11484,8 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11363
11484
|
proxy: opts.proxy
|
|
11364
11485
|
});
|
|
11365
11486
|
for (const f of result2.files) {
|
|
11366
|
-
console.log(
|
|
11367
|
-
console.log(
|
|
11487
|
+
console.log(chalk19.green(` \u2714 ${f.path}`));
|
|
11488
|
+
console.log(chalk19.gray(` ${f.description}`));
|
|
11368
11489
|
}
|
|
11369
11490
|
}
|
|
11370
11491
|
return;
|
|
@@ -11374,19 +11495,19 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11374
11495
|
dslPath = await findLatestDslFile(currentDir);
|
|
11375
11496
|
if (!dslPath) {
|
|
11376
11497
|
console.error(
|
|
11377
|
-
|
|
11498
|
+
chalk19.red(
|
|
11378
11499
|
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
11379
11500
|
)
|
|
11380
11501
|
);
|
|
11381
11502
|
process.exit(1);
|
|
11382
11503
|
}
|
|
11383
|
-
console.log(
|
|
11504
|
+
console.log(chalk19.gray(` Using DSL: ${path22.relative(currentDir, dslPath)}`));
|
|
11384
11505
|
}
|
|
11385
11506
|
let dsl;
|
|
11386
11507
|
try {
|
|
11387
11508
|
dsl = await fs23.readJson(dslPath);
|
|
11388
11509
|
} catch (err) {
|
|
11389
|
-
console.error(
|
|
11510
|
+
console.error(chalk19.red(` Failed to read DSL file: ${err.message}`));
|
|
11390
11511
|
process.exit(1);
|
|
11391
11512
|
}
|
|
11392
11513
|
const result = await generateMockAssets(dsl, currentDir, {
|
|
@@ -11394,58 +11515,58 @@ program.command("mock").description("Generate a standalone mock server + proxy c
|
|
|
11394
11515
|
msw: opts.msw,
|
|
11395
11516
|
proxy: opts.proxy
|
|
11396
11517
|
});
|
|
11397
|
-
console.log(
|
|
11518
|
+
console.log(chalk19.green(`
|
|
11398
11519
|
\u2714 Mock assets generated (${result.files.length} file(s)):`));
|
|
11399
11520
|
for (const f of result.files) {
|
|
11400
|
-
console.log(
|
|
11401
|
-
console.log(
|
|
11521
|
+
console.log(chalk19.green(` ${f.path}`));
|
|
11522
|
+
console.log(chalk19.gray(` ${f.description}`));
|
|
11402
11523
|
}
|
|
11403
11524
|
if (opts.serve) {
|
|
11404
11525
|
const serverJsPath = path22.join(currentDir, "mock", "server.js");
|
|
11405
11526
|
if (!await fs23.pathExists(serverJsPath)) {
|
|
11406
|
-
console.error(
|
|
11527
|
+
console.error(chalk19.red(" mock/server.js not found \u2014 generation may have failed."));
|
|
11407
11528
|
process.exit(1);
|
|
11408
11529
|
}
|
|
11409
11530
|
const pid = startMockServerBackground(serverJsPath, port);
|
|
11410
|
-
console.log(
|
|
11531
|
+
console.log(chalk19.green(`
|
|
11411
11532
|
\u2714 Mock server started (PID ${pid}) \u2192 http://localhost:${port}`));
|
|
11412
11533
|
if (opts.frontend) {
|
|
11413
11534
|
const frontendDir = path22.resolve(opts.frontend);
|
|
11414
11535
|
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
11415
11536
|
await saveMockServerPid(frontendDir, pid);
|
|
11416
11537
|
if (proxyResult.applied) {
|
|
11417
|
-
console.log(
|
|
11418
|
-
console.log(
|
|
11538
|
+
console.log(chalk19.green(` \u2714 Frontend proxy patched (${proxyResult.framework})`));
|
|
11539
|
+
console.log(chalk19.bold.cyan(`
|
|
11419
11540
|
Ready! Open a new terminal and run:`));
|
|
11420
|
-
console.log(
|
|
11421
|
-
console.log(
|
|
11422
|
-
console.log(
|
|
11541
|
+
console.log(chalk19.white(` cd ${frontendDir}`));
|
|
11542
|
+
console.log(chalk19.white(` ${proxyResult.devCommand}`));
|
|
11543
|
+
console.log(chalk19.gray(`
|
|
11423
11544
|
When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
11424
11545
|
} else {
|
|
11425
|
-
console.log(
|
|
11426
|
-
if (proxyResult.note) console.log(
|
|
11546
|
+
console.log(chalk19.yellow(` \u26A0 Auto-patch not available for ${proxyResult.framework}.`));
|
|
11547
|
+
if (proxyResult.note) console.log(chalk19.gray(` ${proxyResult.note}`));
|
|
11427
11548
|
}
|
|
11428
11549
|
} else {
|
|
11429
|
-
console.log(
|
|
11430
|
-
console.log(
|
|
11550
|
+
console.log(chalk19.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
11551
|
+
console.log(chalk19.gray(` Mock server: http://localhost:${port}`));
|
|
11431
11552
|
}
|
|
11432
11553
|
return;
|
|
11433
11554
|
}
|
|
11434
|
-
console.log(
|
|
11435
|
-
console.log(
|
|
11436
|
-
console.log(
|
|
11437
|
-
console.log(
|
|
11438
|
-
console.log(
|
|
11439
|
-
console.log(
|
|
11440
|
-
console.log(
|
|
11441
|
-
console.log(
|
|
11555
|
+
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"));
|
|
11556
|
+
console.log(chalk19.white(` 1. Install express (if not already):`));
|
|
11557
|
+
console.log(chalk19.gray(` npm install --save-dev express`));
|
|
11558
|
+
console.log(chalk19.white(` 2. Start mock server:`));
|
|
11559
|
+
console.log(chalk19.gray(` node mock/server.js`));
|
|
11560
|
+
console.log(chalk19.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
11561
|
+
console.log(chalk19.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
11562
|
+
console.log(chalk19.gray(` http://localhost:${port}`));
|
|
11442
11563
|
if (opts.proxy) {
|
|
11443
|
-
console.log(
|
|
11564
|
+
console.log(chalk19.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
11444
11565
|
}
|
|
11445
11566
|
if (opts.msw) {
|
|
11446
|
-
console.log(
|
|
11447
|
-
console.log(
|
|
11448
|
-
console.log(
|
|
11567
|
+
console.log(chalk19.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
11568
|
+
console.log(chalk19.gray(` import { worker } from './mocks/browser';`));
|
|
11569
|
+
console.log(chalk19.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
11449
11570
|
}
|
|
11450
11571
|
});
|
|
11451
11572
|
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) => {
|
|
@@ -11461,24 +11582,24 @@ program.command("learn").description("Append a lesson or engineering decision di
|
|
|
11461
11582
|
}
|
|
11462
11583
|
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
11463
11584
|
if (result.appended) {
|
|
11464
|
-
console.log(
|
|
11585
|
+
console.log(chalk19.green(`
|
|
11465
11586
|
\u2714 Lesson appended to constitution \xA79`));
|
|
11466
|
-
console.log(
|
|
11587
|
+
console.log(chalk19.gray(` File: .ai-spec-constitution.md`));
|
|
11467
11588
|
} else {
|
|
11468
|
-
console.log(
|
|
11589
|
+
console.log(chalk19.yellow(`
|
|
11469
11590
|
\u26A0 Not appended: ${result.reason}`));
|
|
11470
11591
|
}
|
|
11471
11592
|
});
|
|
11472
11593
|
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) => {
|
|
11473
11594
|
const currentDir = process.cwd();
|
|
11474
11595
|
const snapshot = new RunSnapshot(currentDir, runId);
|
|
11475
|
-
console.log(
|
|
11596
|
+
console.log(chalk19.blue(`Restoring run: ${runId}...`));
|
|
11476
11597
|
const restored = await snapshot.restore();
|
|
11477
11598
|
if (restored.length === 0) {
|
|
11478
|
-
console.log(
|
|
11599
|
+
console.log(chalk19.yellow(" No backup found for this run ID."));
|
|
11479
11600
|
} else {
|
|
11480
|
-
restored.forEach((f) => console.log(
|
|
11481
|
-
console.log(
|
|
11601
|
+
restored.forEach((f) => console.log(chalk19.green(` \u2714 restored: ${f}`)));
|
|
11602
|
+
console.log(chalk19.bold.green(`
|
|
11482
11603
|
\u2714 ${restored.length} file(s) restored.`));
|
|
11483
11604
|
}
|
|
11484
11605
|
});
|