ai-spec-dev 0.41.0 → 0.46.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/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/README.md +33 -17
- package/cli/commands/create.ts +232 -11
- package/cli/commands/init.ts +310 -107
- package/cli/commands/model.ts +7 -11
- package/cli/index.ts +1 -1
- package/cli/pipeline/single-repo.ts +19 -10
- package/cli/utils.ts +72 -4
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +4 -2
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +7 -4
- package/core/openapi-exporter.ts +3 -2
- package/core/provider-utils.ts +8 -7
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/spec-generator.ts +27 -42
- package/core/token-budget.ts +3 -8
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +1042 -533
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1042 -533
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +123 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/RELEASE_LOG.md +0 -2962
- package/purpose.md +0 -1434
package/dist/cli/index.mjs
CHANGED
|
@@ -4,6 +4,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
7
10
|
var __export = (target, all) => {
|
|
8
11
|
for (var name in all)
|
|
9
12
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -126,8 +129,101 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
|
|
|
126
129
|
}
|
|
127
130
|
});
|
|
128
131
|
|
|
129
|
-
// core/
|
|
132
|
+
// core/cli-ui.ts
|
|
130
133
|
import chalk from "chalk";
|
|
134
|
+
function startSpinner(text) {
|
|
135
|
+
const isTTY = process.stderr.isTTY;
|
|
136
|
+
let frame = 0;
|
|
137
|
+
let currentText = text;
|
|
138
|
+
let stopped = false;
|
|
139
|
+
function render() {
|
|
140
|
+
if (stopped) return;
|
|
141
|
+
const symbol = chalk.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
|
|
142
|
+
if (isTTY) {
|
|
143
|
+
process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
|
|
144
|
+
}
|
|
145
|
+
frame++;
|
|
146
|
+
}
|
|
147
|
+
if (!isTTY) {
|
|
148
|
+
process.stderr.write(` \u2026 ${currentText}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
const timer = setInterval(render, 80);
|
|
152
|
+
render();
|
|
153
|
+
return {
|
|
154
|
+
update(newText) {
|
|
155
|
+
currentText = newText;
|
|
156
|
+
},
|
|
157
|
+
stop(finalText) {
|
|
158
|
+
if (stopped) return;
|
|
159
|
+
stopped = true;
|
|
160
|
+
clearInterval(timer);
|
|
161
|
+
if (isTTY) {
|
|
162
|
+
process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
|
|
163
|
+
}
|
|
164
|
+
if (finalText) {
|
|
165
|
+
process.stderr.write(` ${finalText}
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
succeed(successText) {
|
|
170
|
+
this.stop(chalk.green(`\u2714 ${successText}`));
|
|
171
|
+
},
|
|
172
|
+
fail(failText) {
|
|
173
|
+
this.stop(chalk.red(`\u2718 ${failText}`));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function retryCountdown(opts) {
|
|
178
|
+
const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
|
|
179
|
+
const isTTY = process.stderr.isTTY;
|
|
180
|
+
const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
|
|
181
|
+
process.stderr.write("\n");
|
|
182
|
+
process.stderr.write(chalk.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + chalk.gray(`[${label}]`) + chalk.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
|
|
183
|
+
`));
|
|
184
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.white(shortErr) + "\n");
|
|
185
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.gray(`Waiting before retry...`) + "\n");
|
|
186
|
+
const totalSeconds = Math.ceil(waitMs / 1e3);
|
|
187
|
+
for (let s = totalSeconds; s > 0; s--) {
|
|
188
|
+
const bar = chalk.green("\u2588".repeat(totalSeconds - s)) + chalk.gray("\u2591".repeat(s));
|
|
189
|
+
const line = chalk.yellow(` \u2502 `) + `${bar} ${chalk.bold.white(`${s}s`)}`;
|
|
190
|
+
if (isTTY) {
|
|
191
|
+
process.stderr.write(`\r${line}${" ".repeat(10)}`);
|
|
192
|
+
}
|
|
193
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
194
|
+
}
|
|
195
|
+
if (isTTY) {
|
|
196
|
+
process.stderr.write(`\r${" ".repeat(70)}\r`);
|
|
197
|
+
}
|
|
198
|
+
process.stderr.write(chalk.yellow(` \u2514\u2500 `) + chalk.cyan(`Retrying now...`) + "\n\n");
|
|
199
|
+
}
|
|
200
|
+
function startStage(stageKey, label) {
|
|
201
|
+
const icon = STAGE_ICONS[stageKey] ?? "\u25B8";
|
|
202
|
+
return startSpinner(`${icon} ${label}`);
|
|
203
|
+
}
|
|
204
|
+
var SPINNER_FRAMES, STAGE_ICONS;
|
|
205
|
+
var init_cli_ui = __esm({
|
|
206
|
+
"core/cli-ui.ts"() {
|
|
207
|
+
"use strict";
|
|
208
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
209
|
+
STAGE_ICONS = {
|
|
210
|
+
context_load: "\u{1F4C2}",
|
|
211
|
+
design_dialogue: "\u{1F4AC}",
|
|
212
|
+
spec_gen: "\u{1F4DD}",
|
|
213
|
+
spec_refine: "\u270F\uFE0F ",
|
|
214
|
+
spec_assess: "\u{1F4CA}",
|
|
215
|
+
dsl_extract: "\u{1F517}",
|
|
216
|
+
dsl_gap_feedback: "\u{1F50D}",
|
|
217
|
+
codegen: "\u2699\uFE0F ",
|
|
218
|
+
test_gen: "\u{1F9EA}",
|
|
219
|
+
error_feedback: "\u{1F527}",
|
|
220
|
+
review: "\u{1F50E}",
|
|
221
|
+
self_eval: "\u{1F4C8}"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// core/provider-utils.ts
|
|
131
227
|
function classifyError(err, label) {
|
|
132
228
|
const e = err;
|
|
133
229
|
const status = e.status ?? e.response?.status;
|
|
@@ -203,20 +299,23 @@ async function withReliability(fn, opts) {
|
|
|
203
299
|
throw classifyError(err, label);
|
|
204
300
|
}
|
|
205
301
|
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
206
|
-
console.warn(
|
|
207
|
-
chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
|
|
208
|
-
);
|
|
209
302
|
onRetry?.(attempt + 1, err);
|
|
210
|
-
await
|
|
303
|
+
await retryCountdown({
|
|
304
|
+
attempt: attempt + 1,
|
|
305
|
+
maxAttempts: retries + 1,
|
|
306
|
+
waitMs,
|
|
307
|
+
errorMessage: err.message ?? String(err),
|
|
308
|
+
label
|
|
309
|
+
});
|
|
211
310
|
}
|
|
212
311
|
}
|
|
213
312
|
throw new Error("unreachable");
|
|
214
313
|
}
|
|
215
|
-
var
|
|
314
|
+
var ProviderError;
|
|
216
315
|
var init_provider_utils = __esm({
|
|
217
316
|
"core/provider-utils.ts"() {
|
|
218
317
|
"use strict";
|
|
219
|
-
|
|
318
|
+
init_cli_ui();
|
|
220
319
|
ProviderError = class extends Error {
|
|
221
320
|
constructor(message, kind, originalError) {
|
|
222
321
|
super(message);
|
|
@@ -245,7 +344,6 @@ __export(spec_generator_exports, {
|
|
|
245
344
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
246
345
|
import Anthropic from "@anthropic-ai/sdk";
|
|
247
346
|
import OpenAI from "openai";
|
|
248
|
-
import axios from "axios";
|
|
249
347
|
import { ProxyAgent } from "undici";
|
|
250
348
|
function geminiRequestOptions() {
|
|
251
349
|
const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
@@ -291,7 +389,9 @@ var init_spec_generator = __esm({
|
|
|
291
389
|
displayName: "MiMo (Xiaomi)",
|
|
292
390
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
293
391
|
models: ["mimo-v2-pro"],
|
|
294
|
-
envKey: "MIMO_API_KEY"
|
|
392
|
+
envKey: "MIMO_API_KEY",
|
|
393
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
394
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
295
395
|
// baseURL not used — MiMo has a dedicated provider class
|
|
296
396
|
},
|
|
297
397
|
gemini: {
|
|
@@ -460,8 +560,8 @@ var init_spec_generator = __esm({
|
|
|
460
560
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
461
561
|
messages: [{ role: "user", content: prompt }]
|
|
462
562
|
});
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
563
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
564
|
+
if (textBlock) return textBlock.text;
|
|
465
565
|
throw new Error("Unexpected response type from Claude API");
|
|
466
566
|
},
|
|
467
567
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -506,43 +606,29 @@ var init_spec_generator = __esm({
|
|
|
506
606
|
}
|
|
507
607
|
};
|
|
508
608
|
MiMoProvider = class {
|
|
609
|
+
client;
|
|
509
610
|
providerName = "mimo";
|
|
510
611
|
modelName;
|
|
511
|
-
apiKey;
|
|
512
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
513
612
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
514
|
-
|
|
613
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
614
|
+
this.client = new Anthropic({ apiKey, baseURL });
|
|
515
615
|
this.modelName = modelName;
|
|
516
616
|
}
|
|
517
617
|
async generate(prompt, systemInstruction) {
|
|
518
618
|
return withReliability(
|
|
519
619
|
async () => {
|
|
520
|
-
const
|
|
620
|
+
const stream = this.client.messages.stream({
|
|
521
621
|
model: this.modelName,
|
|
522
|
-
max_tokens:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
stream: false,
|
|
526
|
-
temperature: 1,
|
|
527
|
-
stop_sequences: null
|
|
528
|
-
};
|
|
529
|
-
if (systemInstruction) {
|
|
530
|
-
body.system = systemInstruction;
|
|
531
|
-
}
|
|
532
|
-
const response = await axios.post(this.baseUrl, body, {
|
|
533
|
-
headers: {
|
|
534
|
-
"api-key": this.apiKey,
|
|
535
|
-
"Content-Type": "application/json"
|
|
536
|
-
}
|
|
622
|
+
max_tokens: 65536,
|
|
623
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
624
|
+
messages: [{ role: "user", content: prompt }]
|
|
537
625
|
});
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
626
|
+
const message = await stream.finalMessage();
|
|
627
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
628
|
+
if (textBlock) return textBlock.text;
|
|
629
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
630
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
631
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
546
632
|
},
|
|
547
633
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
548
634
|
);
|
|
@@ -603,14 +689,71 @@ ${context.schema.slice(0, 3e3)}`);
|
|
|
603
689
|
}
|
|
604
690
|
});
|
|
605
691
|
|
|
692
|
+
// package.json
|
|
693
|
+
var require_package = __commonJS({
|
|
694
|
+
"package.json"(exports, module) {
|
|
695
|
+
module.exports = {
|
|
696
|
+
name: "ai-spec-dev",
|
|
697
|
+
version: "0.46.0",
|
|
698
|
+
description: "AI-driven Development Orchestrator SDK & CLI",
|
|
699
|
+
main: "dist/index.js",
|
|
700
|
+
types: "dist/index.d.ts",
|
|
701
|
+
bin: {
|
|
702
|
+
"ai-spec": "dist/cli/index.js"
|
|
703
|
+
},
|
|
704
|
+
scripts: {
|
|
705
|
+
build: "tsup",
|
|
706
|
+
dev: "tsup --watch",
|
|
707
|
+
test: "vitest run",
|
|
708
|
+
"test:watch": "vitest"
|
|
709
|
+
},
|
|
710
|
+
keywords: [
|
|
711
|
+
"ai",
|
|
712
|
+
"spec",
|
|
713
|
+
"codegen",
|
|
714
|
+
"gemini",
|
|
715
|
+
"claude"
|
|
716
|
+
],
|
|
717
|
+
author: "",
|
|
718
|
+
license: "MIT",
|
|
719
|
+
dependencies: {
|
|
720
|
+
"@anthropic-ai/sdk": "^0.38.0",
|
|
721
|
+
"@google/generative-ai": "^0.21.0",
|
|
722
|
+
"@inquirer/editor": "^5.0.10",
|
|
723
|
+
"@inquirer/prompts": "^8.3.2",
|
|
724
|
+
"@rollup/rollup-darwin-arm64": "^4.60.1",
|
|
725
|
+
axios: "^1.13.6",
|
|
726
|
+
chalk: "^4.1.2",
|
|
727
|
+
commander: "^13.1.0",
|
|
728
|
+
dotenv: "^16.4.7",
|
|
729
|
+
"fs-extra": "^11.3.0",
|
|
730
|
+
openai: "^6.31.0",
|
|
731
|
+
undici: "^7.24.4"
|
|
732
|
+
},
|
|
733
|
+
devDependencies: {
|
|
734
|
+
"@types/fs-extra": "^11.0.4",
|
|
735
|
+
"@types/glob": "^8.1.0",
|
|
736
|
+
"@types/node": "^22.13.5",
|
|
737
|
+
glob: "^13.0.6",
|
|
738
|
+
"ts-node": "^10.9.2",
|
|
739
|
+
tsup: "^8.4.0",
|
|
740
|
+
typescript: "^5.7.3",
|
|
741
|
+
vitest: "^2.1.0"
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
606
747
|
// cli/index.ts
|
|
607
748
|
import { Command } from "commander";
|
|
608
749
|
import * as dotenv from "dotenv";
|
|
609
750
|
|
|
610
751
|
// cli/commands/create.ts
|
|
611
752
|
init_spec_generator();
|
|
753
|
+
import * as path26 from "path";
|
|
754
|
+
import * as fs27 from "fs-extra";
|
|
612
755
|
import chalk26 from "chalk";
|
|
613
|
-
import { input as input2 } from "@inquirer/prompts";
|
|
756
|
+
import { input as input2, select as select7, checkbox } from "@inquirer/prompts";
|
|
614
757
|
|
|
615
758
|
// core/workspace-loader.ts
|
|
616
759
|
import * as fs from "fs-extra";
|
|
@@ -710,8 +853,8 @@ var WorkspaceLoader = class {
|
|
|
710
853
|
const repos = [];
|
|
711
854
|
for (const entry of entries) {
|
|
712
855
|
const absPath = path.join(this.workspaceRoot, entry);
|
|
713
|
-
const
|
|
714
|
-
if (!
|
|
856
|
+
const stat5 = await fs.stat(absPath).catch(() => null);
|
|
857
|
+
if (!stat5 || !stat5.isDirectory()) continue;
|
|
715
858
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
716
859
|
if (names && !names.includes(entry)) continue;
|
|
717
860
|
const hasManifest = await fs.pathExists(path.join(absPath, "package.json")) || await fs.pathExists(path.join(absPath, "go.mod")) || await fs.pathExists(path.join(absPath, "Cargo.toml")) || await fs.pathExists(path.join(absPath, "pom.xml")) || await fs.pathExists(path.join(absPath, "build.gradle")) || await fs.pathExists(path.join(absPath, "requirements.txt")) || await fs.pathExists(path.join(absPath, "pyproject.toml")) || await fs.pathExists(path.join(absPath, "composer.json"));
|
|
@@ -775,6 +918,7 @@ var WorkspaceLoader = class {
|
|
|
775
918
|
init_spec_generator();
|
|
776
919
|
import * as path3 from "path";
|
|
777
920
|
import * as fs3 from "fs-extra";
|
|
921
|
+
import * as os2 from "os";
|
|
778
922
|
import chalk2 from "chalk";
|
|
779
923
|
import { input, select } from "@inquirer/prompts";
|
|
780
924
|
|
|
@@ -820,17 +964,42 @@ async function clearKey(provider) {
|
|
|
820
964
|
|
|
821
965
|
// cli/utils.ts
|
|
822
966
|
var CONFIG_FILE = ".ai-spec.json";
|
|
967
|
+
var GLOBAL_CONFIG_FILE = path3.join(os2.homedir(), ".ai-spec-config.json");
|
|
968
|
+
async function loadGlobalConfig() {
|
|
969
|
+
try {
|
|
970
|
+
if (await fs3.pathExists(GLOBAL_CONFIG_FILE)) {
|
|
971
|
+
return await fs3.readJson(GLOBAL_CONFIG_FILE);
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
return {};
|
|
976
|
+
}
|
|
977
|
+
async function saveGlobalConfig(config2) {
|
|
978
|
+
await fs3.ensureFile(GLOBAL_CONFIG_FILE);
|
|
979
|
+
await fs3.writeJson(GLOBAL_CONFIG_FILE, config2, { spaces: 2 });
|
|
980
|
+
}
|
|
823
981
|
async function loadConfig(dir) {
|
|
982
|
+
const globalConfig = await loadGlobalConfig();
|
|
983
|
+
let localConfig = {};
|
|
824
984
|
const p = path3.join(dir, CONFIG_FILE);
|
|
825
985
|
if (await fs3.pathExists(p)) {
|
|
826
|
-
|
|
986
|
+
try {
|
|
987
|
+
localConfig = await fs3.readJson(p);
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
827
990
|
}
|
|
828
|
-
return {};
|
|
991
|
+
return { ...globalConfig, ...localConfig };
|
|
829
992
|
}
|
|
830
993
|
async function resolveApiKey(providerName, cliKey) {
|
|
831
994
|
if (cliKey) return cliKey;
|
|
832
995
|
const envVar = ENV_KEY_MAP[providerName];
|
|
833
996
|
if (envVar && process.env[envVar]) return process.env[envVar];
|
|
997
|
+
const meta = PROVIDER_CATALOG[providerName];
|
|
998
|
+
if (meta?.fallbackEnvKeys) {
|
|
999
|
+
for (const key of meta.fallbackEnvKeys) {
|
|
1000
|
+
if (process.env[key]) return process.env[key];
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
834
1003
|
const savedKey = await getSavedKey(providerName);
|
|
835
1004
|
if (savedKey) {
|
|
836
1005
|
const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
|
|
@@ -904,7 +1073,7 @@ var pe = "\0PERIOD" + Math.random() + "\0";
|
|
|
904
1073
|
var is = new RegExp(fe, "g");
|
|
905
1074
|
var rs = new RegExp(ue, "g");
|
|
906
1075
|
var ns = new RegExp(qt, "g");
|
|
907
|
-
var
|
|
1076
|
+
var os3 = new RegExp(de, "g");
|
|
908
1077
|
var hs = new RegExp(pe, "g");
|
|
909
1078
|
var as = /\\\\/g;
|
|
910
1079
|
var ls = /\\{/g;
|
|
@@ -919,7 +1088,7 @@ function ps(n7) {
|
|
|
919
1088
|
return n7.replace(as, fe).replace(ls, ue).replace(cs, qt).replace(fs4, de).replace(us, pe);
|
|
920
1089
|
}
|
|
921
1090
|
function ms(n7) {
|
|
922
|
-
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(
|
|
1091
|
+
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os3, ",").replace(hs, ".");
|
|
923
1092
|
}
|
|
924
1093
|
function me(n7) {
|
|
925
1094
|
if (!n7) return [""];
|
|
@@ -3849,11 +4018,11 @@ Ze.glob = Ze;
|
|
|
3849
4018
|
// core/global-constitution.ts
|
|
3850
4019
|
import * as fs5 from "fs-extra";
|
|
3851
4020
|
import * as path4 from "path";
|
|
3852
|
-
import * as
|
|
4021
|
+
import * as os4 from "os";
|
|
3853
4022
|
var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
|
|
3854
4023
|
var SEARCH_ROOTS = [
|
|
3855
4024
|
// Workspace root is injected at runtime — see loadGlobalConstitution()
|
|
3856
|
-
|
|
4025
|
+
os4.homedir()
|
|
3857
4026
|
];
|
|
3858
4027
|
async function loadGlobalConstitution(extraRoots = []) {
|
|
3859
4028
|
const roots = [...extraRoots, ...SEARCH_ROOTS];
|
|
@@ -3882,7 +4051,7 @@ function mergeConstitutions(globalContent, projectContent) {
|
|
|
3882
4051
|
}
|
|
3883
4052
|
return parts.join("\n");
|
|
3884
4053
|
}
|
|
3885
|
-
async function saveGlobalConstitution(content, targetDir =
|
|
4054
|
+
async function saveGlobalConstitution(content, targetDir = os4.homedir()) {
|
|
3886
4055
|
const filePath = path4.join(targetDir, GLOBAL_CONSTITUTION_FILE);
|
|
3887
4056
|
await fs5.writeFile(filePath, content, "utf-8");
|
|
3888
4057
|
return filePath;
|
|
@@ -4915,32 +5084,32 @@ function validateDsl(raw) {
|
|
|
4915
5084
|
}
|
|
4916
5085
|
return { valid: true, dsl: raw };
|
|
4917
5086
|
}
|
|
4918
|
-
function validateFeature(raw,
|
|
5087
|
+
function validateFeature(raw, path43, errors) {
|
|
4919
5088
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4920
|
-
errors.push({ path:
|
|
5089
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4921
5090
|
return;
|
|
4922
5091
|
}
|
|
4923
5092
|
const f = raw;
|
|
4924
|
-
requireNonEmptyString(f["id"], `${
|
|
4925
|
-
requireNonEmptyString(f["title"], `${
|
|
4926
|
-
requireNonEmptyString(f["description"], `${
|
|
5093
|
+
requireNonEmptyString(f["id"], `${path43}.id`, errors);
|
|
5094
|
+
requireNonEmptyString(f["title"], `${path43}.title`, errors);
|
|
5095
|
+
requireNonEmptyString(f["description"], `${path43}.description`, errors);
|
|
4927
5096
|
}
|
|
4928
|
-
function validateModel(raw,
|
|
5097
|
+
function validateModel(raw, path43, errors) {
|
|
4929
5098
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4930
|
-
errors.push({ path:
|
|
5099
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4931
5100
|
return;
|
|
4932
5101
|
}
|
|
4933
5102
|
const m = raw;
|
|
4934
|
-
requireNonEmptyString(m["name"], `${
|
|
5103
|
+
requireNonEmptyString(m["name"], `${path43}.name`, errors);
|
|
4935
5104
|
if (!Array.isArray(m["fields"])) {
|
|
4936
|
-
errors.push({ path: `${
|
|
5105
|
+
errors.push({ path: `${path43}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4937
5106
|
} else {
|
|
4938
5107
|
const fields = m["fields"];
|
|
4939
5108
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4940
|
-
errors.push({ path: `${
|
|
5109
|
+
errors.push({ path: `${path43}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4941
5110
|
}
|
|
4942
5111
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4943
|
-
validateModelField(fields[j2], `${
|
|
5112
|
+
validateModelField(fields[j2], `${path43}.fields[${j2}]`, errors);
|
|
4944
5113
|
}
|
|
4945
5114
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4946
5115
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -4949,7 +5118,7 @@ function validateModel(raw, path42, errors) {
|
|
|
4949
5118
|
const name = f["name"];
|
|
4950
5119
|
if (seenFieldNames.has(name)) {
|
|
4951
5120
|
errors.push({
|
|
4952
|
-
path: `${
|
|
5121
|
+
path: `${path43}.fields[${j2}].name`,
|
|
4953
5122
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4954
5123
|
});
|
|
4955
5124
|
} else {
|
|
@@ -4960,184 +5129,184 @@ function validateModel(raw, path42, errors) {
|
|
|
4960
5129
|
}
|
|
4961
5130
|
if (m["relations"] !== void 0) {
|
|
4962
5131
|
if (!Array.isArray(m["relations"])) {
|
|
4963
|
-
errors.push({ path: `${
|
|
5132
|
+
errors.push({ path: `${path43}.relations`, message: "Must be an array of strings if present" });
|
|
4964
5133
|
} else {
|
|
4965
5134
|
const rels = m["relations"];
|
|
4966
5135
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4967
5136
|
if (typeof rels[j2] !== "string") {
|
|
4968
|
-
errors.push({ path: `${
|
|
5137
|
+
errors.push({ path: `${path43}.relations[${j2}]`, message: "Must be a string" });
|
|
4969
5138
|
}
|
|
4970
5139
|
}
|
|
4971
5140
|
}
|
|
4972
5141
|
}
|
|
4973
5142
|
}
|
|
4974
|
-
function validateModelField(raw,
|
|
5143
|
+
function validateModelField(raw, path43, errors) {
|
|
4975
5144
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4976
|
-
errors.push({ path:
|
|
5145
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4977
5146
|
return;
|
|
4978
5147
|
}
|
|
4979
5148
|
const f = raw;
|
|
4980
|
-
requireNonEmptyString(f["name"], `${
|
|
4981
|
-
requireNonEmptyString(f["type"], `${
|
|
5149
|
+
requireNonEmptyString(f["name"], `${path43}.name`, errors);
|
|
5150
|
+
requireNonEmptyString(f["type"], `${path43}.type`, errors);
|
|
4982
5151
|
if (typeof f["required"] !== "boolean") {
|
|
4983
|
-
errors.push({ path: `${
|
|
5152
|
+
errors.push({ path: `${path43}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4984
5153
|
}
|
|
4985
5154
|
}
|
|
4986
|
-
function validateEndpoint(raw,
|
|
5155
|
+
function validateEndpoint(raw, path43, errors) {
|
|
4987
5156
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4988
|
-
errors.push({ path:
|
|
5157
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4989
5158
|
return;
|
|
4990
5159
|
}
|
|
4991
5160
|
const e = raw;
|
|
4992
|
-
requireNonEmptyString(e["id"], `${
|
|
4993
|
-
requireNonEmptyString(e["description"], `${
|
|
5161
|
+
requireNonEmptyString(e["id"], `${path43}.id`, errors);
|
|
5162
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
4994
5163
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4995
5164
|
errors.push({
|
|
4996
|
-
path: `${
|
|
5165
|
+
path: `${path43}.method`,
|
|
4997
5166
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4998
5167
|
});
|
|
4999
5168
|
}
|
|
5000
5169
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
5001
5170
|
errors.push({
|
|
5002
|
-
path: `${
|
|
5171
|
+
path: `${path43}.path`,
|
|
5003
5172
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
5004
5173
|
});
|
|
5005
5174
|
}
|
|
5006
5175
|
if (typeof e["auth"] !== "boolean") {
|
|
5007
|
-
errors.push({ path: `${
|
|
5176
|
+
errors.push({ path: `${path43}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
5008
5177
|
}
|
|
5009
5178
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
5010
5179
|
errors.push({
|
|
5011
|
-
path: `${
|
|
5180
|
+
path: `${path43}.successStatus`,
|
|
5012
5181
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
5013
5182
|
});
|
|
5014
5183
|
}
|
|
5015
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
5184
|
+
requireNonEmptyString(e["successDescription"], `${path43}.successDescription`, errors);
|
|
5016
5185
|
if (e["request"] !== void 0) {
|
|
5017
|
-
validateRequestSchema(e["request"], `${
|
|
5186
|
+
validateRequestSchema(e["request"], `${path43}.request`, errors);
|
|
5018
5187
|
}
|
|
5019
5188
|
if (e["errors"] !== void 0) {
|
|
5020
5189
|
if (!Array.isArray(e["errors"])) {
|
|
5021
|
-
errors.push({ path: `${
|
|
5190
|
+
errors.push({ path: `${path43}.errors`, message: "Must be an array if present" });
|
|
5022
5191
|
} else {
|
|
5023
5192
|
const errs = e["errors"];
|
|
5024
5193
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
5025
|
-
errors.push({ path: `${
|
|
5194
|
+
errors.push({ path: `${path43}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
5026
5195
|
}
|
|
5027
5196
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
5028
|
-
validateResponseError(errs[j2], `${
|
|
5197
|
+
validateResponseError(errs[j2], `${path43}.errors[${j2}]`, errors);
|
|
5029
5198
|
}
|
|
5030
5199
|
}
|
|
5031
5200
|
}
|
|
5032
5201
|
}
|
|
5033
|
-
function validateRequestSchema(raw,
|
|
5202
|
+
function validateRequestSchema(raw, path43, errors) {
|
|
5034
5203
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5035
|
-
errors.push({ path:
|
|
5204
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5036
5205
|
return;
|
|
5037
5206
|
}
|
|
5038
5207
|
const r = raw;
|
|
5039
5208
|
for (const key of ["body", "query", "params"]) {
|
|
5040
5209
|
if (r[key] !== void 0) {
|
|
5041
|
-
validateFieldMap(r[key], `${
|
|
5210
|
+
validateFieldMap(r[key], `${path43}.${key}`, errors);
|
|
5042
5211
|
}
|
|
5043
5212
|
}
|
|
5044
5213
|
}
|
|
5045
|
-
function validateFieldMap(raw,
|
|
5214
|
+
function validateFieldMap(raw, path43, errors) {
|
|
5046
5215
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5047
|
-
errors.push({ path:
|
|
5216
|
+
errors.push({ path: path43, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
5048
5217
|
return;
|
|
5049
5218
|
}
|
|
5050
5219
|
const map = raw;
|
|
5051
5220
|
for (const [k2, v2] of Object.entries(map)) {
|
|
5052
5221
|
if (typeof v2 !== "string") {
|
|
5053
|
-
errors.push({ path: `${
|
|
5222
|
+
errors.push({ path: `${path43}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
5054
5223
|
}
|
|
5055
5224
|
}
|
|
5056
5225
|
}
|
|
5057
|
-
function validateResponseError(raw,
|
|
5226
|
+
function validateResponseError(raw, path43, errors) {
|
|
5058
5227
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5059
|
-
errors.push({ path:
|
|
5228
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5060
5229
|
return;
|
|
5061
5230
|
}
|
|
5062
5231
|
const e = raw;
|
|
5063
5232
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5064
|
-
errors.push({ path: `${
|
|
5233
|
+
errors.push({ path: `${path43}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5065
5234
|
}
|
|
5066
|
-
requireNonEmptyString(e["code"], `${
|
|
5067
|
-
requireNonEmptyString(e["description"], `${
|
|
5235
|
+
requireNonEmptyString(e["code"], `${path43}.code`, errors);
|
|
5236
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5068
5237
|
}
|
|
5069
|
-
function validateBehavior(raw,
|
|
5238
|
+
function validateBehavior(raw, path43, errors) {
|
|
5070
5239
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5071
|
-
errors.push({ path:
|
|
5240
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5072
5241
|
return;
|
|
5073
5242
|
}
|
|
5074
5243
|
const b = raw;
|
|
5075
|
-
requireNonEmptyString(b["id"], `${
|
|
5076
|
-
requireNonEmptyString(b["description"], `${
|
|
5244
|
+
requireNonEmptyString(b["id"], `${path43}.id`, errors);
|
|
5245
|
+
requireNonEmptyString(b["description"], `${path43}.description`, errors);
|
|
5077
5246
|
if (b["constraints"] !== void 0) {
|
|
5078
5247
|
if (!Array.isArray(b["constraints"])) {
|
|
5079
|
-
errors.push({ path: `${
|
|
5248
|
+
errors.push({ path: `${path43}.constraints`, message: "Must be an array of strings if present" });
|
|
5080
5249
|
} else {
|
|
5081
5250
|
const cs2 = b["constraints"];
|
|
5082
5251
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5083
5252
|
if (typeof cs2[j2] !== "string") {
|
|
5084
|
-
errors.push({ path: `${
|
|
5253
|
+
errors.push({ path: `${path43}.constraints[${j2}]`, message: "Must be a string" });
|
|
5085
5254
|
}
|
|
5086
5255
|
}
|
|
5087
5256
|
}
|
|
5088
5257
|
}
|
|
5089
5258
|
}
|
|
5090
|
-
function validateComponent(raw,
|
|
5259
|
+
function validateComponent(raw, path43, errors) {
|
|
5091
5260
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5092
|
-
errors.push({ path:
|
|
5261
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5093
5262
|
return;
|
|
5094
5263
|
}
|
|
5095
5264
|
const c = raw;
|
|
5096
|
-
requireNonEmptyString(c["id"], `${
|
|
5097
|
-
requireNonEmptyString(c["name"], `${
|
|
5098
|
-
requireNonEmptyString(c["description"], `${
|
|
5265
|
+
requireNonEmptyString(c["id"], `${path43}.id`, errors);
|
|
5266
|
+
requireNonEmptyString(c["name"], `${path43}.name`, errors);
|
|
5267
|
+
requireNonEmptyString(c["description"], `${path43}.description`, errors);
|
|
5099
5268
|
if (c["props"] !== void 0) {
|
|
5100
5269
|
if (!Array.isArray(c["props"])) {
|
|
5101
|
-
errors.push({ path: `${
|
|
5270
|
+
errors.push({ path: `${path43}.props`, message: "Must be an array if present" });
|
|
5102
5271
|
} else {
|
|
5103
5272
|
const props = c["props"];
|
|
5104
5273
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5105
5274
|
const p = props[j2];
|
|
5106
5275
|
if (typeof p !== "object" || p === null) {
|
|
5107
|
-
errors.push({ path: `${
|
|
5276
|
+
errors.push({ path: `${path43}.props[${j2}]`, message: "Must be an object" });
|
|
5108
5277
|
continue;
|
|
5109
5278
|
}
|
|
5110
|
-
requireNonEmptyString(p["name"], `${
|
|
5111
|
-
requireNonEmptyString(p["type"], `${
|
|
5279
|
+
requireNonEmptyString(p["name"], `${path43}.props[${j2}].name`, errors);
|
|
5280
|
+
requireNonEmptyString(p["type"], `${path43}.props[${j2}].type`, errors);
|
|
5112
5281
|
if (typeof p["required"] !== "boolean") {
|
|
5113
|
-
errors.push({ path: `${
|
|
5282
|
+
errors.push({ path: `${path43}.props[${j2}].required`, message: "Must be boolean" });
|
|
5114
5283
|
}
|
|
5115
5284
|
}
|
|
5116
5285
|
}
|
|
5117
5286
|
}
|
|
5118
5287
|
if (c["events"] !== void 0) {
|
|
5119
5288
|
if (!Array.isArray(c["events"])) {
|
|
5120
|
-
errors.push({ path: `${
|
|
5289
|
+
errors.push({ path: `${path43}.events`, message: "Must be an array if present" });
|
|
5121
5290
|
} else {
|
|
5122
5291
|
const events = c["events"];
|
|
5123
5292
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5124
5293
|
const e = events[j2];
|
|
5125
5294
|
if (typeof e !== "object" || e === null) {
|
|
5126
|
-
errors.push({ path: `${
|
|
5295
|
+
errors.push({ path: `${path43}.events[${j2}]`, message: "Must be an object" });
|
|
5127
5296
|
continue;
|
|
5128
5297
|
}
|
|
5129
|
-
requireNonEmptyString(e["name"], `${
|
|
5298
|
+
requireNonEmptyString(e["name"], `${path43}.events[${j2}].name`, errors);
|
|
5130
5299
|
}
|
|
5131
5300
|
}
|
|
5132
5301
|
}
|
|
5133
5302
|
if (c["state"] !== void 0) {
|
|
5134
5303
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5135
|
-
errors.push({ path: `${
|
|
5304
|
+
errors.push({ path: `${path43}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5136
5305
|
}
|
|
5137
5306
|
}
|
|
5138
5307
|
if (c["apiCalls"] !== void 0) {
|
|
5139
5308
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5140
|
-
errors.push({ path: `${
|
|
5309
|
+
errors.push({ path: `${path43}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5141
5310
|
}
|
|
5142
5311
|
}
|
|
5143
5312
|
}
|
|
@@ -5199,10 +5368,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5199
5368
|
}
|
|
5200
5369
|
}
|
|
5201
5370
|
}
|
|
5202
|
-
function requireNonEmptyString(v2,
|
|
5371
|
+
function requireNonEmptyString(v2, path43, errors) {
|
|
5203
5372
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5204
5373
|
errors.push({
|
|
5205
|
-
path:
|
|
5374
|
+
path: path43,
|
|
5206
5375
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5207
5376
|
});
|
|
5208
5377
|
}
|
|
@@ -5413,13 +5582,18 @@ Output ONLY the corrected JSON. No explanation.`;
|
|
|
5413
5582
|
|
|
5414
5583
|
// core/token-budget.ts
|
|
5415
5584
|
import chalk5 from "chalk";
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5585
|
+
|
|
5586
|
+
// core/config-defaults.ts
|
|
5587
|
+
var DEFAULT_LOG_DIR = ".ai-spec-logs";
|
|
5588
|
+
var DEFAULT_VCR_DIR = ".ai-spec-vcr";
|
|
5589
|
+
var DEFAULT_BACKUP_DIR = ".ai-spec-backup";
|
|
5590
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5591
|
+
var DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
|
|
5592
|
+
var DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 3e4;
|
|
5593
|
+
var DEFAULT_MAX_FIX_FILE_CHARS = 6e4;
|
|
5594
|
+
var DEFAULT_DSL_MAX_RETRIES = 2;
|
|
5595
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5596
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5423
5597
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5424
5598
|
gemini: 9e5,
|
|
5425
5599
|
claude: 18e4,
|
|
@@ -5427,8 +5601,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5427
5601
|
deepseek: 6e4,
|
|
5428
5602
|
default: 1e5
|
|
5429
5603
|
};
|
|
5604
|
+
|
|
5605
|
+
// core/token-budget.ts
|
|
5606
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5607
|
+
function estimateTokens(text) {
|
|
5608
|
+
if (!text) return 0;
|
|
5609
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5610
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5611
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5612
|
+
}
|
|
5613
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5430
5614
|
function getDefaultBudget(providerName) {
|
|
5431
|
-
return
|
|
5615
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5432
5616
|
}
|
|
5433
5617
|
|
|
5434
5618
|
// core/safe-json.ts
|
|
@@ -5499,7 +5683,7 @@ function sanitizeDsl(raw) {
|
|
|
5499
5683
|
}
|
|
5500
5684
|
return dsl;
|
|
5501
5685
|
}
|
|
5502
|
-
var MAX_RETRIES =
|
|
5686
|
+
var MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
|
|
5503
5687
|
var DEFAULT_MAX_SPEC_CHARS = 12e3;
|
|
5504
5688
|
function dslFilePath(specFilePath) {
|
|
5505
5689
|
const dir = path7.dirname(specFilePath);
|
|
@@ -6223,12 +6407,11 @@ Shared component structure patterns:`);
|
|
|
6223
6407
|
// core/run-snapshot.ts
|
|
6224
6408
|
import * as fs10 from "fs-extra";
|
|
6225
6409
|
import * as path9 from "path";
|
|
6226
|
-
var BACKUP_DIR = ".ai-spec-backup";
|
|
6227
6410
|
var RunSnapshot = class {
|
|
6228
6411
|
constructor(workingDir, runId) {
|
|
6229
6412
|
this.workingDir = workingDir;
|
|
6230
6413
|
this.runId = runId;
|
|
6231
|
-
this.backupRoot = path9.join(workingDir,
|
|
6414
|
+
this.backupRoot = path9.join(workingDir, DEFAULT_BACKUP_DIR, runId);
|
|
6232
6415
|
}
|
|
6233
6416
|
backupRoot;
|
|
6234
6417
|
snapshotted = /* @__PURE__ */ new Set();
|
|
@@ -6284,7 +6467,6 @@ function getActiveSnapshot() {
|
|
|
6284
6467
|
import * as fs11 from "fs-extra";
|
|
6285
6468
|
import * as path10 from "path";
|
|
6286
6469
|
import chalk7 from "chalk";
|
|
6287
|
-
var LOG_DIR = ".ai-spec-logs";
|
|
6288
6470
|
function appendJsonlLine(filePath, record) {
|
|
6289
6471
|
try {
|
|
6290
6472
|
fs11.appendFileSync(filePath, JSON.stringify(record) + "\n");
|
|
@@ -6355,8 +6537,8 @@ var RunLogger = class {
|
|
|
6355
6537
|
this.workingDir = workingDir;
|
|
6356
6538
|
this.runId = runId;
|
|
6357
6539
|
this.startMs = Date.now();
|
|
6358
|
-
this.logPath = path10.join(workingDir,
|
|
6359
|
-
this.jsonlPath = path10.join(workingDir,
|
|
6540
|
+
this.logPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
|
|
6541
|
+
this.jsonlPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
|
|
6360
6542
|
this.log = {
|
|
6361
6543
|
runId,
|
|
6362
6544
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6690,6 +6872,7 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6690
6872
|
}
|
|
6691
6873
|
|
|
6692
6874
|
// core/code-generator.ts
|
|
6875
|
+
init_cli_ui();
|
|
6693
6876
|
var CodeGenerator = class {
|
|
6694
6877
|
constructor(provider, mode = "claude-code") {
|
|
6695
6878
|
this.provider = provider;
|
|
@@ -7138,6 +7321,7 @@ ${spec}
|
|
|
7138
7321
|
${constitutionSection}
|
|
7139
7322
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
7140
7323
|
${existingContent || "Output only the complete file content."}`;
|
|
7324
|
+
const fileSpinner = startSpinner(`${prefix}Generating ${chalk9.bold(item.file)}...`);
|
|
7141
7325
|
try {
|
|
7142
7326
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
7143
7327
|
const fileContent = stripCodeFences(raw);
|
|
@@ -7145,11 +7329,11 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
7145
7329
|
await fs12.ensureDir(path11.dirname(fullPath));
|
|
7146
7330
|
await fs12.writeFile(fullPath, fileContent, "utf-8");
|
|
7147
7331
|
getActiveLogger()?.fileWritten(item.file);
|
|
7148
|
-
|
|
7332
|
+
fileSpinner.succeed(`${existingContent ? chalk9.yellow("~") : chalk9.green("+")} ${chalk9.bold(item.file)}`);
|
|
7149
7333
|
successCount++;
|
|
7150
7334
|
writtenFiles.push(item.file);
|
|
7151
7335
|
} catch (err) {
|
|
7152
|
-
|
|
7336
|
+
fileSpinner.fail(`${chalk9.bold(item.file)} \u2014 ${err.message}`);
|
|
7153
7337
|
}
|
|
7154
7338
|
}
|
|
7155
7339
|
if (!taskLabel) {
|
|
@@ -7296,7 +7480,7 @@ ${context.routeSummary}
|
|
|
7296
7480
|
}
|
|
7297
7481
|
if (context.schema) {
|
|
7298
7482
|
parts.push(`=== Prisma Schema ===
|
|
7299
|
-
${context.schema.slice(0,
|
|
7483
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
7300
7484
|
`);
|
|
7301
7485
|
}
|
|
7302
7486
|
if (context.errorPatterns) {
|
|
@@ -7344,7 +7528,7 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
7344
7528
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
7345
7529
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
7346
7530
|
}
|
|
7347
|
-
var REVIEW_HISTORY_FILE =
|
|
7531
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
7348
7532
|
async function loadReviewHistory(projectRoot) {
|
|
7349
7533
|
const historyPath = path13.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
7350
7534
|
try {
|
|
@@ -7475,15 +7659,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
7475
7659
|
${codeContext}`;
|
|
7476
7660
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
7477
7661
|
console.log(chalk11.gray(" Pass 2/3: Implementation review..."));
|
|
7662
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
7478
7663
|
const history = await loadReviewHistory(this.projectRoot);
|
|
7479
7664
|
const historyContext = buildHistoryContext(history);
|
|
7480
7665
|
const implPrompt = `Review the implementation details of this change.
|
|
7481
7666
|
|
|
7482
|
-
=== Feature Spec ===
|
|
7483
|
-
${
|
|
7484
|
-
|
|
7485
|
-
=== Code ===
|
|
7486
|
-
${codeContext}
|
|
7667
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
7668
|
+
${specDigest}
|
|
7487
7669
|
|
|
7488
7670
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
7489
7671
|
${archReview}
|
|
@@ -7492,11 +7674,8 @@ ${historyContext}`;
|
|
|
7492
7674
|
console.log(chalk11.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
7493
7675
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
7494
7676
|
|
|
7495
|
-
=== Feature Spec ===
|
|
7496
|
-
${
|
|
7497
|
-
|
|
7498
|
-
=== Code ===
|
|
7499
|
-
${codeContext}
|
|
7677
|
+
=== Feature Spec (digest) ===
|
|
7678
|
+
${specDigest}
|
|
7500
7679
|
|
|
7501
7680
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
7502
7681
|
${archReview}
|
|
@@ -7570,8 +7749,8 @@ ${sep}
|
|
|
7570
7749
|
filesSection += `
|
|
7571
7750
|
|
|
7572
7751
|
=== ${filePath} ===
|
|
7573
|
-
${content.slice(0,
|
|
7574
|
-
if (content.length >
|
|
7752
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7753
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
7575
7754
|
... (truncated, ${content.length} chars total)`;
|
|
7576
7755
|
} catch {
|
|
7577
7756
|
filesSection += `
|
|
@@ -8175,8 +8354,9 @@ import chalk15 from "chalk";
|
|
|
8175
8354
|
import { execSync as execSync5 } from "child_process";
|
|
8176
8355
|
import * as fs18 from "fs-extra";
|
|
8177
8356
|
import * as path17 from "path";
|
|
8178
|
-
|
|
8179
|
-
var
|
|
8357
|
+
init_cli_ui();
|
|
8358
|
+
var MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
|
|
8359
|
+
var MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
|
|
8180
8360
|
function runCommand(cmd, cwd) {
|
|
8181
8361
|
try {
|
|
8182
8362
|
const output = execSync5(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
|
|
@@ -8366,16 +8546,17 @@ ${errorSummary}
|
|
|
8366
8546
|
${fileContent}
|
|
8367
8547
|
|
|
8368
8548
|
Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
|
|
8549
|
+
const fixSpinner = startSpinner(`Fixing ${chalk15.bold(file)} (${fileErrors.length} error(s))...`);
|
|
8369
8550
|
try {
|
|
8370
8551
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
8371
8552
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
8372
8553
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
8373
8554
|
await fs18.writeFile(fullPath, fixed, "utf-8");
|
|
8374
8555
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
8375
|
-
|
|
8556
|
+
fixSpinner.succeed(`Auto-fixed: ${file}`);
|
|
8376
8557
|
} catch (err) {
|
|
8377
8558
|
results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
|
|
8378
|
-
|
|
8559
|
+
fixSpinner.fail(`Could not auto-fix: ${file}`);
|
|
8379
8560
|
}
|
|
8380
8561
|
}
|
|
8381
8562
|
return results;
|
|
@@ -9384,8 +9565,8 @@ async function findLatestDslFile(projectDir) {
|
|
|
9384
9565
|
const entries = await fs22.readdir(dir);
|
|
9385
9566
|
for (const entry of entries) {
|
|
9386
9567
|
const abs = path21.join(dir, entry);
|
|
9387
|
-
const
|
|
9388
|
-
if (
|
|
9568
|
+
const stat5 = await fs22.stat(abs);
|
|
9569
|
+
if (stat5.isDirectory()) {
|
|
9389
9570
|
await scan(abs);
|
|
9390
9571
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9391
9572
|
allFiles.push(abs);
|
|
@@ -11123,7 +11304,7 @@ ${optionsText}`;
|
|
|
11123
11304
|
import { createHash as createHash2 } from "crypto";
|
|
11124
11305
|
import * as fs24 from "fs-extra";
|
|
11125
11306
|
import * as path23 from "path";
|
|
11126
|
-
var VCR_DIR =
|
|
11307
|
+
var VCR_DIR = DEFAULT_VCR_DIR;
|
|
11127
11308
|
var VcrRecordingProvider = class {
|
|
11128
11309
|
constructor(inner) {
|
|
11129
11310
|
this.inner = inner;
|
|
@@ -11251,6 +11432,7 @@ async function listVcrRecordings(workingDir) {
|
|
|
11251
11432
|
}
|
|
11252
11433
|
|
|
11253
11434
|
// cli/pipeline/single-repo.ts
|
|
11435
|
+
init_cli_ui();
|
|
11254
11436
|
async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
11255
11437
|
const specProviderName = opts.provider || config2.provider || "gemini";
|
|
11256
11438
|
const specModelName = opts.model || config2.model || DEFAULT_MODELS[specProviderName];
|
|
@@ -11313,7 +11495,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11313
11495
|
console.log(chalk25.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11314
11496
|
}
|
|
11315
11497
|
} else {
|
|
11316
|
-
|
|
11498
|
+
const constitutionSpinner = startSpinner("Constitution not found \u2014 auto-generating...");
|
|
11317
11499
|
try {
|
|
11318
11500
|
const constitutionGen = new ConstitutionGenerator(
|
|
11319
11501
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -11321,9 +11503,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11321
11503
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
11322
11504
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
11323
11505
|
context.constitution = constitutionContent;
|
|
11324
|
-
|
|
11506
|
+
constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
|
|
11325
11507
|
} catch (err) {
|
|
11326
|
-
|
|
11508
|
+
constitutionSpinner.fail(`Constitution auto-generation failed (${err.message}), continuing without it.`);
|
|
11327
11509
|
}
|
|
11328
11510
|
}
|
|
11329
11511
|
let architectureDecision;
|
|
@@ -11354,17 +11536,18 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11354
11536
|
let initialSpec;
|
|
11355
11537
|
let initialTasks = [];
|
|
11356
11538
|
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
11539
|
+
const specSpinner = startStage("spec_gen", `Generating spec with ${specProviderName}/${specModelName}...`);
|
|
11357
11540
|
try {
|
|
11358
11541
|
if (opts.skipTasks) {
|
|
11359
11542
|
const { SpecGenerator: SpecGenerator2 } = await Promise.resolve().then(() => (init_spec_generator(), spec_generator_exports));
|
|
11360
11543
|
const generator = new SpecGenerator2(specProvider);
|
|
11361
11544
|
initialSpec = await generator.generateSpec(idea, context, architectureDecision);
|
|
11362
|
-
|
|
11545
|
+
specSpinner.succeed("Spec generated.");
|
|
11363
11546
|
} else {
|
|
11364
11547
|
const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
|
|
11365
11548
|
initialSpec = result.spec;
|
|
11366
11549
|
initialTasks = result.tasks;
|
|
11367
|
-
|
|
11550
|
+
specSpinner.succeed("Spec generated.");
|
|
11368
11551
|
if (initialTasks.length > 0) {
|
|
11369
11552
|
console.log(chalk25.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
11370
11553
|
} else {
|
|
@@ -11373,8 +11556,8 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11373
11556
|
}
|
|
11374
11557
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
11375
11558
|
} catch (err) {
|
|
11559
|
+
specSpinner.fail(`Spec generation failed: ${err.message}`);
|
|
11376
11560
|
runLogger.stageFail("spec_gen", err.message);
|
|
11377
|
-
console.error(chalk25.red(" \u2718 Spec generation failed:"), err);
|
|
11378
11561
|
process.exit(1);
|
|
11379
11562
|
}
|
|
11380
11563
|
let finalSpec;
|
|
@@ -11396,7 +11579,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11396
11579
|
console.log(chalk25.blue("\n[3.4/6] Spec quality assessment..."));
|
|
11397
11580
|
}
|
|
11398
11581
|
runLogger.stageStart("spec_assess");
|
|
11582
|
+
const assessSpinner = startStage("spec_assess", "Evaluating spec quality...");
|
|
11399
11583
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
11584
|
+
assessSpinner.stop();
|
|
11400
11585
|
if (assessment) {
|
|
11401
11586
|
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
11402
11587
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
@@ -11488,21 +11673,24 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11488
11673
|
console.log(chalk25.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
11489
11674
|
console.log(chalk25.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
11490
11675
|
runLogger.stageStart("dsl_extract");
|
|
11676
|
+
const dslSpinner = startStage("dsl_extract", "Extracting DSL from spec...");
|
|
11491
11677
|
try {
|
|
11492
11678
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
11493
|
-
if (isFrontend)
|
|
11679
|
+
if (isFrontend) {
|
|
11680
|
+
dslSpinner.update("\u{1F517} Extracting DSL (frontend ComponentSpec mode)...");
|
|
11681
|
+
}
|
|
11494
11682
|
const dslExtractor = new DslExtractor(specProvider);
|
|
11495
11683
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
11496
11684
|
if (extractedDsl) {
|
|
11497
11685
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
11498
|
-
|
|
11686
|
+
dslSpinner.succeed("DSL extracted and validated.");
|
|
11499
11687
|
} else {
|
|
11500
11688
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
11501
|
-
|
|
11689
|
+
dslSpinner.fail("DSL skipped \u2014 codegen will use Spec + Tasks only.");
|
|
11502
11690
|
}
|
|
11503
11691
|
} catch (err) {
|
|
11504
11692
|
runLogger.stageFail("dsl_extract", err.message);
|
|
11505
|
-
|
|
11693
|
+
dslSpinner.fail(`DSL extraction error: ${err.message} \u2014 continuing without DSL.`);
|
|
11506
11694
|
}
|
|
11507
11695
|
}
|
|
11508
11696
|
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
@@ -11677,6 +11865,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11677
11865
|
if (!opts.skipReview) {
|
|
11678
11866
|
console.log(chalk25.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
11679
11867
|
runLogger.stageStart("review");
|
|
11868
|
+
const reviewSpinner = startStage("review", "Running 3-pass code review...");
|
|
11680
11869
|
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
11681
11870
|
const savedSpec = await fs25.readFile(specFile, "utf-8");
|
|
11682
11871
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
@@ -11684,6 +11873,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11684
11873
|
} else {
|
|
11685
11874
|
reviewResult = await reviewer.reviewCode(savedSpec, specFile);
|
|
11686
11875
|
}
|
|
11876
|
+
reviewSpinner.succeed("Code review complete.");
|
|
11687
11877
|
runLogger.stageEnd("review");
|
|
11688
11878
|
const complianceScore = extractComplianceScore(reviewResult);
|
|
11689
11879
|
const missingCount = extractMissingCount(reviewResult);
|
|
@@ -11812,7 +12002,147 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11812
12002
|
}
|
|
11813
12003
|
}
|
|
11814
12004
|
|
|
12005
|
+
// core/repo-store.ts
|
|
12006
|
+
import * as fs26 from "fs-extra";
|
|
12007
|
+
import * as path25 from "path";
|
|
12008
|
+
import * as os5 from "os";
|
|
12009
|
+
var REPO_STORE_FILE = path25.join(os5.homedir(), ".ai-spec-repos.json");
|
|
12010
|
+
async function readStore2() {
|
|
12011
|
+
try {
|
|
12012
|
+
if (await fs26.pathExists(REPO_STORE_FILE)) {
|
|
12013
|
+
return await fs26.readJson(REPO_STORE_FILE);
|
|
12014
|
+
}
|
|
12015
|
+
} catch (err) {
|
|
12016
|
+
console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${err.message}.`);
|
|
12017
|
+
}
|
|
12018
|
+
return { repos: [] };
|
|
12019
|
+
}
|
|
12020
|
+
async function writeStore2(store) {
|
|
12021
|
+
await fs26.ensureFile(REPO_STORE_FILE);
|
|
12022
|
+
await fs26.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
|
|
12023
|
+
}
|
|
12024
|
+
async function getRegisteredRepos() {
|
|
12025
|
+
const store = await readStore2();
|
|
12026
|
+
return store.repos;
|
|
12027
|
+
}
|
|
12028
|
+
async function registerRepo(repo) {
|
|
12029
|
+
const store = await readStore2();
|
|
12030
|
+
const idx = store.repos.findIndex((r) => r.path === repo.path);
|
|
12031
|
+
if (idx >= 0) {
|
|
12032
|
+
store.repos[idx] = repo;
|
|
12033
|
+
} else {
|
|
12034
|
+
store.repos.push(repo);
|
|
12035
|
+
}
|
|
12036
|
+
await writeStore2(store);
|
|
12037
|
+
}
|
|
12038
|
+
|
|
11815
12039
|
// cli/commands/create.ts
|
|
12040
|
+
init_spec_generator();
|
|
12041
|
+
async function promptRepoRole() {
|
|
12042
|
+
return select7({
|
|
12043
|
+
message: "What type of repo is this?",
|
|
12044
|
+
choices: [
|
|
12045
|
+
{ name: "Frontend", value: "frontend" },
|
|
12046
|
+
{ name: "Backend", value: "backend" },
|
|
12047
|
+
{ name: "Mobile", value: "mobile" },
|
|
12048
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12049
|
+
]
|
|
12050
|
+
});
|
|
12051
|
+
}
|
|
12052
|
+
async function quickRegisterRepo(provider) {
|
|
12053
|
+
const roleOverride = await promptRepoRole();
|
|
12054
|
+
const roleLabels = {
|
|
12055
|
+
frontend: "frontend",
|
|
12056
|
+
backend: "backend",
|
|
12057
|
+
mobile: "mobile",
|
|
12058
|
+
shared: "shared"
|
|
12059
|
+
};
|
|
12060
|
+
const raw = await input2({
|
|
12061
|
+
message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
|
|
12062
|
+
validate: (v2) => {
|
|
12063
|
+
const trimmed = v2.trim();
|
|
12064
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12065
|
+
if (!path26.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12066
|
+
return true;
|
|
12067
|
+
}
|
|
12068
|
+
});
|
|
12069
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12070
|
+
const resolved = path26.resolve(cleaned);
|
|
12071
|
+
if (!await fs27.pathExists(resolved)) {
|
|
12072
|
+
console.log(chalk26.red(` Path does not exist: ${resolved}`));
|
|
12073
|
+
return quickRegisterRepo(provider);
|
|
12074
|
+
}
|
|
12075
|
+
const { type, role: detectedRole } = await detectRepoType(resolved);
|
|
12076
|
+
const role = roleOverride ?? detectedRole;
|
|
12077
|
+
const repoName = path26.basename(resolved);
|
|
12078
|
+
console.log(chalk26.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12079
|
+
const constitutionPath = path26.join(resolved, CONSTITUTION_FILE);
|
|
12080
|
+
let hasConstitution = await fs27.pathExists(constitutionPath);
|
|
12081
|
+
if (!hasConstitution) {
|
|
12082
|
+
console.log(chalk26.blue(` Generating constitution for ${repoName}...`));
|
|
12083
|
+
try {
|
|
12084
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12085
|
+
const content = await gen.generate(resolved);
|
|
12086
|
+
await gen.saveConstitution(resolved, content);
|
|
12087
|
+
hasConstitution = true;
|
|
12088
|
+
console.log(chalk26.green(` \u2714 Constitution saved`));
|
|
12089
|
+
} catch (err) {
|
|
12090
|
+
console.log(chalk26.yellow(` \u26A0 Constitution failed: ${err.message}`));
|
|
12091
|
+
}
|
|
12092
|
+
}
|
|
12093
|
+
const entry = {
|
|
12094
|
+
name: repoName,
|
|
12095
|
+
path: resolved,
|
|
12096
|
+
type,
|
|
12097
|
+
role,
|
|
12098
|
+
hasConstitution,
|
|
12099
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12100
|
+
};
|
|
12101
|
+
await registerRepo(entry);
|
|
12102
|
+
console.log(chalk26.green(` \u2714 Repo registered: ${repoName}`));
|
|
12103
|
+
return entry;
|
|
12104
|
+
}
|
|
12105
|
+
async function selectRepos(registeredRepos, provider) {
|
|
12106
|
+
const ADD_NEW = "__add_new__";
|
|
12107
|
+
const choices = [
|
|
12108
|
+
...registeredRepos.map((r) => ({
|
|
12109
|
+
name: `${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`,
|
|
12110
|
+
value: r.path
|
|
12111
|
+
})),
|
|
12112
|
+
{ name: chalk26.cyan("+ Add new repo"), value: ADD_NEW }
|
|
12113
|
+
];
|
|
12114
|
+
const selected = await checkbox({
|
|
12115
|
+
message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
|
|
12116
|
+
choices,
|
|
12117
|
+
required: true
|
|
12118
|
+
});
|
|
12119
|
+
const result = [];
|
|
12120
|
+
for (const val of selected) {
|
|
12121
|
+
if (val === ADD_NEW) {
|
|
12122
|
+
const newRepo = await quickRegisterRepo(provider);
|
|
12123
|
+
result.push(newRepo);
|
|
12124
|
+
} else {
|
|
12125
|
+
const repo = registeredRepos.find((r) => r.path === val);
|
|
12126
|
+
if (repo) result.push(repo);
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
if (result.length === 0) {
|
|
12130
|
+
console.log(chalk26.yellow(" No repos selected. Please select at least one."));
|
|
12131
|
+
return selectRepos(registeredRepos, provider);
|
|
12132
|
+
}
|
|
12133
|
+
return result;
|
|
12134
|
+
}
|
|
12135
|
+
function buildWorkspaceConfig(repos) {
|
|
12136
|
+
return {
|
|
12137
|
+
name: "ai-spec-workspace",
|
|
12138
|
+
repos: repos.map((r) => ({
|
|
12139
|
+
name: r.name,
|
|
12140
|
+
path: r.path,
|
|
12141
|
+
type: r.type,
|
|
12142
|
+
role: r.role
|
|
12143
|
+
}))
|
|
12144
|
+
};
|
|
12145
|
+
}
|
|
11816
12146
|
function registerCreate(program2) {
|
|
11817
12147
|
program2.command("create").description("Generate a feature spec and kick off code generation").argument("[idea]", "Feature idea in natural language (prompted if omitted)").option(
|
|
11818
12148
|
"--provider <name>",
|
|
@@ -11835,25 +12165,76 @@ function registerCreate(program2) {
|
|
|
11835
12165
|
});
|
|
11836
12166
|
}
|
|
11837
12167
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11838
|
-
const
|
|
11839
|
-
if (
|
|
12168
|
+
const existingWorkspaceConfig = await workspaceLoader.load();
|
|
12169
|
+
if (existingWorkspaceConfig) {
|
|
11840
12170
|
console.log(chalk26.cyan(`
|
|
11841
|
-
[Workspace] Detected workspace: ${
|
|
11842
|
-
console.log(chalk26.gray(` Repos: ${
|
|
11843
|
-
const pipelineResults = await runMultiRepoPipeline(idea,
|
|
12171
|
+
[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
|
|
12172
|
+
console.log(chalk26.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
12173
|
+
const pipelineResults = await runMultiRepoPipeline(idea, existingWorkspaceConfig, opts, currentDir, config2);
|
|
11844
12174
|
if (opts.serve) {
|
|
11845
12175
|
await handleAutoServe(pipelineResults);
|
|
11846
12176
|
}
|
|
11847
12177
|
return;
|
|
11848
12178
|
}
|
|
11849
|
-
|
|
12179
|
+
const providerName = opts.provider || config2.provider || "gemini";
|
|
12180
|
+
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
12181
|
+
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
12182
|
+
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
12183
|
+
const registeredRepos = await getRegisteredRepos();
|
|
12184
|
+
if (registeredRepos.length === 0) {
|
|
12185
|
+
console.log(chalk26.yellow("\n No repos registered. Please register repos first."));
|
|
12186
|
+
console.log(chalk26.gray(" Run: ai-spec init"));
|
|
12187
|
+
console.log(chalk26.gray(" Or add a repo now:\n"));
|
|
12188
|
+
const addNow = await select7({
|
|
12189
|
+
message: "Add a repo now?",
|
|
12190
|
+
choices: [
|
|
12191
|
+
{ name: "Yes \u2014 register a repo and continue", value: "yes" },
|
|
12192
|
+
{ name: "No \u2014 exit", value: "no" }
|
|
12193
|
+
]
|
|
12194
|
+
});
|
|
12195
|
+
if (addNow === "no") {
|
|
12196
|
+
process.exit(0);
|
|
12197
|
+
}
|
|
12198
|
+
const newRepo = await quickRegisterRepo(specProvider);
|
|
12199
|
+
registeredRepos.push(newRepo);
|
|
12200
|
+
}
|
|
12201
|
+
const validRepos = [];
|
|
12202
|
+
for (const r of registeredRepos) {
|
|
12203
|
+
if (await fs27.pathExists(r.path)) {
|
|
12204
|
+
validRepos.push(r);
|
|
12205
|
+
} else {
|
|
12206
|
+
console.log(chalk26.yellow(` \u26A0 Skipping ${r.name}: path not found (${r.path})`));
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
if (validRepos.length === 0) {
|
|
12210
|
+
console.log(chalk26.red(" No valid repos available. Run: ai-spec init"));
|
|
12211
|
+
process.exit(1);
|
|
12212
|
+
}
|
|
12213
|
+
const selectedRepos = await selectRepos(validRepos, specProvider);
|
|
12214
|
+
if (selectedRepos.length === 1) {
|
|
12215
|
+
const repo = selectedRepos[0];
|
|
12216
|
+
console.log(chalk26.cyan(`
|
|
12217
|
+
[Repo] ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12218
|
+
await runSingleRepoPipeline(idea, opts, repo.path, config2);
|
|
12219
|
+
} else {
|
|
12220
|
+
const workspaceConfig = buildWorkspaceConfig(selectedRepos);
|
|
12221
|
+
console.log(chalk26.cyan(`
|
|
12222
|
+
[Workspace] ${selectedRepos.length} repo(s) selected:`));
|
|
12223
|
+
for (const repo of selectedRepos) {
|
|
12224
|
+
console.log(chalk26.gray(` ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12225
|
+
}
|
|
12226
|
+
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
12227
|
+
if (opts.serve) {
|
|
12228
|
+
await handleAutoServe(pipelineResults);
|
|
12229
|
+
}
|
|
12230
|
+
}
|
|
11850
12231
|
});
|
|
11851
12232
|
}
|
|
11852
12233
|
|
|
11853
12234
|
// cli/commands/review.ts
|
|
11854
12235
|
init_spec_generator();
|
|
11855
|
-
import * as
|
|
11856
|
-
import * as
|
|
12236
|
+
import * as path27 from "path";
|
|
12237
|
+
import * as fs28 from "fs-extra";
|
|
11857
12238
|
import chalk27 from "chalk";
|
|
11858
12239
|
function registerReview(program2) {
|
|
11859
12240
|
program2.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(
|
|
@@ -11870,17 +12251,17 @@ function registerReview(program2) {
|
|
|
11870
12251
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
11871
12252
|
let specContent = "";
|
|
11872
12253
|
let resolvedSpecFile;
|
|
11873
|
-
if (specFile && await
|
|
11874
|
-
specContent = await
|
|
12254
|
+
if (specFile && await fs28.pathExists(specFile)) {
|
|
12255
|
+
specContent = await fs28.readFile(specFile, "utf-8");
|
|
11875
12256
|
resolvedSpecFile = specFile;
|
|
11876
12257
|
console.log(chalk27.gray(`Using spec: ${specFile}`));
|
|
11877
12258
|
} else {
|
|
11878
|
-
const specsDir =
|
|
11879
|
-
if (await
|
|
11880
|
-
const files = (await
|
|
12259
|
+
const specsDir = path27.join(currentDir, "specs");
|
|
12260
|
+
if (await fs28.pathExists(specsDir)) {
|
|
12261
|
+
const files = (await fs28.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
11881
12262
|
if (files.length > 0) {
|
|
11882
|
-
const latest =
|
|
11883
|
-
specContent = await
|
|
12263
|
+
const latest = path27.join(specsDir, files[0]);
|
|
12264
|
+
specContent = await fs28.readFile(latest, "utf-8");
|
|
11884
12265
|
resolvedSpecFile = latest;
|
|
11885
12266
|
console.log(chalk27.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
11886
12267
|
}
|
|
@@ -11896,9 +12277,10 @@ function registerReview(program2) {
|
|
|
11896
12277
|
|
|
11897
12278
|
// cli/commands/init.ts
|
|
11898
12279
|
init_spec_generator();
|
|
11899
|
-
import * as
|
|
11900
|
-
import * as
|
|
12280
|
+
import * as path28 from "path";
|
|
12281
|
+
import * as fs29 from "fs-extra";
|
|
11901
12282
|
import chalk28 from "chalk";
|
|
12283
|
+
import { input as input3, select as select8, confirm as confirm2 } from "@inquirer/prompts";
|
|
11902
12284
|
|
|
11903
12285
|
// prompts/global-constitution.prompt.ts
|
|
11904
12286
|
var globalConstitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided multi-project context and generate a "Global Constitution" \u2014 a team-level baseline document that captures cross-project rules, shared conventions, and universal constraints that every repository in this workspace must follow.
|
|
@@ -11959,240 +12341,139 @@ ${summary}
|
|
|
11959
12341
|
return parts.join("\n");
|
|
11960
12342
|
}
|
|
11961
12343
|
|
|
11962
|
-
//
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
"expo",
|
|
11980
|
-
// DB / ORM
|
|
11981
|
-
"prisma",
|
|
11982
|
-
"@prisma/client",
|
|
11983
|
-
"mongoose",
|
|
11984
|
-
"typeorm",
|
|
11985
|
-
"sequelize",
|
|
11986
|
-
"drizzle-orm",
|
|
11987
|
-
// Auth
|
|
11988
|
-
"jsonwebtoken",
|
|
11989
|
-
"passport",
|
|
11990
|
-
"next-auth",
|
|
11991
|
-
"@clerk/nextjs",
|
|
11992
|
-
// Build / Lang
|
|
11993
|
-
"typescript",
|
|
11994
|
-
"vite",
|
|
11995
|
-
"webpack",
|
|
11996
|
-
"esbuild",
|
|
11997
|
-
"turbo",
|
|
11998
|
-
// Testing
|
|
11999
|
-
"jest",
|
|
12000
|
-
"vitest",
|
|
12001
|
-
"mocha",
|
|
12002
|
-
"cypress",
|
|
12003
|
-
"playwright",
|
|
12004
|
-
// Infra
|
|
12005
|
-
"redis",
|
|
12006
|
-
"bull",
|
|
12007
|
-
"socket.io",
|
|
12008
|
-
"graphql",
|
|
12009
|
-
"@trpc/server"
|
|
12010
|
-
];
|
|
12011
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12012
|
-
"node_modules",
|
|
12013
|
-
".git",
|
|
12014
|
-
".svn",
|
|
12015
|
-
"dist",
|
|
12016
|
-
"build",
|
|
12017
|
-
"out",
|
|
12018
|
-
".next",
|
|
12019
|
-
".nuxt",
|
|
12020
|
-
"coverage",
|
|
12021
|
-
".turbo",
|
|
12022
|
-
".cache",
|
|
12023
|
-
"__pycache__",
|
|
12024
|
-
"vendor",
|
|
12025
|
-
".ai-spec-vcr",
|
|
12026
|
-
".ai-spec-logs",
|
|
12027
|
-
"specs"
|
|
12028
|
-
]);
|
|
12029
|
-
var MANIFEST_FILES = [
|
|
12030
|
-
"package.json",
|
|
12031
|
-
"go.mod",
|
|
12032
|
-
"Cargo.toml",
|
|
12033
|
-
"pom.xml",
|
|
12034
|
-
"build.gradle",
|
|
12035
|
-
"build.gradle.kts",
|
|
12036
|
-
"requirements.txt",
|
|
12037
|
-
"pyproject.toml",
|
|
12038
|
-
"setup.py",
|
|
12039
|
-
"composer.json"
|
|
12040
|
-
];
|
|
12041
|
-
async function isProjectRoot(absPath) {
|
|
12042
|
-
for (const manifest of MANIFEST_FILES) {
|
|
12043
|
-
if (await fs27.pathExists(path26.join(absPath, manifest))) return true;
|
|
12044
|
-
}
|
|
12045
|
-
return false;
|
|
12344
|
+
// cli/commands/init.ts
|
|
12345
|
+
var ROLE_LABELS = {
|
|
12346
|
+
frontend: "frontend",
|
|
12347
|
+
backend: "backend",
|
|
12348
|
+
mobile: "mobile",
|
|
12349
|
+
shared: "shared"
|
|
12350
|
+
};
|
|
12351
|
+
async function promptRepoRole2() {
|
|
12352
|
+
return select8({
|
|
12353
|
+
message: "What type of repo is this?",
|
|
12354
|
+
choices: [
|
|
12355
|
+
{ name: "Frontend", value: "frontend" },
|
|
12356
|
+
{ name: "Backend", value: "backend" },
|
|
12357
|
+
{ name: "Mobile", value: "mobile" },
|
|
12358
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12359
|
+
]
|
|
12360
|
+
});
|
|
12046
12361
|
}
|
|
12047
|
-
async function
|
|
12048
|
-
const
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
}
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12362
|
+
async function promptRepoPath(role) {
|
|
12363
|
+
const label = ROLE_LABELS[role];
|
|
12364
|
+
const raw = await input3({
|
|
12365
|
+
message: `Enter your ${label} repo path (absolute path):`,
|
|
12366
|
+
validate: (v2) => {
|
|
12367
|
+
const trimmed = v2.trim();
|
|
12368
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12369
|
+
if (!path28.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12370
|
+
return true;
|
|
12371
|
+
}
|
|
12372
|
+
});
|
|
12373
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12374
|
+
const resolved = path28.resolve(cleaned);
|
|
12375
|
+
if (!await fs29.pathExists(resolved)) {
|
|
12376
|
+
console.log(chalk28.red(` Path does not exist: ${resolved}`));
|
|
12377
|
+
return promptRepoPath(role);
|
|
12378
|
+
}
|
|
12379
|
+
const stat5 = await fs29.stat(resolved);
|
|
12380
|
+
if (!stat5.isDirectory()) {
|
|
12381
|
+
console.log(chalk28.red(` Not a directory: ${resolved}`));
|
|
12382
|
+
return promptRepoPath(role);
|
|
12383
|
+
}
|
|
12384
|
+
return resolved;
|
|
12385
|
+
}
|
|
12386
|
+
async function registerSingleRepo(repoPath, provider, roleOverride) {
|
|
12387
|
+
const { type, role: detectedRole } = await detectRepoType(repoPath);
|
|
12388
|
+
const role = roleOverride ?? detectedRole;
|
|
12389
|
+
const repoName = path28.basename(repoPath);
|
|
12390
|
+
console.log(chalk28.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12391
|
+
const constitutionPath = path28.join(repoPath, CONSTITUTION_FILE);
|
|
12392
|
+
let hasConstitution = await fs29.pathExists(constitutionPath);
|
|
12393
|
+
if (!hasConstitution) {
|
|
12394
|
+
console.log(chalk28.blue(` Generating project constitution for ${repoName}...`));
|
|
12395
|
+
try {
|
|
12396
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12397
|
+
const content = await gen.generate(repoPath);
|
|
12398
|
+
await gen.saveConstitution(repoPath, content);
|
|
12399
|
+
hasConstitution = true;
|
|
12400
|
+
console.log(chalk28.green(` \u2714 Constitution saved: ${constitutionPath}`));
|
|
12401
|
+
} catch (err) {
|
|
12402
|
+
console.log(chalk28.yellow(` \u26A0 Constitution generation failed: ${err.message}`));
|
|
12403
|
+
}
|
|
12404
|
+
} else {
|
|
12405
|
+
console.log(chalk28.green(` \u2714 Constitution already exists: ${constitutionPath}`));
|
|
12406
|
+
}
|
|
12407
|
+
const entry = {
|
|
12408
|
+
name: repoName,
|
|
12409
|
+
path: repoPath,
|
|
12410
|
+
type,
|
|
12411
|
+
role,
|
|
12412
|
+
hasConstitution,
|
|
12413
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12065
12414
|
};
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
let entries;
|
|
12415
|
+
await registerRepo(entry);
|
|
12416
|
+
return entry;
|
|
12417
|
+
}
|
|
12418
|
+
async function buildProjectSummaries(repos) {
|
|
12419
|
+
const summaries = [];
|
|
12420
|
+
for (const repo of repos) {
|
|
12421
|
+
if (!await fs29.pathExists(repo.path)) continue;
|
|
12422
|
+
const lines = [
|
|
12423
|
+
`Type: ${repo.type} (${repo.role})`
|
|
12424
|
+
];
|
|
12077
12425
|
try {
|
|
12078
|
-
|
|
12426
|
+
const loader = new ContextLoader(repo.path);
|
|
12427
|
+
const ctx = await loader.loadProjectContext();
|
|
12428
|
+
lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
|
|
12429
|
+
lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
|
|
12079
12430
|
} catch {
|
|
12080
|
-
|
|
12431
|
+
lines.push(`Tech stack: ${repo.type}`);
|
|
12081
12432
|
}
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
const gitStat = await fs27.stat(gitPath);
|
|
12089
|
-
if (gitStat.isFile()) continue;
|
|
12090
|
-
}
|
|
12091
|
-
if (await isProjectRoot(childAbs)) {
|
|
12092
|
-
found.push(path26.relative(rootDir, childAbs));
|
|
12093
|
-
} else {
|
|
12094
|
-
await walk(childAbs, depth + 1);
|
|
12433
|
+
if (repo.hasConstitution) {
|
|
12434
|
+
try {
|
|
12435
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12436
|
+
const raw = await fs29.readFile(constitutionPath, "utf-8");
|
|
12437
|
+
lines.push("", "Constitution excerpt:", raw.slice(0, 2e3));
|
|
12438
|
+
} catch {
|
|
12095
12439
|
}
|
|
12096
12440
|
}
|
|
12441
|
+
summaries.push({ name: repo.name, summary: lines.join("\n") });
|
|
12097
12442
|
}
|
|
12098
|
-
|
|
12099
|
-
return found;
|
|
12443
|
+
return summaries;
|
|
12100
12444
|
}
|
|
12101
|
-
async function
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12445
|
+
async function generateGlobalConstitution(provider, repos, currentDir) {
|
|
12446
|
+
console.log(chalk28.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12447
|
+
console.log(chalk28.gray(` Based on ${repos.length} registered repo(s)`));
|
|
12448
|
+
const summaries = await buildProjectSummaries(repos);
|
|
12449
|
+
if (summaries.length === 0) {
|
|
12450
|
+
console.log(chalk28.yellow(" No valid repos found \u2014 skipping global constitution."));
|
|
12451
|
+
return;
|
|
12107
12452
|
}
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
}
|
|
12114
|
-
|
|
12115
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12116
|
-
const existing = await loadIndex(scanRoot);
|
|
12117
|
-
const existingMap = new Map(
|
|
12118
|
-
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
12119
|
-
);
|
|
12120
|
-
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
12121
|
-
const added = [];
|
|
12122
|
-
const updated = [];
|
|
12123
|
-
const unchanged = [];
|
|
12124
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
12125
|
-
for (const relPath of discoveredPaths) {
|
|
12126
|
-
const absPath = path26.join(scanRoot, relPath);
|
|
12127
|
-
seenPaths.add(relPath);
|
|
12128
|
-
const { type, role } = await detectRepoType(absPath);
|
|
12129
|
-
const techStack = await extractTechStack(absPath, type);
|
|
12130
|
-
const hasConstitution = await fs27.pathExists(path26.join(absPath, CONSTITUTION_FILE));
|
|
12131
|
-
const hasWorkspace = await fs27.pathExists(path26.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
12132
|
-
const name = path26.basename(relPath);
|
|
12133
|
-
const prev = existingMap.get(relPath);
|
|
12134
|
-
if (!prev) {
|
|
12135
|
-
const entry = {
|
|
12136
|
-
name,
|
|
12137
|
-
path: relPath,
|
|
12138
|
-
type,
|
|
12139
|
-
role,
|
|
12140
|
-
techStack,
|
|
12141
|
-
hasConstitution,
|
|
12142
|
-
hasWorkspace,
|
|
12143
|
-
firstSeen: now,
|
|
12144
|
-
lastSeen: now
|
|
12145
|
-
};
|
|
12146
|
-
added.push(entry);
|
|
12147
|
-
existingMap.set(relPath, entry);
|
|
12148
|
-
} else {
|
|
12149
|
-
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
12150
|
-
const entry = {
|
|
12151
|
-
...prev,
|
|
12152
|
-
type,
|
|
12153
|
-
role,
|
|
12154
|
-
techStack,
|
|
12155
|
-
hasConstitution,
|
|
12156
|
-
hasWorkspace,
|
|
12157
|
-
lastSeen: now,
|
|
12158
|
-
missing: void 0
|
|
12159
|
-
// clear missing flag if it came back
|
|
12160
|
-
};
|
|
12161
|
-
existingMap.set(relPath, entry);
|
|
12162
|
-
if (changed) {
|
|
12163
|
-
updated.push(entry);
|
|
12164
|
-
} else {
|
|
12165
|
-
unchanged.push(entry);
|
|
12166
|
-
}
|
|
12167
|
-
}
|
|
12453
|
+
const prompt = buildGlobalConstitutionPrompt(summaries);
|
|
12454
|
+
let globalConstitution;
|
|
12455
|
+
try {
|
|
12456
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
12457
|
+
} catch (err) {
|
|
12458
|
+
console.error(chalk28.red(` \u2718 Failed to generate global constitution: ${err.message}`));
|
|
12459
|
+
return;
|
|
12168
12460
|
}
|
|
12169
|
-
const
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12461
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
12462
|
+
console.log(chalk28.green(` \u2714 Global constitution saved: ${saved}`));
|
|
12463
|
+
console.log(chalk28.gray(" Project constitutions will be merged with this at runtime."));
|
|
12464
|
+
const lines = globalConstitution.split("\n");
|
|
12465
|
+
console.log(chalk28.bold("\n Preview:"));
|
|
12466
|
+
console.log(chalk28.gray(lines.slice(0, 10).join("\n")));
|
|
12467
|
+
if (lines.length > 10) {
|
|
12468
|
+
console.log(chalk28.gray(` ... (${lines.length} lines total)`));
|
|
12176
12469
|
}
|
|
12177
|
-
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
12178
|
-
const index = {
|
|
12179
|
-
scanRoot,
|
|
12180
|
-
lastScanned: now,
|
|
12181
|
-
projects
|
|
12182
|
-
};
|
|
12183
|
-
return { index, added, updated, unchanged, nowMissing };
|
|
12184
12470
|
}
|
|
12185
|
-
|
|
12186
|
-
// cli/commands/init.ts
|
|
12187
12471
|
function registerInit(program2) {
|
|
12188
|
-
program2.command("init").description(
|
|
12472
|
+
program2.command("init").description("Setup workspace: register repos, generate constitutions").option(
|
|
12189
12473
|
"--provider <name>",
|
|
12190
12474
|
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
12191
12475
|
void 0
|
|
12192
|
-
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing
|
|
12193
|
-
"--global",
|
|
12194
|
-
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
12195
|
-
).option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules (prune & rebase)").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").action(async (opts) => {
|
|
12476
|
+
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing constitutions").option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").option("--add-repo", "Add a new repo to the registered list").action(async (opts) => {
|
|
12196
12477
|
const currentDir = process.cwd();
|
|
12197
12478
|
const config2 = await loadConfig(currentDir);
|
|
12198
12479
|
const providerName = opts.provider || config2.provider || "gemini";
|
|
@@ -12211,7 +12492,7 @@ function registerInit(program2) {
|
|
|
12211
12492
|
console.log(chalk28.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
12212
12493
|
console.log(chalk28.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
12213
12494
|
if (result.backupPath) {
|
|
12214
|
-
console.log(chalk28.gray(` Backup: ${
|
|
12495
|
+
console.log(chalk28.gray(` Backup: ${path28.basename(result.backupPath)}`));
|
|
12215
12496
|
}
|
|
12216
12497
|
}
|
|
12217
12498
|
} catch (err) {
|
|
@@ -12220,119 +12501,125 @@ function registerInit(program2) {
|
|
|
12220
12501
|
}
|
|
12221
12502
|
return;
|
|
12222
12503
|
}
|
|
12223
|
-
if (opts.
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12504
|
+
if (opts.addRepo) {
|
|
12505
|
+
console.log(chalk28.blue("\n\u2500\u2500\u2500 Register New Repo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12506
|
+
const role = await promptRepoRole2();
|
|
12507
|
+
const repoPath = await promptRepoPath(role);
|
|
12508
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12509
|
+
console.log(chalk28.green(`
|
|
12510
|
+
\u2714 Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
|
|
12511
|
+
console.log(chalk28.gray(` Saved to: ${REPO_STORE_FILE}`));
|
|
12512
|
+
const updateGlobal = await confirm2({
|
|
12513
|
+
message: "Update global constitution with this repo's context?",
|
|
12514
|
+
default: true
|
|
12515
|
+
});
|
|
12516
|
+
if (updateGlobal) {
|
|
12517
|
+
const allRepos2 = await getRegisteredRepos();
|
|
12518
|
+
await generateGlobalConstitution(provider, allRepos2, currentDir);
|
|
12230
12519
|
}
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12520
|
+
return;
|
|
12521
|
+
}
|
|
12522
|
+
console.log(chalk28.blue("\n" + "\u2500".repeat(52)));
|
|
12523
|
+
console.log(chalk28.bold(" ai-spec init \u2014 Workspace Setup"));
|
|
12524
|
+
console.log(chalk28.blue("\u2500".repeat(52)));
|
|
12525
|
+
console.log(chalk28.gray(` Provider: ${providerName}/${modelName}
|
|
12526
|
+
`));
|
|
12527
|
+
const existingRepos = await getRegisteredRepos();
|
|
12528
|
+
if (existingRepos.length > 0) {
|
|
12529
|
+
console.log(chalk28.cyan(" Registered repos:"));
|
|
12530
|
+
for (const r of existingRepos) {
|
|
12531
|
+
const constitutionIcon = r.hasConstitution ? chalk28.green("\u2714") : chalk28.gray("\u25CB");
|
|
12532
|
+
console.log(chalk28.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`));
|
|
12533
|
+
}
|
|
12534
|
+
console.log();
|
|
12535
|
+
}
|
|
12536
|
+
const action = existingRepos.length > 0 ? await select8({
|
|
12537
|
+
message: "What would you like to do?",
|
|
12538
|
+
choices: [
|
|
12539
|
+
{ name: "Add new repo(s)", value: "add" },
|
|
12540
|
+
{ name: "Re-generate constitutions for existing repos", value: "regen" },
|
|
12541
|
+
{ name: "Skip \u2014 proceed to global constitution", value: "skip" }
|
|
12542
|
+
]
|
|
12543
|
+
}) : "add";
|
|
12544
|
+
const newRepos = [];
|
|
12545
|
+
if (action === "add") {
|
|
12546
|
+
let addMore = true;
|
|
12547
|
+
while (addMore) {
|
|
12548
|
+
console.log(chalk28.blue(`
|
|
12549
|
+
\u2500\u2500 Register Repo #${existingRepos.length + newRepos.length + 1} \u2500\u2500`));
|
|
12550
|
+
const role = await promptRepoRole2();
|
|
12551
|
+
const repoPath = await promptRepoPath(role);
|
|
12552
|
+
const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
|
|
12553
|
+
if (alreadyRegistered) {
|
|
12554
|
+
console.log(chalk28.yellow(` Already registered: ${alreadyRegistered.name}`));
|
|
12555
|
+
} else {
|
|
12556
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12557
|
+
newRepos.push(entry);
|
|
12558
|
+
console.log(chalk28.green(` \u2714 Registered: ${entry.name}`));
|
|
12254
12559
|
}
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
const loader = new ContextLoader(currentDir);
|
|
12259
|
-
const ctx = await loader.loadProjectContext();
|
|
12260
|
-
projectSummaries.push({
|
|
12261
|
-
name: path27.basename(currentDir),
|
|
12262
|
-
summary: [
|
|
12263
|
-
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
12264
|
-
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
12265
|
-
].join("\n")
|
|
12560
|
+
addMore = await confirm2({
|
|
12561
|
+
message: "Add another repo?",
|
|
12562
|
+
default: false
|
|
12266
12563
|
});
|
|
12267
12564
|
}
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12565
|
+
}
|
|
12566
|
+
if (action === "regen") {
|
|
12567
|
+
console.log(chalk28.blue("\n Re-generating project constitutions..."));
|
|
12568
|
+
for (const repo of existingRepos) {
|
|
12569
|
+
if (!await fs29.pathExists(repo.path)) {
|
|
12570
|
+
console.log(chalk28.yellow(` \u26A0 ${repo.name}: path not found \u2014 skipping`));
|
|
12571
|
+
continue;
|
|
12572
|
+
}
|
|
12573
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12574
|
+
if (await fs29.pathExists(constitutionPath) && !opts.force) {
|
|
12575
|
+
console.log(chalk28.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
|
|
12576
|
+
continue;
|
|
12577
|
+
}
|
|
12578
|
+
console.log(chalk28.blue(` ${repo.name}: generating constitution...`));
|
|
12579
|
+
try {
|
|
12580
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12581
|
+
const content = await gen.generate(repo.path);
|
|
12582
|
+
await gen.saveConstitution(repo.path, content);
|
|
12583
|
+
console.log(chalk28.green(` \u2714 ${repo.name}: constitution saved`));
|
|
12584
|
+
} catch (err) {
|
|
12585
|
+
console.log(chalk28.yellow(` \u26A0 ${repo.name}: failed \u2014 ${err.message}`));
|
|
12586
|
+
}
|
|
12286
12587
|
}
|
|
12287
|
-
return;
|
|
12288
12588
|
}
|
|
12289
|
-
const
|
|
12290
|
-
if (
|
|
12291
|
-
console.log(chalk28.yellow(`
|
|
12292
|
-
${CONSTITUTION_FILE} already exists.`));
|
|
12293
|
-
console.log(chalk28.gray(" Use --force to overwrite it."));
|
|
12294
|
-
console.log(chalk28.gray(` Or edit it directly: ${constitutionPath}`));
|
|
12589
|
+
const allRepos = await getRegisteredRepos();
|
|
12590
|
+
if (allRepos.length === 0) {
|
|
12591
|
+
console.log(chalk28.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
|
|
12295
12592
|
return;
|
|
12296
12593
|
}
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
}
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
}
|
|
12316
|
-
console.log(chalk28.green(`
|
|
12317
|
-
\u2714 Constitution saved: ${saved}`));
|
|
12318
|
-
console.log(chalk28.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
12319
|
-
console.log(chalk28.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
12320
|
-
console.log(chalk28.bold(" Preview:"));
|
|
12321
|
-
console.log(chalk28.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
12322
|
-
if (constitution.split("\n").length > 15) {
|
|
12323
|
-
console.log(chalk28.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
12324
|
-
}
|
|
12594
|
+
const existingGlobal = await loadGlobalConstitution([currentDir]);
|
|
12595
|
+
const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0 ? true : await confirm2({
|
|
12596
|
+
message: "Global constitution exists. Re-generate it?",
|
|
12597
|
+
default: false
|
|
12598
|
+
});
|
|
12599
|
+
if (shouldGenerateGlobal) {
|
|
12600
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
12601
|
+
}
|
|
12602
|
+
console.log(chalk28.bold.green("\n\u2714 Init complete!"));
|
|
12603
|
+
console.log(chalk28.gray(` Repos registered: ${allRepos.length}`));
|
|
12604
|
+
for (const r of allRepos) {
|
|
12605
|
+
const icon = r.hasConstitution ? chalk28.green("\u2714") : chalk28.gray("\u25CB");
|
|
12606
|
+
console.log(chalk28.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
|
|
12607
|
+
}
|
|
12608
|
+
console.log(chalk28.gray(`
|
|
12609
|
+
Repo store: ${REPO_STORE_FILE}`));
|
|
12610
|
+
console.log(chalk28.gray(` Next step: ai-spec create "your feature idea"`));
|
|
12611
|
+
process.exit(0);
|
|
12325
12612
|
});
|
|
12326
12613
|
}
|
|
12327
12614
|
|
|
12328
12615
|
// cli/commands/config.ts
|
|
12329
|
-
import * as
|
|
12330
|
-
import * as
|
|
12616
|
+
import * as path29 from "path";
|
|
12617
|
+
import * as fs30 from "fs-extra";
|
|
12331
12618
|
import chalk29 from "chalk";
|
|
12332
12619
|
function registerConfig(program2) {
|
|
12333
12620
|
program2.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("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)").option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
12334
12621
|
const currentDir = process.cwd();
|
|
12335
|
-
const configPath =
|
|
12622
|
+
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12336
12623
|
if (opts.clearKeys) {
|
|
12337
12624
|
await clearAllKeys();
|
|
12338
12625
|
console.log(chalk29.green(`\u2714 All saved API keys cleared.`));
|
|
@@ -12344,7 +12631,7 @@ function registerConfig(program2) {
|
|
|
12344
12631
|
return;
|
|
12345
12632
|
}
|
|
12346
12633
|
if (opts.listKeys) {
|
|
12347
|
-
const store = await
|
|
12634
|
+
const store = await fs30.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
12348
12635
|
const providers = Object.keys(store);
|
|
12349
12636
|
if (providers.length === 0) {
|
|
12350
12637
|
console.log(chalk29.gray("No saved API keys."));
|
|
@@ -12360,7 +12647,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12360
12647
|
return;
|
|
12361
12648
|
}
|
|
12362
12649
|
if (opts.reset) {
|
|
12363
|
-
await
|
|
12650
|
+
await fs30.writeJson(configPath, {}, { spaces: 2 });
|
|
12364
12651
|
console.log(chalk29.green(`\u2714 Config reset: ${configPath}`));
|
|
12365
12652
|
return;
|
|
12366
12653
|
}
|
|
@@ -12404,7 +12691,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12404
12691
|
}
|
|
12405
12692
|
updated.maxErrorCycles = cycles;
|
|
12406
12693
|
}
|
|
12407
|
-
await
|
|
12694
|
+
await fs30.writeJson(configPath, updated, { spaces: 2 });
|
|
12408
12695
|
console.log(chalk29.green(`\u2714 Config saved to ${configPath}`));
|
|
12409
12696
|
console.log(JSON.stringify(updated, null, 2));
|
|
12410
12697
|
});
|
|
@@ -12412,14 +12699,10 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12412
12699
|
|
|
12413
12700
|
// cli/commands/model.ts
|
|
12414
12701
|
init_spec_generator();
|
|
12415
|
-
import * as path29 from "path";
|
|
12416
|
-
import * as fs30 from "fs-extra";
|
|
12417
12702
|
import chalk30 from "chalk";
|
|
12418
|
-
import { input as
|
|
12703
|
+
import { input as input4, select as select9, confirm as confirm3 } from "@inquirer/prompts";
|
|
12419
12704
|
function registerModel(program2) {
|
|
12420
12705
|
program2.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) => {
|
|
12421
|
-
const currentDir = process.cwd();
|
|
12422
|
-
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12423
12706
|
if (opts.list) {
|
|
12424
12707
|
console.log(chalk30.bold("\nAvailable providers & models:\n"));
|
|
12425
12708
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
@@ -12436,8 +12719,9 @@ function registerModel(program2) {
|
|
|
12436
12719
|
}
|
|
12437
12720
|
return;
|
|
12438
12721
|
}
|
|
12439
|
-
const existing = await
|
|
12722
|
+
const existing = await loadGlobalConfig();
|
|
12440
12723
|
console.log(chalk30.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"));
|
|
12724
|
+
console.log(chalk30.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
|
|
12441
12725
|
if (Object.keys(existing).length > 0) {
|
|
12442
12726
|
console.log(
|
|
12443
12727
|
chalk30.gray(
|
|
@@ -12446,7 +12730,7 @@ function registerModel(program2) {
|
|
|
12446
12730
|
);
|
|
12447
12731
|
}
|
|
12448
12732
|
console.log();
|
|
12449
|
-
const target = await
|
|
12733
|
+
const target = await select9({
|
|
12450
12734
|
message: "Configure model for:",
|
|
12451
12735
|
choices: [
|
|
12452
12736
|
{ name: "Spec generation (used for spec writing & refinement)", value: "spec" },
|
|
@@ -12455,7 +12739,7 @@ function registerModel(program2) {
|
|
|
12455
12739
|
]
|
|
12456
12740
|
});
|
|
12457
12741
|
async function pickProviderAndModel(label) {
|
|
12458
|
-
const providerKey = await
|
|
12742
|
+
const providerKey = await select9({
|
|
12459
12743
|
message: `${label} \u2014 select provider:`,
|
|
12460
12744
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
12461
12745
|
name: `${meta2.displayName.padEnd(22)} ${chalk30.gray(meta2.description)}`,
|
|
@@ -12468,12 +12752,12 @@ function registerModel(program2) {
|
|
|
12468
12752
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
12469
12753
|
{ name: chalk30.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
12470
12754
|
];
|
|
12471
|
-
let chosenModel = await
|
|
12755
|
+
let chosenModel = await select9({
|
|
12472
12756
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
12473
12757
|
choices: modelChoices
|
|
12474
12758
|
});
|
|
12475
12759
|
if (chosenModel === "__custom__") {
|
|
12476
|
-
chosenModel = await
|
|
12760
|
+
chosenModel = await input4({
|
|
12477
12761
|
message: "Enter model name:",
|
|
12478
12762
|
validate: (v2) => v2.trim().length > 0 || "Model name cannot be empty"
|
|
12479
12763
|
});
|
|
@@ -12518,14 +12802,14 @@ function registerModel(program2) {
|
|
|
12518
12802
|
)
|
|
12519
12803
|
);
|
|
12520
12804
|
}
|
|
12521
|
-
const ok = await
|
|
12805
|
+
const ok = await confirm3({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
|
|
12522
12806
|
if (!ok) {
|
|
12523
12807
|
console.log(chalk30.gray(" Cancelled."));
|
|
12524
12808
|
return;
|
|
12525
12809
|
}
|
|
12526
|
-
await
|
|
12810
|
+
await saveGlobalConfig(updated);
|
|
12527
12811
|
console.log(chalk30.green(`
|
|
12528
|
-
\u2714 Saved to ${
|
|
12812
|
+
\u2714 Saved to ${GLOBAL_CONFIG_FILE}`));
|
|
12529
12813
|
const providerToCheck = updated.provider ?? "gemini";
|
|
12530
12814
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
12531
12815
|
if (envKey && !process.env[envKey]) {
|
|
@@ -12542,14 +12826,14 @@ function registerModel(program2) {
|
|
|
12542
12826
|
import * as path30 from "path";
|
|
12543
12827
|
import * as fs31 from "fs-extra";
|
|
12544
12828
|
import chalk31 from "chalk";
|
|
12545
|
-
import { input as
|
|
12829
|
+
import { input as input5, select as select10, confirm as confirm4 } from "@inquirer/prompts";
|
|
12546
12830
|
function registerWorkspace(program2) {
|
|
12547
12831
|
const workspaceCmd = program2.command("workspace").description("Manage multi-repo workspace configuration");
|
|
12548
12832
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
12549
12833
|
const currentDir = process.cwd();
|
|
12550
12834
|
const configPath = path30.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
12551
12835
|
if (await fs31.pathExists(configPath)) {
|
|
12552
|
-
const overwrite = await
|
|
12836
|
+
const overwrite = await confirm4({
|
|
12553
12837
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
12554
12838
|
default: false
|
|
12555
12839
|
});
|
|
@@ -12559,12 +12843,12 @@ function registerWorkspace(program2) {
|
|
|
12559
12843
|
}
|
|
12560
12844
|
}
|
|
12561
12845
|
console.log(chalk31.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"));
|
|
12562
|
-
const workspaceName = await
|
|
12846
|
+
const workspaceName = await input5({
|
|
12563
12847
|
message: "Workspace name:",
|
|
12564
12848
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
12565
12849
|
});
|
|
12566
12850
|
const repos = [];
|
|
12567
|
-
const useAutoScan = await
|
|
12851
|
+
const useAutoScan = await confirm4({
|
|
12568
12852
|
message: "Auto-scan sibling directories for repos?",
|
|
12569
12853
|
default: true
|
|
12570
12854
|
});
|
|
@@ -12578,7 +12862,7 @@ function registerWorkspace(program2) {
|
|
|
12578
12862
|
for (const r of detected) {
|
|
12579
12863
|
console.log(chalk31.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12580
12864
|
}
|
|
12581
|
-
const keepAll = await
|
|
12865
|
+
const keepAll = await confirm4({
|
|
12582
12866
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
12583
12867
|
default: true
|
|
12584
12868
|
});
|
|
@@ -12586,7 +12870,7 @@ function registerWorkspace(program2) {
|
|
|
12586
12870
|
repos.push(...detected);
|
|
12587
12871
|
} else {
|
|
12588
12872
|
for (const r of detected) {
|
|
12589
|
-
const keep = await
|
|
12873
|
+
const keep = await confirm4({
|
|
12590
12874
|
message: `Include "${r.name}" (${r.role}, ${r.type})?`,
|
|
12591
12875
|
default: true
|
|
12592
12876
|
});
|
|
@@ -12610,14 +12894,14 @@ function registerWorkspace(program2) {
|
|
|
12610
12894
|
{ name: "react-native (React Native mobile)", value: "react-native" },
|
|
12611
12895
|
{ name: "unknown", value: "unknown" }
|
|
12612
12896
|
];
|
|
12613
|
-
let addMore = await
|
|
12897
|
+
let addMore = await confirm4({
|
|
12614
12898
|
message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
|
|
12615
12899
|
default: repos.length === 0
|
|
12616
12900
|
});
|
|
12617
12901
|
while (addMore) {
|
|
12618
12902
|
console.log(chalk31.cyan(`
|
|
12619
12903
|
Adding repo #${repos.length + 1}`));
|
|
12620
|
-
const repoName = await
|
|
12904
|
+
const repoName = await input5({
|
|
12621
12905
|
message: "Repo name (e.g. api, web, app):",
|
|
12622
12906
|
validate: (v2) => {
|
|
12623
12907
|
if (!v2.trim()) return "Name cannot be empty";
|
|
@@ -12625,7 +12909,7 @@ function registerWorkspace(program2) {
|
|
|
12625
12909
|
return true;
|
|
12626
12910
|
}
|
|
12627
12911
|
});
|
|
12628
|
-
const repoPath = await
|
|
12912
|
+
const repoPath = await input5({
|
|
12629
12913
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
12630
12914
|
default: `./${repoName}`
|
|
12631
12915
|
});
|
|
@@ -12640,12 +12924,12 @@ function registerWorkspace(program2) {
|
|
|
12640
12924
|
} else {
|
|
12641
12925
|
console.log(chalk31.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
12642
12926
|
}
|
|
12643
|
-
const repoType = await
|
|
12927
|
+
const repoType = await select10({
|
|
12644
12928
|
message: `Repo type for "${repoName}":`,
|
|
12645
12929
|
choices: repoTypeChoices,
|
|
12646
12930
|
default: detectedType
|
|
12647
12931
|
});
|
|
12648
|
-
const repoRole = await
|
|
12932
|
+
const repoRole = await select10({
|
|
12649
12933
|
message: `Repo role for "${repoName}":`,
|
|
12650
12934
|
choices: [
|
|
12651
12935
|
{ name: "backend", value: "backend" },
|
|
@@ -12662,7 +12946,7 @@ function registerWorkspace(program2) {
|
|
|
12662
12946
|
role: repoRole
|
|
12663
12947
|
});
|
|
12664
12948
|
console.log(chalk31.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
12665
|
-
addMore = await
|
|
12949
|
+
addMore = await confirm4({
|
|
12666
12950
|
message: "Add another repo?",
|
|
12667
12951
|
default: false
|
|
12668
12952
|
});
|
|
@@ -12673,7 +12957,7 @@ function registerWorkspace(program2) {
|
|
|
12673
12957
|
for (const r of repos) {
|
|
12674
12958
|
console.log(chalk31.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12675
12959
|
}
|
|
12676
|
-
const ok = await
|
|
12960
|
+
const ok = await confirm4({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
12677
12961
|
if (!ok) {
|
|
12678
12962
|
console.log(chalk31.gray(" Cancelled."));
|
|
12679
12963
|
return;
|
|
@@ -12718,7 +13002,7 @@ init_spec_generator();
|
|
|
12718
13002
|
import * as path32 from "path";
|
|
12719
13003
|
import * as fs33 from "fs-extra";
|
|
12720
13004
|
import chalk33 from "chalk";
|
|
12721
|
-
import { input as
|
|
13005
|
+
import { input as input6 } from "@inquirer/prompts";
|
|
12722
13006
|
|
|
12723
13007
|
// core/spec-updater.ts
|
|
12724
13008
|
import chalk32 from "chalk";
|
|
@@ -12952,7 +13236,7 @@ function registerUpdate(program2) {
|
|
|
12952
13236
|
const currentDir = process.cwd();
|
|
12953
13237
|
const config2 = await loadConfig(currentDir);
|
|
12954
13238
|
if (!change) {
|
|
12955
|
-
change = await
|
|
13239
|
+
change = await input6({
|
|
12956
13240
|
message: "Describe the change you want to make:",
|
|
12957
13241
|
validate: (v2) => v2.trim().length > 0 || "Change description cannot be empty"
|
|
12958
13242
|
});
|
|
@@ -13270,7 +13554,7 @@ function buildYamlDoc(obj) {
|
|
|
13270
13554
|
return `${k2}: ${valStr}`;
|
|
13271
13555
|
}).join("\n") + "\n";
|
|
13272
13556
|
}
|
|
13273
|
-
function dslToOpenApi(dsl, serverUrl =
|
|
13557
|
+
function dslToOpenApi(dsl, serverUrl = DEFAULT_OPENAPI_SERVER_URL) {
|
|
13274
13558
|
const info = {
|
|
13275
13559
|
title: dsl.feature.title,
|
|
13276
13560
|
description: dsl.feature.description,
|
|
@@ -13317,7 +13601,7 @@ function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
|
|
|
13317
13601
|
}
|
|
13318
13602
|
async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
13319
13603
|
const format = opts.format ?? "yaml";
|
|
13320
|
-
const serverUrl = opts.serverUrl ??
|
|
13604
|
+
const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
|
|
13321
13605
|
const defaultName = `openapi.${format}`;
|
|
13322
13606
|
const outputPath = opts.outputPath ? path33.isAbsolute(opts.outputPath) ? opts.outputPath : path33.join(projectDir, opts.outputPath) : path33.join(projectDir, defaultName);
|
|
13323
13607
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
@@ -13513,12 +13797,12 @@ function registerMock(program2) {
|
|
|
13513
13797
|
|
|
13514
13798
|
// cli/commands/learn.ts
|
|
13515
13799
|
import chalk36 from "chalk";
|
|
13516
|
-
import { input as
|
|
13800
|
+
import { input as input7 } from "@inquirer/prompts";
|
|
13517
13801
|
function registerLearn(program2) {
|
|
13518
13802
|
program2.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) => {
|
|
13519
13803
|
const currentDir = process.cwd();
|
|
13520
13804
|
if (!lesson) {
|
|
13521
|
-
lesson = await
|
|
13805
|
+
lesson = await input7({
|
|
13522
13806
|
message: "What lesson or engineering decision should be recorded?",
|
|
13523
13807
|
validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
|
|
13524
13808
|
});
|
|
@@ -13560,9 +13844,8 @@ import chalk39 from "chalk";
|
|
|
13560
13844
|
import * as fs37 from "fs-extra";
|
|
13561
13845
|
import * as path36 from "path";
|
|
13562
13846
|
import chalk38 from "chalk";
|
|
13563
|
-
var LOG_DIR2 = ".ai-spec-logs";
|
|
13564
13847
|
async function loadRunLogs(workingDir) {
|
|
13565
|
-
const logDir = path36.join(workingDir,
|
|
13848
|
+
const logDir = path36.join(workingDir, DEFAULT_LOG_DIR);
|
|
13566
13849
|
if (!await fs37.pathExists(logDir)) return [];
|
|
13567
13850
|
const files = await fs37.readdir(logDir);
|
|
13568
13851
|
const jsonFiles = new Set(files.filter((f) => f.endsWith(".json")));
|
|
@@ -13707,7 +13990,7 @@ function printTrendReport(report, workingDir) {
|
|
|
13707
13990
|
` ${chalk38.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
|
|
13708
13991
|
);
|
|
13709
13992
|
}
|
|
13710
|
-
const logRelDir = path36.relative(workingDir, path36.join(workingDir,
|
|
13993
|
+
const logRelDir = path36.relative(workingDir, path36.join(workingDir, DEFAULT_LOG_DIR));
|
|
13711
13994
|
console.log(chalk38.gray(`
|
|
13712
13995
|
${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
|
|
13713
13996
|
console.log(chalk38.cyan("\u2500".repeat(63)));
|
|
@@ -14366,7 +14649,233 @@ function registerVcr(program2) {
|
|
|
14366
14649
|
|
|
14367
14650
|
// cli/commands/scan.ts
|
|
14368
14651
|
import chalk44 from "chalk";
|
|
14652
|
+
import * as path42 from "path";
|
|
14653
|
+
|
|
14654
|
+
// core/project-index.ts
|
|
14655
|
+
import * as fs42 from "fs-extra";
|
|
14369
14656
|
import * as path41 from "path";
|
|
14657
|
+
var INDEX_FILE = ".ai-spec-index.json";
|
|
14658
|
+
var KEY_DEPS = [
|
|
14659
|
+
// Frameworks
|
|
14660
|
+
"express",
|
|
14661
|
+
"fastify",
|
|
14662
|
+
"koa",
|
|
14663
|
+
"@nestjs/core",
|
|
14664
|
+
"hapi",
|
|
14665
|
+
"next",
|
|
14666
|
+
"react",
|
|
14667
|
+
"vue",
|
|
14668
|
+
"nuxt",
|
|
14669
|
+
"svelte",
|
|
14670
|
+
"react-native",
|
|
14671
|
+
"expo",
|
|
14672
|
+
// DB / ORM
|
|
14673
|
+
"prisma",
|
|
14674
|
+
"@prisma/client",
|
|
14675
|
+
"mongoose",
|
|
14676
|
+
"typeorm",
|
|
14677
|
+
"sequelize",
|
|
14678
|
+
"drizzle-orm",
|
|
14679
|
+
// Auth
|
|
14680
|
+
"jsonwebtoken",
|
|
14681
|
+
"passport",
|
|
14682
|
+
"next-auth",
|
|
14683
|
+
"@clerk/nextjs",
|
|
14684
|
+
// Build / Lang
|
|
14685
|
+
"typescript",
|
|
14686
|
+
"vite",
|
|
14687
|
+
"webpack",
|
|
14688
|
+
"esbuild",
|
|
14689
|
+
"turbo",
|
|
14690
|
+
// Testing
|
|
14691
|
+
"jest",
|
|
14692
|
+
"vitest",
|
|
14693
|
+
"mocha",
|
|
14694
|
+
"cypress",
|
|
14695
|
+
"playwright",
|
|
14696
|
+
// Infra
|
|
14697
|
+
"redis",
|
|
14698
|
+
"bull",
|
|
14699
|
+
"socket.io",
|
|
14700
|
+
"graphql",
|
|
14701
|
+
"@trpc/server"
|
|
14702
|
+
];
|
|
14703
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14704
|
+
"node_modules",
|
|
14705
|
+
".git",
|
|
14706
|
+
".svn",
|
|
14707
|
+
"dist",
|
|
14708
|
+
"build",
|
|
14709
|
+
"out",
|
|
14710
|
+
".next",
|
|
14711
|
+
".nuxt",
|
|
14712
|
+
"coverage",
|
|
14713
|
+
".turbo",
|
|
14714
|
+
".cache",
|
|
14715
|
+
"__pycache__",
|
|
14716
|
+
"vendor",
|
|
14717
|
+
".ai-spec-vcr",
|
|
14718
|
+
".ai-spec-logs",
|
|
14719
|
+
"specs"
|
|
14720
|
+
]);
|
|
14721
|
+
var MANIFEST_FILES = [
|
|
14722
|
+
"package.json",
|
|
14723
|
+
"go.mod",
|
|
14724
|
+
"Cargo.toml",
|
|
14725
|
+
"pom.xml",
|
|
14726
|
+
"build.gradle",
|
|
14727
|
+
"build.gradle.kts",
|
|
14728
|
+
"requirements.txt",
|
|
14729
|
+
"pyproject.toml",
|
|
14730
|
+
"setup.py",
|
|
14731
|
+
"composer.json"
|
|
14732
|
+
];
|
|
14733
|
+
async function isProjectRoot(absPath) {
|
|
14734
|
+
for (const manifest of MANIFEST_FILES) {
|
|
14735
|
+
if (await fs42.pathExists(path41.join(absPath, manifest))) return true;
|
|
14736
|
+
}
|
|
14737
|
+
return false;
|
|
14738
|
+
}
|
|
14739
|
+
async function extractTechStack(absPath, type) {
|
|
14740
|
+
const stack = [];
|
|
14741
|
+
if (type === "go") stack.push("go");
|
|
14742
|
+
if (type === "rust") stack.push("rust");
|
|
14743
|
+
if (type === "java") stack.push("java");
|
|
14744
|
+
if (type === "python") stack.push("python");
|
|
14745
|
+
if (type === "php") stack.push("php");
|
|
14746
|
+
const pkgPath = path41.join(absPath, "package.json");
|
|
14747
|
+
if (!await fs42.pathExists(pkgPath)) return stack;
|
|
14748
|
+
let pkg = {};
|
|
14749
|
+
try {
|
|
14750
|
+
pkg = await fs42.readJson(pkgPath);
|
|
14751
|
+
} catch {
|
|
14752
|
+
return stack;
|
|
14753
|
+
}
|
|
14754
|
+
const allDeps = {
|
|
14755
|
+
...pkg.dependencies ?? {},
|
|
14756
|
+
...pkg.devDependencies ?? {}
|
|
14757
|
+
};
|
|
14758
|
+
const depKeys = new Set(Object.keys(allDeps));
|
|
14759
|
+
for (const dep of KEY_DEPS) {
|
|
14760
|
+
if (depKeys.has(dep)) stack.push(dep);
|
|
14761
|
+
}
|
|
14762
|
+
return stack;
|
|
14763
|
+
}
|
|
14764
|
+
async function discoverProjects(rootDir, maxDepth) {
|
|
14765
|
+
const found = [];
|
|
14766
|
+
async function walk(absDir, depth) {
|
|
14767
|
+
if (depth > maxDepth) return;
|
|
14768
|
+
let entries;
|
|
14769
|
+
try {
|
|
14770
|
+
entries = await fs42.readdir(absDir, { withFileTypes: true });
|
|
14771
|
+
} catch {
|
|
14772
|
+
return;
|
|
14773
|
+
}
|
|
14774
|
+
for (const entry of entries) {
|
|
14775
|
+
if (!entry.isDirectory()) continue;
|
|
14776
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
14777
|
+
const childAbs = path41.join(absDir, entry.name);
|
|
14778
|
+
const gitPath = path41.join(childAbs, ".git");
|
|
14779
|
+
if (await fs42.pathExists(gitPath)) {
|
|
14780
|
+
const gitStat = await fs42.stat(gitPath);
|
|
14781
|
+
if (gitStat.isFile()) continue;
|
|
14782
|
+
}
|
|
14783
|
+
if (await isProjectRoot(childAbs)) {
|
|
14784
|
+
found.push(path41.relative(rootDir, childAbs));
|
|
14785
|
+
} else {
|
|
14786
|
+
await walk(childAbs, depth + 1);
|
|
14787
|
+
}
|
|
14788
|
+
}
|
|
14789
|
+
}
|
|
14790
|
+
await walk(rootDir, 0);
|
|
14791
|
+
return found;
|
|
14792
|
+
}
|
|
14793
|
+
async function loadIndex(scanRoot) {
|
|
14794
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14795
|
+
try {
|
|
14796
|
+
return await fs42.readJson(filePath);
|
|
14797
|
+
} catch {
|
|
14798
|
+
return null;
|
|
14799
|
+
}
|
|
14800
|
+
}
|
|
14801
|
+
async function saveIndex(scanRoot, index) {
|
|
14802
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14803
|
+
await fs42.writeJson(filePath, index, { spaces: 2 });
|
|
14804
|
+
return filePath;
|
|
14805
|
+
}
|
|
14806
|
+
async function runScan(scanRoot, maxDepth = 2) {
|
|
14807
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14808
|
+
const existing = await loadIndex(scanRoot);
|
|
14809
|
+
const existingMap = new Map(
|
|
14810
|
+
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
14811
|
+
);
|
|
14812
|
+
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
14813
|
+
const added = [];
|
|
14814
|
+
const updated = [];
|
|
14815
|
+
const unchanged = [];
|
|
14816
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
14817
|
+
for (const relPath of discoveredPaths) {
|
|
14818
|
+
const absPath = path41.join(scanRoot, relPath);
|
|
14819
|
+
seenPaths.add(relPath);
|
|
14820
|
+
const { type, role } = await detectRepoType(absPath);
|
|
14821
|
+
const techStack = await extractTechStack(absPath, type);
|
|
14822
|
+
const hasConstitution = await fs42.pathExists(path41.join(absPath, CONSTITUTION_FILE));
|
|
14823
|
+
const hasWorkspace = await fs42.pathExists(path41.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
14824
|
+
const name = path41.basename(relPath);
|
|
14825
|
+
const prev = existingMap.get(relPath);
|
|
14826
|
+
if (!prev) {
|
|
14827
|
+
const entry = {
|
|
14828
|
+
name,
|
|
14829
|
+
path: relPath,
|
|
14830
|
+
type,
|
|
14831
|
+
role,
|
|
14832
|
+
techStack,
|
|
14833
|
+
hasConstitution,
|
|
14834
|
+
hasWorkspace,
|
|
14835
|
+
firstSeen: now,
|
|
14836
|
+
lastSeen: now
|
|
14837
|
+
};
|
|
14838
|
+
added.push(entry);
|
|
14839
|
+
existingMap.set(relPath, entry);
|
|
14840
|
+
} else {
|
|
14841
|
+
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
14842
|
+
const entry = {
|
|
14843
|
+
...prev,
|
|
14844
|
+
type,
|
|
14845
|
+
role,
|
|
14846
|
+
techStack,
|
|
14847
|
+
hasConstitution,
|
|
14848
|
+
hasWorkspace,
|
|
14849
|
+
lastSeen: now,
|
|
14850
|
+
missing: void 0
|
|
14851
|
+
// clear missing flag if it came back
|
|
14852
|
+
};
|
|
14853
|
+
existingMap.set(relPath, entry);
|
|
14854
|
+
if (changed) {
|
|
14855
|
+
updated.push(entry);
|
|
14856
|
+
} else {
|
|
14857
|
+
unchanged.push(entry);
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
}
|
|
14861
|
+
const nowMissing = [];
|
|
14862
|
+
for (const [relPath, entry] of existingMap) {
|
|
14863
|
+
if (!seenPaths.has(relPath) && !entry.missing) {
|
|
14864
|
+
const gone = { ...entry, missing: true };
|
|
14865
|
+
existingMap.set(relPath, gone);
|
|
14866
|
+
nowMissing.push(gone);
|
|
14867
|
+
}
|
|
14868
|
+
}
|
|
14869
|
+
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
14870
|
+
const index = {
|
|
14871
|
+
scanRoot,
|
|
14872
|
+
lastScanned: now,
|
|
14873
|
+
projects
|
|
14874
|
+
};
|
|
14875
|
+
return { index, added, updated, unchanged, nowMissing };
|
|
14876
|
+
}
|
|
14877
|
+
|
|
14878
|
+
// cli/commands/scan.ts
|
|
14370
14879
|
var ROLE_COLOR = {
|
|
14371
14880
|
backend: chalk44.blue,
|
|
14372
14881
|
frontend: chalk44.green,
|
|
@@ -14440,7 +14949,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14440
14949
|
}
|
|
14441
14950
|
console.log(chalk44.cyan("\n\u2500".repeat(52)));
|
|
14442
14951
|
console.log(chalk44.gray(" \xA7C = has constitution W = workspace root"));
|
|
14443
|
-
console.log(chalk44.gray(` Index saved : ${
|
|
14952
|
+
console.log(chalk44.gray(` Index saved : ${path42.relative(cwd, path42.join(cwd, INDEX_FILE))}`));
|
|
14444
14953
|
console.log(chalk44.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
|
|
14445
14954
|
});
|
|
14446
14955
|
}
|
|
@@ -14448,7 +14957,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14448
14957
|
// cli/index.ts
|
|
14449
14958
|
dotenv.config();
|
|
14450
14959
|
var program = new Command();
|
|
14451
|
-
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(
|
|
14960
|
+
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(require_package().version);
|
|
14452
14961
|
registerCreate(program);
|
|
14453
14962
|
registerReview(program);
|
|
14454
14963
|
registerInit(program);
|