ai-spec-dev 0.42.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/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/utils.ts +72 -4
- 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 +3 -2
- package/core/openapi-exporter.ts +3 -2
- 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 +919 -519
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +912 -512
- 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 +43 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- package/demo-frontend/tsconfig.json +0 -13
package/dist/cli/index.mjs
CHANGED
|
@@ -4,6 +4,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
7
10
|
var __export = (target, all) => {
|
|
8
11
|
for (var name in all)
|
|
9
12
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -341,7 +344,6 @@ __export(spec_generator_exports, {
|
|
|
341
344
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
342
345
|
import Anthropic from "@anthropic-ai/sdk";
|
|
343
346
|
import OpenAI from "openai";
|
|
344
|
-
import axios from "axios";
|
|
345
347
|
import { ProxyAgent } from "undici";
|
|
346
348
|
function geminiRequestOptions() {
|
|
347
349
|
const proxyUrl = process.env.GEMINI_PROXY || process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
@@ -387,7 +389,9 @@ var init_spec_generator = __esm({
|
|
|
387
389
|
displayName: "MiMo (Xiaomi)",
|
|
388
390
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
389
391
|
models: ["mimo-v2-pro"],
|
|
390
|
-
envKey: "MIMO_API_KEY"
|
|
392
|
+
envKey: "MIMO_API_KEY",
|
|
393
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
394
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
391
395
|
// baseURL not used — MiMo has a dedicated provider class
|
|
392
396
|
},
|
|
393
397
|
gemini: {
|
|
@@ -556,8 +560,8 @@ var init_spec_generator = __esm({
|
|
|
556
560
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
557
561
|
messages: [{ role: "user", content: prompt }]
|
|
558
562
|
});
|
|
559
|
-
const
|
|
560
|
-
if (
|
|
563
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
564
|
+
if (textBlock) return textBlock.text;
|
|
561
565
|
throw new Error("Unexpected response type from Claude API");
|
|
562
566
|
},
|
|
563
567
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -602,43 +606,29 @@ var init_spec_generator = __esm({
|
|
|
602
606
|
}
|
|
603
607
|
};
|
|
604
608
|
MiMoProvider = class {
|
|
609
|
+
client;
|
|
605
610
|
providerName = "mimo";
|
|
606
611
|
modelName;
|
|
607
|
-
apiKey;
|
|
608
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
609
612
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
610
|
-
|
|
613
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
614
|
+
this.client = new Anthropic({ apiKey, baseURL });
|
|
611
615
|
this.modelName = modelName;
|
|
612
616
|
}
|
|
613
617
|
async generate(prompt, systemInstruction) {
|
|
614
618
|
return withReliability(
|
|
615
619
|
async () => {
|
|
616
|
-
const
|
|
620
|
+
const stream = this.client.messages.stream({
|
|
617
621
|
model: this.modelName,
|
|
618
|
-
max_tokens:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
stream: false,
|
|
622
|
-
temperature: 1,
|
|
623
|
-
stop_sequences: null
|
|
624
|
-
};
|
|
625
|
-
if (systemInstruction) {
|
|
626
|
-
body.system = systemInstruction;
|
|
627
|
-
}
|
|
628
|
-
const response = await axios.post(this.baseUrl, body, {
|
|
629
|
-
headers: {
|
|
630
|
-
"api-key": this.apiKey,
|
|
631
|
-
"Content-Type": "application/json"
|
|
632
|
-
}
|
|
622
|
+
max_tokens: 65536,
|
|
623
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
624
|
+
messages: [{ role: "user", content: prompt }]
|
|
633
625
|
});
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
626
|
+
const message = await stream.finalMessage();
|
|
627
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
628
|
+
if (textBlock) return textBlock.text;
|
|
629
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
630
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
631
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
642
632
|
},
|
|
643
633
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
644
634
|
);
|
|
@@ -699,14 +689,71 @@ ${context.schema.slice(0, 3e3)}`);
|
|
|
699
689
|
}
|
|
700
690
|
});
|
|
701
691
|
|
|
692
|
+
// package.json
|
|
693
|
+
var require_package = __commonJS({
|
|
694
|
+
"package.json"(exports, module) {
|
|
695
|
+
module.exports = {
|
|
696
|
+
name: "ai-spec-dev",
|
|
697
|
+
version: "0.46.0",
|
|
698
|
+
description: "AI-driven Development Orchestrator SDK & CLI",
|
|
699
|
+
main: "dist/index.js",
|
|
700
|
+
types: "dist/index.d.ts",
|
|
701
|
+
bin: {
|
|
702
|
+
"ai-spec": "dist/cli/index.js"
|
|
703
|
+
},
|
|
704
|
+
scripts: {
|
|
705
|
+
build: "tsup",
|
|
706
|
+
dev: "tsup --watch",
|
|
707
|
+
test: "vitest run",
|
|
708
|
+
"test:watch": "vitest"
|
|
709
|
+
},
|
|
710
|
+
keywords: [
|
|
711
|
+
"ai",
|
|
712
|
+
"spec",
|
|
713
|
+
"codegen",
|
|
714
|
+
"gemini",
|
|
715
|
+
"claude"
|
|
716
|
+
],
|
|
717
|
+
author: "",
|
|
718
|
+
license: "MIT",
|
|
719
|
+
dependencies: {
|
|
720
|
+
"@anthropic-ai/sdk": "^0.38.0",
|
|
721
|
+
"@google/generative-ai": "^0.21.0",
|
|
722
|
+
"@inquirer/editor": "^5.0.10",
|
|
723
|
+
"@inquirer/prompts": "^8.3.2",
|
|
724
|
+
"@rollup/rollup-darwin-arm64": "^4.60.1",
|
|
725
|
+
axios: "^1.13.6",
|
|
726
|
+
chalk: "^4.1.2",
|
|
727
|
+
commander: "^13.1.0",
|
|
728
|
+
dotenv: "^16.4.7",
|
|
729
|
+
"fs-extra": "^11.3.0",
|
|
730
|
+
openai: "^6.31.0",
|
|
731
|
+
undici: "^7.24.4"
|
|
732
|
+
},
|
|
733
|
+
devDependencies: {
|
|
734
|
+
"@types/fs-extra": "^11.0.4",
|
|
735
|
+
"@types/glob": "^8.1.0",
|
|
736
|
+
"@types/node": "^22.13.5",
|
|
737
|
+
glob: "^13.0.6",
|
|
738
|
+
"ts-node": "^10.9.2",
|
|
739
|
+
tsup: "^8.4.0",
|
|
740
|
+
typescript: "^5.7.3",
|
|
741
|
+
vitest: "^2.1.0"
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
702
747
|
// cli/index.ts
|
|
703
748
|
import { Command } from "commander";
|
|
704
749
|
import * as dotenv from "dotenv";
|
|
705
750
|
|
|
706
751
|
// cli/commands/create.ts
|
|
707
752
|
init_spec_generator();
|
|
753
|
+
import * as path26 from "path";
|
|
754
|
+
import * as fs27 from "fs-extra";
|
|
708
755
|
import chalk26 from "chalk";
|
|
709
|
-
import { input as input2 } from "@inquirer/prompts";
|
|
756
|
+
import { input as input2, select as select7, checkbox } from "@inquirer/prompts";
|
|
710
757
|
|
|
711
758
|
// core/workspace-loader.ts
|
|
712
759
|
import * as fs from "fs-extra";
|
|
@@ -806,8 +853,8 @@ var WorkspaceLoader = class {
|
|
|
806
853
|
const repos = [];
|
|
807
854
|
for (const entry of entries) {
|
|
808
855
|
const absPath = path.join(this.workspaceRoot, entry);
|
|
809
|
-
const
|
|
810
|
-
if (!
|
|
856
|
+
const stat5 = await fs.stat(absPath).catch(() => null);
|
|
857
|
+
if (!stat5 || !stat5.isDirectory()) continue;
|
|
811
858
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
812
859
|
if (names && !names.includes(entry)) continue;
|
|
813
860
|
const hasManifest = await fs.pathExists(path.join(absPath, "package.json")) || await fs.pathExists(path.join(absPath, "go.mod")) || await fs.pathExists(path.join(absPath, "Cargo.toml")) || await fs.pathExists(path.join(absPath, "pom.xml")) || await fs.pathExists(path.join(absPath, "build.gradle")) || await fs.pathExists(path.join(absPath, "requirements.txt")) || await fs.pathExists(path.join(absPath, "pyproject.toml")) || await fs.pathExists(path.join(absPath, "composer.json"));
|
|
@@ -871,6 +918,7 @@ var WorkspaceLoader = class {
|
|
|
871
918
|
init_spec_generator();
|
|
872
919
|
import * as path3 from "path";
|
|
873
920
|
import * as fs3 from "fs-extra";
|
|
921
|
+
import * as os2 from "os";
|
|
874
922
|
import chalk2 from "chalk";
|
|
875
923
|
import { input, select } from "@inquirer/prompts";
|
|
876
924
|
|
|
@@ -916,17 +964,42 @@ async function clearKey(provider) {
|
|
|
916
964
|
|
|
917
965
|
// cli/utils.ts
|
|
918
966
|
var CONFIG_FILE = ".ai-spec.json";
|
|
967
|
+
var GLOBAL_CONFIG_FILE = path3.join(os2.homedir(), ".ai-spec-config.json");
|
|
968
|
+
async function loadGlobalConfig() {
|
|
969
|
+
try {
|
|
970
|
+
if (await fs3.pathExists(GLOBAL_CONFIG_FILE)) {
|
|
971
|
+
return await fs3.readJson(GLOBAL_CONFIG_FILE);
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
return {};
|
|
976
|
+
}
|
|
977
|
+
async function saveGlobalConfig(config2) {
|
|
978
|
+
await fs3.ensureFile(GLOBAL_CONFIG_FILE);
|
|
979
|
+
await fs3.writeJson(GLOBAL_CONFIG_FILE, config2, { spaces: 2 });
|
|
980
|
+
}
|
|
919
981
|
async function loadConfig(dir) {
|
|
982
|
+
const globalConfig = await loadGlobalConfig();
|
|
983
|
+
let localConfig = {};
|
|
920
984
|
const p = path3.join(dir, CONFIG_FILE);
|
|
921
985
|
if (await fs3.pathExists(p)) {
|
|
922
|
-
|
|
986
|
+
try {
|
|
987
|
+
localConfig = await fs3.readJson(p);
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
923
990
|
}
|
|
924
|
-
return {};
|
|
991
|
+
return { ...globalConfig, ...localConfig };
|
|
925
992
|
}
|
|
926
993
|
async function resolveApiKey(providerName, cliKey) {
|
|
927
994
|
if (cliKey) return cliKey;
|
|
928
995
|
const envVar = ENV_KEY_MAP[providerName];
|
|
929
996
|
if (envVar && process.env[envVar]) return process.env[envVar];
|
|
997
|
+
const meta = PROVIDER_CATALOG[providerName];
|
|
998
|
+
if (meta?.fallbackEnvKeys) {
|
|
999
|
+
for (const key of meta.fallbackEnvKeys) {
|
|
1000
|
+
if (process.env[key]) return process.env[key];
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
930
1003
|
const savedKey = await getSavedKey(providerName);
|
|
931
1004
|
if (savedKey) {
|
|
932
1005
|
const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
|
|
@@ -1000,7 +1073,7 @@ var pe = "\0PERIOD" + Math.random() + "\0";
|
|
|
1000
1073
|
var is = new RegExp(fe, "g");
|
|
1001
1074
|
var rs = new RegExp(ue, "g");
|
|
1002
1075
|
var ns = new RegExp(qt, "g");
|
|
1003
|
-
var
|
|
1076
|
+
var os3 = new RegExp(de, "g");
|
|
1004
1077
|
var hs = new RegExp(pe, "g");
|
|
1005
1078
|
var as = /\\\\/g;
|
|
1006
1079
|
var ls = /\\{/g;
|
|
@@ -1015,7 +1088,7 @@ function ps(n7) {
|
|
|
1015
1088
|
return n7.replace(as, fe).replace(ls, ue).replace(cs, qt).replace(fs4, de).replace(us, pe);
|
|
1016
1089
|
}
|
|
1017
1090
|
function ms(n7) {
|
|
1018
|
-
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(
|
|
1091
|
+
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os3, ",").replace(hs, ".");
|
|
1019
1092
|
}
|
|
1020
1093
|
function me(n7) {
|
|
1021
1094
|
if (!n7) return [""];
|
|
@@ -3945,11 +4018,11 @@ Ze.glob = Ze;
|
|
|
3945
4018
|
// core/global-constitution.ts
|
|
3946
4019
|
import * as fs5 from "fs-extra";
|
|
3947
4020
|
import * as path4 from "path";
|
|
3948
|
-
import * as
|
|
4021
|
+
import * as os4 from "os";
|
|
3949
4022
|
var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
|
|
3950
4023
|
var SEARCH_ROOTS = [
|
|
3951
4024
|
// Workspace root is injected at runtime — see loadGlobalConstitution()
|
|
3952
|
-
|
|
4025
|
+
os4.homedir()
|
|
3953
4026
|
];
|
|
3954
4027
|
async function loadGlobalConstitution(extraRoots = []) {
|
|
3955
4028
|
const roots = [...extraRoots, ...SEARCH_ROOTS];
|
|
@@ -3978,7 +4051,7 @@ function mergeConstitutions(globalContent, projectContent) {
|
|
|
3978
4051
|
}
|
|
3979
4052
|
return parts.join("\n");
|
|
3980
4053
|
}
|
|
3981
|
-
async function saveGlobalConstitution(content, targetDir =
|
|
4054
|
+
async function saveGlobalConstitution(content, targetDir = os4.homedir()) {
|
|
3982
4055
|
const filePath = path4.join(targetDir, GLOBAL_CONSTITUTION_FILE);
|
|
3983
4056
|
await fs5.writeFile(filePath, content, "utf-8");
|
|
3984
4057
|
return filePath;
|
|
@@ -5011,32 +5084,32 @@ function validateDsl(raw) {
|
|
|
5011
5084
|
}
|
|
5012
5085
|
return { valid: true, dsl: raw };
|
|
5013
5086
|
}
|
|
5014
|
-
function validateFeature(raw,
|
|
5087
|
+
function validateFeature(raw, path43, errors) {
|
|
5015
5088
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5016
|
-
errors.push({ path:
|
|
5089
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5017
5090
|
return;
|
|
5018
5091
|
}
|
|
5019
5092
|
const f = raw;
|
|
5020
|
-
requireNonEmptyString(f["id"], `${
|
|
5021
|
-
requireNonEmptyString(f["title"], `${
|
|
5022
|
-
requireNonEmptyString(f["description"], `${
|
|
5093
|
+
requireNonEmptyString(f["id"], `${path43}.id`, errors);
|
|
5094
|
+
requireNonEmptyString(f["title"], `${path43}.title`, errors);
|
|
5095
|
+
requireNonEmptyString(f["description"], `${path43}.description`, errors);
|
|
5023
5096
|
}
|
|
5024
|
-
function validateModel(raw,
|
|
5097
|
+
function validateModel(raw, path43, errors) {
|
|
5025
5098
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5026
|
-
errors.push({ path:
|
|
5099
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5027
5100
|
return;
|
|
5028
5101
|
}
|
|
5029
5102
|
const m = raw;
|
|
5030
|
-
requireNonEmptyString(m["name"], `${
|
|
5103
|
+
requireNonEmptyString(m["name"], `${path43}.name`, errors);
|
|
5031
5104
|
if (!Array.isArray(m["fields"])) {
|
|
5032
|
-
errors.push({ path: `${
|
|
5105
|
+
errors.push({ path: `${path43}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
5033
5106
|
} else {
|
|
5034
5107
|
const fields = m["fields"];
|
|
5035
5108
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
5036
|
-
errors.push({ path: `${
|
|
5109
|
+
errors.push({ path: `${path43}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
5037
5110
|
}
|
|
5038
5111
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
5039
|
-
validateModelField(fields[j2], `${
|
|
5112
|
+
validateModelField(fields[j2], `${path43}.fields[${j2}]`, errors);
|
|
5040
5113
|
}
|
|
5041
5114
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
5042
5115
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -5045,7 +5118,7 @@ function validateModel(raw, path42, errors) {
|
|
|
5045
5118
|
const name = f["name"];
|
|
5046
5119
|
if (seenFieldNames.has(name)) {
|
|
5047
5120
|
errors.push({
|
|
5048
|
-
path: `${
|
|
5121
|
+
path: `${path43}.fields[${j2}].name`,
|
|
5049
5122
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
5050
5123
|
});
|
|
5051
5124
|
} else {
|
|
@@ -5056,184 +5129,184 @@ function validateModel(raw, path42, errors) {
|
|
|
5056
5129
|
}
|
|
5057
5130
|
if (m["relations"] !== void 0) {
|
|
5058
5131
|
if (!Array.isArray(m["relations"])) {
|
|
5059
|
-
errors.push({ path: `${
|
|
5132
|
+
errors.push({ path: `${path43}.relations`, message: "Must be an array of strings if present" });
|
|
5060
5133
|
} else {
|
|
5061
5134
|
const rels = m["relations"];
|
|
5062
5135
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
5063
5136
|
if (typeof rels[j2] !== "string") {
|
|
5064
|
-
errors.push({ path: `${
|
|
5137
|
+
errors.push({ path: `${path43}.relations[${j2}]`, message: "Must be a string" });
|
|
5065
5138
|
}
|
|
5066
5139
|
}
|
|
5067
5140
|
}
|
|
5068
5141
|
}
|
|
5069
5142
|
}
|
|
5070
|
-
function validateModelField(raw,
|
|
5143
|
+
function validateModelField(raw, path43, errors) {
|
|
5071
5144
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5072
|
-
errors.push({ path:
|
|
5145
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5073
5146
|
return;
|
|
5074
5147
|
}
|
|
5075
5148
|
const f = raw;
|
|
5076
|
-
requireNonEmptyString(f["name"], `${
|
|
5077
|
-
requireNonEmptyString(f["type"], `${
|
|
5149
|
+
requireNonEmptyString(f["name"], `${path43}.name`, errors);
|
|
5150
|
+
requireNonEmptyString(f["type"], `${path43}.type`, errors);
|
|
5078
5151
|
if (typeof f["required"] !== "boolean") {
|
|
5079
|
-
errors.push({ path: `${
|
|
5152
|
+
errors.push({ path: `${path43}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
5080
5153
|
}
|
|
5081
5154
|
}
|
|
5082
|
-
function validateEndpoint(raw,
|
|
5155
|
+
function validateEndpoint(raw, path43, errors) {
|
|
5083
5156
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5084
|
-
errors.push({ path:
|
|
5157
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5085
5158
|
return;
|
|
5086
5159
|
}
|
|
5087
5160
|
const e = raw;
|
|
5088
|
-
requireNonEmptyString(e["id"], `${
|
|
5089
|
-
requireNonEmptyString(e["description"], `${
|
|
5161
|
+
requireNonEmptyString(e["id"], `${path43}.id`, errors);
|
|
5162
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5090
5163
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
5091
5164
|
errors.push({
|
|
5092
|
-
path: `${
|
|
5165
|
+
path: `${path43}.method`,
|
|
5093
5166
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
5094
5167
|
});
|
|
5095
5168
|
}
|
|
5096
5169
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
5097
5170
|
errors.push({
|
|
5098
|
-
path: `${
|
|
5171
|
+
path: `${path43}.path`,
|
|
5099
5172
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
5100
5173
|
});
|
|
5101
5174
|
}
|
|
5102
5175
|
if (typeof e["auth"] !== "boolean") {
|
|
5103
|
-
errors.push({ path: `${
|
|
5176
|
+
errors.push({ path: `${path43}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
5104
5177
|
}
|
|
5105
5178
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
5106
5179
|
errors.push({
|
|
5107
|
-
path: `${
|
|
5180
|
+
path: `${path43}.successStatus`,
|
|
5108
5181
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
5109
5182
|
});
|
|
5110
5183
|
}
|
|
5111
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
5184
|
+
requireNonEmptyString(e["successDescription"], `${path43}.successDescription`, errors);
|
|
5112
5185
|
if (e["request"] !== void 0) {
|
|
5113
|
-
validateRequestSchema(e["request"], `${
|
|
5186
|
+
validateRequestSchema(e["request"], `${path43}.request`, errors);
|
|
5114
5187
|
}
|
|
5115
5188
|
if (e["errors"] !== void 0) {
|
|
5116
5189
|
if (!Array.isArray(e["errors"])) {
|
|
5117
|
-
errors.push({ path: `${
|
|
5190
|
+
errors.push({ path: `${path43}.errors`, message: "Must be an array if present" });
|
|
5118
5191
|
} else {
|
|
5119
5192
|
const errs = e["errors"];
|
|
5120
5193
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
5121
|
-
errors.push({ path: `${
|
|
5194
|
+
errors.push({ path: `${path43}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
5122
5195
|
}
|
|
5123
5196
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
5124
|
-
validateResponseError(errs[j2], `${
|
|
5197
|
+
validateResponseError(errs[j2], `${path43}.errors[${j2}]`, errors);
|
|
5125
5198
|
}
|
|
5126
5199
|
}
|
|
5127
5200
|
}
|
|
5128
5201
|
}
|
|
5129
|
-
function validateRequestSchema(raw,
|
|
5202
|
+
function validateRequestSchema(raw, path43, errors) {
|
|
5130
5203
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5131
|
-
errors.push({ path:
|
|
5204
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5132
5205
|
return;
|
|
5133
5206
|
}
|
|
5134
5207
|
const r = raw;
|
|
5135
5208
|
for (const key of ["body", "query", "params"]) {
|
|
5136
5209
|
if (r[key] !== void 0) {
|
|
5137
|
-
validateFieldMap(r[key], `${
|
|
5210
|
+
validateFieldMap(r[key], `${path43}.${key}`, errors);
|
|
5138
5211
|
}
|
|
5139
5212
|
}
|
|
5140
5213
|
}
|
|
5141
|
-
function validateFieldMap(raw,
|
|
5214
|
+
function validateFieldMap(raw, path43, errors) {
|
|
5142
5215
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5143
|
-
errors.push({ path:
|
|
5216
|
+
errors.push({ path: path43, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
5144
5217
|
return;
|
|
5145
5218
|
}
|
|
5146
5219
|
const map = raw;
|
|
5147
5220
|
for (const [k2, v2] of Object.entries(map)) {
|
|
5148
5221
|
if (typeof v2 !== "string") {
|
|
5149
|
-
errors.push({ path: `${
|
|
5222
|
+
errors.push({ path: `${path43}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
5150
5223
|
}
|
|
5151
5224
|
}
|
|
5152
5225
|
}
|
|
5153
|
-
function validateResponseError(raw,
|
|
5226
|
+
function validateResponseError(raw, path43, errors) {
|
|
5154
5227
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5155
|
-
errors.push({ path:
|
|
5228
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5156
5229
|
return;
|
|
5157
5230
|
}
|
|
5158
5231
|
const e = raw;
|
|
5159
5232
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5160
|
-
errors.push({ path: `${
|
|
5233
|
+
errors.push({ path: `${path43}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5161
5234
|
}
|
|
5162
|
-
requireNonEmptyString(e["code"], `${
|
|
5163
|
-
requireNonEmptyString(e["description"], `${
|
|
5235
|
+
requireNonEmptyString(e["code"], `${path43}.code`, errors);
|
|
5236
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5164
5237
|
}
|
|
5165
|
-
function validateBehavior(raw,
|
|
5238
|
+
function validateBehavior(raw, path43, errors) {
|
|
5166
5239
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5167
|
-
errors.push({ path:
|
|
5240
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5168
5241
|
return;
|
|
5169
5242
|
}
|
|
5170
5243
|
const b = raw;
|
|
5171
|
-
requireNonEmptyString(b["id"], `${
|
|
5172
|
-
requireNonEmptyString(b["description"], `${
|
|
5244
|
+
requireNonEmptyString(b["id"], `${path43}.id`, errors);
|
|
5245
|
+
requireNonEmptyString(b["description"], `${path43}.description`, errors);
|
|
5173
5246
|
if (b["constraints"] !== void 0) {
|
|
5174
5247
|
if (!Array.isArray(b["constraints"])) {
|
|
5175
|
-
errors.push({ path: `${
|
|
5248
|
+
errors.push({ path: `${path43}.constraints`, message: "Must be an array of strings if present" });
|
|
5176
5249
|
} else {
|
|
5177
5250
|
const cs2 = b["constraints"];
|
|
5178
5251
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5179
5252
|
if (typeof cs2[j2] !== "string") {
|
|
5180
|
-
errors.push({ path: `${
|
|
5253
|
+
errors.push({ path: `${path43}.constraints[${j2}]`, message: "Must be a string" });
|
|
5181
5254
|
}
|
|
5182
5255
|
}
|
|
5183
5256
|
}
|
|
5184
5257
|
}
|
|
5185
5258
|
}
|
|
5186
|
-
function validateComponent(raw,
|
|
5259
|
+
function validateComponent(raw, path43, errors) {
|
|
5187
5260
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5188
|
-
errors.push({ path:
|
|
5261
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5189
5262
|
return;
|
|
5190
5263
|
}
|
|
5191
5264
|
const c = raw;
|
|
5192
|
-
requireNonEmptyString(c["id"], `${
|
|
5193
|
-
requireNonEmptyString(c["name"], `${
|
|
5194
|
-
requireNonEmptyString(c["description"], `${
|
|
5265
|
+
requireNonEmptyString(c["id"], `${path43}.id`, errors);
|
|
5266
|
+
requireNonEmptyString(c["name"], `${path43}.name`, errors);
|
|
5267
|
+
requireNonEmptyString(c["description"], `${path43}.description`, errors);
|
|
5195
5268
|
if (c["props"] !== void 0) {
|
|
5196
5269
|
if (!Array.isArray(c["props"])) {
|
|
5197
|
-
errors.push({ path: `${
|
|
5270
|
+
errors.push({ path: `${path43}.props`, message: "Must be an array if present" });
|
|
5198
5271
|
} else {
|
|
5199
5272
|
const props = c["props"];
|
|
5200
5273
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5201
5274
|
const p = props[j2];
|
|
5202
5275
|
if (typeof p !== "object" || p === null) {
|
|
5203
|
-
errors.push({ path: `${
|
|
5276
|
+
errors.push({ path: `${path43}.props[${j2}]`, message: "Must be an object" });
|
|
5204
5277
|
continue;
|
|
5205
5278
|
}
|
|
5206
|
-
requireNonEmptyString(p["name"], `${
|
|
5207
|
-
requireNonEmptyString(p["type"], `${
|
|
5279
|
+
requireNonEmptyString(p["name"], `${path43}.props[${j2}].name`, errors);
|
|
5280
|
+
requireNonEmptyString(p["type"], `${path43}.props[${j2}].type`, errors);
|
|
5208
5281
|
if (typeof p["required"] !== "boolean") {
|
|
5209
|
-
errors.push({ path: `${
|
|
5282
|
+
errors.push({ path: `${path43}.props[${j2}].required`, message: "Must be boolean" });
|
|
5210
5283
|
}
|
|
5211
5284
|
}
|
|
5212
5285
|
}
|
|
5213
5286
|
}
|
|
5214
5287
|
if (c["events"] !== void 0) {
|
|
5215
5288
|
if (!Array.isArray(c["events"])) {
|
|
5216
|
-
errors.push({ path: `${
|
|
5289
|
+
errors.push({ path: `${path43}.events`, message: "Must be an array if present" });
|
|
5217
5290
|
} else {
|
|
5218
5291
|
const events = c["events"];
|
|
5219
5292
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5220
5293
|
const e = events[j2];
|
|
5221
5294
|
if (typeof e !== "object" || e === null) {
|
|
5222
|
-
errors.push({ path: `${
|
|
5295
|
+
errors.push({ path: `${path43}.events[${j2}]`, message: "Must be an object" });
|
|
5223
5296
|
continue;
|
|
5224
5297
|
}
|
|
5225
|
-
requireNonEmptyString(e["name"], `${
|
|
5298
|
+
requireNonEmptyString(e["name"], `${path43}.events[${j2}].name`, errors);
|
|
5226
5299
|
}
|
|
5227
5300
|
}
|
|
5228
5301
|
}
|
|
5229
5302
|
if (c["state"] !== void 0) {
|
|
5230
5303
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5231
|
-
errors.push({ path: `${
|
|
5304
|
+
errors.push({ path: `${path43}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5232
5305
|
}
|
|
5233
5306
|
}
|
|
5234
5307
|
if (c["apiCalls"] !== void 0) {
|
|
5235
5308
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5236
|
-
errors.push({ path: `${
|
|
5309
|
+
errors.push({ path: `${path43}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5237
5310
|
}
|
|
5238
5311
|
}
|
|
5239
5312
|
}
|
|
@@ -5295,10 +5368,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5295
5368
|
}
|
|
5296
5369
|
}
|
|
5297
5370
|
}
|
|
5298
|
-
function requireNonEmptyString(v2,
|
|
5371
|
+
function requireNonEmptyString(v2, path43, errors) {
|
|
5299
5372
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5300
5373
|
errors.push({
|
|
5301
|
-
path:
|
|
5374
|
+
path: path43,
|
|
5302
5375
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5303
5376
|
});
|
|
5304
5377
|
}
|
|
@@ -5509,13 +5582,18 @@ Output ONLY the corrected JSON. No explanation.`;
|
|
|
5509
5582
|
|
|
5510
5583
|
// core/token-budget.ts
|
|
5511
5584
|
import chalk5 from "chalk";
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5585
|
+
|
|
5586
|
+
// core/config-defaults.ts
|
|
5587
|
+
var DEFAULT_LOG_DIR = ".ai-spec-logs";
|
|
5588
|
+
var DEFAULT_VCR_DIR = ".ai-spec-vcr";
|
|
5589
|
+
var DEFAULT_BACKUP_DIR = ".ai-spec-backup";
|
|
5590
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5591
|
+
var DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
|
|
5592
|
+
var DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 3e4;
|
|
5593
|
+
var DEFAULT_MAX_FIX_FILE_CHARS = 6e4;
|
|
5594
|
+
var DEFAULT_DSL_MAX_RETRIES = 2;
|
|
5595
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5596
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5519
5597
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5520
5598
|
gemini: 9e5,
|
|
5521
5599
|
claude: 18e4,
|
|
@@ -5523,8 +5601,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5523
5601
|
deepseek: 6e4,
|
|
5524
5602
|
default: 1e5
|
|
5525
5603
|
};
|
|
5604
|
+
|
|
5605
|
+
// core/token-budget.ts
|
|
5606
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5607
|
+
function estimateTokens(text) {
|
|
5608
|
+
if (!text) return 0;
|
|
5609
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5610
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5611
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5612
|
+
}
|
|
5613
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5526
5614
|
function getDefaultBudget(providerName) {
|
|
5527
|
-
return
|
|
5615
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5528
5616
|
}
|
|
5529
5617
|
|
|
5530
5618
|
// core/safe-json.ts
|
|
@@ -5595,7 +5683,7 @@ function sanitizeDsl(raw) {
|
|
|
5595
5683
|
}
|
|
5596
5684
|
return dsl;
|
|
5597
5685
|
}
|
|
5598
|
-
var MAX_RETRIES =
|
|
5686
|
+
var MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
|
|
5599
5687
|
var DEFAULT_MAX_SPEC_CHARS = 12e3;
|
|
5600
5688
|
function dslFilePath(specFilePath) {
|
|
5601
5689
|
const dir = path7.dirname(specFilePath);
|
|
@@ -6319,12 +6407,11 @@ Shared component structure patterns:`);
|
|
|
6319
6407
|
// core/run-snapshot.ts
|
|
6320
6408
|
import * as fs10 from "fs-extra";
|
|
6321
6409
|
import * as path9 from "path";
|
|
6322
|
-
var BACKUP_DIR = ".ai-spec-backup";
|
|
6323
6410
|
var RunSnapshot = class {
|
|
6324
6411
|
constructor(workingDir, runId) {
|
|
6325
6412
|
this.workingDir = workingDir;
|
|
6326
6413
|
this.runId = runId;
|
|
6327
|
-
this.backupRoot = path9.join(workingDir,
|
|
6414
|
+
this.backupRoot = path9.join(workingDir, DEFAULT_BACKUP_DIR, runId);
|
|
6328
6415
|
}
|
|
6329
6416
|
backupRoot;
|
|
6330
6417
|
snapshotted = /* @__PURE__ */ new Set();
|
|
@@ -6380,7 +6467,6 @@ function getActiveSnapshot() {
|
|
|
6380
6467
|
import * as fs11 from "fs-extra";
|
|
6381
6468
|
import * as path10 from "path";
|
|
6382
6469
|
import chalk7 from "chalk";
|
|
6383
|
-
var LOG_DIR = ".ai-spec-logs";
|
|
6384
6470
|
function appendJsonlLine(filePath, record) {
|
|
6385
6471
|
try {
|
|
6386
6472
|
fs11.appendFileSync(filePath, JSON.stringify(record) + "\n");
|
|
@@ -6451,8 +6537,8 @@ var RunLogger = class {
|
|
|
6451
6537
|
this.workingDir = workingDir;
|
|
6452
6538
|
this.runId = runId;
|
|
6453
6539
|
this.startMs = Date.now();
|
|
6454
|
-
this.logPath = path10.join(workingDir,
|
|
6455
|
-
this.jsonlPath = path10.join(workingDir,
|
|
6540
|
+
this.logPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
|
|
6541
|
+
this.jsonlPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
|
|
6456
6542
|
this.log = {
|
|
6457
6543
|
runId,
|
|
6458
6544
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7394,7 +7480,7 @@ ${context.routeSummary}
|
|
|
7394
7480
|
}
|
|
7395
7481
|
if (context.schema) {
|
|
7396
7482
|
parts.push(`=== Prisma Schema ===
|
|
7397
|
-
${context.schema.slice(0,
|
|
7483
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
7398
7484
|
`);
|
|
7399
7485
|
}
|
|
7400
7486
|
if (context.errorPatterns) {
|
|
@@ -7442,7 +7528,7 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
7442
7528
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
7443
7529
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
7444
7530
|
}
|
|
7445
|
-
var REVIEW_HISTORY_FILE =
|
|
7531
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
7446
7532
|
async function loadReviewHistory(projectRoot) {
|
|
7447
7533
|
const historyPath = path13.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
7448
7534
|
try {
|
|
@@ -7573,15 +7659,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
7573
7659
|
${codeContext}`;
|
|
7574
7660
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
7575
7661
|
console.log(chalk11.gray(" Pass 2/3: Implementation review..."));
|
|
7662
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
7576
7663
|
const history = await loadReviewHistory(this.projectRoot);
|
|
7577
7664
|
const historyContext = buildHistoryContext(history);
|
|
7578
7665
|
const implPrompt = `Review the implementation details of this change.
|
|
7579
7666
|
|
|
7580
|
-
=== Feature Spec ===
|
|
7581
|
-
${
|
|
7582
|
-
|
|
7583
|
-
=== Code ===
|
|
7584
|
-
${codeContext}
|
|
7667
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
7668
|
+
${specDigest}
|
|
7585
7669
|
|
|
7586
7670
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
7587
7671
|
${archReview}
|
|
@@ -7590,11 +7674,8 @@ ${historyContext}`;
|
|
|
7590
7674
|
console.log(chalk11.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
7591
7675
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
7592
7676
|
|
|
7593
|
-
=== Feature Spec ===
|
|
7594
|
-
${
|
|
7595
|
-
|
|
7596
|
-
=== Code ===
|
|
7597
|
-
${codeContext}
|
|
7677
|
+
=== Feature Spec (digest) ===
|
|
7678
|
+
${specDigest}
|
|
7598
7679
|
|
|
7599
7680
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
7600
7681
|
${archReview}
|
|
@@ -7668,8 +7749,8 @@ ${sep}
|
|
|
7668
7749
|
filesSection += `
|
|
7669
7750
|
|
|
7670
7751
|
=== ${filePath} ===
|
|
7671
|
-
${content.slice(0,
|
|
7672
|
-
if (content.length >
|
|
7752
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7753
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
7673
7754
|
... (truncated, ${content.length} chars total)`;
|
|
7674
7755
|
} catch {
|
|
7675
7756
|
filesSection += `
|
|
@@ -8274,8 +8355,8 @@ import { execSync as execSync5 } from "child_process";
|
|
|
8274
8355
|
import * as fs18 from "fs-extra";
|
|
8275
8356
|
import * as path17 from "path";
|
|
8276
8357
|
init_cli_ui();
|
|
8277
|
-
var MAX_COMMAND_OUTPUT_CHARS =
|
|
8278
|
-
var MAX_FIX_FILE_CHARS =
|
|
8358
|
+
var MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
|
|
8359
|
+
var MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
|
|
8279
8360
|
function runCommand(cmd, cwd) {
|
|
8280
8361
|
try {
|
|
8281
8362
|
const output = execSync5(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
|
|
@@ -9484,8 +9565,8 @@ async function findLatestDslFile(projectDir) {
|
|
|
9484
9565
|
const entries = await fs22.readdir(dir);
|
|
9485
9566
|
for (const entry of entries) {
|
|
9486
9567
|
const abs = path21.join(dir, entry);
|
|
9487
|
-
const
|
|
9488
|
-
if (
|
|
9568
|
+
const stat5 = await fs22.stat(abs);
|
|
9569
|
+
if (stat5.isDirectory()) {
|
|
9489
9570
|
await scan(abs);
|
|
9490
9571
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9491
9572
|
allFiles.push(abs);
|
|
@@ -11223,7 +11304,7 @@ ${optionsText}`;
|
|
|
11223
11304
|
import { createHash as createHash2 } from "crypto";
|
|
11224
11305
|
import * as fs24 from "fs-extra";
|
|
11225
11306
|
import * as path23 from "path";
|
|
11226
|
-
var VCR_DIR =
|
|
11307
|
+
var VCR_DIR = DEFAULT_VCR_DIR;
|
|
11227
11308
|
var VcrRecordingProvider = class {
|
|
11228
11309
|
constructor(inner) {
|
|
11229
11310
|
this.inner = inner;
|
|
@@ -11921,7 +12002,147 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11921
12002
|
}
|
|
11922
12003
|
}
|
|
11923
12004
|
|
|
12005
|
+
// core/repo-store.ts
|
|
12006
|
+
import * as fs26 from "fs-extra";
|
|
12007
|
+
import * as path25 from "path";
|
|
12008
|
+
import * as os5 from "os";
|
|
12009
|
+
var REPO_STORE_FILE = path25.join(os5.homedir(), ".ai-spec-repos.json");
|
|
12010
|
+
async function readStore2() {
|
|
12011
|
+
try {
|
|
12012
|
+
if (await fs26.pathExists(REPO_STORE_FILE)) {
|
|
12013
|
+
return await fs26.readJson(REPO_STORE_FILE);
|
|
12014
|
+
}
|
|
12015
|
+
} catch (err) {
|
|
12016
|
+
console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${err.message}.`);
|
|
12017
|
+
}
|
|
12018
|
+
return { repos: [] };
|
|
12019
|
+
}
|
|
12020
|
+
async function writeStore2(store) {
|
|
12021
|
+
await fs26.ensureFile(REPO_STORE_FILE);
|
|
12022
|
+
await fs26.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
|
|
12023
|
+
}
|
|
12024
|
+
async function getRegisteredRepos() {
|
|
12025
|
+
const store = await readStore2();
|
|
12026
|
+
return store.repos;
|
|
12027
|
+
}
|
|
12028
|
+
async function registerRepo(repo) {
|
|
12029
|
+
const store = await readStore2();
|
|
12030
|
+
const idx = store.repos.findIndex((r) => r.path === repo.path);
|
|
12031
|
+
if (idx >= 0) {
|
|
12032
|
+
store.repos[idx] = repo;
|
|
12033
|
+
} else {
|
|
12034
|
+
store.repos.push(repo);
|
|
12035
|
+
}
|
|
12036
|
+
await writeStore2(store);
|
|
12037
|
+
}
|
|
12038
|
+
|
|
11924
12039
|
// cli/commands/create.ts
|
|
12040
|
+
init_spec_generator();
|
|
12041
|
+
async function promptRepoRole() {
|
|
12042
|
+
return select7({
|
|
12043
|
+
message: "What type of repo is this?",
|
|
12044
|
+
choices: [
|
|
12045
|
+
{ name: "Frontend", value: "frontend" },
|
|
12046
|
+
{ name: "Backend", value: "backend" },
|
|
12047
|
+
{ name: "Mobile", value: "mobile" },
|
|
12048
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12049
|
+
]
|
|
12050
|
+
});
|
|
12051
|
+
}
|
|
12052
|
+
async function quickRegisterRepo(provider) {
|
|
12053
|
+
const roleOverride = await promptRepoRole();
|
|
12054
|
+
const roleLabels = {
|
|
12055
|
+
frontend: "frontend",
|
|
12056
|
+
backend: "backend",
|
|
12057
|
+
mobile: "mobile",
|
|
12058
|
+
shared: "shared"
|
|
12059
|
+
};
|
|
12060
|
+
const raw = await input2({
|
|
12061
|
+
message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
|
|
12062
|
+
validate: (v2) => {
|
|
12063
|
+
const trimmed = v2.trim();
|
|
12064
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12065
|
+
if (!path26.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12066
|
+
return true;
|
|
12067
|
+
}
|
|
12068
|
+
});
|
|
12069
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12070
|
+
const resolved = path26.resolve(cleaned);
|
|
12071
|
+
if (!await fs27.pathExists(resolved)) {
|
|
12072
|
+
console.log(chalk26.red(` Path does not exist: ${resolved}`));
|
|
12073
|
+
return quickRegisterRepo(provider);
|
|
12074
|
+
}
|
|
12075
|
+
const { type, role: detectedRole } = await detectRepoType(resolved);
|
|
12076
|
+
const role = roleOverride ?? detectedRole;
|
|
12077
|
+
const repoName = path26.basename(resolved);
|
|
12078
|
+
console.log(chalk26.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12079
|
+
const constitutionPath = path26.join(resolved, CONSTITUTION_FILE);
|
|
12080
|
+
let hasConstitution = await fs27.pathExists(constitutionPath);
|
|
12081
|
+
if (!hasConstitution) {
|
|
12082
|
+
console.log(chalk26.blue(` Generating constitution for ${repoName}...`));
|
|
12083
|
+
try {
|
|
12084
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12085
|
+
const content = await gen.generate(resolved);
|
|
12086
|
+
await gen.saveConstitution(resolved, content);
|
|
12087
|
+
hasConstitution = true;
|
|
12088
|
+
console.log(chalk26.green(` \u2714 Constitution saved`));
|
|
12089
|
+
} catch (err) {
|
|
12090
|
+
console.log(chalk26.yellow(` \u26A0 Constitution failed: ${err.message}`));
|
|
12091
|
+
}
|
|
12092
|
+
}
|
|
12093
|
+
const entry = {
|
|
12094
|
+
name: repoName,
|
|
12095
|
+
path: resolved,
|
|
12096
|
+
type,
|
|
12097
|
+
role,
|
|
12098
|
+
hasConstitution,
|
|
12099
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12100
|
+
};
|
|
12101
|
+
await registerRepo(entry);
|
|
12102
|
+
console.log(chalk26.green(` \u2714 Repo registered: ${repoName}`));
|
|
12103
|
+
return entry;
|
|
12104
|
+
}
|
|
12105
|
+
async function selectRepos(registeredRepos, provider) {
|
|
12106
|
+
const ADD_NEW = "__add_new__";
|
|
12107
|
+
const choices = [
|
|
12108
|
+
...registeredRepos.map((r) => ({
|
|
12109
|
+
name: `${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`,
|
|
12110
|
+
value: r.path
|
|
12111
|
+
})),
|
|
12112
|
+
{ name: chalk26.cyan("+ Add new repo"), value: ADD_NEW }
|
|
12113
|
+
];
|
|
12114
|
+
const selected = await checkbox({
|
|
12115
|
+
message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
|
|
12116
|
+
choices,
|
|
12117
|
+
required: true
|
|
12118
|
+
});
|
|
12119
|
+
const result = [];
|
|
12120
|
+
for (const val of selected) {
|
|
12121
|
+
if (val === ADD_NEW) {
|
|
12122
|
+
const newRepo = await quickRegisterRepo(provider);
|
|
12123
|
+
result.push(newRepo);
|
|
12124
|
+
} else {
|
|
12125
|
+
const repo = registeredRepos.find((r) => r.path === val);
|
|
12126
|
+
if (repo) result.push(repo);
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
if (result.length === 0) {
|
|
12130
|
+
console.log(chalk26.yellow(" No repos selected. Please select at least one."));
|
|
12131
|
+
return selectRepos(registeredRepos, provider);
|
|
12132
|
+
}
|
|
12133
|
+
return result;
|
|
12134
|
+
}
|
|
12135
|
+
function buildWorkspaceConfig(repos) {
|
|
12136
|
+
return {
|
|
12137
|
+
name: "ai-spec-workspace",
|
|
12138
|
+
repos: repos.map((r) => ({
|
|
12139
|
+
name: r.name,
|
|
12140
|
+
path: r.path,
|
|
12141
|
+
type: r.type,
|
|
12142
|
+
role: r.role
|
|
12143
|
+
}))
|
|
12144
|
+
};
|
|
12145
|
+
}
|
|
11925
12146
|
function registerCreate(program2) {
|
|
11926
12147
|
program2.command("create").description("Generate a feature spec and kick off code generation").argument("[idea]", "Feature idea in natural language (prompted if omitted)").option(
|
|
11927
12148
|
"--provider <name>",
|
|
@@ -11944,25 +12165,76 @@ function registerCreate(program2) {
|
|
|
11944
12165
|
});
|
|
11945
12166
|
}
|
|
11946
12167
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11947
|
-
const
|
|
11948
|
-
if (
|
|
12168
|
+
const existingWorkspaceConfig = await workspaceLoader.load();
|
|
12169
|
+
if (existingWorkspaceConfig) {
|
|
11949
12170
|
console.log(chalk26.cyan(`
|
|
11950
|
-
[Workspace] Detected workspace: ${
|
|
11951
|
-
console.log(chalk26.gray(` Repos: ${
|
|
11952
|
-
const pipelineResults = await runMultiRepoPipeline(idea,
|
|
12171
|
+
[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
|
|
12172
|
+
console.log(chalk26.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
12173
|
+
const pipelineResults = await runMultiRepoPipeline(idea, existingWorkspaceConfig, opts, currentDir, config2);
|
|
11953
12174
|
if (opts.serve) {
|
|
11954
12175
|
await handleAutoServe(pipelineResults);
|
|
11955
12176
|
}
|
|
11956
12177
|
return;
|
|
11957
12178
|
}
|
|
11958
|
-
|
|
12179
|
+
const providerName = opts.provider || config2.provider || "gemini";
|
|
12180
|
+
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
12181
|
+
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
12182
|
+
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
12183
|
+
const registeredRepos = await getRegisteredRepos();
|
|
12184
|
+
if (registeredRepos.length === 0) {
|
|
12185
|
+
console.log(chalk26.yellow("\n No repos registered. Please register repos first."));
|
|
12186
|
+
console.log(chalk26.gray(" Run: ai-spec init"));
|
|
12187
|
+
console.log(chalk26.gray(" Or add a repo now:\n"));
|
|
12188
|
+
const addNow = await select7({
|
|
12189
|
+
message: "Add a repo now?",
|
|
12190
|
+
choices: [
|
|
12191
|
+
{ name: "Yes \u2014 register a repo and continue", value: "yes" },
|
|
12192
|
+
{ name: "No \u2014 exit", value: "no" }
|
|
12193
|
+
]
|
|
12194
|
+
});
|
|
12195
|
+
if (addNow === "no") {
|
|
12196
|
+
process.exit(0);
|
|
12197
|
+
}
|
|
12198
|
+
const newRepo = await quickRegisterRepo(specProvider);
|
|
12199
|
+
registeredRepos.push(newRepo);
|
|
12200
|
+
}
|
|
12201
|
+
const validRepos = [];
|
|
12202
|
+
for (const r of registeredRepos) {
|
|
12203
|
+
if (await fs27.pathExists(r.path)) {
|
|
12204
|
+
validRepos.push(r);
|
|
12205
|
+
} else {
|
|
12206
|
+
console.log(chalk26.yellow(` \u26A0 Skipping ${r.name}: path not found (${r.path})`));
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
if (validRepos.length === 0) {
|
|
12210
|
+
console.log(chalk26.red(" No valid repos available. Run: ai-spec init"));
|
|
12211
|
+
process.exit(1);
|
|
12212
|
+
}
|
|
12213
|
+
const selectedRepos = await selectRepos(validRepos, specProvider);
|
|
12214
|
+
if (selectedRepos.length === 1) {
|
|
12215
|
+
const repo = selectedRepos[0];
|
|
12216
|
+
console.log(chalk26.cyan(`
|
|
12217
|
+
[Repo] ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12218
|
+
await runSingleRepoPipeline(idea, opts, repo.path, config2);
|
|
12219
|
+
} else {
|
|
12220
|
+
const workspaceConfig = buildWorkspaceConfig(selectedRepos);
|
|
12221
|
+
console.log(chalk26.cyan(`
|
|
12222
|
+
[Workspace] ${selectedRepos.length} repo(s) selected:`));
|
|
12223
|
+
for (const repo of selectedRepos) {
|
|
12224
|
+
console.log(chalk26.gray(` ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12225
|
+
}
|
|
12226
|
+
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
12227
|
+
if (opts.serve) {
|
|
12228
|
+
await handleAutoServe(pipelineResults);
|
|
12229
|
+
}
|
|
12230
|
+
}
|
|
11959
12231
|
});
|
|
11960
12232
|
}
|
|
11961
12233
|
|
|
11962
12234
|
// cli/commands/review.ts
|
|
11963
12235
|
init_spec_generator();
|
|
11964
|
-
import * as
|
|
11965
|
-
import * as
|
|
12236
|
+
import * as path27 from "path";
|
|
12237
|
+
import * as fs28 from "fs-extra";
|
|
11966
12238
|
import chalk27 from "chalk";
|
|
11967
12239
|
function registerReview(program2) {
|
|
11968
12240
|
program2.command("review").description("Run AI code review on current git diff against a spec").argument("[specFile]", "Path to spec file (auto-detects latest in specs/ if omitted)").option(
|
|
@@ -11979,17 +12251,17 @@ function registerReview(program2) {
|
|
|
11979
12251
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
11980
12252
|
let specContent = "";
|
|
11981
12253
|
let resolvedSpecFile;
|
|
11982
|
-
if (specFile && await
|
|
11983
|
-
specContent = await
|
|
12254
|
+
if (specFile && await fs28.pathExists(specFile)) {
|
|
12255
|
+
specContent = await fs28.readFile(specFile, "utf-8");
|
|
11984
12256
|
resolvedSpecFile = specFile;
|
|
11985
12257
|
console.log(chalk27.gray(`Using spec: ${specFile}`));
|
|
11986
12258
|
} else {
|
|
11987
|
-
const specsDir =
|
|
11988
|
-
if (await
|
|
11989
|
-
const files = (await
|
|
12259
|
+
const specsDir = path27.join(currentDir, "specs");
|
|
12260
|
+
if (await fs28.pathExists(specsDir)) {
|
|
12261
|
+
const files = (await fs28.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
11990
12262
|
if (files.length > 0) {
|
|
11991
|
-
const latest =
|
|
11992
|
-
specContent = await
|
|
12263
|
+
const latest = path27.join(specsDir, files[0]);
|
|
12264
|
+
specContent = await fs28.readFile(latest, "utf-8");
|
|
11993
12265
|
resolvedSpecFile = latest;
|
|
11994
12266
|
console.log(chalk27.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
11995
12267
|
}
|
|
@@ -12005,9 +12277,10 @@ function registerReview(program2) {
|
|
|
12005
12277
|
|
|
12006
12278
|
// cli/commands/init.ts
|
|
12007
12279
|
init_spec_generator();
|
|
12008
|
-
import * as
|
|
12009
|
-
import * as
|
|
12280
|
+
import * as path28 from "path";
|
|
12281
|
+
import * as fs29 from "fs-extra";
|
|
12010
12282
|
import chalk28 from "chalk";
|
|
12283
|
+
import { input as input3, select as select8, confirm as confirm2 } from "@inquirer/prompts";
|
|
12011
12284
|
|
|
12012
12285
|
// prompts/global-constitution.prompt.ts
|
|
12013
12286
|
var globalConstitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided multi-project context and generate a "Global Constitution" \u2014 a team-level baseline document that captures cross-project rules, shared conventions, and universal constraints that every repository in this workspace must follow.
|
|
@@ -12068,240 +12341,139 @@ ${summary}
|
|
|
12068
12341
|
return parts.join("\n");
|
|
12069
12342
|
}
|
|
12070
12343
|
|
|
12071
|
-
//
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
"expo",
|
|
12089
|
-
// DB / ORM
|
|
12090
|
-
"prisma",
|
|
12091
|
-
"@prisma/client",
|
|
12092
|
-
"mongoose",
|
|
12093
|
-
"typeorm",
|
|
12094
|
-
"sequelize",
|
|
12095
|
-
"drizzle-orm",
|
|
12096
|
-
// Auth
|
|
12097
|
-
"jsonwebtoken",
|
|
12098
|
-
"passport",
|
|
12099
|
-
"next-auth",
|
|
12100
|
-
"@clerk/nextjs",
|
|
12101
|
-
// Build / Lang
|
|
12102
|
-
"typescript",
|
|
12103
|
-
"vite",
|
|
12104
|
-
"webpack",
|
|
12105
|
-
"esbuild",
|
|
12106
|
-
"turbo",
|
|
12107
|
-
// Testing
|
|
12108
|
-
"jest",
|
|
12109
|
-
"vitest",
|
|
12110
|
-
"mocha",
|
|
12111
|
-
"cypress",
|
|
12112
|
-
"playwright",
|
|
12113
|
-
// Infra
|
|
12114
|
-
"redis",
|
|
12115
|
-
"bull",
|
|
12116
|
-
"socket.io",
|
|
12117
|
-
"graphql",
|
|
12118
|
-
"@trpc/server"
|
|
12119
|
-
];
|
|
12120
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12121
|
-
"node_modules",
|
|
12122
|
-
".git",
|
|
12123
|
-
".svn",
|
|
12124
|
-
"dist",
|
|
12125
|
-
"build",
|
|
12126
|
-
"out",
|
|
12127
|
-
".next",
|
|
12128
|
-
".nuxt",
|
|
12129
|
-
"coverage",
|
|
12130
|
-
".turbo",
|
|
12131
|
-
".cache",
|
|
12132
|
-
"__pycache__",
|
|
12133
|
-
"vendor",
|
|
12134
|
-
".ai-spec-vcr",
|
|
12135
|
-
".ai-spec-logs",
|
|
12136
|
-
"specs"
|
|
12137
|
-
]);
|
|
12138
|
-
var MANIFEST_FILES = [
|
|
12139
|
-
"package.json",
|
|
12140
|
-
"go.mod",
|
|
12141
|
-
"Cargo.toml",
|
|
12142
|
-
"pom.xml",
|
|
12143
|
-
"build.gradle",
|
|
12144
|
-
"build.gradle.kts",
|
|
12145
|
-
"requirements.txt",
|
|
12146
|
-
"pyproject.toml",
|
|
12147
|
-
"setup.py",
|
|
12148
|
-
"composer.json"
|
|
12149
|
-
];
|
|
12150
|
-
async function isProjectRoot(absPath) {
|
|
12151
|
-
for (const manifest of MANIFEST_FILES) {
|
|
12152
|
-
if (await fs27.pathExists(path26.join(absPath, manifest))) return true;
|
|
12153
|
-
}
|
|
12154
|
-
return false;
|
|
12344
|
+
// cli/commands/init.ts
|
|
12345
|
+
var ROLE_LABELS = {
|
|
12346
|
+
frontend: "frontend",
|
|
12347
|
+
backend: "backend",
|
|
12348
|
+
mobile: "mobile",
|
|
12349
|
+
shared: "shared"
|
|
12350
|
+
};
|
|
12351
|
+
async function promptRepoRole2() {
|
|
12352
|
+
return select8({
|
|
12353
|
+
message: "What type of repo is this?",
|
|
12354
|
+
choices: [
|
|
12355
|
+
{ name: "Frontend", value: "frontend" },
|
|
12356
|
+
{ name: "Backend", value: "backend" },
|
|
12357
|
+
{ name: "Mobile", value: "mobile" },
|
|
12358
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12359
|
+
]
|
|
12360
|
+
});
|
|
12155
12361
|
}
|
|
12156
|
-
async function
|
|
12157
|
-
const
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
}
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12362
|
+
async function promptRepoPath(role) {
|
|
12363
|
+
const label = ROLE_LABELS[role];
|
|
12364
|
+
const raw = await input3({
|
|
12365
|
+
message: `Enter your ${label} repo path (absolute path):`,
|
|
12366
|
+
validate: (v2) => {
|
|
12367
|
+
const trimmed = v2.trim();
|
|
12368
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12369
|
+
if (!path28.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12370
|
+
return true;
|
|
12371
|
+
}
|
|
12372
|
+
});
|
|
12373
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12374
|
+
const resolved = path28.resolve(cleaned);
|
|
12375
|
+
if (!await fs29.pathExists(resolved)) {
|
|
12376
|
+
console.log(chalk28.red(` Path does not exist: ${resolved}`));
|
|
12377
|
+
return promptRepoPath(role);
|
|
12378
|
+
}
|
|
12379
|
+
const stat5 = await fs29.stat(resolved);
|
|
12380
|
+
if (!stat5.isDirectory()) {
|
|
12381
|
+
console.log(chalk28.red(` Not a directory: ${resolved}`));
|
|
12382
|
+
return promptRepoPath(role);
|
|
12383
|
+
}
|
|
12384
|
+
return resolved;
|
|
12385
|
+
}
|
|
12386
|
+
async function registerSingleRepo(repoPath, provider, roleOverride) {
|
|
12387
|
+
const { type, role: detectedRole } = await detectRepoType(repoPath);
|
|
12388
|
+
const role = roleOverride ?? detectedRole;
|
|
12389
|
+
const repoName = path28.basename(repoPath);
|
|
12390
|
+
console.log(chalk28.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12391
|
+
const constitutionPath = path28.join(repoPath, CONSTITUTION_FILE);
|
|
12392
|
+
let hasConstitution = await fs29.pathExists(constitutionPath);
|
|
12393
|
+
if (!hasConstitution) {
|
|
12394
|
+
console.log(chalk28.blue(` Generating project constitution for ${repoName}...`));
|
|
12395
|
+
try {
|
|
12396
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12397
|
+
const content = await gen.generate(repoPath);
|
|
12398
|
+
await gen.saveConstitution(repoPath, content);
|
|
12399
|
+
hasConstitution = true;
|
|
12400
|
+
console.log(chalk28.green(` \u2714 Constitution saved: ${constitutionPath}`));
|
|
12401
|
+
} catch (err) {
|
|
12402
|
+
console.log(chalk28.yellow(` \u26A0 Constitution generation failed: ${err.message}`));
|
|
12403
|
+
}
|
|
12404
|
+
} else {
|
|
12405
|
+
console.log(chalk28.green(` \u2714 Constitution already exists: ${constitutionPath}`));
|
|
12406
|
+
}
|
|
12407
|
+
const entry = {
|
|
12408
|
+
name: repoName,
|
|
12409
|
+
path: repoPath,
|
|
12410
|
+
type,
|
|
12411
|
+
role,
|
|
12412
|
+
hasConstitution,
|
|
12413
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12174
12414
|
};
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
let entries;
|
|
12415
|
+
await registerRepo(entry);
|
|
12416
|
+
return entry;
|
|
12417
|
+
}
|
|
12418
|
+
async function buildProjectSummaries(repos) {
|
|
12419
|
+
const summaries = [];
|
|
12420
|
+
for (const repo of repos) {
|
|
12421
|
+
if (!await fs29.pathExists(repo.path)) continue;
|
|
12422
|
+
const lines = [
|
|
12423
|
+
`Type: ${repo.type} (${repo.role})`
|
|
12424
|
+
];
|
|
12186
12425
|
try {
|
|
12187
|
-
|
|
12426
|
+
const loader = new ContextLoader(repo.path);
|
|
12427
|
+
const ctx = await loader.loadProjectContext();
|
|
12428
|
+
lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
|
|
12429
|
+
lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
|
|
12188
12430
|
} catch {
|
|
12189
|
-
|
|
12431
|
+
lines.push(`Tech stack: ${repo.type}`);
|
|
12190
12432
|
}
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
const gitStat = await fs27.stat(gitPath);
|
|
12198
|
-
if (gitStat.isFile()) continue;
|
|
12199
|
-
}
|
|
12200
|
-
if (await isProjectRoot(childAbs)) {
|
|
12201
|
-
found.push(path26.relative(rootDir, childAbs));
|
|
12202
|
-
} else {
|
|
12203
|
-
await walk(childAbs, depth + 1);
|
|
12433
|
+
if (repo.hasConstitution) {
|
|
12434
|
+
try {
|
|
12435
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12436
|
+
const raw = await fs29.readFile(constitutionPath, "utf-8");
|
|
12437
|
+
lines.push("", "Constitution excerpt:", raw.slice(0, 2e3));
|
|
12438
|
+
} catch {
|
|
12204
12439
|
}
|
|
12205
12440
|
}
|
|
12441
|
+
summaries.push({ name: repo.name, summary: lines.join("\n") });
|
|
12206
12442
|
}
|
|
12207
|
-
|
|
12208
|
-
return found;
|
|
12443
|
+
return summaries;
|
|
12209
12444
|
}
|
|
12210
|
-
async function
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12445
|
+
async function generateGlobalConstitution(provider, repos, currentDir) {
|
|
12446
|
+
console.log(chalk28.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12447
|
+
console.log(chalk28.gray(` Based on ${repos.length} registered repo(s)`));
|
|
12448
|
+
const summaries = await buildProjectSummaries(repos);
|
|
12449
|
+
if (summaries.length === 0) {
|
|
12450
|
+
console.log(chalk28.yellow(" No valid repos found \u2014 skipping global constitution."));
|
|
12451
|
+
return;
|
|
12216
12452
|
}
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
}
|
|
12223
|
-
|
|
12224
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12225
|
-
const existing = await loadIndex(scanRoot);
|
|
12226
|
-
const existingMap = new Map(
|
|
12227
|
-
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
12228
|
-
);
|
|
12229
|
-
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
12230
|
-
const added = [];
|
|
12231
|
-
const updated = [];
|
|
12232
|
-
const unchanged = [];
|
|
12233
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
12234
|
-
for (const relPath of discoveredPaths) {
|
|
12235
|
-
const absPath = path26.join(scanRoot, relPath);
|
|
12236
|
-
seenPaths.add(relPath);
|
|
12237
|
-
const { type, role } = await detectRepoType(absPath);
|
|
12238
|
-
const techStack = await extractTechStack(absPath, type);
|
|
12239
|
-
const hasConstitution = await fs27.pathExists(path26.join(absPath, CONSTITUTION_FILE));
|
|
12240
|
-
const hasWorkspace = await fs27.pathExists(path26.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
12241
|
-
const name = path26.basename(relPath);
|
|
12242
|
-
const prev = existingMap.get(relPath);
|
|
12243
|
-
if (!prev) {
|
|
12244
|
-
const entry = {
|
|
12245
|
-
name,
|
|
12246
|
-
path: relPath,
|
|
12247
|
-
type,
|
|
12248
|
-
role,
|
|
12249
|
-
techStack,
|
|
12250
|
-
hasConstitution,
|
|
12251
|
-
hasWorkspace,
|
|
12252
|
-
firstSeen: now,
|
|
12253
|
-
lastSeen: now
|
|
12254
|
-
};
|
|
12255
|
-
added.push(entry);
|
|
12256
|
-
existingMap.set(relPath, entry);
|
|
12257
|
-
} else {
|
|
12258
|
-
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
12259
|
-
const entry = {
|
|
12260
|
-
...prev,
|
|
12261
|
-
type,
|
|
12262
|
-
role,
|
|
12263
|
-
techStack,
|
|
12264
|
-
hasConstitution,
|
|
12265
|
-
hasWorkspace,
|
|
12266
|
-
lastSeen: now,
|
|
12267
|
-
missing: void 0
|
|
12268
|
-
// clear missing flag if it came back
|
|
12269
|
-
};
|
|
12270
|
-
existingMap.set(relPath, entry);
|
|
12271
|
-
if (changed) {
|
|
12272
|
-
updated.push(entry);
|
|
12273
|
-
} else {
|
|
12274
|
-
unchanged.push(entry);
|
|
12275
|
-
}
|
|
12276
|
-
}
|
|
12453
|
+
const prompt = buildGlobalConstitutionPrompt(summaries);
|
|
12454
|
+
let globalConstitution;
|
|
12455
|
+
try {
|
|
12456
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
12457
|
+
} catch (err) {
|
|
12458
|
+
console.error(chalk28.red(` \u2718 Failed to generate global constitution: ${err.message}`));
|
|
12459
|
+
return;
|
|
12277
12460
|
}
|
|
12278
|
-
const
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12461
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
12462
|
+
console.log(chalk28.green(` \u2714 Global constitution saved: ${saved}`));
|
|
12463
|
+
console.log(chalk28.gray(" Project constitutions will be merged with this at runtime."));
|
|
12464
|
+
const lines = globalConstitution.split("\n");
|
|
12465
|
+
console.log(chalk28.bold("\n Preview:"));
|
|
12466
|
+
console.log(chalk28.gray(lines.slice(0, 10).join("\n")));
|
|
12467
|
+
if (lines.length > 10) {
|
|
12468
|
+
console.log(chalk28.gray(` ... (${lines.length} lines total)`));
|
|
12285
12469
|
}
|
|
12286
|
-
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
12287
|
-
const index = {
|
|
12288
|
-
scanRoot,
|
|
12289
|
-
lastScanned: now,
|
|
12290
|
-
projects
|
|
12291
|
-
};
|
|
12292
|
-
return { index, added, updated, unchanged, nowMissing };
|
|
12293
12470
|
}
|
|
12294
|
-
|
|
12295
|
-
// cli/commands/init.ts
|
|
12296
12471
|
function registerInit(program2) {
|
|
12297
|
-
program2.command("init").description(
|
|
12472
|
+
program2.command("init").description("Setup workspace: register repos, generate constitutions").option(
|
|
12298
12473
|
"--provider <name>",
|
|
12299
12474
|
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
12300
12475
|
void 0
|
|
12301
|
-
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing
|
|
12302
|
-
"--global",
|
|
12303
|
-
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
12304
|
-
).option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules (prune & rebase)").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").action(async (opts) => {
|
|
12476
|
+
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing constitutions").option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").option("--add-repo", "Add a new repo to the registered list").action(async (opts) => {
|
|
12305
12477
|
const currentDir = process.cwd();
|
|
12306
12478
|
const config2 = await loadConfig(currentDir);
|
|
12307
12479
|
const providerName = opts.provider || config2.provider || "gemini";
|
|
@@ -12320,7 +12492,7 @@ function registerInit(program2) {
|
|
|
12320
12492
|
console.log(chalk28.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
12321
12493
|
console.log(chalk28.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
12322
12494
|
if (result.backupPath) {
|
|
12323
|
-
console.log(chalk28.gray(` Backup: ${
|
|
12495
|
+
console.log(chalk28.gray(` Backup: ${path28.basename(result.backupPath)}`));
|
|
12324
12496
|
}
|
|
12325
12497
|
}
|
|
12326
12498
|
} catch (err) {
|
|
@@ -12329,119 +12501,125 @@ function registerInit(program2) {
|
|
|
12329
12501
|
}
|
|
12330
12502
|
return;
|
|
12331
12503
|
}
|
|
12332
|
-
if (opts.
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12504
|
+
if (opts.addRepo) {
|
|
12505
|
+
console.log(chalk28.blue("\n\u2500\u2500\u2500 Register New Repo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12506
|
+
const role = await promptRepoRole2();
|
|
12507
|
+
const repoPath = await promptRepoPath(role);
|
|
12508
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12509
|
+
console.log(chalk28.green(`
|
|
12510
|
+
\u2714 Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
|
|
12511
|
+
console.log(chalk28.gray(` Saved to: ${REPO_STORE_FILE}`));
|
|
12512
|
+
const updateGlobal = await confirm2({
|
|
12513
|
+
message: "Update global constitution with this repo's context?",
|
|
12514
|
+
default: true
|
|
12515
|
+
});
|
|
12516
|
+
if (updateGlobal) {
|
|
12517
|
+
const allRepos2 = await getRegisteredRepos();
|
|
12518
|
+
await generateGlobalConstitution(provider, allRepos2, currentDir);
|
|
12339
12519
|
}
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12520
|
+
return;
|
|
12521
|
+
}
|
|
12522
|
+
console.log(chalk28.blue("\n" + "\u2500".repeat(52)));
|
|
12523
|
+
console.log(chalk28.bold(" ai-spec init \u2014 Workspace Setup"));
|
|
12524
|
+
console.log(chalk28.blue("\u2500".repeat(52)));
|
|
12525
|
+
console.log(chalk28.gray(` Provider: ${providerName}/${modelName}
|
|
12526
|
+
`));
|
|
12527
|
+
const existingRepos = await getRegisteredRepos();
|
|
12528
|
+
if (existingRepos.length > 0) {
|
|
12529
|
+
console.log(chalk28.cyan(" Registered repos:"));
|
|
12530
|
+
for (const r of existingRepos) {
|
|
12531
|
+
const constitutionIcon = r.hasConstitution ? chalk28.green("\u2714") : chalk28.gray("\u25CB");
|
|
12532
|
+
console.log(chalk28.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`));
|
|
12533
|
+
}
|
|
12534
|
+
console.log();
|
|
12535
|
+
}
|
|
12536
|
+
const action = existingRepos.length > 0 ? await select8({
|
|
12537
|
+
message: "What would you like to do?",
|
|
12538
|
+
choices: [
|
|
12539
|
+
{ name: "Add new repo(s)", value: "add" },
|
|
12540
|
+
{ name: "Re-generate constitutions for existing repos", value: "regen" },
|
|
12541
|
+
{ name: "Skip \u2014 proceed to global constitution", value: "skip" }
|
|
12542
|
+
]
|
|
12543
|
+
}) : "add";
|
|
12544
|
+
const newRepos = [];
|
|
12545
|
+
if (action === "add") {
|
|
12546
|
+
let addMore = true;
|
|
12547
|
+
while (addMore) {
|
|
12548
|
+
console.log(chalk28.blue(`
|
|
12549
|
+
\u2500\u2500 Register Repo #${existingRepos.length + newRepos.length + 1} \u2500\u2500`));
|
|
12550
|
+
const role = await promptRepoRole2();
|
|
12551
|
+
const repoPath = await promptRepoPath(role);
|
|
12552
|
+
const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
|
|
12553
|
+
if (alreadyRegistered) {
|
|
12554
|
+
console.log(chalk28.yellow(` Already registered: ${alreadyRegistered.name}`));
|
|
12555
|
+
} else {
|
|
12556
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12557
|
+
newRepos.push(entry);
|
|
12558
|
+
console.log(chalk28.green(` \u2714 Registered: ${entry.name}`));
|
|
12363
12559
|
}
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
const loader = new ContextLoader(currentDir);
|
|
12368
|
-
const ctx = await loader.loadProjectContext();
|
|
12369
|
-
projectSummaries.push({
|
|
12370
|
-
name: path27.basename(currentDir),
|
|
12371
|
-
summary: [
|
|
12372
|
-
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
12373
|
-
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
12374
|
-
].join("\n")
|
|
12560
|
+
addMore = await confirm2({
|
|
12561
|
+
message: "Add another repo?",
|
|
12562
|
+
default: false
|
|
12375
12563
|
});
|
|
12376
12564
|
}
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12565
|
+
}
|
|
12566
|
+
if (action === "regen") {
|
|
12567
|
+
console.log(chalk28.blue("\n Re-generating project constitutions..."));
|
|
12568
|
+
for (const repo of existingRepos) {
|
|
12569
|
+
if (!await fs29.pathExists(repo.path)) {
|
|
12570
|
+
console.log(chalk28.yellow(` \u26A0 ${repo.name}: path not found \u2014 skipping`));
|
|
12571
|
+
continue;
|
|
12572
|
+
}
|
|
12573
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12574
|
+
if (await fs29.pathExists(constitutionPath) && !opts.force) {
|
|
12575
|
+
console.log(chalk28.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
|
|
12576
|
+
continue;
|
|
12577
|
+
}
|
|
12578
|
+
console.log(chalk28.blue(` ${repo.name}: generating constitution...`));
|
|
12579
|
+
try {
|
|
12580
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12581
|
+
const content = await gen.generate(repo.path);
|
|
12582
|
+
await gen.saveConstitution(repo.path, content);
|
|
12583
|
+
console.log(chalk28.green(` \u2714 ${repo.name}: constitution saved`));
|
|
12584
|
+
} catch (err) {
|
|
12585
|
+
console.log(chalk28.yellow(` \u26A0 ${repo.name}: failed \u2014 ${err.message}`));
|
|
12586
|
+
}
|
|
12395
12587
|
}
|
|
12396
|
-
return;
|
|
12397
12588
|
}
|
|
12398
|
-
const
|
|
12399
|
-
if (
|
|
12400
|
-
console.log(chalk28.yellow(`
|
|
12401
|
-
${CONSTITUTION_FILE} already exists.`));
|
|
12402
|
-
console.log(chalk28.gray(" Use --force to overwrite it."));
|
|
12403
|
-
console.log(chalk28.gray(` Or edit it directly: ${constitutionPath}`));
|
|
12589
|
+
const allRepos = await getRegisteredRepos();
|
|
12590
|
+
if (allRepos.length === 0) {
|
|
12591
|
+
console.log(chalk28.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
|
|
12404
12592
|
return;
|
|
12405
12593
|
}
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
}
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
}
|
|
12425
|
-
console.log(chalk28.green(`
|
|
12426
|
-
\u2714 Constitution saved: ${saved}`));
|
|
12427
|
-
console.log(chalk28.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
12428
|
-
console.log(chalk28.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
12429
|
-
console.log(chalk28.bold(" Preview:"));
|
|
12430
|
-
console.log(chalk28.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
12431
|
-
if (constitution.split("\n").length > 15) {
|
|
12432
|
-
console.log(chalk28.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
12433
|
-
}
|
|
12594
|
+
const existingGlobal = await loadGlobalConstitution([currentDir]);
|
|
12595
|
+
const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0 ? true : await confirm2({
|
|
12596
|
+
message: "Global constitution exists. Re-generate it?",
|
|
12597
|
+
default: false
|
|
12598
|
+
});
|
|
12599
|
+
if (shouldGenerateGlobal) {
|
|
12600
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
12601
|
+
}
|
|
12602
|
+
console.log(chalk28.bold.green("\n\u2714 Init complete!"));
|
|
12603
|
+
console.log(chalk28.gray(` Repos registered: ${allRepos.length}`));
|
|
12604
|
+
for (const r of allRepos) {
|
|
12605
|
+
const icon = r.hasConstitution ? chalk28.green("\u2714") : chalk28.gray("\u25CB");
|
|
12606
|
+
console.log(chalk28.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
|
|
12607
|
+
}
|
|
12608
|
+
console.log(chalk28.gray(`
|
|
12609
|
+
Repo store: ${REPO_STORE_FILE}`));
|
|
12610
|
+
console.log(chalk28.gray(` Next step: ai-spec create "your feature idea"`));
|
|
12611
|
+
process.exit(0);
|
|
12434
12612
|
});
|
|
12435
12613
|
}
|
|
12436
12614
|
|
|
12437
12615
|
// cli/commands/config.ts
|
|
12438
|
-
import * as
|
|
12439
|
-
import * as
|
|
12616
|
+
import * as path29 from "path";
|
|
12617
|
+
import * as fs30 from "fs-extra";
|
|
12440
12618
|
import chalk29 from "chalk";
|
|
12441
12619
|
function registerConfig(program2) {
|
|
12442
12620
|
program2.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)").option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
12443
12621
|
const currentDir = process.cwd();
|
|
12444
|
-
const configPath =
|
|
12622
|
+
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12445
12623
|
if (opts.clearKeys) {
|
|
12446
12624
|
await clearAllKeys();
|
|
12447
12625
|
console.log(chalk29.green(`\u2714 All saved API keys cleared.`));
|
|
@@ -12453,7 +12631,7 @@ function registerConfig(program2) {
|
|
|
12453
12631
|
return;
|
|
12454
12632
|
}
|
|
12455
12633
|
if (opts.listKeys) {
|
|
12456
|
-
const store = await
|
|
12634
|
+
const store = await fs30.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
12457
12635
|
const providers = Object.keys(store);
|
|
12458
12636
|
if (providers.length === 0) {
|
|
12459
12637
|
console.log(chalk29.gray("No saved API keys."));
|
|
@@ -12469,7 +12647,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12469
12647
|
return;
|
|
12470
12648
|
}
|
|
12471
12649
|
if (opts.reset) {
|
|
12472
|
-
await
|
|
12650
|
+
await fs30.writeJson(configPath, {}, { spaces: 2 });
|
|
12473
12651
|
console.log(chalk29.green(`\u2714 Config reset: ${configPath}`));
|
|
12474
12652
|
return;
|
|
12475
12653
|
}
|
|
@@ -12513,7 +12691,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12513
12691
|
}
|
|
12514
12692
|
updated.maxErrorCycles = cycles;
|
|
12515
12693
|
}
|
|
12516
|
-
await
|
|
12694
|
+
await fs30.writeJson(configPath, updated, { spaces: 2 });
|
|
12517
12695
|
console.log(chalk29.green(`\u2714 Config saved to ${configPath}`));
|
|
12518
12696
|
console.log(JSON.stringify(updated, null, 2));
|
|
12519
12697
|
});
|
|
@@ -12521,14 +12699,10 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12521
12699
|
|
|
12522
12700
|
// cli/commands/model.ts
|
|
12523
12701
|
init_spec_generator();
|
|
12524
|
-
import * as path29 from "path";
|
|
12525
|
-
import * as fs30 from "fs-extra";
|
|
12526
12702
|
import chalk30 from "chalk";
|
|
12527
|
-
import { input as
|
|
12703
|
+
import { input as input4, select as select9, confirm as confirm3 } from "@inquirer/prompts";
|
|
12528
12704
|
function registerModel(program2) {
|
|
12529
12705
|
program2.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
|
|
12530
|
-
const currentDir = process.cwd();
|
|
12531
|
-
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12532
12706
|
if (opts.list) {
|
|
12533
12707
|
console.log(chalk30.bold("\nAvailable providers & models:\n"));
|
|
12534
12708
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
@@ -12545,8 +12719,9 @@ function registerModel(program2) {
|
|
|
12545
12719
|
}
|
|
12546
12720
|
return;
|
|
12547
12721
|
}
|
|
12548
|
-
const existing = await
|
|
12722
|
+
const existing = await loadGlobalConfig();
|
|
12549
12723
|
console.log(chalk30.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12724
|
+
console.log(chalk30.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
|
|
12550
12725
|
if (Object.keys(existing).length > 0) {
|
|
12551
12726
|
console.log(
|
|
12552
12727
|
chalk30.gray(
|
|
@@ -12555,7 +12730,7 @@ function registerModel(program2) {
|
|
|
12555
12730
|
);
|
|
12556
12731
|
}
|
|
12557
12732
|
console.log();
|
|
12558
|
-
const target = await
|
|
12733
|
+
const target = await select9({
|
|
12559
12734
|
message: "Configure model for:",
|
|
12560
12735
|
choices: [
|
|
12561
12736
|
{ name: "Spec generation (used for spec writing & refinement)", value: "spec" },
|
|
@@ -12564,7 +12739,7 @@ function registerModel(program2) {
|
|
|
12564
12739
|
]
|
|
12565
12740
|
});
|
|
12566
12741
|
async function pickProviderAndModel(label) {
|
|
12567
|
-
const providerKey = await
|
|
12742
|
+
const providerKey = await select9({
|
|
12568
12743
|
message: `${label} \u2014 select provider:`,
|
|
12569
12744
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
12570
12745
|
name: `${meta2.displayName.padEnd(22)} ${chalk30.gray(meta2.description)}`,
|
|
@@ -12577,12 +12752,12 @@ function registerModel(program2) {
|
|
|
12577
12752
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
12578
12753
|
{ name: chalk30.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
12579
12754
|
];
|
|
12580
|
-
let chosenModel = await
|
|
12755
|
+
let chosenModel = await select9({
|
|
12581
12756
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
12582
12757
|
choices: modelChoices
|
|
12583
12758
|
});
|
|
12584
12759
|
if (chosenModel === "__custom__") {
|
|
12585
|
-
chosenModel = await
|
|
12760
|
+
chosenModel = await input4({
|
|
12586
12761
|
message: "Enter model name:",
|
|
12587
12762
|
validate: (v2) => v2.trim().length > 0 || "Model name cannot be empty"
|
|
12588
12763
|
});
|
|
@@ -12627,14 +12802,14 @@ function registerModel(program2) {
|
|
|
12627
12802
|
)
|
|
12628
12803
|
);
|
|
12629
12804
|
}
|
|
12630
|
-
const ok = await
|
|
12805
|
+
const ok = await confirm3({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
|
|
12631
12806
|
if (!ok) {
|
|
12632
12807
|
console.log(chalk30.gray(" Cancelled."));
|
|
12633
12808
|
return;
|
|
12634
12809
|
}
|
|
12635
|
-
await
|
|
12810
|
+
await saveGlobalConfig(updated);
|
|
12636
12811
|
console.log(chalk30.green(`
|
|
12637
|
-
\u2714 Saved to ${
|
|
12812
|
+
\u2714 Saved to ${GLOBAL_CONFIG_FILE}`));
|
|
12638
12813
|
const providerToCheck = updated.provider ?? "gemini";
|
|
12639
12814
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
12640
12815
|
if (envKey && !process.env[envKey]) {
|
|
@@ -12651,14 +12826,14 @@ function registerModel(program2) {
|
|
|
12651
12826
|
import * as path30 from "path";
|
|
12652
12827
|
import * as fs31 from "fs-extra";
|
|
12653
12828
|
import chalk31 from "chalk";
|
|
12654
|
-
import { input as
|
|
12829
|
+
import { input as input5, select as select10, confirm as confirm4 } from "@inquirer/prompts";
|
|
12655
12830
|
function registerWorkspace(program2) {
|
|
12656
12831
|
const workspaceCmd = program2.command("workspace").description("Manage multi-repo workspace configuration");
|
|
12657
12832
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
12658
12833
|
const currentDir = process.cwd();
|
|
12659
12834
|
const configPath = path30.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
12660
12835
|
if (await fs31.pathExists(configPath)) {
|
|
12661
|
-
const overwrite = await
|
|
12836
|
+
const overwrite = await confirm4({
|
|
12662
12837
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
12663
12838
|
default: false
|
|
12664
12839
|
});
|
|
@@ -12668,12 +12843,12 @@ function registerWorkspace(program2) {
|
|
|
12668
12843
|
}
|
|
12669
12844
|
}
|
|
12670
12845
|
console.log(chalk31.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12671
|
-
const workspaceName = await
|
|
12846
|
+
const workspaceName = await input5({
|
|
12672
12847
|
message: "Workspace name:",
|
|
12673
12848
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
12674
12849
|
});
|
|
12675
12850
|
const repos = [];
|
|
12676
|
-
const useAutoScan = await
|
|
12851
|
+
const useAutoScan = await confirm4({
|
|
12677
12852
|
message: "Auto-scan sibling directories for repos?",
|
|
12678
12853
|
default: true
|
|
12679
12854
|
});
|
|
@@ -12687,7 +12862,7 @@ function registerWorkspace(program2) {
|
|
|
12687
12862
|
for (const r of detected) {
|
|
12688
12863
|
console.log(chalk31.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12689
12864
|
}
|
|
12690
|
-
const keepAll = await
|
|
12865
|
+
const keepAll = await confirm4({
|
|
12691
12866
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
12692
12867
|
default: true
|
|
12693
12868
|
});
|
|
@@ -12695,7 +12870,7 @@ function registerWorkspace(program2) {
|
|
|
12695
12870
|
repos.push(...detected);
|
|
12696
12871
|
} else {
|
|
12697
12872
|
for (const r of detected) {
|
|
12698
|
-
const keep = await
|
|
12873
|
+
const keep = await confirm4({
|
|
12699
12874
|
message: `Include "${r.name}" (${r.role}, ${r.type})?`,
|
|
12700
12875
|
default: true
|
|
12701
12876
|
});
|
|
@@ -12719,14 +12894,14 @@ function registerWorkspace(program2) {
|
|
|
12719
12894
|
{ name: "react-native (React Native mobile)", value: "react-native" },
|
|
12720
12895
|
{ name: "unknown", value: "unknown" }
|
|
12721
12896
|
];
|
|
12722
|
-
let addMore = await
|
|
12897
|
+
let addMore = await confirm4({
|
|
12723
12898
|
message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
|
|
12724
12899
|
default: repos.length === 0
|
|
12725
12900
|
});
|
|
12726
12901
|
while (addMore) {
|
|
12727
12902
|
console.log(chalk31.cyan(`
|
|
12728
12903
|
Adding repo #${repos.length + 1}`));
|
|
12729
|
-
const repoName = await
|
|
12904
|
+
const repoName = await input5({
|
|
12730
12905
|
message: "Repo name (e.g. api, web, app):",
|
|
12731
12906
|
validate: (v2) => {
|
|
12732
12907
|
if (!v2.trim()) return "Name cannot be empty";
|
|
@@ -12734,7 +12909,7 @@ function registerWorkspace(program2) {
|
|
|
12734
12909
|
return true;
|
|
12735
12910
|
}
|
|
12736
12911
|
});
|
|
12737
|
-
const repoPath = await
|
|
12912
|
+
const repoPath = await input5({
|
|
12738
12913
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
12739
12914
|
default: `./${repoName}`
|
|
12740
12915
|
});
|
|
@@ -12749,12 +12924,12 @@ function registerWorkspace(program2) {
|
|
|
12749
12924
|
} else {
|
|
12750
12925
|
console.log(chalk31.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
12751
12926
|
}
|
|
12752
|
-
const repoType = await
|
|
12927
|
+
const repoType = await select10({
|
|
12753
12928
|
message: `Repo type for "${repoName}":`,
|
|
12754
12929
|
choices: repoTypeChoices,
|
|
12755
12930
|
default: detectedType
|
|
12756
12931
|
});
|
|
12757
|
-
const repoRole = await
|
|
12932
|
+
const repoRole = await select10({
|
|
12758
12933
|
message: `Repo role for "${repoName}":`,
|
|
12759
12934
|
choices: [
|
|
12760
12935
|
{ name: "backend", value: "backend" },
|
|
@@ -12771,7 +12946,7 @@ function registerWorkspace(program2) {
|
|
|
12771
12946
|
role: repoRole
|
|
12772
12947
|
});
|
|
12773
12948
|
console.log(chalk31.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
12774
|
-
addMore = await
|
|
12949
|
+
addMore = await confirm4({
|
|
12775
12950
|
message: "Add another repo?",
|
|
12776
12951
|
default: false
|
|
12777
12952
|
});
|
|
@@ -12782,7 +12957,7 @@ function registerWorkspace(program2) {
|
|
|
12782
12957
|
for (const r of repos) {
|
|
12783
12958
|
console.log(chalk31.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12784
12959
|
}
|
|
12785
|
-
const ok = await
|
|
12960
|
+
const ok = await confirm4({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
12786
12961
|
if (!ok) {
|
|
12787
12962
|
console.log(chalk31.gray(" Cancelled."));
|
|
12788
12963
|
return;
|
|
@@ -12827,7 +13002,7 @@ init_spec_generator();
|
|
|
12827
13002
|
import * as path32 from "path";
|
|
12828
13003
|
import * as fs33 from "fs-extra";
|
|
12829
13004
|
import chalk33 from "chalk";
|
|
12830
|
-
import { input as
|
|
13005
|
+
import { input as input6 } from "@inquirer/prompts";
|
|
12831
13006
|
|
|
12832
13007
|
// core/spec-updater.ts
|
|
12833
13008
|
import chalk32 from "chalk";
|
|
@@ -13061,7 +13236,7 @@ function registerUpdate(program2) {
|
|
|
13061
13236
|
const currentDir = process.cwd();
|
|
13062
13237
|
const config2 = await loadConfig(currentDir);
|
|
13063
13238
|
if (!change) {
|
|
13064
|
-
change = await
|
|
13239
|
+
change = await input6({
|
|
13065
13240
|
message: "Describe the change you want to make:",
|
|
13066
13241
|
validate: (v2) => v2.trim().length > 0 || "Change description cannot be empty"
|
|
13067
13242
|
});
|
|
@@ -13379,7 +13554,7 @@ function buildYamlDoc(obj) {
|
|
|
13379
13554
|
return `${k2}: ${valStr}`;
|
|
13380
13555
|
}).join("\n") + "\n";
|
|
13381
13556
|
}
|
|
13382
|
-
function dslToOpenApi(dsl, serverUrl =
|
|
13557
|
+
function dslToOpenApi(dsl, serverUrl = DEFAULT_OPENAPI_SERVER_URL) {
|
|
13383
13558
|
const info = {
|
|
13384
13559
|
title: dsl.feature.title,
|
|
13385
13560
|
description: dsl.feature.description,
|
|
@@ -13426,7 +13601,7 @@ function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
|
|
|
13426
13601
|
}
|
|
13427
13602
|
async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
13428
13603
|
const format = opts.format ?? "yaml";
|
|
13429
|
-
const serverUrl = opts.serverUrl ??
|
|
13604
|
+
const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
|
|
13430
13605
|
const defaultName = `openapi.${format}`;
|
|
13431
13606
|
const outputPath = opts.outputPath ? path33.isAbsolute(opts.outputPath) ? opts.outputPath : path33.join(projectDir, opts.outputPath) : path33.join(projectDir, defaultName);
|
|
13432
13607
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
@@ -13622,12 +13797,12 @@ function registerMock(program2) {
|
|
|
13622
13797
|
|
|
13623
13798
|
// cli/commands/learn.ts
|
|
13624
13799
|
import chalk36 from "chalk";
|
|
13625
|
-
import { input as
|
|
13800
|
+
import { input as input7 } from "@inquirer/prompts";
|
|
13626
13801
|
function registerLearn(program2) {
|
|
13627
13802
|
program2.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
13628
13803
|
const currentDir = process.cwd();
|
|
13629
13804
|
if (!lesson) {
|
|
13630
|
-
lesson = await
|
|
13805
|
+
lesson = await input7({
|
|
13631
13806
|
message: "What lesson or engineering decision should be recorded?",
|
|
13632
13807
|
validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
|
|
13633
13808
|
});
|
|
@@ -13669,9 +13844,8 @@ import chalk39 from "chalk";
|
|
|
13669
13844
|
import * as fs37 from "fs-extra";
|
|
13670
13845
|
import * as path36 from "path";
|
|
13671
13846
|
import chalk38 from "chalk";
|
|
13672
|
-
var LOG_DIR2 = ".ai-spec-logs";
|
|
13673
13847
|
async function loadRunLogs(workingDir) {
|
|
13674
|
-
const logDir = path36.join(workingDir,
|
|
13848
|
+
const logDir = path36.join(workingDir, DEFAULT_LOG_DIR);
|
|
13675
13849
|
if (!await fs37.pathExists(logDir)) return [];
|
|
13676
13850
|
const files = await fs37.readdir(logDir);
|
|
13677
13851
|
const jsonFiles = new Set(files.filter((f) => f.endsWith(".json")));
|
|
@@ -13816,7 +13990,7 @@ function printTrendReport(report, workingDir) {
|
|
|
13816
13990
|
` ${chalk38.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
|
|
13817
13991
|
);
|
|
13818
13992
|
}
|
|
13819
|
-
const logRelDir = path36.relative(workingDir, path36.join(workingDir,
|
|
13993
|
+
const logRelDir = path36.relative(workingDir, path36.join(workingDir, DEFAULT_LOG_DIR));
|
|
13820
13994
|
console.log(chalk38.gray(`
|
|
13821
13995
|
${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
|
|
13822
13996
|
console.log(chalk38.cyan("\u2500".repeat(63)));
|
|
@@ -14475,7 +14649,233 @@ function registerVcr(program2) {
|
|
|
14475
14649
|
|
|
14476
14650
|
// cli/commands/scan.ts
|
|
14477
14651
|
import chalk44 from "chalk";
|
|
14652
|
+
import * as path42 from "path";
|
|
14653
|
+
|
|
14654
|
+
// core/project-index.ts
|
|
14655
|
+
import * as fs42 from "fs-extra";
|
|
14478
14656
|
import * as path41 from "path";
|
|
14657
|
+
var INDEX_FILE = ".ai-spec-index.json";
|
|
14658
|
+
var KEY_DEPS = [
|
|
14659
|
+
// Frameworks
|
|
14660
|
+
"express",
|
|
14661
|
+
"fastify",
|
|
14662
|
+
"koa",
|
|
14663
|
+
"@nestjs/core",
|
|
14664
|
+
"hapi",
|
|
14665
|
+
"next",
|
|
14666
|
+
"react",
|
|
14667
|
+
"vue",
|
|
14668
|
+
"nuxt",
|
|
14669
|
+
"svelte",
|
|
14670
|
+
"react-native",
|
|
14671
|
+
"expo",
|
|
14672
|
+
// DB / ORM
|
|
14673
|
+
"prisma",
|
|
14674
|
+
"@prisma/client",
|
|
14675
|
+
"mongoose",
|
|
14676
|
+
"typeorm",
|
|
14677
|
+
"sequelize",
|
|
14678
|
+
"drizzle-orm",
|
|
14679
|
+
// Auth
|
|
14680
|
+
"jsonwebtoken",
|
|
14681
|
+
"passport",
|
|
14682
|
+
"next-auth",
|
|
14683
|
+
"@clerk/nextjs",
|
|
14684
|
+
// Build / Lang
|
|
14685
|
+
"typescript",
|
|
14686
|
+
"vite",
|
|
14687
|
+
"webpack",
|
|
14688
|
+
"esbuild",
|
|
14689
|
+
"turbo",
|
|
14690
|
+
// Testing
|
|
14691
|
+
"jest",
|
|
14692
|
+
"vitest",
|
|
14693
|
+
"mocha",
|
|
14694
|
+
"cypress",
|
|
14695
|
+
"playwright",
|
|
14696
|
+
// Infra
|
|
14697
|
+
"redis",
|
|
14698
|
+
"bull",
|
|
14699
|
+
"socket.io",
|
|
14700
|
+
"graphql",
|
|
14701
|
+
"@trpc/server"
|
|
14702
|
+
];
|
|
14703
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14704
|
+
"node_modules",
|
|
14705
|
+
".git",
|
|
14706
|
+
".svn",
|
|
14707
|
+
"dist",
|
|
14708
|
+
"build",
|
|
14709
|
+
"out",
|
|
14710
|
+
".next",
|
|
14711
|
+
".nuxt",
|
|
14712
|
+
"coverage",
|
|
14713
|
+
".turbo",
|
|
14714
|
+
".cache",
|
|
14715
|
+
"__pycache__",
|
|
14716
|
+
"vendor",
|
|
14717
|
+
".ai-spec-vcr",
|
|
14718
|
+
".ai-spec-logs",
|
|
14719
|
+
"specs"
|
|
14720
|
+
]);
|
|
14721
|
+
var MANIFEST_FILES = [
|
|
14722
|
+
"package.json",
|
|
14723
|
+
"go.mod",
|
|
14724
|
+
"Cargo.toml",
|
|
14725
|
+
"pom.xml",
|
|
14726
|
+
"build.gradle",
|
|
14727
|
+
"build.gradle.kts",
|
|
14728
|
+
"requirements.txt",
|
|
14729
|
+
"pyproject.toml",
|
|
14730
|
+
"setup.py",
|
|
14731
|
+
"composer.json"
|
|
14732
|
+
];
|
|
14733
|
+
async function isProjectRoot(absPath) {
|
|
14734
|
+
for (const manifest of MANIFEST_FILES) {
|
|
14735
|
+
if (await fs42.pathExists(path41.join(absPath, manifest))) return true;
|
|
14736
|
+
}
|
|
14737
|
+
return false;
|
|
14738
|
+
}
|
|
14739
|
+
async function extractTechStack(absPath, type) {
|
|
14740
|
+
const stack = [];
|
|
14741
|
+
if (type === "go") stack.push("go");
|
|
14742
|
+
if (type === "rust") stack.push("rust");
|
|
14743
|
+
if (type === "java") stack.push("java");
|
|
14744
|
+
if (type === "python") stack.push("python");
|
|
14745
|
+
if (type === "php") stack.push("php");
|
|
14746
|
+
const pkgPath = path41.join(absPath, "package.json");
|
|
14747
|
+
if (!await fs42.pathExists(pkgPath)) return stack;
|
|
14748
|
+
let pkg = {};
|
|
14749
|
+
try {
|
|
14750
|
+
pkg = await fs42.readJson(pkgPath);
|
|
14751
|
+
} catch {
|
|
14752
|
+
return stack;
|
|
14753
|
+
}
|
|
14754
|
+
const allDeps = {
|
|
14755
|
+
...pkg.dependencies ?? {},
|
|
14756
|
+
...pkg.devDependencies ?? {}
|
|
14757
|
+
};
|
|
14758
|
+
const depKeys = new Set(Object.keys(allDeps));
|
|
14759
|
+
for (const dep of KEY_DEPS) {
|
|
14760
|
+
if (depKeys.has(dep)) stack.push(dep);
|
|
14761
|
+
}
|
|
14762
|
+
return stack;
|
|
14763
|
+
}
|
|
14764
|
+
async function discoverProjects(rootDir, maxDepth) {
|
|
14765
|
+
const found = [];
|
|
14766
|
+
async function walk(absDir, depth) {
|
|
14767
|
+
if (depth > maxDepth) return;
|
|
14768
|
+
let entries;
|
|
14769
|
+
try {
|
|
14770
|
+
entries = await fs42.readdir(absDir, { withFileTypes: true });
|
|
14771
|
+
} catch {
|
|
14772
|
+
return;
|
|
14773
|
+
}
|
|
14774
|
+
for (const entry of entries) {
|
|
14775
|
+
if (!entry.isDirectory()) continue;
|
|
14776
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
14777
|
+
const childAbs = path41.join(absDir, entry.name);
|
|
14778
|
+
const gitPath = path41.join(childAbs, ".git");
|
|
14779
|
+
if (await fs42.pathExists(gitPath)) {
|
|
14780
|
+
const gitStat = await fs42.stat(gitPath);
|
|
14781
|
+
if (gitStat.isFile()) continue;
|
|
14782
|
+
}
|
|
14783
|
+
if (await isProjectRoot(childAbs)) {
|
|
14784
|
+
found.push(path41.relative(rootDir, childAbs));
|
|
14785
|
+
} else {
|
|
14786
|
+
await walk(childAbs, depth + 1);
|
|
14787
|
+
}
|
|
14788
|
+
}
|
|
14789
|
+
}
|
|
14790
|
+
await walk(rootDir, 0);
|
|
14791
|
+
return found;
|
|
14792
|
+
}
|
|
14793
|
+
async function loadIndex(scanRoot) {
|
|
14794
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14795
|
+
try {
|
|
14796
|
+
return await fs42.readJson(filePath);
|
|
14797
|
+
} catch {
|
|
14798
|
+
return null;
|
|
14799
|
+
}
|
|
14800
|
+
}
|
|
14801
|
+
async function saveIndex(scanRoot, index) {
|
|
14802
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14803
|
+
await fs42.writeJson(filePath, index, { spaces: 2 });
|
|
14804
|
+
return filePath;
|
|
14805
|
+
}
|
|
14806
|
+
async function runScan(scanRoot, maxDepth = 2) {
|
|
14807
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14808
|
+
const existing = await loadIndex(scanRoot);
|
|
14809
|
+
const existingMap = new Map(
|
|
14810
|
+
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
14811
|
+
);
|
|
14812
|
+
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
14813
|
+
const added = [];
|
|
14814
|
+
const updated = [];
|
|
14815
|
+
const unchanged = [];
|
|
14816
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
14817
|
+
for (const relPath of discoveredPaths) {
|
|
14818
|
+
const absPath = path41.join(scanRoot, relPath);
|
|
14819
|
+
seenPaths.add(relPath);
|
|
14820
|
+
const { type, role } = await detectRepoType(absPath);
|
|
14821
|
+
const techStack = await extractTechStack(absPath, type);
|
|
14822
|
+
const hasConstitution = await fs42.pathExists(path41.join(absPath, CONSTITUTION_FILE));
|
|
14823
|
+
const hasWorkspace = await fs42.pathExists(path41.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
14824
|
+
const name = path41.basename(relPath);
|
|
14825
|
+
const prev = existingMap.get(relPath);
|
|
14826
|
+
if (!prev) {
|
|
14827
|
+
const entry = {
|
|
14828
|
+
name,
|
|
14829
|
+
path: relPath,
|
|
14830
|
+
type,
|
|
14831
|
+
role,
|
|
14832
|
+
techStack,
|
|
14833
|
+
hasConstitution,
|
|
14834
|
+
hasWorkspace,
|
|
14835
|
+
firstSeen: now,
|
|
14836
|
+
lastSeen: now
|
|
14837
|
+
};
|
|
14838
|
+
added.push(entry);
|
|
14839
|
+
existingMap.set(relPath, entry);
|
|
14840
|
+
} else {
|
|
14841
|
+
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
14842
|
+
const entry = {
|
|
14843
|
+
...prev,
|
|
14844
|
+
type,
|
|
14845
|
+
role,
|
|
14846
|
+
techStack,
|
|
14847
|
+
hasConstitution,
|
|
14848
|
+
hasWorkspace,
|
|
14849
|
+
lastSeen: now,
|
|
14850
|
+
missing: void 0
|
|
14851
|
+
// clear missing flag if it came back
|
|
14852
|
+
};
|
|
14853
|
+
existingMap.set(relPath, entry);
|
|
14854
|
+
if (changed) {
|
|
14855
|
+
updated.push(entry);
|
|
14856
|
+
} else {
|
|
14857
|
+
unchanged.push(entry);
|
|
14858
|
+
}
|
|
14859
|
+
}
|
|
14860
|
+
}
|
|
14861
|
+
const nowMissing = [];
|
|
14862
|
+
for (const [relPath, entry] of existingMap) {
|
|
14863
|
+
if (!seenPaths.has(relPath) && !entry.missing) {
|
|
14864
|
+
const gone = { ...entry, missing: true };
|
|
14865
|
+
existingMap.set(relPath, gone);
|
|
14866
|
+
nowMissing.push(gone);
|
|
14867
|
+
}
|
|
14868
|
+
}
|
|
14869
|
+
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
14870
|
+
const index = {
|
|
14871
|
+
scanRoot,
|
|
14872
|
+
lastScanned: now,
|
|
14873
|
+
projects
|
|
14874
|
+
};
|
|
14875
|
+
return { index, added, updated, unchanged, nowMissing };
|
|
14876
|
+
}
|
|
14877
|
+
|
|
14878
|
+
// cli/commands/scan.ts
|
|
14479
14879
|
var ROLE_COLOR = {
|
|
14480
14880
|
backend: chalk44.blue,
|
|
14481
14881
|
frontend: chalk44.green,
|
|
@@ -14549,7 +14949,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14549
14949
|
}
|
|
14550
14950
|
console.log(chalk44.cyan("\n\u2500".repeat(52)));
|
|
14551
14951
|
console.log(chalk44.gray(" \xA7C = has constitution W = workspace root"));
|
|
14552
|
-
console.log(chalk44.gray(` Index saved : ${
|
|
14952
|
+
console.log(chalk44.gray(` Index saved : ${path42.relative(cwd, path42.join(cwd, INDEX_FILE))}`));
|
|
14553
14953
|
console.log(chalk44.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
|
|
14554
14954
|
});
|
|
14555
14955
|
}
|
|
@@ -14557,7 +14957,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14557
14957
|
// cli/index.ts
|
|
14558
14958
|
dotenv.config();
|
|
14559
14959
|
var program = new Command();
|
|
14560
|
-
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(
|
|
14960
|
+
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(require_package().version);
|
|
14561
14961
|
registerCreate(program);
|
|
14562
14962
|
registerReview(program);
|
|
14563
14963
|
registerInit(program);
|