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.js
CHANGED
|
@@ -9,6 +9,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
9
9
|
var __esm = (fn, res) => function __init() {
|
|
10
10
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
11
|
};
|
|
12
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
13
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
14
|
+
};
|
|
12
15
|
var __export = (target, all) => {
|
|
13
16
|
for (var name in all)
|
|
14
17
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -147,6 +150,100 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
|
|
|
147
150
|
}
|
|
148
151
|
});
|
|
149
152
|
|
|
153
|
+
// core/cli-ui.ts
|
|
154
|
+
function startSpinner(text) {
|
|
155
|
+
const isTTY = process.stderr.isTTY;
|
|
156
|
+
let frame = 0;
|
|
157
|
+
let currentText = text;
|
|
158
|
+
let stopped = false;
|
|
159
|
+
function render() {
|
|
160
|
+
if (stopped) return;
|
|
161
|
+
const symbol = import_chalk.default.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
|
|
162
|
+
if (isTTY) {
|
|
163
|
+
process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
|
|
164
|
+
}
|
|
165
|
+
frame++;
|
|
166
|
+
}
|
|
167
|
+
if (!isTTY) {
|
|
168
|
+
process.stderr.write(` \u2026 ${currentText}
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
const timer = setInterval(render, 80);
|
|
172
|
+
render();
|
|
173
|
+
return {
|
|
174
|
+
update(newText) {
|
|
175
|
+
currentText = newText;
|
|
176
|
+
},
|
|
177
|
+
stop(finalText) {
|
|
178
|
+
if (stopped) return;
|
|
179
|
+
stopped = true;
|
|
180
|
+
clearInterval(timer);
|
|
181
|
+
if (isTTY) {
|
|
182
|
+
process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
|
|
183
|
+
}
|
|
184
|
+
if (finalText) {
|
|
185
|
+
process.stderr.write(` ${finalText}
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
succeed(successText) {
|
|
190
|
+
this.stop(import_chalk.default.green(`\u2714 ${successText}`));
|
|
191
|
+
},
|
|
192
|
+
fail(failText) {
|
|
193
|
+
this.stop(import_chalk.default.red(`\u2718 ${failText}`));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function retryCountdown(opts) {
|
|
198
|
+
const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
|
|
199
|
+
const isTTY = process.stderr.isTTY;
|
|
200
|
+
const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
|
|
201
|
+
process.stderr.write("\n");
|
|
202
|
+
process.stderr.write(import_chalk.default.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + import_chalk.default.gray(`[${label}]`) + import_chalk.default.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
|
|
203
|
+
`));
|
|
204
|
+
process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.white(shortErr) + "\n");
|
|
205
|
+
process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.gray(`Waiting before retry...`) + "\n");
|
|
206
|
+
const totalSeconds = Math.ceil(waitMs / 1e3);
|
|
207
|
+
for (let s = totalSeconds; s > 0; s--) {
|
|
208
|
+
const bar = import_chalk.default.green("\u2588".repeat(totalSeconds - s)) + import_chalk.default.gray("\u2591".repeat(s));
|
|
209
|
+
const line = import_chalk.default.yellow(` \u2502 `) + `${bar} ${import_chalk.default.bold.white(`${s}s`)}`;
|
|
210
|
+
if (isTTY) {
|
|
211
|
+
process.stderr.write(`\r${line}${" ".repeat(10)}`);
|
|
212
|
+
}
|
|
213
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
214
|
+
}
|
|
215
|
+
if (isTTY) {
|
|
216
|
+
process.stderr.write(`\r${" ".repeat(70)}\r`);
|
|
217
|
+
}
|
|
218
|
+
process.stderr.write(import_chalk.default.yellow(` \u2514\u2500 `) + import_chalk.default.cyan(`Retrying now...`) + "\n\n");
|
|
219
|
+
}
|
|
220
|
+
function startStage(stageKey, label) {
|
|
221
|
+
const icon = STAGE_ICONS[stageKey] ?? "\u25B8";
|
|
222
|
+
return startSpinner(`${icon} ${label}`);
|
|
223
|
+
}
|
|
224
|
+
var import_chalk, SPINNER_FRAMES, STAGE_ICONS;
|
|
225
|
+
var init_cli_ui = __esm({
|
|
226
|
+
"core/cli-ui.ts"() {
|
|
227
|
+
"use strict";
|
|
228
|
+
import_chalk = __toESM(require("chalk"));
|
|
229
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
230
|
+
STAGE_ICONS = {
|
|
231
|
+
context_load: "\u{1F4C2}",
|
|
232
|
+
design_dialogue: "\u{1F4AC}",
|
|
233
|
+
spec_gen: "\u{1F4DD}",
|
|
234
|
+
spec_refine: "\u270F\uFE0F ",
|
|
235
|
+
spec_assess: "\u{1F4CA}",
|
|
236
|
+
dsl_extract: "\u{1F517}",
|
|
237
|
+
dsl_gap_feedback: "\u{1F50D}",
|
|
238
|
+
codegen: "\u2699\uFE0F ",
|
|
239
|
+
test_gen: "\u{1F9EA}",
|
|
240
|
+
error_feedback: "\u{1F527}",
|
|
241
|
+
review: "\u{1F50E}",
|
|
242
|
+
self_eval: "\u{1F4C8}"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
150
247
|
// core/provider-utils.ts
|
|
151
248
|
function classifyError(err, label) {
|
|
152
249
|
const e = err;
|
|
@@ -223,21 +320,23 @@ async function withReliability(fn, opts) {
|
|
|
223
320
|
throw classifyError(err, label);
|
|
224
321
|
}
|
|
225
322
|
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
226
|
-
console.warn(
|
|
227
|
-
import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
|
|
228
|
-
);
|
|
229
323
|
onRetry?.(attempt + 1, err);
|
|
230
|
-
await
|
|
324
|
+
await retryCountdown({
|
|
325
|
+
attempt: attempt + 1,
|
|
326
|
+
maxAttempts: retries + 1,
|
|
327
|
+
waitMs,
|
|
328
|
+
errorMessage: err.message ?? String(err),
|
|
329
|
+
label
|
|
330
|
+
});
|
|
231
331
|
}
|
|
232
332
|
}
|
|
233
333
|
throw new Error("unreachable");
|
|
234
334
|
}
|
|
235
|
-
var
|
|
335
|
+
var ProviderError;
|
|
236
336
|
var init_provider_utils = __esm({
|
|
237
337
|
"core/provider-utils.ts"() {
|
|
238
338
|
"use strict";
|
|
239
|
-
|
|
240
|
-
sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
339
|
+
init_cli_ui();
|
|
241
340
|
ProviderError = class extends Error {
|
|
242
341
|
constructor(message, kind, originalError) {
|
|
243
342
|
super(message);
|
|
@@ -295,14 +394,13 @@ function createProvider(providerName, apiKey, modelName) {
|
|
|
295
394
|
);
|
|
296
395
|
}
|
|
297
396
|
}
|
|
298
|
-
var import_generative_ai, import_sdk, import_openai,
|
|
397
|
+
var import_generative_ai, import_sdk, import_openai, import_undici, PROVIDER_CATALOG, SUPPORTED_PROVIDERS, DEFAULT_MODELS, ENV_KEY_MAP, GeminiProvider, ClaudeProvider, OpenAICompatibleProvider, MiMoProvider, SpecGenerator;
|
|
299
398
|
var init_spec_generator = __esm({
|
|
300
399
|
"core/spec-generator.ts"() {
|
|
301
400
|
"use strict";
|
|
302
401
|
import_generative_ai = require("@google/generative-ai");
|
|
303
402
|
import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
304
403
|
import_openai = __toESM(require("openai"));
|
|
305
|
-
import_axios = __toESM(require("axios"));
|
|
306
404
|
import_undici = require("undici");
|
|
307
405
|
init_spec_prompt();
|
|
308
406
|
init_provider_utils();
|
|
@@ -312,7 +410,9 @@ var init_spec_generator = __esm({
|
|
|
312
410
|
displayName: "MiMo (Xiaomi)",
|
|
313
411
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
314
412
|
models: ["mimo-v2-pro"],
|
|
315
|
-
envKey: "MIMO_API_KEY"
|
|
413
|
+
envKey: "MIMO_API_KEY",
|
|
414
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
415
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
316
416
|
// baseURL not used — MiMo has a dedicated provider class
|
|
317
417
|
},
|
|
318
418
|
gemini: {
|
|
@@ -481,8 +581,8 @@ var init_spec_generator = __esm({
|
|
|
481
581
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
482
582
|
messages: [{ role: "user", content: prompt }]
|
|
483
583
|
});
|
|
484
|
-
const
|
|
485
|
-
if (
|
|
584
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
585
|
+
if (textBlock) return textBlock.text;
|
|
486
586
|
throw new Error("Unexpected response type from Claude API");
|
|
487
587
|
},
|
|
488
588
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -527,43 +627,29 @@ var init_spec_generator = __esm({
|
|
|
527
627
|
}
|
|
528
628
|
};
|
|
529
629
|
MiMoProvider = class {
|
|
630
|
+
client;
|
|
530
631
|
providerName = "mimo";
|
|
531
632
|
modelName;
|
|
532
|
-
apiKey;
|
|
533
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
534
633
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
535
|
-
|
|
634
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
635
|
+
this.client = new import_sdk.default({ apiKey, baseURL });
|
|
536
636
|
this.modelName = modelName;
|
|
537
637
|
}
|
|
538
638
|
async generate(prompt, systemInstruction) {
|
|
539
639
|
return withReliability(
|
|
540
640
|
async () => {
|
|
541
|
-
const
|
|
641
|
+
const stream = this.client.messages.stream({
|
|
542
642
|
model: this.modelName,
|
|
543
|
-
max_tokens:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
stream: false,
|
|
547
|
-
temperature: 1,
|
|
548
|
-
stop_sequences: null
|
|
549
|
-
};
|
|
550
|
-
if (systemInstruction) {
|
|
551
|
-
body.system = systemInstruction;
|
|
552
|
-
}
|
|
553
|
-
const response = await import_axios.default.post(this.baseUrl, body, {
|
|
554
|
-
headers: {
|
|
555
|
-
"api-key": this.apiKey,
|
|
556
|
-
"Content-Type": "application/json"
|
|
557
|
-
}
|
|
643
|
+
max_tokens: 65536,
|
|
644
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
645
|
+
messages: [{ role: "user", content: prompt }]
|
|
558
646
|
});
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
647
|
+
const message = await stream.finalMessage();
|
|
648
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
649
|
+
if (textBlock) return textBlock.text;
|
|
650
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
651
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
652
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
567
653
|
},
|
|
568
654
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
569
655
|
);
|
|
@@ -624,11 +710,68 @@ ${context.schema.slice(0, 3e3)}`);
|
|
|
624
710
|
}
|
|
625
711
|
});
|
|
626
712
|
|
|
713
|
+
// package.json
|
|
714
|
+
var require_package = __commonJS({
|
|
715
|
+
"package.json"(exports2, module2) {
|
|
716
|
+
module2.exports = {
|
|
717
|
+
name: "ai-spec-dev",
|
|
718
|
+
version: "0.46.0",
|
|
719
|
+
description: "AI-driven Development Orchestrator SDK & CLI",
|
|
720
|
+
main: "dist/index.js",
|
|
721
|
+
types: "dist/index.d.ts",
|
|
722
|
+
bin: {
|
|
723
|
+
"ai-spec": "dist/cli/index.js"
|
|
724
|
+
},
|
|
725
|
+
scripts: {
|
|
726
|
+
build: "tsup",
|
|
727
|
+
dev: "tsup --watch",
|
|
728
|
+
test: "vitest run",
|
|
729
|
+
"test:watch": "vitest"
|
|
730
|
+
},
|
|
731
|
+
keywords: [
|
|
732
|
+
"ai",
|
|
733
|
+
"spec",
|
|
734
|
+
"codegen",
|
|
735
|
+
"gemini",
|
|
736
|
+
"claude"
|
|
737
|
+
],
|
|
738
|
+
author: "",
|
|
739
|
+
license: "MIT",
|
|
740
|
+
dependencies: {
|
|
741
|
+
"@anthropic-ai/sdk": "^0.38.0",
|
|
742
|
+
"@google/generative-ai": "^0.21.0",
|
|
743
|
+
"@inquirer/editor": "^5.0.10",
|
|
744
|
+
"@inquirer/prompts": "^8.3.2",
|
|
745
|
+
"@rollup/rollup-darwin-arm64": "^4.60.1",
|
|
746
|
+
axios: "^1.13.6",
|
|
747
|
+
chalk: "^4.1.2",
|
|
748
|
+
commander: "^13.1.0",
|
|
749
|
+
dotenv: "^16.4.7",
|
|
750
|
+
"fs-extra": "^11.3.0",
|
|
751
|
+
openai: "^6.31.0",
|
|
752
|
+
undici: "^7.24.4"
|
|
753
|
+
},
|
|
754
|
+
devDependencies: {
|
|
755
|
+
"@types/fs-extra": "^11.0.4",
|
|
756
|
+
"@types/glob": "^8.1.0",
|
|
757
|
+
"@types/node": "^22.13.5",
|
|
758
|
+
glob: "^13.0.6",
|
|
759
|
+
"ts-node": "^10.9.2",
|
|
760
|
+
tsup: "^8.4.0",
|
|
761
|
+
typescript: "^5.7.3",
|
|
762
|
+
vitest: "^2.1.0"
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
627
768
|
// cli/index.ts
|
|
628
769
|
var import_commander = require("commander");
|
|
629
770
|
var dotenv = __toESM(require("dotenv"));
|
|
630
771
|
|
|
631
772
|
// cli/commands/create.ts
|
|
773
|
+
var path26 = __toESM(require("path"));
|
|
774
|
+
var fs27 = __toESM(require("fs-extra"));
|
|
632
775
|
var import_chalk26 = __toESM(require("chalk"));
|
|
633
776
|
var import_prompts7 = require("@inquirer/prompts");
|
|
634
777
|
init_spec_generator();
|
|
@@ -731,8 +874,8 @@ var WorkspaceLoader = class {
|
|
|
731
874
|
const repos = [];
|
|
732
875
|
for (const entry of entries) {
|
|
733
876
|
const absPath = path.join(this.workspaceRoot, entry);
|
|
734
|
-
const
|
|
735
|
-
if (!
|
|
877
|
+
const stat5 = await fs.stat(absPath).catch(() => null);
|
|
878
|
+
if (!stat5 || !stat5.isDirectory()) continue;
|
|
736
879
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
737
880
|
if (names && !names.includes(entry)) continue;
|
|
738
881
|
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"));
|
|
@@ -795,6 +938,7 @@ var WorkspaceLoader = class {
|
|
|
795
938
|
// cli/utils.ts
|
|
796
939
|
var path3 = __toESM(require("path"));
|
|
797
940
|
var fs3 = __toESM(require("fs-extra"));
|
|
941
|
+
var os2 = __toESM(require("os"));
|
|
798
942
|
var import_chalk2 = __toESM(require("chalk"));
|
|
799
943
|
var import_prompts = require("@inquirer/prompts");
|
|
800
944
|
init_spec_generator();
|
|
@@ -841,17 +985,42 @@ async function clearKey(provider) {
|
|
|
841
985
|
|
|
842
986
|
// cli/utils.ts
|
|
843
987
|
var CONFIG_FILE = ".ai-spec.json";
|
|
988
|
+
var GLOBAL_CONFIG_FILE = path3.join(os2.homedir(), ".ai-spec-config.json");
|
|
989
|
+
async function loadGlobalConfig() {
|
|
990
|
+
try {
|
|
991
|
+
if (await fs3.pathExists(GLOBAL_CONFIG_FILE)) {
|
|
992
|
+
return await fs3.readJson(GLOBAL_CONFIG_FILE);
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
return {};
|
|
997
|
+
}
|
|
998
|
+
async function saveGlobalConfig(config2) {
|
|
999
|
+
await fs3.ensureFile(GLOBAL_CONFIG_FILE);
|
|
1000
|
+
await fs3.writeJson(GLOBAL_CONFIG_FILE, config2, { spaces: 2 });
|
|
1001
|
+
}
|
|
844
1002
|
async function loadConfig(dir) {
|
|
1003
|
+
const globalConfig = await loadGlobalConfig();
|
|
1004
|
+
let localConfig = {};
|
|
845
1005
|
const p = path3.join(dir, CONFIG_FILE);
|
|
846
1006
|
if (await fs3.pathExists(p)) {
|
|
847
|
-
|
|
1007
|
+
try {
|
|
1008
|
+
localConfig = await fs3.readJson(p);
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
848
1011
|
}
|
|
849
|
-
return {};
|
|
1012
|
+
return { ...globalConfig, ...localConfig };
|
|
850
1013
|
}
|
|
851
1014
|
async function resolveApiKey(providerName, cliKey) {
|
|
852
1015
|
if (cliKey) return cliKey;
|
|
853
1016
|
const envVar = ENV_KEY_MAP[providerName];
|
|
854
1017
|
if (envVar && process.env[envVar]) return process.env[envVar];
|
|
1018
|
+
const meta = PROVIDER_CATALOG[providerName];
|
|
1019
|
+
if (meta?.fallbackEnvKeys) {
|
|
1020
|
+
for (const key of meta.fallbackEnvKeys) {
|
|
1021
|
+
if (process.env[key]) return process.env[key];
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
855
1024
|
const savedKey = await getSavedKey(providerName);
|
|
856
1025
|
if (savedKey) {
|
|
857
1026
|
const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
|
|
@@ -925,7 +1094,7 @@ var pe = "\0PERIOD" + Math.random() + "\0";
|
|
|
925
1094
|
var is = new RegExp(fe, "g");
|
|
926
1095
|
var rs = new RegExp(ue, "g");
|
|
927
1096
|
var ns = new RegExp(qt, "g");
|
|
928
|
-
var
|
|
1097
|
+
var os3 = new RegExp(de, "g");
|
|
929
1098
|
var hs = new RegExp(pe, "g");
|
|
930
1099
|
var as = /\\\\/g;
|
|
931
1100
|
var ls = /\\{/g;
|
|
@@ -940,7 +1109,7 @@ function ps(n7) {
|
|
|
940
1109
|
return n7.replace(as, fe).replace(ls, ue).replace(cs, qt).replace(fs4, de).replace(us, pe);
|
|
941
1110
|
}
|
|
942
1111
|
function ms(n7) {
|
|
943
|
-
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(
|
|
1112
|
+
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os3, ",").replace(hs, ".");
|
|
944
1113
|
}
|
|
945
1114
|
function me(n7) {
|
|
946
1115
|
if (!n7) return [""];
|
|
@@ -3870,11 +4039,11 @@ Ze.glob = Ze;
|
|
|
3870
4039
|
// core/global-constitution.ts
|
|
3871
4040
|
var fs5 = __toESM(require("fs-extra"));
|
|
3872
4041
|
var path4 = __toESM(require("path"));
|
|
3873
|
-
var
|
|
4042
|
+
var os4 = __toESM(require("os"));
|
|
3874
4043
|
var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
|
|
3875
4044
|
var SEARCH_ROOTS = [
|
|
3876
4045
|
// Workspace root is injected at runtime — see loadGlobalConstitution()
|
|
3877
|
-
|
|
4046
|
+
os4.homedir()
|
|
3878
4047
|
];
|
|
3879
4048
|
async function loadGlobalConstitution(extraRoots = []) {
|
|
3880
4049
|
const roots = [...extraRoots, ...SEARCH_ROOTS];
|
|
@@ -3903,7 +4072,7 @@ function mergeConstitutions(globalContent, projectContent) {
|
|
|
3903
4072
|
}
|
|
3904
4073
|
return parts.join("\n");
|
|
3905
4074
|
}
|
|
3906
|
-
async function saveGlobalConstitution(content, targetDir =
|
|
4075
|
+
async function saveGlobalConstitution(content, targetDir = os4.homedir()) {
|
|
3907
4076
|
const filePath = path4.join(targetDir, GLOBAL_CONSTITUTION_FILE);
|
|
3908
4077
|
await fs5.writeFile(filePath, content, "utf-8");
|
|
3909
4078
|
return filePath;
|
|
@@ -4936,32 +5105,32 @@ function validateDsl(raw) {
|
|
|
4936
5105
|
}
|
|
4937
5106
|
return { valid: true, dsl: raw };
|
|
4938
5107
|
}
|
|
4939
|
-
function validateFeature(raw,
|
|
5108
|
+
function validateFeature(raw, path43, errors) {
|
|
4940
5109
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4941
|
-
errors.push({ path:
|
|
5110
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4942
5111
|
return;
|
|
4943
5112
|
}
|
|
4944
5113
|
const f = raw;
|
|
4945
|
-
requireNonEmptyString(f["id"], `${
|
|
4946
|
-
requireNonEmptyString(f["title"], `${
|
|
4947
|
-
requireNonEmptyString(f["description"], `${
|
|
5114
|
+
requireNonEmptyString(f["id"], `${path43}.id`, errors);
|
|
5115
|
+
requireNonEmptyString(f["title"], `${path43}.title`, errors);
|
|
5116
|
+
requireNonEmptyString(f["description"], `${path43}.description`, errors);
|
|
4948
5117
|
}
|
|
4949
|
-
function validateModel(raw,
|
|
5118
|
+
function validateModel(raw, path43, errors) {
|
|
4950
5119
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4951
|
-
errors.push({ path:
|
|
5120
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4952
5121
|
return;
|
|
4953
5122
|
}
|
|
4954
5123
|
const m = raw;
|
|
4955
|
-
requireNonEmptyString(m["name"], `${
|
|
5124
|
+
requireNonEmptyString(m["name"], `${path43}.name`, errors);
|
|
4956
5125
|
if (!Array.isArray(m["fields"])) {
|
|
4957
|
-
errors.push({ path: `${
|
|
5126
|
+
errors.push({ path: `${path43}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4958
5127
|
} else {
|
|
4959
5128
|
const fields = m["fields"];
|
|
4960
5129
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4961
|
-
errors.push({ path: `${
|
|
5130
|
+
errors.push({ path: `${path43}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4962
5131
|
}
|
|
4963
5132
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4964
|
-
validateModelField(fields[j2], `${
|
|
5133
|
+
validateModelField(fields[j2], `${path43}.fields[${j2}]`, errors);
|
|
4965
5134
|
}
|
|
4966
5135
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4967
5136
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -4970,7 +5139,7 @@ function validateModel(raw, path42, errors) {
|
|
|
4970
5139
|
const name = f["name"];
|
|
4971
5140
|
if (seenFieldNames.has(name)) {
|
|
4972
5141
|
errors.push({
|
|
4973
|
-
path: `${
|
|
5142
|
+
path: `${path43}.fields[${j2}].name`,
|
|
4974
5143
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4975
5144
|
});
|
|
4976
5145
|
} else {
|
|
@@ -4981,184 +5150,184 @@ function validateModel(raw, path42, errors) {
|
|
|
4981
5150
|
}
|
|
4982
5151
|
if (m["relations"] !== void 0) {
|
|
4983
5152
|
if (!Array.isArray(m["relations"])) {
|
|
4984
|
-
errors.push({ path: `${
|
|
5153
|
+
errors.push({ path: `${path43}.relations`, message: "Must be an array of strings if present" });
|
|
4985
5154
|
} else {
|
|
4986
5155
|
const rels = m["relations"];
|
|
4987
5156
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4988
5157
|
if (typeof rels[j2] !== "string") {
|
|
4989
|
-
errors.push({ path: `${
|
|
5158
|
+
errors.push({ path: `${path43}.relations[${j2}]`, message: "Must be a string" });
|
|
4990
5159
|
}
|
|
4991
5160
|
}
|
|
4992
5161
|
}
|
|
4993
5162
|
}
|
|
4994
5163
|
}
|
|
4995
|
-
function validateModelField(raw,
|
|
5164
|
+
function validateModelField(raw, path43, errors) {
|
|
4996
5165
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4997
|
-
errors.push({ path:
|
|
5166
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4998
5167
|
return;
|
|
4999
5168
|
}
|
|
5000
5169
|
const f = raw;
|
|
5001
|
-
requireNonEmptyString(f["name"], `${
|
|
5002
|
-
requireNonEmptyString(f["type"], `${
|
|
5170
|
+
requireNonEmptyString(f["name"], `${path43}.name`, errors);
|
|
5171
|
+
requireNonEmptyString(f["type"], `${path43}.type`, errors);
|
|
5003
5172
|
if (typeof f["required"] !== "boolean") {
|
|
5004
|
-
errors.push({ path: `${
|
|
5173
|
+
errors.push({ path: `${path43}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
5005
5174
|
}
|
|
5006
5175
|
}
|
|
5007
|
-
function validateEndpoint(raw,
|
|
5176
|
+
function validateEndpoint(raw, path43, errors) {
|
|
5008
5177
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5009
|
-
errors.push({ path:
|
|
5178
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5010
5179
|
return;
|
|
5011
5180
|
}
|
|
5012
5181
|
const e = raw;
|
|
5013
|
-
requireNonEmptyString(e["id"], `${
|
|
5014
|
-
requireNonEmptyString(e["description"], `${
|
|
5182
|
+
requireNonEmptyString(e["id"], `${path43}.id`, errors);
|
|
5183
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5015
5184
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
5016
5185
|
errors.push({
|
|
5017
|
-
path: `${
|
|
5186
|
+
path: `${path43}.method`,
|
|
5018
5187
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
5019
5188
|
});
|
|
5020
5189
|
}
|
|
5021
5190
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
5022
5191
|
errors.push({
|
|
5023
|
-
path: `${
|
|
5192
|
+
path: `${path43}.path`,
|
|
5024
5193
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
5025
5194
|
});
|
|
5026
5195
|
}
|
|
5027
5196
|
if (typeof e["auth"] !== "boolean") {
|
|
5028
|
-
errors.push({ path: `${
|
|
5197
|
+
errors.push({ path: `${path43}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
5029
5198
|
}
|
|
5030
5199
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
5031
5200
|
errors.push({
|
|
5032
|
-
path: `${
|
|
5201
|
+
path: `${path43}.successStatus`,
|
|
5033
5202
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
5034
5203
|
});
|
|
5035
5204
|
}
|
|
5036
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
5205
|
+
requireNonEmptyString(e["successDescription"], `${path43}.successDescription`, errors);
|
|
5037
5206
|
if (e["request"] !== void 0) {
|
|
5038
|
-
validateRequestSchema(e["request"], `${
|
|
5207
|
+
validateRequestSchema(e["request"], `${path43}.request`, errors);
|
|
5039
5208
|
}
|
|
5040
5209
|
if (e["errors"] !== void 0) {
|
|
5041
5210
|
if (!Array.isArray(e["errors"])) {
|
|
5042
|
-
errors.push({ path: `${
|
|
5211
|
+
errors.push({ path: `${path43}.errors`, message: "Must be an array if present" });
|
|
5043
5212
|
} else {
|
|
5044
5213
|
const errs = e["errors"];
|
|
5045
5214
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
5046
|
-
errors.push({ path: `${
|
|
5215
|
+
errors.push({ path: `${path43}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
5047
5216
|
}
|
|
5048
5217
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
5049
|
-
validateResponseError(errs[j2], `${
|
|
5218
|
+
validateResponseError(errs[j2], `${path43}.errors[${j2}]`, errors);
|
|
5050
5219
|
}
|
|
5051
5220
|
}
|
|
5052
5221
|
}
|
|
5053
5222
|
}
|
|
5054
|
-
function validateRequestSchema(raw,
|
|
5223
|
+
function validateRequestSchema(raw, path43, errors) {
|
|
5055
5224
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5056
|
-
errors.push({ path:
|
|
5225
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5057
5226
|
return;
|
|
5058
5227
|
}
|
|
5059
5228
|
const r = raw;
|
|
5060
5229
|
for (const key of ["body", "query", "params"]) {
|
|
5061
5230
|
if (r[key] !== void 0) {
|
|
5062
|
-
validateFieldMap(r[key], `${
|
|
5231
|
+
validateFieldMap(r[key], `${path43}.${key}`, errors);
|
|
5063
5232
|
}
|
|
5064
5233
|
}
|
|
5065
5234
|
}
|
|
5066
|
-
function validateFieldMap(raw,
|
|
5235
|
+
function validateFieldMap(raw, path43, errors) {
|
|
5067
5236
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5068
|
-
errors.push({ path:
|
|
5237
|
+
errors.push({ path: path43, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
5069
5238
|
return;
|
|
5070
5239
|
}
|
|
5071
5240
|
const map = raw;
|
|
5072
5241
|
for (const [k2, v2] of Object.entries(map)) {
|
|
5073
5242
|
if (typeof v2 !== "string") {
|
|
5074
|
-
errors.push({ path: `${
|
|
5243
|
+
errors.push({ path: `${path43}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
5075
5244
|
}
|
|
5076
5245
|
}
|
|
5077
5246
|
}
|
|
5078
|
-
function validateResponseError(raw,
|
|
5247
|
+
function validateResponseError(raw, path43, errors) {
|
|
5079
5248
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5080
|
-
errors.push({ path:
|
|
5249
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5081
5250
|
return;
|
|
5082
5251
|
}
|
|
5083
5252
|
const e = raw;
|
|
5084
5253
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5085
|
-
errors.push({ path: `${
|
|
5254
|
+
errors.push({ path: `${path43}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5086
5255
|
}
|
|
5087
|
-
requireNonEmptyString(e["code"], `${
|
|
5088
|
-
requireNonEmptyString(e["description"], `${
|
|
5256
|
+
requireNonEmptyString(e["code"], `${path43}.code`, errors);
|
|
5257
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5089
5258
|
}
|
|
5090
|
-
function validateBehavior(raw,
|
|
5259
|
+
function validateBehavior(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 b = raw;
|
|
5096
|
-
requireNonEmptyString(b["id"], `${
|
|
5097
|
-
requireNonEmptyString(b["description"], `${
|
|
5265
|
+
requireNonEmptyString(b["id"], `${path43}.id`, errors);
|
|
5266
|
+
requireNonEmptyString(b["description"], `${path43}.description`, errors);
|
|
5098
5267
|
if (b["constraints"] !== void 0) {
|
|
5099
5268
|
if (!Array.isArray(b["constraints"])) {
|
|
5100
|
-
errors.push({ path: `${
|
|
5269
|
+
errors.push({ path: `${path43}.constraints`, message: "Must be an array of strings if present" });
|
|
5101
5270
|
} else {
|
|
5102
5271
|
const cs2 = b["constraints"];
|
|
5103
5272
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5104
5273
|
if (typeof cs2[j2] !== "string") {
|
|
5105
|
-
errors.push({ path: `${
|
|
5274
|
+
errors.push({ path: `${path43}.constraints[${j2}]`, message: "Must be a string" });
|
|
5106
5275
|
}
|
|
5107
5276
|
}
|
|
5108
5277
|
}
|
|
5109
5278
|
}
|
|
5110
5279
|
}
|
|
5111
|
-
function validateComponent(raw,
|
|
5280
|
+
function validateComponent(raw, path43, errors) {
|
|
5112
5281
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5113
|
-
errors.push({ path:
|
|
5282
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5114
5283
|
return;
|
|
5115
5284
|
}
|
|
5116
5285
|
const c = raw;
|
|
5117
|
-
requireNonEmptyString(c["id"], `${
|
|
5118
|
-
requireNonEmptyString(c["name"], `${
|
|
5119
|
-
requireNonEmptyString(c["description"], `${
|
|
5286
|
+
requireNonEmptyString(c["id"], `${path43}.id`, errors);
|
|
5287
|
+
requireNonEmptyString(c["name"], `${path43}.name`, errors);
|
|
5288
|
+
requireNonEmptyString(c["description"], `${path43}.description`, errors);
|
|
5120
5289
|
if (c["props"] !== void 0) {
|
|
5121
5290
|
if (!Array.isArray(c["props"])) {
|
|
5122
|
-
errors.push({ path: `${
|
|
5291
|
+
errors.push({ path: `${path43}.props`, message: "Must be an array if present" });
|
|
5123
5292
|
} else {
|
|
5124
5293
|
const props = c["props"];
|
|
5125
5294
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5126
5295
|
const p = props[j2];
|
|
5127
5296
|
if (typeof p !== "object" || p === null) {
|
|
5128
|
-
errors.push({ path: `${
|
|
5297
|
+
errors.push({ path: `${path43}.props[${j2}]`, message: "Must be an object" });
|
|
5129
5298
|
continue;
|
|
5130
5299
|
}
|
|
5131
|
-
requireNonEmptyString(p["name"], `${
|
|
5132
|
-
requireNonEmptyString(p["type"], `${
|
|
5300
|
+
requireNonEmptyString(p["name"], `${path43}.props[${j2}].name`, errors);
|
|
5301
|
+
requireNonEmptyString(p["type"], `${path43}.props[${j2}].type`, errors);
|
|
5133
5302
|
if (typeof p["required"] !== "boolean") {
|
|
5134
|
-
errors.push({ path: `${
|
|
5303
|
+
errors.push({ path: `${path43}.props[${j2}].required`, message: "Must be boolean" });
|
|
5135
5304
|
}
|
|
5136
5305
|
}
|
|
5137
5306
|
}
|
|
5138
5307
|
}
|
|
5139
5308
|
if (c["events"] !== void 0) {
|
|
5140
5309
|
if (!Array.isArray(c["events"])) {
|
|
5141
|
-
errors.push({ path: `${
|
|
5310
|
+
errors.push({ path: `${path43}.events`, message: "Must be an array if present" });
|
|
5142
5311
|
} else {
|
|
5143
5312
|
const events = c["events"];
|
|
5144
5313
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5145
5314
|
const e = events[j2];
|
|
5146
5315
|
if (typeof e !== "object" || e === null) {
|
|
5147
|
-
errors.push({ path: `${
|
|
5316
|
+
errors.push({ path: `${path43}.events[${j2}]`, message: "Must be an object" });
|
|
5148
5317
|
continue;
|
|
5149
5318
|
}
|
|
5150
|
-
requireNonEmptyString(e["name"], `${
|
|
5319
|
+
requireNonEmptyString(e["name"], `${path43}.events[${j2}].name`, errors);
|
|
5151
5320
|
}
|
|
5152
5321
|
}
|
|
5153
5322
|
}
|
|
5154
5323
|
if (c["state"] !== void 0) {
|
|
5155
5324
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5156
|
-
errors.push({ path: `${
|
|
5325
|
+
errors.push({ path: `${path43}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5157
5326
|
}
|
|
5158
5327
|
}
|
|
5159
5328
|
if (c["apiCalls"] !== void 0) {
|
|
5160
5329
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5161
|
-
errors.push({ path: `${
|
|
5330
|
+
errors.push({ path: `${path43}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5162
5331
|
}
|
|
5163
5332
|
}
|
|
5164
5333
|
}
|
|
@@ -5220,10 +5389,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5220
5389
|
}
|
|
5221
5390
|
}
|
|
5222
5391
|
}
|
|
5223
|
-
function requireNonEmptyString(v2,
|
|
5392
|
+
function requireNonEmptyString(v2, path43, errors) {
|
|
5224
5393
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5225
5394
|
errors.push({
|
|
5226
|
-
path:
|
|
5395
|
+
path: path43,
|
|
5227
5396
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5228
5397
|
});
|
|
5229
5398
|
}
|
|
@@ -5434,13 +5603,18 @@ Output ONLY the corrected JSON. No explanation.`;
|
|
|
5434
5603
|
|
|
5435
5604
|
// core/token-budget.ts
|
|
5436
5605
|
var import_chalk5 = __toESM(require("chalk"));
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5606
|
+
|
|
5607
|
+
// core/config-defaults.ts
|
|
5608
|
+
var DEFAULT_LOG_DIR = ".ai-spec-logs";
|
|
5609
|
+
var DEFAULT_VCR_DIR = ".ai-spec-vcr";
|
|
5610
|
+
var DEFAULT_BACKUP_DIR = ".ai-spec-backup";
|
|
5611
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5612
|
+
var DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
|
|
5613
|
+
var DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 3e4;
|
|
5614
|
+
var DEFAULT_MAX_FIX_FILE_CHARS = 6e4;
|
|
5615
|
+
var DEFAULT_DSL_MAX_RETRIES = 2;
|
|
5616
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5617
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5444
5618
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5445
5619
|
gemini: 9e5,
|
|
5446
5620
|
claude: 18e4,
|
|
@@ -5448,8 +5622,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5448
5622
|
deepseek: 6e4,
|
|
5449
5623
|
default: 1e5
|
|
5450
5624
|
};
|
|
5625
|
+
|
|
5626
|
+
// core/token-budget.ts
|
|
5627
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5628
|
+
function estimateTokens(text) {
|
|
5629
|
+
if (!text) return 0;
|
|
5630
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5631
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5632
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5633
|
+
}
|
|
5634
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5451
5635
|
function getDefaultBudget(providerName) {
|
|
5452
|
-
return
|
|
5636
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5453
5637
|
}
|
|
5454
5638
|
|
|
5455
5639
|
// core/safe-json.ts
|
|
@@ -5520,7 +5704,7 @@ function sanitizeDsl(raw) {
|
|
|
5520
5704
|
}
|
|
5521
5705
|
return dsl;
|
|
5522
5706
|
}
|
|
5523
|
-
var MAX_RETRIES =
|
|
5707
|
+
var MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
|
|
5524
5708
|
var DEFAULT_MAX_SPEC_CHARS = 12e3;
|
|
5525
5709
|
function dslFilePath(specFilePath) {
|
|
5526
5710
|
const dir = path7.dirname(specFilePath);
|
|
@@ -6244,12 +6428,11 @@ Shared component structure patterns:`);
|
|
|
6244
6428
|
// core/run-snapshot.ts
|
|
6245
6429
|
var fs10 = __toESM(require("fs-extra"));
|
|
6246
6430
|
var path9 = __toESM(require("path"));
|
|
6247
|
-
var BACKUP_DIR = ".ai-spec-backup";
|
|
6248
6431
|
var RunSnapshot = class {
|
|
6249
6432
|
constructor(workingDir, runId) {
|
|
6250
6433
|
this.workingDir = workingDir;
|
|
6251
6434
|
this.runId = runId;
|
|
6252
|
-
this.backupRoot = path9.join(workingDir,
|
|
6435
|
+
this.backupRoot = path9.join(workingDir, DEFAULT_BACKUP_DIR, runId);
|
|
6253
6436
|
}
|
|
6254
6437
|
backupRoot;
|
|
6255
6438
|
snapshotted = /* @__PURE__ */ new Set();
|
|
@@ -6305,7 +6488,6 @@ function getActiveSnapshot() {
|
|
|
6305
6488
|
var fs11 = __toESM(require("fs-extra"));
|
|
6306
6489
|
var path10 = __toESM(require("path"));
|
|
6307
6490
|
var import_chalk7 = __toESM(require("chalk"));
|
|
6308
|
-
var LOG_DIR = ".ai-spec-logs";
|
|
6309
6491
|
function appendJsonlLine(filePath, record) {
|
|
6310
6492
|
try {
|
|
6311
6493
|
fs11.appendFileSync(filePath, JSON.stringify(record) + "\n");
|
|
@@ -6376,8 +6558,8 @@ var RunLogger = class {
|
|
|
6376
6558
|
this.workingDir = workingDir;
|
|
6377
6559
|
this.runId = runId;
|
|
6378
6560
|
this.startMs = Date.now();
|
|
6379
|
-
this.logPath = path10.join(workingDir,
|
|
6380
|
-
this.jsonlPath = path10.join(workingDir,
|
|
6561
|
+
this.logPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
|
|
6562
|
+
this.jsonlPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
|
|
6381
6563
|
this.log = {
|
|
6382
6564
|
runId,
|
|
6383
6565
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6711,6 +6893,7 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6711
6893
|
}
|
|
6712
6894
|
|
|
6713
6895
|
// core/code-generator.ts
|
|
6896
|
+
init_cli_ui();
|
|
6714
6897
|
var CodeGenerator = class {
|
|
6715
6898
|
constructor(provider, mode = "claude-code") {
|
|
6716
6899
|
this.provider = provider;
|
|
@@ -7159,6 +7342,7 @@ ${spec}
|
|
|
7159
7342
|
${constitutionSection}
|
|
7160
7343
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
7161
7344
|
${existingContent || "Output only the complete file content."}`;
|
|
7345
|
+
const fileSpinner = startSpinner(`${prefix}Generating ${import_chalk9.default.bold(item.file)}...`);
|
|
7162
7346
|
try {
|
|
7163
7347
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
7164
7348
|
const fileContent = stripCodeFences(raw);
|
|
@@ -7166,11 +7350,11 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
7166
7350
|
await fs12.ensureDir(path11.dirname(fullPath));
|
|
7167
7351
|
await fs12.writeFile(fullPath, fileContent, "utf-8");
|
|
7168
7352
|
getActiveLogger()?.fileWritten(item.file);
|
|
7169
|
-
|
|
7353
|
+
fileSpinner.succeed(`${existingContent ? import_chalk9.default.yellow("~") : import_chalk9.default.green("+")} ${import_chalk9.default.bold(item.file)}`);
|
|
7170
7354
|
successCount++;
|
|
7171
7355
|
writtenFiles.push(item.file);
|
|
7172
7356
|
} catch (err) {
|
|
7173
|
-
|
|
7357
|
+
fileSpinner.fail(`${import_chalk9.default.bold(item.file)} \u2014 ${err.message}`);
|
|
7174
7358
|
}
|
|
7175
7359
|
}
|
|
7176
7360
|
if (!taskLabel) {
|
|
@@ -7317,7 +7501,7 @@ ${context.routeSummary}
|
|
|
7317
7501
|
}
|
|
7318
7502
|
if (context.schema) {
|
|
7319
7503
|
parts.push(`=== Prisma Schema ===
|
|
7320
|
-
${context.schema.slice(0,
|
|
7504
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
7321
7505
|
`);
|
|
7322
7506
|
}
|
|
7323
7507
|
if (context.errorPatterns) {
|
|
@@ -7365,7 +7549,7 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
7365
7549
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
7366
7550
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
7367
7551
|
}
|
|
7368
|
-
var REVIEW_HISTORY_FILE =
|
|
7552
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
7369
7553
|
async function loadReviewHistory(projectRoot) {
|
|
7370
7554
|
const historyPath = path13.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
7371
7555
|
try {
|
|
@@ -7496,15 +7680,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
7496
7680
|
${codeContext}`;
|
|
7497
7681
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
7498
7682
|
console.log(import_chalk11.default.gray(" Pass 2/3: Implementation review..."));
|
|
7683
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
7499
7684
|
const history = await loadReviewHistory(this.projectRoot);
|
|
7500
7685
|
const historyContext = buildHistoryContext(history);
|
|
7501
7686
|
const implPrompt = `Review the implementation details of this change.
|
|
7502
7687
|
|
|
7503
|
-
=== Feature Spec ===
|
|
7504
|
-
${
|
|
7505
|
-
|
|
7506
|
-
=== Code ===
|
|
7507
|
-
${codeContext}
|
|
7688
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
7689
|
+
${specDigest}
|
|
7508
7690
|
|
|
7509
7691
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
7510
7692
|
${archReview}
|
|
@@ -7513,11 +7695,8 @@ ${historyContext}`;
|
|
|
7513
7695
|
console.log(import_chalk11.default.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
7514
7696
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
7515
7697
|
|
|
7516
|
-
=== Feature Spec ===
|
|
7517
|
-
${
|
|
7518
|
-
|
|
7519
|
-
=== Code ===
|
|
7520
|
-
${codeContext}
|
|
7698
|
+
=== Feature Spec (digest) ===
|
|
7699
|
+
${specDigest}
|
|
7521
7700
|
|
|
7522
7701
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
7523
7702
|
${archReview}
|
|
@@ -7591,8 +7770,8 @@ ${sep}
|
|
|
7591
7770
|
filesSection += `
|
|
7592
7771
|
|
|
7593
7772
|
=== ${filePath} ===
|
|
7594
|
-
${content.slice(0,
|
|
7595
|
-
if (content.length >
|
|
7773
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7774
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
7596
7775
|
... (truncated, ${content.length} chars total)`;
|
|
7597
7776
|
} catch {
|
|
7598
7777
|
filesSection += `
|
|
@@ -8196,8 +8375,9 @@ var import_chalk15 = __toESM(require("chalk"));
|
|
|
8196
8375
|
var import_child_process5 = require("child_process");
|
|
8197
8376
|
var fs18 = __toESM(require("fs-extra"));
|
|
8198
8377
|
var path17 = __toESM(require("path"));
|
|
8199
|
-
|
|
8200
|
-
var
|
|
8378
|
+
init_cli_ui();
|
|
8379
|
+
var MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
|
|
8380
|
+
var MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
|
|
8201
8381
|
function runCommand(cmd, cwd) {
|
|
8202
8382
|
try {
|
|
8203
8383
|
const output = (0, import_child_process5.execSync)(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
|
|
@@ -8387,16 +8567,17 @@ ${errorSummary}
|
|
|
8387
8567
|
${fileContent}
|
|
8388
8568
|
|
|
8389
8569
|
Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
|
|
8570
|
+
const fixSpinner = startSpinner(`Fixing ${import_chalk15.default.bold(file)} (${fileErrors.length} error(s))...`);
|
|
8390
8571
|
try {
|
|
8391
8572
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
8392
8573
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
8393
8574
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
8394
8575
|
await fs18.writeFile(fullPath, fixed, "utf-8");
|
|
8395
8576
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
8396
|
-
|
|
8577
|
+
fixSpinner.succeed(`Auto-fixed: ${file}`);
|
|
8397
8578
|
} catch (err) {
|
|
8398
8579
|
results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
|
|
8399
|
-
|
|
8580
|
+
fixSpinner.fail(`Could not auto-fix: ${file}`);
|
|
8400
8581
|
}
|
|
8401
8582
|
}
|
|
8402
8583
|
return results;
|
|
@@ -9405,8 +9586,8 @@ async function findLatestDslFile(projectDir) {
|
|
|
9405
9586
|
const entries = await fs22.readdir(dir);
|
|
9406
9587
|
for (const entry of entries) {
|
|
9407
9588
|
const abs = path21.join(dir, entry);
|
|
9408
|
-
const
|
|
9409
|
-
if (
|
|
9589
|
+
const stat5 = await fs22.stat(abs);
|
|
9590
|
+
if (stat5.isDirectory()) {
|
|
9410
9591
|
await scan(abs);
|
|
9411
9592
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9412
9593
|
allFiles.push(abs);
|
|
@@ -11144,7 +11325,7 @@ ${optionsText}`;
|
|
|
11144
11325
|
var import_crypto2 = require("crypto");
|
|
11145
11326
|
var fs24 = __toESM(require("fs-extra"));
|
|
11146
11327
|
var path23 = __toESM(require("path"));
|
|
11147
|
-
var VCR_DIR =
|
|
11328
|
+
var VCR_DIR = DEFAULT_VCR_DIR;
|
|
11148
11329
|
var VcrRecordingProvider = class {
|
|
11149
11330
|
constructor(inner) {
|
|
11150
11331
|
this.inner = inner;
|
|
@@ -11272,6 +11453,7 @@ async function listVcrRecordings(workingDir) {
|
|
|
11272
11453
|
}
|
|
11273
11454
|
|
|
11274
11455
|
// cli/pipeline/single-repo.ts
|
|
11456
|
+
init_cli_ui();
|
|
11275
11457
|
async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
11276
11458
|
const specProviderName = opts.provider || config2.provider || "gemini";
|
|
11277
11459
|
const specModelName = opts.model || config2.model || DEFAULT_MODELS[specProviderName];
|
|
@@ -11334,7 +11516,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11334
11516
|
console.log(import_chalk25.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11335
11517
|
}
|
|
11336
11518
|
} else {
|
|
11337
|
-
|
|
11519
|
+
const constitutionSpinner = startSpinner("Constitution not found \u2014 auto-generating...");
|
|
11338
11520
|
try {
|
|
11339
11521
|
const constitutionGen = new ConstitutionGenerator(
|
|
11340
11522
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -11342,9 +11524,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11342
11524
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
11343
11525
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
11344
11526
|
context.constitution = constitutionContent;
|
|
11345
|
-
|
|
11527
|
+
constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
|
|
11346
11528
|
} catch (err) {
|
|
11347
|
-
|
|
11529
|
+
constitutionSpinner.fail(`Constitution auto-generation failed (${err.message}), continuing without it.`);
|
|
11348
11530
|
}
|
|
11349
11531
|
}
|
|
11350
11532
|
let architectureDecision;
|
|
@@ -11375,17 +11557,18 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11375
11557
|
let initialSpec;
|
|
11376
11558
|
let initialTasks = [];
|
|
11377
11559
|
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
11560
|
+
const specSpinner = startStage("spec_gen", `Generating spec with ${specProviderName}/${specModelName}...`);
|
|
11378
11561
|
try {
|
|
11379
11562
|
if (opts.skipTasks) {
|
|
11380
11563
|
const { SpecGenerator: SpecGenerator2 } = await Promise.resolve().then(() => (init_spec_generator(), spec_generator_exports));
|
|
11381
11564
|
const generator = new SpecGenerator2(specProvider);
|
|
11382
11565
|
initialSpec = await generator.generateSpec(idea, context, architectureDecision);
|
|
11383
|
-
|
|
11566
|
+
specSpinner.succeed("Spec generated.");
|
|
11384
11567
|
} else {
|
|
11385
11568
|
const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
|
|
11386
11569
|
initialSpec = result.spec;
|
|
11387
11570
|
initialTasks = result.tasks;
|
|
11388
|
-
|
|
11571
|
+
specSpinner.succeed("Spec generated.");
|
|
11389
11572
|
if (initialTasks.length > 0) {
|
|
11390
11573
|
console.log(import_chalk25.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
11391
11574
|
} else {
|
|
@@ -11394,8 +11577,8 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11394
11577
|
}
|
|
11395
11578
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
11396
11579
|
} catch (err) {
|
|
11580
|
+
specSpinner.fail(`Spec generation failed: ${err.message}`);
|
|
11397
11581
|
runLogger.stageFail("spec_gen", err.message);
|
|
11398
|
-
console.error(import_chalk25.default.red(" \u2718 Spec generation failed:"), err);
|
|
11399
11582
|
process.exit(1);
|
|
11400
11583
|
}
|
|
11401
11584
|
let finalSpec;
|
|
@@ -11417,7 +11600,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11417
11600
|
console.log(import_chalk25.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
11418
11601
|
}
|
|
11419
11602
|
runLogger.stageStart("spec_assess");
|
|
11603
|
+
const assessSpinner = startStage("spec_assess", "Evaluating spec quality...");
|
|
11420
11604
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
11605
|
+
assessSpinner.stop();
|
|
11421
11606
|
if (assessment) {
|
|
11422
11607
|
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
11423
11608
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
@@ -11509,21 +11694,24 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11509
11694
|
console.log(import_chalk25.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
11510
11695
|
console.log(import_chalk25.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
11511
11696
|
runLogger.stageStart("dsl_extract");
|
|
11697
|
+
const dslSpinner = startStage("dsl_extract", "Extracting DSL from spec...");
|
|
11512
11698
|
try {
|
|
11513
11699
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
11514
|
-
if (isFrontend)
|
|
11700
|
+
if (isFrontend) {
|
|
11701
|
+
dslSpinner.update("\u{1F517} Extracting DSL (frontend ComponentSpec mode)...");
|
|
11702
|
+
}
|
|
11515
11703
|
const dslExtractor = new DslExtractor(specProvider);
|
|
11516
11704
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
11517
11705
|
if (extractedDsl) {
|
|
11518
11706
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
11519
|
-
|
|
11707
|
+
dslSpinner.succeed("DSL extracted and validated.");
|
|
11520
11708
|
} else {
|
|
11521
11709
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
11522
|
-
|
|
11710
|
+
dslSpinner.fail("DSL skipped \u2014 codegen will use Spec + Tasks only.");
|
|
11523
11711
|
}
|
|
11524
11712
|
} catch (err) {
|
|
11525
11713
|
runLogger.stageFail("dsl_extract", err.message);
|
|
11526
|
-
|
|
11714
|
+
dslSpinner.fail(`DSL extraction error: ${err.message} \u2014 continuing without DSL.`);
|
|
11527
11715
|
}
|
|
11528
11716
|
}
|
|
11529
11717
|
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
@@ -11698,6 +11886,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11698
11886
|
if (!opts.skipReview) {
|
|
11699
11887
|
console.log(import_chalk25.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
11700
11888
|
runLogger.stageStart("review");
|
|
11889
|
+
const reviewSpinner = startStage("review", "Running 3-pass code review...");
|
|
11701
11890
|
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
11702
11891
|
const savedSpec = await fs25.readFile(specFile, "utf-8");
|
|
11703
11892
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
@@ -11705,6 +11894,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11705
11894
|
} else {
|
|
11706
11895
|
reviewResult = await reviewer.reviewCode(savedSpec, specFile);
|
|
11707
11896
|
}
|
|
11897
|
+
reviewSpinner.succeed("Code review complete.");
|
|
11708
11898
|
runLogger.stageEnd("review");
|
|
11709
11899
|
const complianceScore = extractComplianceScore(reviewResult);
|
|
11710
11900
|
const missingCount = extractMissingCount(reviewResult);
|
|
@@ -11833,7 +12023,147 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11833
12023
|
}
|
|
11834
12024
|
}
|
|
11835
12025
|
|
|
12026
|
+
// core/repo-store.ts
|
|
12027
|
+
var fs26 = __toESM(require("fs-extra"));
|
|
12028
|
+
var path25 = __toESM(require("path"));
|
|
12029
|
+
var os5 = __toESM(require("os"));
|
|
12030
|
+
var REPO_STORE_FILE = path25.join(os5.homedir(), ".ai-spec-repos.json");
|
|
12031
|
+
async function readStore2() {
|
|
12032
|
+
try {
|
|
12033
|
+
if (await fs26.pathExists(REPO_STORE_FILE)) {
|
|
12034
|
+
return await fs26.readJson(REPO_STORE_FILE);
|
|
12035
|
+
}
|
|
12036
|
+
} catch (err) {
|
|
12037
|
+
console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${err.message}.`);
|
|
12038
|
+
}
|
|
12039
|
+
return { repos: [] };
|
|
12040
|
+
}
|
|
12041
|
+
async function writeStore2(store) {
|
|
12042
|
+
await fs26.ensureFile(REPO_STORE_FILE);
|
|
12043
|
+
await fs26.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
|
|
12044
|
+
}
|
|
12045
|
+
async function getRegisteredRepos() {
|
|
12046
|
+
const store = await readStore2();
|
|
12047
|
+
return store.repos;
|
|
12048
|
+
}
|
|
12049
|
+
async function registerRepo(repo) {
|
|
12050
|
+
const store = await readStore2();
|
|
12051
|
+
const idx = store.repos.findIndex((r) => r.path === repo.path);
|
|
12052
|
+
if (idx >= 0) {
|
|
12053
|
+
store.repos[idx] = repo;
|
|
12054
|
+
} else {
|
|
12055
|
+
store.repos.push(repo);
|
|
12056
|
+
}
|
|
12057
|
+
await writeStore2(store);
|
|
12058
|
+
}
|
|
12059
|
+
|
|
11836
12060
|
// cli/commands/create.ts
|
|
12061
|
+
init_spec_generator();
|
|
12062
|
+
async function promptRepoRole() {
|
|
12063
|
+
return (0, import_prompts7.select)({
|
|
12064
|
+
message: "What type of repo is this?",
|
|
12065
|
+
choices: [
|
|
12066
|
+
{ name: "Frontend", value: "frontend" },
|
|
12067
|
+
{ name: "Backend", value: "backend" },
|
|
12068
|
+
{ name: "Mobile", value: "mobile" },
|
|
12069
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12070
|
+
]
|
|
12071
|
+
});
|
|
12072
|
+
}
|
|
12073
|
+
async function quickRegisterRepo(provider) {
|
|
12074
|
+
const roleOverride = await promptRepoRole();
|
|
12075
|
+
const roleLabels = {
|
|
12076
|
+
frontend: "frontend",
|
|
12077
|
+
backend: "backend",
|
|
12078
|
+
mobile: "mobile",
|
|
12079
|
+
shared: "shared"
|
|
12080
|
+
};
|
|
12081
|
+
const raw = await (0, import_prompts7.input)({
|
|
12082
|
+
message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
|
|
12083
|
+
validate: (v2) => {
|
|
12084
|
+
const trimmed = v2.trim();
|
|
12085
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12086
|
+
if (!path26.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12087
|
+
return true;
|
|
12088
|
+
}
|
|
12089
|
+
});
|
|
12090
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12091
|
+
const resolved = path26.resolve(cleaned);
|
|
12092
|
+
if (!await fs27.pathExists(resolved)) {
|
|
12093
|
+
console.log(import_chalk26.default.red(` Path does not exist: ${resolved}`));
|
|
12094
|
+
return quickRegisterRepo(provider);
|
|
12095
|
+
}
|
|
12096
|
+
const { type, role: detectedRole } = await detectRepoType(resolved);
|
|
12097
|
+
const role = roleOverride ?? detectedRole;
|
|
12098
|
+
const repoName = path26.basename(resolved);
|
|
12099
|
+
console.log(import_chalk26.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12100
|
+
const constitutionPath = path26.join(resolved, CONSTITUTION_FILE);
|
|
12101
|
+
let hasConstitution = await fs27.pathExists(constitutionPath);
|
|
12102
|
+
if (!hasConstitution) {
|
|
12103
|
+
console.log(import_chalk26.default.blue(` Generating constitution for ${repoName}...`));
|
|
12104
|
+
try {
|
|
12105
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12106
|
+
const content = await gen.generate(resolved);
|
|
12107
|
+
await gen.saveConstitution(resolved, content);
|
|
12108
|
+
hasConstitution = true;
|
|
12109
|
+
console.log(import_chalk26.default.green(` \u2714 Constitution saved`));
|
|
12110
|
+
} catch (err) {
|
|
12111
|
+
console.log(import_chalk26.default.yellow(` \u26A0 Constitution failed: ${err.message}`));
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
const entry = {
|
|
12115
|
+
name: repoName,
|
|
12116
|
+
path: resolved,
|
|
12117
|
+
type,
|
|
12118
|
+
role,
|
|
12119
|
+
hasConstitution,
|
|
12120
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12121
|
+
};
|
|
12122
|
+
await registerRepo(entry);
|
|
12123
|
+
console.log(import_chalk26.default.green(` \u2714 Repo registered: ${repoName}`));
|
|
12124
|
+
return entry;
|
|
12125
|
+
}
|
|
12126
|
+
async function selectRepos(registeredRepos, provider) {
|
|
12127
|
+
const ADD_NEW = "__add_new__";
|
|
12128
|
+
const choices = [
|
|
12129
|
+
...registeredRepos.map((r) => ({
|
|
12130
|
+
name: `${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`,
|
|
12131
|
+
value: r.path
|
|
12132
|
+
})),
|
|
12133
|
+
{ name: import_chalk26.default.cyan("+ Add new repo"), value: ADD_NEW }
|
|
12134
|
+
];
|
|
12135
|
+
const selected = await (0, import_prompts7.checkbox)({
|
|
12136
|
+
message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
|
|
12137
|
+
choices,
|
|
12138
|
+
required: true
|
|
12139
|
+
});
|
|
12140
|
+
const result = [];
|
|
12141
|
+
for (const val of selected) {
|
|
12142
|
+
if (val === ADD_NEW) {
|
|
12143
|
+
const newRepo = await quickRegisterRepo(provider);
|
|
12144
|
+
result.push(newRepo);
|
|
12145
|
+
} else {
|
|
12146
|
+
const repo = registeredRepos.find((r) => r.path === val);
|
|
12147
|
+
if (repo) result.push(repo);
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
if (result.length === 0) {
|
|
12151
|
+
console.log(import_chalk26.default.yellow(" No repos selected. Please select at least one."));
|
|
12152
|
+
return selectRepos(registeredRepos, provider);
|
|
12153
|
+
}
|
|
12154
|
+
return result;
|
|
12155
|
+
}
|
|
12156
|
+
function buildWorkspaceConfig(repos) {
|
|
12157
|
+
return {
|
|
12158
|
+
name: "ai-spec-workspace",
|
|
12159
|
+
repos: repos.map((r) => ({
|
|
12160
|
+
name: r.name,
|
|
12161
|
+
path: r.path,
|
|
12162
|
+
type: r.type,
|
|
12163
|
+
role: r.role
|
|
12164
|
+
}))
|
|
12165
|
+
};
|
|
12166
|
+
}
|
|
11837
12167
|
function registerCreate(program2) {
|
|
11838
12168
|
program2.command("create").description("Generate a feature spec and kick off code generation").argument("[idea]", "Feature idea in natural language (prompted if omitted)").option(
|
|
11839
12169
|
"--provider <name>",
|
|
@@ -11856,24 +12186,75 @@ function registerCreate(program2) {
|
|
|
11856
12186
|
});
|
|
11857
12187
|
}
|
|
11858
12188
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11859
|
-
const
|
|
11860
|
-
if (
|
|
12189
|
+
const existingWorkspaceConfig = await workspaceLoader.load();
|
|
12190
|
+
if (existingWorkspaceConfig) {
|
|
11861
12191
|
console.log(import_chalk26.default.cyan(`
|
|
11862
|
-
[Workspace] Detected workspace: ${
|
|
11863
|
-
console.log(import_chalk26.default.gray(` Repos: ${
|
|
11864
|
-
const pipelineResults = await runMultiRepoPipeline(idea,
|
|
12192
|
+
[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
|
|
12193
|
+
console.log(import_chalk26.default.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
12194
|
+
const pipelineResults = await runMultiRepoPipeline(idea, existingWorkspaceConfig, opts, currentDir, config2);
|
|
11865
12195
|
if (opts.serve) {
|
|
11866
12196
|
await handleAutoServe(pipelineResults);
|
|
11867
12197
|
}
|
|
11868
12198
|
return;
|
|
11869
12199
|
}
|
|
11870
|
-
|
|
12200
|
+
const providerName = opts.provider || config2.provider || "gemini";
|
|
12201
|
+
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
12202
|
+
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
12203
|
+
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
12204
|
+
const registeredRepos = await getRegisteredRepos();
|
|
12205
|
+
if (registeredRepos.length === 0) {
|
|
12206
|
+
console.log(import_chalk26.default.yellow("\n No repos registered. Please register repos first."));
|
|
12207
|
+
console.log(import_chalk26.default.gray(" Run: ai-spec init"));
|
|
12208
|
+
console.log(import_chalk26.default.gray(" Or add a repo now:\n"));
|
|
12209
|
+
const addNow = await (0, import_prompts7.select)({
|
|
12210
|
+
message: "Add a repo now?",
|
|
12211
|
+
choices: [
|
|
12212
|
+
{ name: "Yes \u2014 register a repo and continue", value: "yes" },
|
|
12213
|
+
{ name: "No \u2014 exit", value: "no" }
|
|
12214
|
+
]
|
|
12215
|
+
});
|
|
12216
|
+
if (addNow === "no") {
|
|
12217
|
+
process.exit(0);
|
|
12218
|
+
}
|
|
12219
|
+
const newRepo = await quickRegisterRepo(specProvider);
|
|
12220
|
+
registeredRepos.push(newRepo);
|
|
12221
|
+
}
|
|
12222
|
+
const validRepos = [];
|
|
12223
|
+
for (const r of registeredRepos) {
|
|
12224
|
+
if (await fs27.pathExists(r.path)) {
|
|
12225
|
+
validRepos.push(r);
|
|
12226
|
+
} else {
|
|
12227
|
+
console.log(import_chalk26.default.yellow(` \u26A0 Skipping ${r.name}: path not found (${r.path})`));
|
|
12228
|
+
}
|
|
12229
|
+
}
|
|
12230
|
+
if (validRepos.length === 0) {
|
|
12231
|
+
console.log(import_chalk26.default.red(" No valid repos available. Run: ai-spec init"));
|
|
12232
|
+
process.exit(1);
|
|
12233
|
+
}
|
|
12234
|
+
const selectedRepos = await selectRepos(validRepos, specProvider);
|
|
12235
|
+
if (selectedRepos.length === 1) {
|
|
12236
|
+
const repo = selectedRepos[0];
|
|
12237
|
+
console.log(import_chalk26.default.cyan(`
|
|
12238
|
+
[Repo] ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12239
|
+
await runSingleRepoPipeline(idea, opts, repo.path, config2);
|
|
12240
|
+
} else {
|
|
12241
|
+
const workspaceConfig = buildWorkspaceConfig(selectedRepos);
|
|
12242
|
+
console.log(import_chalk26.default.cyan(`
|
|
12243
|
+
[Workspace] ${selectedRepos.length} repo(s) selected:`));
|
|
12244
|
+
for (const repo of selectedRepos) {
|
|
12245
|
+
console.log(import_chalk26.default.gray(` ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12246
|
+
}
|
|
12247
|
+
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
12248
|
+
if (opts.serve) {
|
|
12249
|
+
await handleAutoServe(pipelineResults);
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
11871
12252
|
});
|
|
11872
12253
|
}
|
|
11873
12254
|
|
|
11874
12255
|
// cli/commands/review.ts
|
|
11875
|
-
var
|
|
11876
|
-
var
|
|
12256
|
+
var path27 = __toESM(require("path"));
|
|
12257
|
+
var fs28 = __toESM(require("fs-extra"));
|
|
11877
12258
|
var import_chalk27 = __toESM(require("chalk"));
|
|
11878
12259
|
init_spec_generator();
|
|
11879
12260
|
function registerReview(program2) {
|
|
@@ -11891,17 +12272,17 @@ function registerReview(program2) {
|
|
|
11891
12272
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
11892
12273
|
let specContent = "";
|
|
11893
12274
|
let resolvedSpecFile;
|
|
11894
|
-
if (specFile && await
|
|
11895
|
-
specContent = await
|
|
12275
|
+
if (specFile && await fs28.pathExists(specFile)) {
|
|
12276
|
+
specContent = await fs28.readFile(specFile, "utf-8");
|
|
11896
12277
|
resolvedSpecFile = specFile;
|
|
11897
12278
|
console.log(import_chalk27.default.gray(`Using spec: ${specFile}`));
|
|
11898
12279
|
} else {
|
|
11899
|
-
const specsDir =
|
|
11900
|
-
if (await
|
|
11901
|
-
const files = (await
|
|
12280
|
+
const specsDir = path27.join(currentDir, "specs");
|
|
12281
|
+
if (await fs28.pathExists(specsDir)) {
|
|
12282
|
+
const files = (await fs28.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
11902
12283
|
if (files.length > 0) {
|
|
11903
|
-
const latest =
|
|
11904
|
-
specContent = await
|
|
12284
|
+
const latest = path27.join(specsDir, files[0]);
|
|
12285
|
+
specContent = await fs28.readFile(latest, "utf-8");
|
|
11905
12286
|
resolvedSpecFile = latest;
|
|
11906
12287
|
console.log(import_chalk27.default.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
11907
12288
|
}
|
|
@@ -11916,9 +12297,10 @@ function registerReview(program2) {
|
|
|
11916
12297
|
}
|
|
11917
12298
|
|
|
11918
12299
|
// cli/commands/init.ts
|
|
11919
|
-
var
|
|
11920
|
-
var
|
|
12300
|
+
var path28 = __toESM(require("path"));
|
|
12301
|
+
var fs29 = __toESM(require("fs-extra"));
|
|
11921
12302
|
var import_chalk28 = __toESM(require("chalk"));
|
|
12303
|
+
var import_prompts8 = require("@inquirer/prompts");
|
|
11922
12304
|
init_spec_generator();
|
|
11923
12305
|
|
|
11924
12306
|
// prompts/global-constitution.prompt.ts
|
|
@@ -11980,240 +12362,139 @@ ${summary}
|
|
|
11980
12362
|
return parts.join("\n");
|
|
11981
12363
|
}
|
|
11982
12364
|
|
|
11983
|
-
//
|
|
11984
|
-
var
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
"expo",
|
|
12001
|
-
// DB / ORM
|
|
12002
|
-
"prisma",
|
|
12003
|
-
"@prisma/client",
|
|
12004
|
-
"mongoose",
|
|
12005
|
-
"typeorm",
|
|
12006
|
-
"sequelize",
|
|
12007
|
-
"drizzle-orm",
|
|
12008
|
-
// Auth
|
|
12009
|
-
"jsonwebtoken",
|
|
12010
|
-
"passport",
|
|
12011
|
-
"next-auth",
|
|
12012
|
-
"@clerk/nextjs",
|
|
12013
|
-
// Build / Lang
|
|
12014
|
-
"typescript",
|
|
12015
|
-
"vite",
|
|
12016
|
-
"webpack",
|
|
12017
|
-
"esbuild",
|
|
12018
|
-
"turbo",
|
|
12019
|
-
// Testing
|
|
12020
|
-
"jest",
|
|
12021
|
-
"vitest",
|
|
12022
|
-
"mocha",
|
|
12023
|
-
"cypress",
|
|
12024
|
-
"playwright",
|
|
12025
|
-
// Infra
|
|
12026
|
-
"redis",
|
|
12027
|
-
"bull",
|
|
12028
|
-
"socket.io",
|
|
12029
|
-
"graphql",
|
|
12030
|
-
"@trpc/server"
|
|
12031
|
-
];
|
|
12032
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12033
|
-
"node_modules",
|
|
12034
|
-
".git",
|
|
12035
|
-
".svn",
|
|
12036
|
-
"dist",
|
|
12037
|
-
"build",
|
|
12038
|
-
"out",
|
|
12039
|
-
".next",
|
|
12040
|
-
".nuxt",
|
|
12041
|
-
"coverage",
|
|
12042
|
-
".turbo",
|
|
12043
|
-
".cache",
|
|
12044
|
-
"__pycache__",
|
|
12045
|
-
"vendor",
|
|
12046
|
-
".ai-spec-vcr",
|
|
12047
|
-
".ai-spec-logs",
|
|
12048
|
-
"specs"
|
|
12049
|
-
]);
|
|
12050
|
-
var MANIFEST_FILES = [
|
|
12051
|
-
"package.json",
|
|
12052
|
-
"go.mod",
|
|
12053
|
-
"Cargo.toml",
|
|
12054
|
-
"pom.xml",
|
|
12055
|
-
"build.gradle",
|
|
12056
|
-
"build.gradle.kts",
|
|
12057
|
-
"requirements.txt",
|
|
12058
|
-
"pyproject.toml",
|
|
12059
|
-
"setup.py",
|
|
12060
|
-
"composer.json"
|
|
12061
|
-
];
|
|
12062
|
-
async function isProjectRoot(absPath) {
|
|
12063
|
-
for (const manifest of MANIFEST_FILES) {
|
|
12064
|
-
if (await fs27.pathExists(path26.join(absPath, manifest))) return true;
|
|
12065
|
-
}
|
|
12066
|
-
return false;
|
|
12365
|
+
// cli/commands/init.ts
|
|
12366
|
+
var ROLE_LABELS = {
|
|
12367
|
+
frontend: "frontend",
|
|
12368
|
+
backend: "backend",
|
|
12369
|
+
mobile: "mobile",
|
|
12370
|
+
shared: "shared"
|
|
12371
|
+
};
|
|
12372
|
+
async function promptRepoRole2() {
|
|
12373
|
+
return (0, import_prompts8.select)({
|
|
12374
|
+
message: "What type of repo is this?",
|
|
12375
|
+
choices: [
|
|
12376
|
+
{ name: "Frontend", value: "frontend" },
|
|
12377
|
+
{ name: "Backend", value: "backend" },
|
|
12378
|
+
{ name: "Mobile", value: "mobile" },
|
|
12379
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12380
|
+
]
|
|
12381
|
+
});
|
|
12067
12382
|
}
|
|
12068
|
-
async function
|
|
12069
|
-
const
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
}
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12383
|
+
async function promptRepoPath(role) {
|
|
12384
|
+
const label = ROLE_LABELS[role];
|
|
12385
|
+
const raw = await (0, import_prompts8.input)({
|
|
12386
|
+
message: `Enter your ${label} repo path (absolute path):`,
|
|
12387
|
+
validate: (v2) => {
|
|
12388
|
+
const trimmed = v2.trim();
|
|
12389
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12390
|
+
if (!path28.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12391
|
+
return true;
|
|
12392
|
+
}
|
|
12393
|
+
});
|
|
12394
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12395
|
+
const resolved = path28.resolve(cleaned);
|
|
12396
|
+
if (!await fs29.pathExists(resolved)) {
|
|
12397
|
+
console.log(import_chalk28.default.red(` Path does not exist: ${resolved}`));
|
|
12398
|
+
return promptRepoPath(role);
|
|
12399
|
+
}
|
|
12400
|
+
const stat5 = await fs29.stat(resolved);
|
|
12401
|
+
if (!stat5.isDirectory()) {
|
|
12402
|
+
console.log(import_chalk28.default.red(` Not a directory: ${resolved}`));
|
|
12403
|
+
return promptRepoPath(role);
|
|
12404
|
+
}
|
|
12405
|
+
return resolved;
|
|
12406
|
+
}
|
|
12407
|
+
async function registerSingleRepo(repoPath, provider, roleOverride) {
|
|
12408
|
+
const { type, role: detectedRole } = await detectRepoType(repoPath);
|
|
12409
|
+
const role = roleOverride ?? detectedRole;
|
|
12410
|
+
const repoName = path28.basename(repoPath);
|
|
12411
|
+
console.log(import_chalk28.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12412
|
+
const constitutionPath = path28.join(repoPath, CONSTITUTION_FILE);
|
|
12413
|
+
let hasConstitution = await fs29.pathExists(constitutionPath);
|
|
12414
|
+
if (!hasConstitution) {
|
|
12415
|
+
console.log(import_chalk28.default.blue(` Generating project constitution for ${repoName}...`));
|
|
12416
|
+
try {
|
|
12417
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12418
|
+
const content = await gen.generate(repoPath);
|
|
12419
|
+
await gen.saveConstitution(repoPath, content);
|
|
12420
|
+
hasConstitution = true;
|
|
12421
|
+
console.log(import_chalk28.default.green(` \u2714 Constitution saved: ${constitutionPath}`));
|
|
12422
|
+
} catch (err) {
|
|
12423
|
+
console.log(import_chalk28.default.yellow(` \u26A0 Constitution generation failed: ${err.message}`));
|
|
12424
|
+
}
|
|
12425
|
+
} else {
|
|
12426
|
+
console.log(import_chalk28.default.green(` \u2714 Constitution already exists: ${constitutionPath}`));
|
|
12427
|
+
}
|
|
12428
|
+
const entry = {
|
|
12429
|
+
name: repoName,
|
|
12430
|
+
path: repoPath,
|
|
12431
|
+
type,
|
|
12432
|
+
role,
|
|
12433
|
+
hasConstitution,
|
|
12434
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12086
12435
|
};
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
let entries;
|
|
12436
|
+
await registerRepo(entry);
|
|
12437
|
+
return entry;
|
|
12438
|
+
}
|
|
12439
|
+
async function buildProjectSummaries(repos) {
|
|
12440
|
+
const summaries = [];
|
|
12441
|
+
for (const repo of repos) {
|
|
12442
|
+
if (!await fs29.pathExists(repo.path)) continue;
|
|
12443
|
+
const lines = [
|
|
12444
|
+
`Type: ${repo.type} (${repo.role})`
|
|
12445
|
+
];
|
|
12098
12446
|
try {
|
|
12099
|
-
|
|
12447
|
+
const loader = new ContextLoader(repo.path);
|
|
12448
|
+
const ctx = await loader.loadProjectContext();
|
|
12449
|
+
lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
|
|
12450
|
+
lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
|
|
12100
12451
|
} catch {
|
|
12101
|
-
|
|
12452
|
+
lines.push(`Tech stack: ${repo.type}`);
|
|
12102
12453
|
}
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
const gitStat = await fs27.stat(gitPath);
|
|
12110
|
-
if (gitStat.isFile()) continue;
|
|
12111
|
-
}
|
|
12112
|
-
if (await isProjectRoot(childAbs)) {
|
|
12113
|
-
found.push(path26.relative(rootDir, childAbs));
|
|
12114
|
-
} else {
|
|
12115
|
-
await walk(childAbs, depth + 1);
|
|
12454
|
+
if (repo.hasConstitution) {
|
|
12455
|
+
try {
|
|
12456
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12457
|
+
const raw = await fs29.readFile(constitutionPath, "utf-8");
|
|
12458
|
+
lines.push("", "Constitution excerpt:", raw.slice(0, 2e3));
|
|
12459
|
+
} catch {
|
|
12116
12460
|
}
|
|
12117
12461
|
}
|
|
12462
|
+
summaries.push({ name: repo.name, summary: lines.join("\n") });
|
|
12118
12463
|
}
|
|
12119
|
-
|
|
12120
|
-
return found;
|
|
12464
|
+
return summaries;
|
|
12121
12465
|
}
|
|
12122
|
-
async function
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12466
|
+
async function generateGlobalConstitution(provider, repos, currentDir) {
|
|
12467
|
+
console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12468
|
+
console.log(import_chalk28.default.gray(` Based on ${repos.length} registered repo(s)`));
|
|
12469
|
+
const summaries = await buildProjectSummaries(repos);
|
|
12470
|
+
if (summaries.length === 0) {
|
|
12471
|
+
console.log(import_chalk28.default.yellow(" No valid repos found \u2014 skipping global constitution."));
|
|
12472
|
+
return;
|
|
12128
12473
|
}
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
}
|
|
12135
|
-
|
|
12136
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12137
|
-
const existing = await loadIndex(scanRoot);
|
|
12138
|
-
const existingMap = new Map(
|
|
12139
|
-
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
12140
|
-
);
|
|
12141
|
-
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
12142
|
-
const added = [];
|
|
12143
|
-
const updated = [];
|
|
12144
|
-
const unchanged = [];
|
|
12145
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
12146
|
-
for (const relPath of discoveredPaths) {
|
|
12147
|
-
const absPath = path26.join(scanRoot, relPath);
|
|
12148
|
-
seenPaths.add(relPath);
|
|
12149
|
-
const { type, role } = await detectRepoType(absPath);
|
|
12150
|
-
const techStack = await extractTechStack(absPath, type);
|
|
12151
|
-
const hasConstitution = await fs27.pathExists(path26.join(absPath, CONSTITUTION_FILE));
|
|
12152
|
-
const hasWorkspace = await fs27.pathExists(path26.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
12153
|
-
const name = path26.basename(relPath);
|
|
12154
|
-
const prev = existingMap.get(relPath);
|
|
12155
|
-
if (!prev) {
|
|
12156
|
-
const entry = {
|
|
12157
|
-
name,
|
|
12158
|
-
path: relPath,
|
|
12159
|
-
type,
|
|
12160
|
-
role,
|
|
12161
|
-
techStack,
|
|
12162
|
-
hasConstitution,
|
|
12163
|
-
hasWorkspace,
|
|
12164
|
-
firstSeen: now,
|
|
12165
|
-
lastSeen: now
|
|
12166
|
-
};
|
|
12167
|
-
added.push(entry);
|
|
12168
|
-
existingMap.set(relPath, entry);
|
|
12169
|
-
} else {
|
|
12170
|
-
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
12171
|
-
const entry = {
|
|
12172
|
-
...prev,
|
|
12173
|
-
type,
|
|
12174
|
-
role,
|
|
12175
|
-
techStack,
|
|
12176
|
-
hasConstitution,
|
|
12177
|
-
hasWorkspace,
|
|
12178
|
-
lastSeen: now,
|
|
12179
|
-
missing: void 0
|
|
12180
|
-
// clear missing flag if it came back
|
|
12181
|
-
};
|
|
12182
|
-
existingMap.set(relPath, entry);
|
|
12183
|
-
if (changed) {
|
|
12184
|
-
updated.push(entry);
|
|
12185
|
-
} else {
|
|
12186
|
-
unchanged.push(entry);
|
|
12187
|
-
}
|
|
12188
|
-
}
|
|
12474
|
+
const prompt = buildGlobalConstitutionPrompt(summaries);
|
|
12475
|
+
let globalConstitution;
|
|
12476
|
+
try {
|
|
12477
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
12478
|
+
} catch (err) {
|
|
12479
|
+
console.error(import_chalk28.default.red(` \u2718 Failed to generate global constitution: ${err.message}`));
|
|
12480
|
+
return;
|
|
12189
12481
|
}
|
|
12190
|
-
const
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12482
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
12483
|
+
console.log(import_chalk28.default.green(` \u2714 Global constitution saved: ${saved}`));
|
|
12484
|
+
console.log(import_chalk28.default.gray(" Project constitutions will be merged with this at runtime."));
|
|
12485
|
+
const lines = globalConstitution.split("\n");
|
|
12486
|
+
console.log(import_chalk28.default.bold("\n Preview:"));
|
|
12487
|
+
console.log(import_chalk28.default.gray(lines.slice(0, 10).join("\n")));
|
|
12488
|
+
if (lines.length > 10) {
|
|
12489
|
+
console.log(import_chalk28.default.gray(` ... (${lines.length} lines total)`));
|
|
12197
12490
|
}
|
|
12198
|
-
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
12199
|
-
const index = {
|
|
12200
|
-
scanRoot,
|
|
12201
|
-
lastScanned: now,
|
|
12202
|
-
projects
|
|
12203
|
-
};
|
|
12204
|
-
return { index, added, updated, unchanged, nowMissing };
|
|
12205
12491
|
}
|
|
12206
|
-
|
|
12207
|
-
// cli/commands/init.ts
|
|
12208
12492
|
function registerInit(program2) {
|
|
12209
|
-
program2.command("init").description(
|
|
12493
|
+
program2.command("init").description("Setup workspace: register repos, generate constitutions").option(
|
|
12210
12494
|
"--provider <name>",
|
|
12211
12495
|
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
12212
12496
|
void 0
|
|
12213
|
-
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing
|
|
12214
|
-
"--global",
|
|
12215
|
-
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
12216
|
-
).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) => {
|
|
12497
|
+
).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) => {
|
|
12217
12498
|
const currentDir = process.cwd();
|
|
12218
12499
|
const config2 = await loadConfig(currentDir);
|
|
12219
12500
|
const providerName = opts.provider || config2.provider || "gemini";
|
|
@@ -12232,7 +12513,7 @@ function registerInit(program2) {
|
|
|
12232
12513
|
console.log(import_chalk28.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
12233
12514
|
console.log(import_chalk28.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
12234
12515
|
if (result.backupPath) {
|
|
12235
|
-
console.log(import_chalk28.default.gray(` Backup: ${
|
|
12516
|
+
console.log(import_chalk28.default.gray(` Backup: ${path28.basename(result.backupPath)}`));
|
|
12236
12517
|
}
|
|
12237
12518
|
}
|
|
12238
12519
|
} catch (err) {
|
|
@@ -12241,119 +12522,125 @@ function registerInit(program2) {
|
|
|
12241
12522
|
}
|
|
12242
12523
|
return;
|
|
12243
12524
|
}
|
|
12244
|
-
if (opts.
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12525
|
+
if (opts.addRepo) {
|
|
12526
|
+
console.log(import_chalk28.default.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"));
|
|
12527
|
+
const role = await promptRepoRole2();
|
|
12528
|
+
const repoPath = await promptRepoPath(role);
|
|
12529
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12530
|
+
console.log(import_chalk28.default.green(`
|
|
12531
|
+
\u2714 Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
|
|
12532
|
+
console.log(import_chalk28.default.gray(` Saved to: ${REPO_STORE_FILE}`));
|
|
12533
|
+
const updateGlobal = await (0, import_prompts8.confirm)({
|
|
12534
|
+
message: "Update global constitution with this repo's context?",
|
|
12535
|
+
default: true
|
|
12536
|
+
});
|
|
12537
|
+
if (updateGlobal) {
|
|
12538
|
+
const allRepos2 = await getRegisteredRepos();
|
|
12539
|
+
await generateGlobalConstitution(provider, allRepos2, currentDir);
|
|
12251
12540
|
}
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12541
|
+
return;
|
|
12542
|
+
}
|
|
12543
|
+
console.log(import_chalk28.default.blue("\n" + "\u2500".repeat(52)));
|
|
12544
|
+
console.log(import_chalk28.default.bold(" ai-spec init \u2014 Workspace Setup"));
|
|
12545
|
+
console.log(import_chalk28.default.blue("\u2500".repeat(52)));
|
|
12546
|
+
console.log(import_chalk28.default.gray(` Provider: ${providerName}/${modelName}
|
|
12547
|
+
`));
|
|
12548
|
+
const existingRepos = await getRegisteredRepos();
|
|
12549
|
+
if (existingRepos.length > 0) {
|
|
12550
|
+
console.log(import_chalk28.default.cyan(" Registered repos:"));
|
|
12551
|
+
for (const r of existingRepos) {
|
|
12552
|
+
const constitutionIcon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
|
|
12553
|
+
console.log(import_chalk28.default.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`));
|
|
12554
|
+
}
|
|
12555
|
+
console.log();
|
|
12556
|
+
}
|
|
12557
|
+
const action = existingRepos.length > 0 ? await (0, import_prompts8.select)({
|
|
12558
|
+
message: "What would you like to do?",
|
|
12559
|
+
choices: [
|
|
12560
|
+
{ name: "Add new repo(s)", value: "add" },
|
|
12561
|
+
{ name: "Re-generate constitutions for existing repos", value: "regen" },
|
|
12562
|
+
{ name: "Skip \u2014 proceed to global constitution", value: "skip" }
|
|
12563
|
+
]
|
|
12564
|
+
}) : "add";
|
|
12565
|
+
const newRepos = [];
|
|
12566
|
+
if (action === "add") {
|
|
12567
|
+
let addMore = true;
|
|
12568
|
+
while (addMore) {
|
|
12569
|
+
console.log(import_chalk28.default.blue(`
|
|
12570
|
+
\u2500\u2500 Register Repo #${existingRepos.length + newRepos.length + 1} \u2500\u2500`));
|
|
12571
|
+
const role = await promptRepoRole2();
|
|
12572
|
+
const repoPath = await promptRepoPath(role);
|
|
12573
|
+
const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
|
|
12574
|
+
if (alreadyRegistered) {
|
|
12575
|
+
console.log(import_chalk28.default.yellow(` Already registered: ${alreadyRegistered.name}`));
|
|
12576
|
+
} else {
|
|
12577
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12578
|
+
newRepos.push(entry);
|
|
12579
|
+
console.log(import_chalk28.default.green(` \u2714 Registered: ${entry.name}`));
|
|
12275
12580
|
}
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
const loader = new ContextLoader(currentDir);
|
|
12280
|
-
const ctx = await loader.loadProjectContext();
|
|
12281
|
-
projectSummaries.push({
|
|
12282
|
-
name: path27.basename(currentDir),
|
|
12283
|
-
summary: [
|
|
12284
|
-
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
12285
|
-
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
12286
|
-
].join("\n")
|
|
12581
|
+
addMore = await (0, import_prompts8.confirm)({
|
|
12582
|
+
message: "Add another repo?",
|
|
12583
|
+
default: false
|
|
12287
12584
|
});
|
|
12288
12585
|
}
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12586
|
+
}
|
|
12587
|
+
if (action === "regen") {
|
|
12588
|
+
console.log(import_chalk28.default.blue("\n Re-generating project constitutions..."));
|
|
12589
|
+
for (const repo of existingRepos) {
|
|
12590
|
+
if (!await fs29.pathExists(repo.path)) {
|
|
12591
|
+
console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: path not found \u2014 skipping`));
|
|
12592
|
+
continue;
|
|
12593
|
+
}
|
|
12594
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12595
|
+
if (await fs29.pathExists(constitutionPath) && !opts.force) {
|
|
12596
|
+
console.log(import_chalk28.default.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
|
|
12597
|
+
continue;
|
|
12598
|
+
}
|
|
12599
|
+
console.log(import_chalk28.default.blue(` ${repo.name}: generating constitution...`));
|
|
12600
|
+
try {
|
|
12601
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12602
|
+
const content = await gen.generate(repo.path);
|
|
12603
|
+
await gen.saveConstitution(repo.path, content);
|
|
12604
|
+
console.log(import_chalk28.default.green(` \u2714 ${repo.name}: constitution saved`));
|
|
12605
|
+
} catch (err) {
|
|
12606
|
+
console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: failed \u2014 ${err.message}`));
|
|
12607
|
+
}
|
|
12307
12608
|
}
|
|
12308
|
-
return;
|
|
12309
12609
|
}
|
|
12310
|
-
const
|
|
12311
|
-
if (
|
|
12312
|
-
console.log(import_chalk28.default.yellow(`
|
|
12313
|
-
${CONSTITUTION_FILE} already exists.`));
|
|
12314
|
-
console.log(import_chalk28.default.gray(" Use --force to overwrite it."));
|
|
12315
|
-
console.log(import_chalk28.default.gray(` Or edit it directly: ${constitutionPath}`));
|
|
12610
|
+
const allRepos = await getRegisteredRepos();
|
|
12611
|
+
if (allRepos.length === 0) {
|
|
12612
|
+
console.log(import_chalk28.default.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
|
|
12316
12613
|
return;
|
|
12317
12614
|
}
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
}
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
}
|
|
12337
|
-
console.log(import_chalk28.default.green(`
|
|
12338
|
-
\u2714 Constitution saved: ${saved}`));
|
|
12339
|
-
console.log(import_chalk28.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
12340
|
-
console.log(import_chalk28.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
12341
|
-
console.log(import_chalk28.default.bold(" Preview:"));
|
|
12342
|
-
console.log(import_chalk28.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
12343
|
-
if (constitution.split("\n").length > 15) {
|
|
12344
|
-
console.log(import_chalk28.default.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
12345
|
-
}
|
|
12615
|
+
const existingGlobal = await loadGlobalConstitution([currentDir]);
|
|
12616
|
+
const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0 ? true : await (0, import_prompts8.confirm)({
|
|
12617
|
+
message: "Global constitution exists. Re-generate it?",
|
|
12618
|
+
default: false
|
|
12619
|
+
});
|
|
12620
|
+
if (shouldGenerateGlobal) {
|
|
12621
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
12622
|
+
}
|
|
12623
|
+
console.log(import_chalk28.default.bold.green("\n\u2714 Init complete!"));
|
|
12624
|
+
console.log(import_chalk28.default.gray(` Repos registered: ${allRepos.length}`));
|
|
12625
|
+
for (const r of allRepos) {
|
|
12626
|
+
const icon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
|
|
12627
|
+
console.log(import_chalk28.default.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
|
|
12628
|
+
}
|
|
12629
|
+
console.log(import_chalk28.default.gray(`
|
|
12630
|
+
Repo store: ${REPO_STORE_FILE}`));
|
|
12631
|
+
console.log(import_chalk28.default.gray(` Next step: ai-spec create "your feature idea"`));
|
|
12632
|
+
process.exit(0);
|
|
12346
12633
|
});
|
|
12347
12634
|
}
|
|
12348
12635
|
|
|
12349
12636
|
// cli/commands/config.ts
|
|
12350
|
-
var
|
|
12351
|
-
var
|
|
12637
|
+
var path29 = __toESM(require("path"));
|
|
12638
|
+
var fs30 = __toESM(require("fs-extra"));
|
|
12352
12639
|
var import_chalk29 = __toESM(require("chalk"));
|
|
12353
12640
|
function registerConfig(program2) {
|
|
12354
12641
|
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) => {
|
|
12355
12642
|
const currentDir = process.cwd();
|
|
12356
|
-
const configPath =
|
|
12643
|
+
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12357
12644
|
if (opts.clearKeys) {
|
|
12358
12645
|
await clearAllKeys();
|
|
12359
12646
|
console.log(import_chalk29.default.green(`\u2714 All saved API keys cleared.`));
|
|
@@ -12365,7 +12652,7 @@ function registerConfig(program2) {
|
|
|
12365
12652
|
return;
|
|
12366
12653
|
}
|
|
12367
12654
|
if (opts.listKeys) {
|
|
12368
|
-
const store = await
|
|
12655
|
+
const store = await fs30.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
12369
12656
|
const providers = Object.keys(store);
|
|
12370
12657
|
if (providers.length === 0) {
|
|
12371
12658
|
console.log(import_chalk29.default.gray("No saved API keys."));
|
|
@@ -12381,7 +12668,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12381
12668
|
return;
|
|
12382
12669
|
}
|
|
12383
12670
|
if (opts.reset) {
|
|
12384
|
-
await
|
|
12671
|
+
await fs30.writeJson(configPath, {}, { spaces: 2 });
|
|
12385
12672
|
console.log(import_chalk29.default.green(`\u2714 Config reset: ${configPath}`));
|
|
12386
12673
|
return;
|
|
12387
12674
|
}
|
|
@@ -12425,22 +12712,18 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12425
12712
|
}
|
|
12426
12713
|
updated.maxErrorCycles = cycles;
|
|
12427
12714
|
}
|
|
12428
|
-
await
|
|
12715
|
+
await fs30.writeJson(configPath, updated, { spaces: 2 });
|
|
12429
12716
|
console.log(import_chalk29.default.green(`\u2714 Config saved to ${configPath}`));
|
|
12430
12717
|
console.log(JSON.stringify(updated, null, 2));
|
|
12431
12718
|
});
|
|
12432
12719
|
}
|
|
12433
12720
|
|
|
12434
12721
|
// cli/commands/model.ts
|
|
12435
|
-
var path29 = __toESM(require("path"));
|
|
12436
|
-
var fs30 = __toESM(require("fs-extra"));
|
|
12437
12722
|
var import_chalk30 = __toESM(require("chalk"));
|
|
12438
|
-
var
|
|
12723
|
+
var import_prompts9 = require("@inquirer/prompts");
|
|
12439
12724
|
init_spec_generator();
|
|
12440
12725
|
function registerModel(program2) {
|
|
12441
12726
|
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) => {
|
|
12442
|
-
const currentDir = process.cwd();
|
|
12443
|
-
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12444
12727
|
if (opts.list) {
|
|
12445
12728
|
console.log(import_chalk30.default.bold("\nAvailable providers & models:\n"));
|
|
12446
12729
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
@@ -12457,8 +12740,9 @@ function registerModel(program2) {
|
|
|
12457
12740
|
}
|
|
12458
12741
|
return;
|
|
12459
12742
|
}
|
|
12460
|
-
const existing = await
|
|
12743
|
+
const existing = await loadGlobalConfig();
|
|
12461
12744
|
console.log(import_chalk30.default.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"));
|
|
12745
|
+
console.log(import_chalk30.default.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
|
|
12462
12746
|
if (Object.keys(existing).length > 0) {
|
|
12463
12747
|
console.log(
|
|
12464
12748
|
import_chalk30.default.gray(
|
|
@@ -12467,7 +12751,7 @@ function registerModel(program2) {
|
|
|
12467
12751
|
);
|
|
12468
12752
|
}
|
|
12469
12753
|
console.log();
|
|
12470
|
-
const target = await (0,
|
|
12754
|
+
const target = await (0, import_prompts9.select)({
|
|
12471
12755
|
message: "Configure model for:",
|
|
12472
12756
|
choices: [
|
|
12473
12757
|
{ name: "Spec generation (used for spec writing & refinement)", value: "spec" },
|
|
@@ -12476,7 +12760,7 @@ function registerModel(program2) {
|
|
|
12476
12760
|
]
|
|
12477
12761
|
});
|
|
12478
12762
|
async function pickProviderAndModel(label) {
|
|
12479
|
-
const providerKey = await (0,
|
|
12763
|
+
const providerKey = await (0, import_prompts9.select)({
|
|
12480
12764
|
message: `${label} \u2014 select provider:`,
|
|
12481
12765
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
12482
12766
|
name: `${meta2.displayName.padEnd(22)} ${import_chalk30.default.gray(meta2.description)}`,
|
|
@@ -12489,12 +12773,12 @@ function registerModel(program2) {
|
|
|
12489
12773
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
12490
12774
|
{ name: import_chalk30.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
12491
12775
|
];
|
|
12492
|
-
let chosenModel = await (0,
|
|
12776
|
+
let chosenModel = await (0, import_prompts9.select)({
|
|
12493
12777
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
12494
12778
|
choices: modelChoices
|
|
12495
12779
|
});
|
|
12496
12780
|
if (chosenModel === "__custom__") {
|
|
12497
|
-
chosenModel = await (0,
|
|
12781
|
+
chosenModel = await (0, import_prompts9.input)({
|
|
12498
12782
|
message: "Enter model name:",
|
|
12499
12783
|
validate: (v2) => v2.trim().length > 0 || "Model name cannot be empty"
|
|
12500
12784
|
});
|
|
@@ -12539,14 +12823,14 @@ function registerModel(program2) {
|
|
|
12539
12823
|
)
|
|
12540
12824
|
);
|
|
12541
12825
|
}
|
|
12542
|
-
const ok = await (0,
|
|
12826
|
+
const ok = await (0, import_prompts9.confirm)({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
|
|
12543
12827
|
if (!ok) {
|
|
12544
12828
|
console.log(import_chalk30.default.gray(" Cancelled."));
|
|
12545
12829
|
return;
|
|
12546
12830
|
}
|
|
12547
|
-
await
|
|
12831
|
+
await saveGlobalConfig(updated);
|
|
12548
12832
|
console.log(import_chalk30.default.green(`
|
|
12549
|
-
\u2714 Saved to ${
|
|
12833
|
+
\u2714 Saved to ${GLOBAL_CONFIG_FILE}`));
|
|
12550
12834
|
const providerToCheck = updated.provider ?? "gemini";
|
|
12551
12835
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
12552
12836
|
if (envKey && !process.env[envKey]) {
|
|
@@ -12563,14 +12847,14 @@ function registerModel(program2) {
|
|
|
12563
12847
|
var path30 = __toESM(require("path"));
|
|
12564
12848
|
var fs31 = __toESM(require("fs-extra"));
|
|
12565
12849
|
var import_chalk31 = __toESM(require("chalk"));
|
|
12566
|
-
var
|
|
12850
|
+
var import_prompts10 = require("@inquirer/prompts");
|
|
12567
12851
|
function registerWorkspace(program2) {
|
|
12568
12852
|
const workspaceCmd = program2.command("workspace").description("Manage multi-repo workspace configuration");
|
|
12569
12853
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
12570
12854
|
const currentDir = process.cwd();
|
|
12571
12855
|
const configPath = path30.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
12572
12856
|
if (await fs31.pathExists(configPath)) {
|
|
12573
|
-
const overwrite = await (0,
|
|
12857
|
+
const overwrite = await (0, import_prompts10.confirm)({
|
|
12574
12858
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
12575
12859
|
default: false
|
|
12576
12860
|
});
|
|
@@ -12580,12 +12864,12 @@ function registerWorkspace(program2) {
|
|
|
12580
12864
|
}
|
|
12581
12865
|
}
|
|
12582
12866
|
console.log(import_chalk31.default.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"));
|
|
12583
|
-
const workspaceName = await (0,
|
|
12867
|
+
const workspaceName = await (0, import_prompts10.input)({
|
|
12584
12868
|
message: "Workspace name:",
|
|
12585
12869
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
12586
12870
|
});
|
|
12587
12871
|
const repos = [];
|
|
12588
|
-
const useAutoScan = await (0,
|
|
12872
|
+
const useAutoScan = await (0, import_prompts10.confirm)({
|
|
12589
12873
|
message: "Auto-scan sibling directories for repos?",
|
|
12590
12874
|
default: true
|
|
12591
12875
|
});
|
|
@@ -12599,7 +12883,7 @@ function registerWorkspace(program2) {
|
|
|
12599
12883
|
for (const r of detected) {
|
|
12600
12884
|
console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12601
12885
|
}
|
|
12602
|
-
const keepAll = await (0,
|
|
12886
|
+
const keepAll = await (0, import_prompts10.confirm)({
|
|
12603
12887
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
12604
12888
|
default: true
|
|
12605
12889
|
});
|
|
@@ -12607,7 +12891,7 @@ function registerWorkspace(program2) {
|
|
|
12607
12891
|
repos.push(...detected);
|
|
12608
12892
|
} else {
|
|
12609
12893
|
for (const r of detected) {
|
|
12610
|
-
const keep = await (0,
|
|
12894
|
+
const keep = await (0, import_prompts10.confirm)({
|
|
12611
12895
|
message: `Include "${r.name}" (${r.role}, ${r.type})?`,
|
|
12612
12896
|
default: true
|
|
12613
12897
|
});
|
|
@@ -12631,14 +12915,14 @@ function registerWorkspace(program2) {
|
|
|
12631
12915
|
{ name: "react-native (React Native mobile)", value: "react-native" },
|
|
12632
12916
|
{ name: "unknown", value: "unknown" }
|
|
12633
12917
|
];
|
|
12634
|
-
let addMore = await (0,
|
|
12918
|
+
let addMore = await (0, import_prompts10.confirm)({
|
|
12635
12919
|
message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
|
|
12636
12920
|
default: repos.length === 0
|
|
12637
12921
|
});
|
|
12638
12922
|
while (addMore) {
|
|
12639
12923
|
console.log(import_chalk31.default.cyan(`
|
|
12640
12924
|
Adding repo #${repos.length + 1}`));
|
|
12641
|
-
const repoName = await (0,
|
|
12925
|
+
const repoName = await (0, import_prompts10.input)({
|
|
12642
12926
|
message: "Repo name (e.g. api, web, app):",
|
|
12643
12927
|
validate: (v2) => {
|
|
12644
12928
|
if (!v2.trim()) return "Name cannot be empty";
|
|
@@ -12646,7 +12930,7 @@ function registerWorkspace(program2) {
|
|
|
12646
12930
|
return true;
|
|
12647
12931
|
}
|
|
12648
12932
|
});
|
|
12649
|
-
const repoPath = await (0,
|
|
12933
|
+
const repoPath = await (0, import_prompts10.input)({
|
|
12650
12934
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
12651
12935
|
default: `./${repoName}`
|
|
12652
12936
|
});
|
|
@@ -12661,12 +12945,12 @@ function registerWorkspace(program2) {
|
|
|
12661
12945
|
} else {
|
|
12662
12946
|
console.log(import_chalk31.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
12663
12947
|
}
|
|
12664
|
-
const repoType = await (0,
|
|
12948
|
+
const repoType = await (0, import_prompts10.select)({
|
|
12665
12949
|
message: `Repo type for "${repoName}":`,
|
|
12666
12950
|
choices: repoTypeChoices,
|
|
12667
12951
|
default: detectedType
|
|
12668
12952
|
});
|
|
12669
|
-
const repoRole = await (0,
|
|
12953
|
+
const repoRole = await (0, import_prompts10.select)({
|
|
12670
12954
|
message: `Repo role for "${repoName}":`,
|
|
12671
12955
|
choices: [
|
|
12672
12956
|
{ name: "backend", value: "backend" },
|
|
@@ -12683,7 +12967,7 @@ function registerWorkspace(program2) {
|
|
|
12683
12967
|
role: repoRole
|
|
12684
12968
|
});
|
|
12685
12969
|
console.log(import_chalk31.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
12686
|
-
addMore = await (0,
|
|
12970
|
+
addMore = await (0, import_prompts10.confirm)({
|
|
12687
12971
|
message: "Add another repo?",
|
|
12688
12972
|
default: false
|
|
12689
12973
|
});
|
|
@@ -12694,7 +12978,7 @@ function registerWorkspace(program2) {
|
|
|
12694
12978
|
for (const r of repos) {
|
|
12695
12979
|
console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12696
12980
|
}
|
|
12697
|
-
const ok = await (0,
|
|
12981
|
+
const ok = await (0, import_prompts10.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
12698
12982
|
if (!ok) {
|
|
12699
12983
|
console.log(import_chalk31.default.gray(" Cancelled."));
|
|
12700
12984
|
return;
|
|
@@ -12738,7 +13022,7 @@ Workspace: ${config2.name}`));
|
|
|
12738
13022
|
var path32 = __toESM(require("path"));
|
|
12739
13023
|
var fs33 = __toESM(require("fs-extra"));
|
|
12740
13024
|
var import_chalk33 = __toESM(require("chalk"));
|
|
12741
|
-
var
|
|
13025
|
+
var import_prompts11 = require("@inquirer/prompts");
|
|
12742
13026
|
init_spec_generator();
|
|
12743
13027
|
|
|
12744
13028
|
// core/spec-updater.ts
|
|
@@ -12973,7 +13257,7 @@ function registerUpdate(program2) {
|
|
|
12973
13257
|
const currentDir = process.cwd();
|
|
12974
13258
|
const config2 = await loadConfig(currentDir);
|
|
12975
13259
|
if (!change) {
|
|
12976
|
-
change = await (0,
|
|
13260
|
+
change = await (0, import_prompts11.input)({
|
|
12977
13261
|
message: "Describe the change you want to make:",
|
|
12978
13262
|
validate: (v2) => v2.trim().length > 0 || "Change description cannot be empty"
|
|
12979
13263
|
});
|
|
@@ -13291,7 +13575,7 @@ function buildYamlDoc(obj) {
|
|
|
13291
13575
|
return `${k2}: ${valStr}`;
|
|
13292
13576
|
}).join("\n") + "\n";
|
|
13293
13577
|
}
|
|
13294
|
-
function dslToOpenApi(dsl, serverUrl =
|
|
13578
|
+
function dslToOpenApi(dsl, serverUrl = DEFAULT_OPENAPI_SERVER_URL) {
|
|
13295
13579
|
const info = {
|
|
13296
13580
|
title: dsl.feature.title,
|
|
13297
13581
|
description: dsl.feature.description,
|
|
@@ -13338,7 +13622,7 @@ function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
|
|
|
13338
13622
|
}
|
|
13339
13623
|
async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
13340
13624
|
const format = opts.format ?? "yaml";
|
|
13341
|
-
const serverUrl = opts.serverUrl ??
|
|
13625
|
+
const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
|
|
13342
13626
|
const defaultName = `openapi.${format}`;
|
|
13343
13627
|
const outputPath = opts.outputPath ? path33.isAbsolute(opts.outputPath) ? opts.outputPath : path33.join(projectDir, opts.outputPath) : path33.join(projectDir, defaultName);
|
|
13344
13628
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
@@ -13534,12 +13818,12 @@ function registerMock(program2) {
|
|
|
13534
13818
|
|
|
13535
13819
|
// cli/commands/learn.ts
|
|
13536
13820
|
var import_chalk36 = __toESM(require("chalk"));
|
|
13537
|
-
var
|
|
13821
|
+
var import_prompts12 = require("@inquirer/prompts");
|
|
13538
13822
|
function registerLearn(program2) {
|
|
13539
13823
|
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) => {
|
|
13540
13824
|
const currentDir = process.cwd();
|
|
13541
13825
|
if (!lesson) {
|
|
13542
|
-
lesson = await (0,
|
|
13826
|
+
lesson = await (0, import_prompts12.input)({
|
|
13543
13827
|
message: "What lesson or engineering decision should be recorded?",
|
|
13544
13828
|
validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
|
|
13545
13829
|
});
|
|
@@ -13581,9 +13865,8 @@ var import_chalk39 = __toESM(require("chalk"));
|
|
|
13581
13865
|
var fs37 = __toESM(require("fs-extra"));
|
|
13582
13866
|
var path36 = __toESM(require("path"));
|
|
13583
13867
|
var import_chalk38 = __toESM(require("chalk"));
|
|
13584
|
-
var LOG_DIR2 = ".ai-spec-logs";
|
|
13585
13868
|
async function loadRunLogs(workingDir) {
|
|
13586
|
-
const logDir = path36.join(workingDir,
|
|
13869
|
+
const logDir = path36.join(workingDir, DEFAULT_LOG_DIR);
|
|
13587
13870
|
if (!await fs37.pathExists(logDir)) return [];
|
|
13588
13871
|
const files = await fs37.readdir(logDir);
|
|
13589
13872
|
const jsonFiles = new Set(files.filter((f) => f.endsWith(".json")));
|
|
@@ -13728,7 +14011,7 @@ function printTrendReport(report, workingDir) {
|
|
|
13728
14011
|
` ${import_chalk38.default.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
|
|
13729
14012
|
);
|
|
13730
14013
|
}
|
|
13731
|
-
const logRelDir = path36.relative(workingDir, path36.join(workingDir,
|
|
14014
|
+
const logRelDir = path36.relative(workingDir, path36.join(workingDir, DEFAULT_LOG_DIR));
|
|
13732
14015
|
console.log(import_chalk38.default.gray(`
|
|
13733
14016
|
${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
|
|
13734
14017
|
console.log(import_chalk38.default.cyan("\u2500".repeat(63)));
|
|
@@ -14387,7 +14670,233 @@ function registerVcr(program2) {
|
|
|
14387
14670
|
|
|
14388
14671
|
// cli/commands/scan.ts
|
|
14389
14672
|
var import_chalk44 = __toESM(require("chalk"));
|
|
14673
|
+
var path42 = __toESM(require("path"));
|
|
14674
|
+
|
|
14675
|
+
// core/project-index.ts
|
|
14676
|
+
var fs42 = __toESM(require("fs-extra"));
|
|
14390
14677
|
var path41 = __toESM(require("path"));
|
|
14678
|
+
var INDEX_FILE = ".ai-spec-index.json";
|
|
14679
|
+
var KEY_DEPS = [
|
|
14680
|
+
// Frameworks
|
|
14681
|
+
"express",
|
|
14682
|
+
"fastify",
|
|
14683
|
+
"koa",
|
|
14684
|
+
"@nestjs/core",
|
|
14685
|
+
"hapi",
|
|
14686
|
+
"next",
|
|
14687
|
+
"react",
|
|
14688
|
+
"vue",
|
|
14689
|
+
"nuxt",
|
|
14690
|
+
"svelte",
|
|
14691
|
+
"react-native",
|
|
14692
|
+
"expo",
|
|
14693
|
+
// DB / ORM
|
|
14694
|
+
"prisma",
|
|
14695
|
+
"@prisma/client",
|
|
14696
|
+
"mongoose",
|
|
14697
|
+
"typeorm",
|
|
14698
|
+
"sequelize",
|
|
14699
|
+
"drizzle-orm",
|
|
14700
|
+
// Auth
|
|
14701
|
+
"jsonwebtoken",
|
|
14702
|
+
"passport",
|
|
14703
|
+
"next-auth",
|
|
14704
|
+
"@clerk/nextjs",
|
|
14705
|
+
// Build / Lang
|
|
14706
|
+
"typescript",
|
|
14707
|
+
"vite",
|
|
14708
|
+
"webpack",
|
|
14709
|
+
"esbuild",
|
|
14710
|
+
"turbo",
|
|
14711
|
+
// Testing
|
|
14712
|
+
"jest",
|
|
14713
|
+
"vitest",
|
|
14714
|
+
"mocha",
|
|
14715
|
+
"cypress",
|
|
14716
|
+
"playwright",
|
|
14717
|
+
// Infra
|
|
14718
|
+
"redis",
|
|
14719
|
+
"bull",
|
|
14720
|
+
"socket.io",
|
|
14721
|
+
"graphql",
|
|
14722
|
+
"@trpc/server"
|
|
14723
|
+
];
|
|
14724
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14725
|
+
"node_modules",
|
|
14726
|
+
".git",
|
|
14727
|
+
".svn",
|
|
14728
|
+
"dist",
|
|
14729
|
+
"build",
|
|
14730
|
+
"out",
|
|
14731
|
+
".next",
|
|
14732
|
+
".nuxt",
|
|
14733
|
+
"coverage",
|
|
14734
|
+
".turbo",
|
|
14735
|
+
".cache",
|
|
14736
|
+
"__pycache__",
|
|
14737
|
+
"vendor",
|
|
14738
|
+
".ai-spec-vcr",
|
|
14739
|
+
".ai-spec-logs",
|
|
14740
|
+
"specs"
|
|
14741
|
+
]);
|
|
14742
|
+
var MANIFEST_FILES = [
|
|
14743
|
+
"package.json",
|
|
14744
|
+
"go.mod",
|
|
14745
|
+
"Cargo.toml",
|
|
14746
|
+
"pom.xml",
|
|
14747
|
+
"build.gradle",
|
|
14748
|
+
"build.gradle.kts",
|
|
14749
|
+
"requirements.txt",
|
|
14750
|
+
"pyproject.toml",
|
|
14751
|
+
"setup.py",
|
|
14752
|
+
"composer.json"
|
|
14753
|
+
];
|
|
14754
|
+
async function isProjectRoot(absPath) {
|
|
14755
|
+
for (const manifest of MANIFEST_FILES) {
|
|
14756
|
+
if (await fs42.pathExists(path41.join(absPath, manifest))) return true;
|
|
14757
|
+
}
|
|
14758
|
+
return false;
|
|
14759
|
+
}
|
|
14760
|
+
async function extractTechStack(absPath, type) {
|
|
14761
|
+
const stack = [];
|
|
14762
|
+
if (type === "go") stack.push("go");
|
|
14763
|
+
if (type === "rust") stack.push("rust");
|
|
14764
|
+
if (type === "java") stack.push("java");
|
|
14765
|
+
if (type === "python") stack.push("python");
|
|
14766
|
+
if (type === "php") stack.push("php");
|
|
14767
|
+
const pkgPath = path41.join(absPath, "package.json");
|
|
14768
|
+
if (!await fs42.pathExists(pkgPath)) return stack;
|
|
14769
|
+
let pkg = {};
|
|
14770
|
+
try {
|
|
14771
|
+
pkg = await fs42.readJson(pkgPath);
|
|
14772
|
+
} catch {
|
|
14773
|
+
return stack;
|
|
14774
|
+
}
|
|
14775
|
+
const allDeps = {
|
|
14776
|
+
...pkg.dependencies ?? {},
|
|
14777
|
+
...pkg.devDependencies ?? {}
|
|
14778
|
+
};
|
|
14779
|
+
const depKeys = new Set(Object.keys(allDeps));
|
|
14780
|
+
for (const dep of KEY_DEPS) {
|
|
14781
|
+
if (depKeys.has(dep)) stack.push(dep);
|
|
14782
|
+
}
|
|
14783
|
+
return stack;
|
|
14784
|
+
}
|
|
14785
|
+
async function discoverProjects(rootDir, maxDepth) {
|
|
14786
|
+
const found = [];
|
|
14787
|
+
async function walk(absDir, depth) {
|
|
14788
|
+
if (depth > maxDepth) return;
|
|
14789
|
+
let entries;
|
|
14790
|
+
try {
|
|
14791
|
+
entries = await fs42.readdir(absDir, { withFileTypes: true });
|
|
14792
|
+
} catch {
|
|
14793
|
+
return;
|
|
14794
|
+
}
|
|
14795
|
+
for (const entry of entries) {
|
|
14796
|
+
if (!entry.isDirectory()) continue;
|
|
14797
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
14798
|
+
const childAbs = path41.join(absDir, entry.name);
|
|
14799
|
+
const gitPath = path41.join(childAbs, ".git");
|
|
14800
|
+
if (await fs42.pathExists(gitPath)) {
|
|
14801
|
+
const gitStat = await fs42.stat(gitPath);
|
|
14802
|
+
if (gitStat.isFile()) continue;
|
|
14803
|
+
}
|
|
14804
|
+
if (await isProjectRoot(childAbs)) {
|
|
14805
|
+
found.push(path41.relative(rootDir, childAbs));
|
|
14806
|
+
} else {
|
|
14807
|
+
await walk(childAbs, depth + 1);
|
|
14808
|
+
}
|
|
14809
|
+
}
|
|
14810
|
+
}
|
|
14811
|
+
await walk(rootDir, 0);
|
|
14812
|
+
return found;
|
|
14813
|
+
}
|
|
14814
|
+
async function loadIndex(scanRoot) {
|
|
14815
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14816
|
+
try {
|
|
14817
|
+
return await fs42.readJson(filePath);
|
|
14818
|
+
} catch {
|
|
14819
|
+
return null;
|
|
14820
|
+
}
|
|
14821
|
+
}
|
|
14822
|
+
async function saveIndex(scanRoot, index) {
|
|
14823
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14824
|
+
await fs42.writeJson(filePath, index, { spaces: 2 });
|
|
14825
|
+
return filePath;
|
|
14826
|
+
}
|
|
14827
|
+
async function runScan(scanRoot, maxDepth = 2) {
|
|
14828
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14829
|
+
const existing = await loadIndex(scanRoot);
|
|
14830
|
+
const existingMap = new Map(
|
|
14831
|
+
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
14832
|
+
);
|
|
14833
|
+
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
14834
|
+
const added = [];
|
|
14835
|
+
const updated = [];
|
|
14836
|
+
const unchanged = [];
|
|
14837
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
14838
|
+
for (const relPath of discoveredPaths) {
|
|
14839
|
+
const absPath = path41.join(scanRoot, relPath);
|
|
14840
|
+
seenPaths.add(relPath);
|
|
14841
|
+
const { type, role } = await detectRepoType(absPath);
|
|
14842
|
+
const techStack = await extractTechStack(absPath, type);
|
|
14843
|
+
const hasConstitution = await fs42.pathExists(path41.join(absPath, CONSTITUTION_FILE));
|
|
14844
|
+
const hasWorkspace = await fs42.pathExists(path41.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
14845
|
+
const name = path41.basename(relPath);
|
|
14846
|
+
const prev = existingMap.get(relPath);
|
|
14847
|
+
if (!prev) {
|
|
14848
|
+
const entry = {
|
|
14849
|
+
name,
|
|
14850
|
+
path: relPath,
|
|
14851
|
+
type,
|
|
14852
|
+
role,
|
|
14853
|
+
techStack,
|
|
14854
|
+
hasConstitution,
|
|
14855
|
+
hasWorkspace,
|
|
14856
|
+
firstSeen: now,
|
|
14857
|
+
lastSeen: now
|
|
14858
|
+
};
|
|
14859
|
+
added.push(entry);
|
|
14860
|
+
existingMap.set(relPath, entry);
|
|
14861
|
+
} else {
|
|
14862
|
+
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
14863
|
+
const entry = {
|
|
14864
|
+
...prev,
|
|
14865
|
+
type,
|
|
14866
|
+
role,
|
|
14867
|
+
techStack,
|
|
14868
|
+
hasConstitution,
|
|
14869
|
+
hasWorkspace,
|
|
14870
|
+
lastSeen: now,
|
|
14871
|
+
missing: void 0
|
|
14872
|
+
// clear missing flag if it came back
|
|
14873
|
+
};
|
|
14874
|
+
existingMap.set(relPath, entry);
|
|
14875
|
+
if (changed) {
|
|
14876
|
+
updated.push(entry);
|
|
14877
|
+
} else {
|
|
14878
|
+
unchanged.push(entry);
|
|
14879
|
+
}
|
|
14880
|
+
}
|
|
14881
|
+
}
|
|
14882
|
+
const nowMissing = [];
|
|
14883
|
+
for (const [relPath, entry] of existingMap) {
|
|
14884
|
+
if (!seenPaths.has(relPath) && !entry.missing) {
|
|
14885
|
+
const gone = { ...entry, missing: true };
|
|
14886
|
+
existingMap.set(relPath, gone);
|
|
14887
|
+
nowMissing.push(gone);
|
|
14888
|
+
}
|
|
14889
|
+
}
|
|
14890
|
+
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
14891
|
+
const index = {
|
|
14892
|
+
scanRoot,
|
|
14893
|
+
lastScanned: now,
|
|
14894
|
+
projects
|
|
14895
|
+
};
|
|
14896
|
+
return { index, added, updated, unchanged, nowMissing };
|
|
14897
|
+
}
|
|
14898
|
+
|
|
14899
|
+
// cli/commands/scan.ts
|
|
14391
14900
|
var ROLE_COLOR = {
|
|
14392
14901
|
backend: import_chalk44.default.blue,
|
|
14393
14902
|
frontend: import_chalk44.default.green,
|
|
@@ -14461,7 +14970,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14461
14970
|
}
|
|
14462
14971
|
console.log(import_chalk44.default.cyan("\n\u2500".repeat(52)));
|
|
14463
14972
|
console.log(import_chalk44.default.gray(" \xA7C = has constitution W = workspace root"));
|
|
14464
|
-
console.log(import_chalk44.default.gray(` Index saved : ${
|
|
14973
|
+
console.log(import_chalk44.default.gray(` Index saved : ${path42.relative(cwd, path42.join(cwd, INDEX_FILE))}`));
|
|
14465
14974
|
console.log(import_chalk44.default.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
|
|
14466
14975
|
});
|
|
14467
14976
|
}
|
|
@@ -14469,7 +14978,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14469
14978
|
// cli/index.ts
|
|
14470
14979
|
dotenv.config();
|
|
14471
14980
|
var program = new import_commander.Command();
|
|
14472
|
-
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(
|
|
14981
|
+
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(require_package().version);
|
|
14473
14982
|
registerCreate(program);
|
|
14474
14983
|
registerReview(program);
|
|
14475
14984
|
registerInit(program);
|