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.js
CHANGED
|
@@ -9,6 +9,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
9
9
|
var __esm = (fn, res) => function __init() {
|
|
10
10
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
11
|
};
|
|
12
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
13
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
14
|
+
};
|
|
12
15
|
var __export = (target, all) => {
|
|
13
16
|
for (var name in all)
|
|
14
17
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -391,14 +394,13 @@ function createProvider(providerName, apiKey, modelName) {
|
|
|
391
394
|
);
|
|
392
395
|
}
|
|
393
396
|
}
|
|
394
|
-
var import_generative_ai, import_sdk, import_openai,
|
|
397
|
+
var import_generative_ai, import_sdk, import_openai, import_undici, PROVIDER_CATALOG, SUPPORTED_PROVIDERS, DEFAULT_MODELS, ENV_KEY_MAP, GeminiProvider, ClaudeProvider, OpenAICompatibleProvider, MiMoProvider, SpecGenerator;
|
|
395
398
|
var init_spec_generator = __esm({
|
|
396
399
|
"core/spec-generator.ts"() {
|
|
397
400
|
"use strict";
|
|
398
401
|
import_generative_ai = require("@google/generative-ai");
|
|
399
402
|
import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
400
403
|
import_openai = __toESM(require("openai"));
|
|
401
|
-
import_axios = __toESM(require("axios"));
|
|
402
404
|
import_undici = require("undici");
|
|
403
405
|
init_spec_prompt();
|
|
404
406
|
init_provider_utils();
|
|
@@ -408,7 +410,9 @@ var init_spec_generator = __esm({
|
|
|
408
410
|
displayName: "MiMo (Xiaomi)",
|
|
409
411
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
410
412
|
models: ["mimo-v2-pro"],
|
|
411
|
-
envKey: "MIMO_API_KEY"
|
|
413
|
+
envKey: "MIMO_API_KEY",
|
|
414
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
415
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
412
416
|
// baseURL not used — MiMo has a dedicated provider class
|
|
413
417
|
},
|
|
414
418
|
gemini: {
|
|
@@ -577,8 +581,8 @@ var init_spec_generator = __esm({
|
|
|
577
581
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
578
582
|
messages: [{ role: "user", content: prompt }]
|
|
579
583
|
});
|
|
580
|
-
const
|
|
581
|
-
if (
|
|
584
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
585
|
+
if (textBlock) return textBlock.text;
|
|
582
586
|
throw new Error("Unexpected response type from Claude API");
|
|
583
587
|
},
|
|
584
588
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -623,43 +627,29 @@ var init_spec_generator = __esm({
|
|
|
623
627
|
}
|
|
624
628
|
};
|
|
625
629
|
MiMoProvider = class {
|
|
630
|
+
client;
|
|
626
631
|
providerName = "mimo";
|
|
627
632
|
modelName;
|
|
628
|
-
apiKey;
|
|
629
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
630
633
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
631
|
-
|
|
634
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
635
|
+
this.client = new import_sdk.default({ apiKey, baseURL });
|
|
632
636
|
this.modelName = modelName;
|
|
633
637
|
}
|
|
634
638
|
async generate(prompt, systemInstruction) {
|
|
635
639
|
return withReliability(
|
|
636
640
|
async () => {
|
|
637
|
-
const
|
|
641
|
+
const stream = this.client.messages.stream({
|
|
638
642
|
model: this.modelName,
|
|
639
|
-
max_tokens:
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
stream: false,
|
|
643
|
-
temperature: 1,
|
|
644
|
-
stop_sequences: null
|
|
645
|
-
};
|
|
646
|
-
if (systemInstruction) {
|
|
647
|
-
body.system = systemInstruction;
|
|
648
|
-
}
|
|
649
|
-
const response = await import_axios.default.post(this.baseUrl, body, {
|
|
650
|
-
headers: {
|
|
651
|
-
"api-key": this.apiKey,
|
|
652
|
-
"Content-Type": "application/json"
|
|
653
|
-
}
|
|
643
|
+
max_tokens: 65536,
|
|
644
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
645
|
+
messages: [{ role: "user", content: prompt }]
|
|
654
646
|
});
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
647
|
+
const message = await stream.finalMessage();
|
|
648
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
649
|
+
if (textBlock) return textBlock.text;
|
|
650
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
651
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
652
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
663
653
|
},
|
|
664
654
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
665
655
|
);
|
|
@@ -720,11 +710,68 @@ ${context.schema.slice(0, 3e3)}`);
|
|
|
720
710
|
}
|
|
721
711
|
});
|
|
722
712
|
|
|
713
|
+
// package.json
|
|
714
|
+
var require_package = __commonJS({
|
|
715
|
+
"package.json"(exports2, module2) {
|
|
716
|
+
module2.exports = {
|
|
717
|
+
name: "ai-spec-dev",
|
|
718
|
+
version: "0.46.0",
|
|
719
|
+
description: "AI-driven Development Orchestrator SDK & CLI",
|
|
720
|
+
main: "dist/index.js",
|
|
721
|
+
types: "dist/index.d.ts",
|
|
722
|
+
bin: {
|
|
723
|
+
"ai-spec": "dist/cli/index.js"
|
|
724
|
+
},
|
|
725
|
+
scripts: {
|
|
726
|
+
build: "tsup",
|
|
727
|
+
dev: "tsup --watch",
|
|
728
|
+
test: "vitest run",
|
|
729
|
+
"test:watch": "vitest"
|
|
730
|
+
},
|
|
731
|
+
keywords: [
|
|
732
|
+
"ai",
|
|
733
|
+
"spec",
|
|
734
|
+
"codegen",
|
|
735
|
+
"gemini",
|
|
736
|
+
"claude"
|
|
737
|
+
],
|
|
738
|
+
author: "",
|
|
739
|
+
license: "MIT",
|
|
740
|
+
dependencies: {
|
|
741
|
+
"@anthropic-ai/sdk": "^0.38.0",
|
|
742
|
+
"@google/generative-ai": "^0.21.0",
|
|
743
|
+
"@inquirer/editor": "^5.0.10",
|
|
744
|
+
"@inquirer/prompts": "^8.3.2",
|
|
745
|
+
"@rollup/rollup-darwin-arm64": "^4.60.1",
|
|
746
|
+
axios: "^1.13.6",
|
|
747
|
+
chalk: "^4.1.2",
|
|
748
|
+
commander: "^13.1.0",
|
|
749
|
+
dotenv: "^16.4.7",
|
|
750
|
+
"fs-extra": "^11.3.0",
|
|
751
|
+
openai: "^6.31.0",
|
|
752
|
+
undici: "^7.24.4"
|
|
753
|
+
},
|
|
754
|
+
devDependencies: {
|
|
755
|
+
"@types/fs-extra": "^11.0.4",
|
|
756
|
+
"@types/glob": "^8.1.0",
|
|
757
|
+
"@types/node": "^22.13.5",
|
|
758
|
+
glob: "^13.0.6",
|
|
759
|
+
"ts-node": "^10.9.2",
|
|
760
|
+
tsup: "^8.4.0",
|
|
761
|
+
typescript: "^5.7.3",
|
|
762
|
+
vitest: "^2.1.0"
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
723
768
|
// cli/index.ts
|
|
724
769
|
var import_commander = require("commander");
|
|
725
770
|
var dotenv = __toESM(require("dotenv"));
|
|
726
771
|
|
|
727
772
|
// cli/commands/create.ts
|
|
773
|
+
var path26 = __toESM(require("path"));
|
|
774
|
+
var fs27 = __toESM(require("fs-extra"));
|
|
728
775
|
var import_chalk26 = __toESM(require("chalk"));
|
|
729
776
|
var import_prompts7 = require("@inquirer/prompts");
|
|
730
777
|
init_spec_generator();
|
|
@@ -827,8 +874,8 @@ var WorkspaceLoader = class {
|
|
|
827
874
|
const repos = [];
|
|
828
875
|
for (const entry of entries) {
|
|
829
876
|
const absPath = path.join(this.workspaceRoot, entry);
|
|
830
|
-
const
|
|
831
|
-
if (!
|
|
877
|
+
const stat5 = await fs.stat(absPath).catch(() => null);
|
|
878
|
+
if (!stat5 || !stat5.isDirectory()) continue;
|
|
832
879
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
833
880
|
if (names && !names.includes(entry)) continue;
|
|
834
881
|
const hasManifest = await fs.pathExists(path.join(absPath, "package.json")) || await fs.pathExists(path.join(absPath, "go.mod")) || await fs.pathExists(path.join(absPath, "Cargo.toml")) || await fs.pathExists(path.join(absPath, "pom.xml")) || await fs.pathExists(path.join(absPath, "build.gradle")) || await fs.pathExists(path.join(absPath, "requirements.txt")) || await fs.pathExists(path.join(absPath, "pyproject.toml")) || await fs.pathExists(path.join(absPath, "composer.json"));
|
|
@@ -891,6 +938,7 @@ var WorkspaceLoader = class {
|
|
|
891
938
|
// cli/utils.ts
|
|
892
939
|
var path3 = __toESM(require("path"));
|
|
893
940
|
var fs3 = __toESM(require("fs-extra"));
|
|
941
|
+
var os2 = __toESM(require("os"));
|
|
894
942
|
var import_chalk2 = __toESM(require("chalk"));
|
|
895
943
|
var import_prompts = require("@inquirer/prompts");
|
|
896
944
|
init_spec_generator();
|
|
@@ -937,17 +985,42 @@ async function clearKey(provider) {
|
|
|
937
985
|
|
|
938
986
|
// cli/utils.ts
|
|
939
987
|
var CONFIG_FILE = ".ai-spec.json";
|
|
988
|
+
var GLOBAL_CONFIG_FILE = path3.join(os2.homedir(), ".ai-spec-config.json");
|
|
989
|
+
async function loadGlobalConfig() {
|
|
990
|
+
try {
|
|
991
|
+
if (await fs3.pathExists(GLOBAL_CONFIG_FILE)) {
|
|
992
|
+
return await fs3.readJson(GLOBAL_CONFIG_FILE);
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
return {};
|
|
997
|
+
}
|
|
998
|
+
async function saveGlobalConfig(config2) {
|
|
999
|
+
await fs3.ensureFile(GLOBAL_CONFIG_FILE);
|
|
1000
|
+
await fs3.writeJson(GLOBAL_CONFIG_FILE, config2, { spaces: 2 });
|
|
1001
|
+
}
|
|
940
1002
|
async function loadConfig(dir) {
|
|
1003
|
+
const globalConfig = await loadGlobalConfig();
|
|
1004
|
+
let localConfig = {};
|
|
941
1005
|
const p = path3.join(dir, CONFIG_FILE);
|
|
942
1006
|
if (await fs3.pathExists(p)) {
|
|
943
|
-
|
|
1007
|
+
try {
|
|
1008
|
+
localConfig = await fs3.readJson(p);
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
944
1011
|
}
|
|
945
|
-
return {};
|
|
1012
|
+
return { ...globalConfig, ...localConfig };
|
|
946
1013
|
}
|
|
947
1014
|
async function resolveApiKey(providerName, cliKey) {
|
|
948
1015
|
if (cliKey) return cliKey;
|
|
949
1016
|
const envVar = ENV_KEY_MAP[providerName];
|
|
950
1017
|
if (envVar && process.env[envVar]) return process.env[envVar];
|
|
1018
|
+
const meta = PROVIDER_CATALOG[providerName];
|
|
1019
|
+
if (meta?.fallbackEnvKeys) {
|
|
1020
|
+
for (const key of meta.fallbackEnvKeys) {
|
|
1021
|
+
if (process.env[key]) return process.env[key];
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
951
1024
|
const savedKey = await getSavedKey(providerName);
|
|
952
1025
|
if (savedKey) {
|
|
953
1026
|
const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
|
|
@@ -1021,7 +1094,7 @@ var pe = "\0PERIOD" + Math.random() + "\0";
|
|
|
1021
1094
|
var is = new RegExp(fe, "g");
|
|
1022
1095
|
var rs = new RegExp(ue, "g");
|
|
1023
1096
|
var ns = new RegExp(qt, "g");
|
|
1024
|
-
var
|
|
1097
|
+
var os3 = new RegExp(de, "g");
|
|
1025
1098
|
var hs = new RegExp(pe, "g");
|
|
1026
1099
|
var as = /\\\\/g;
|
|
1027
1100
|
var ls = /\\{/g;
|
|
@@ -1036,7 +1109,7 @@ function ps(n7) {
|
|
|
1036
1109
|
return n7.replace(as, fe).replace(ls, ue).replace(cs, qt).replace(fs4, de).replace(us, pe);
|
|
1037
1110
|
}
|
|
1038
1111
|
function ms(n7) {
|
|
1039
|
-
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(
|
|
1112
|
+
return n7.replace(is, "\\").replace(rs, "{").replace(ns, "}").replace(os3, ",").replace(hs, ".");
|
|
1040
1113
|
}
|
|
1041
1114
|
function me(n7) {
|
|
1042
1115
|
if (!n7) return [""];
|
|
@@ -3966,11 +4039,11 @@ Ze.glob = Ze;
|
|
|
3966
4039
|
// core/global-constitution.ts
|
|
3967
4040
|
var fs5 = __toESM(require("fs-extra"));
|
|
3968
4041
|
var path4 = __toESM(require("path"));
|
|
3969
|
-
var
|
|
4042
|
+
var os4 = __toESM(require("os"));
|
|
3970
4043
|
var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
|
|
3971
4044
|
var SEARCH_ROOTS = [
|
|
3972
4045
|
// Workspace root is injected at runtime — see loadGlobalConstitution()
|
|
3973
|
-
|
|
4046
|
+
os4.homedir()
|
|
3974
4047
|
];
|
|
3975
4048
|
async function loadGlobalConstitution(extraRoots = []) {
|
|
3976
4049
|
const roots = [...extraRoots, ...SEARCH_ROOTS];
|
|
@@ -3999,7 +4072,7 @@ function mergeConstitutions(globalContent, projectContent) {
|
|
|
3999
4072
|
}
|
|
4000
4073
|
return parts.join("\n");
|
|
4001
4074
|
}
|
|
4002
|
-
async function saveGlobalConstitution(content, targetDir =
|
|
4075
|
+
async function saveGlobalConstitution(content, targetDir = os4.homedir()) {
|
|
4003
4076
|
const filePath = path4.join(targetDir, GLOBAL_CONSTITUTION_FILE);
|
|
4004
4077
|
await fs5.writeFile(filePath, content, "utf-8");
|
|
4005
4078
|
return filePath;
|
|
@@ -5032,32 +5105,32 @@ function validateDsl(raw) {
|
|
|
5032
5105
|
}
|
|
5033
5106
|
return { valid: true, dsl: raw };
|
|
5034
5107
|
}
|
|
5035
|
-
function validateFeature(raw,
|
|
5108
|
+
function validateFeature(raw, path43, errors) {
|
|
5036
5109
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5037
|
-
errors.push({ path:
|
|
5110
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5038
5111
|
return;
|
|
5039
5112
|
}
|
|
5040
5113
|
const f = raw;
|
|
5041
|
-
requireNonEmptyString(f["id"], `${
|
|
5042
|
-
requireNonEmptyString(f["title"], `${
|
|
5043
|
-
requireNonEmptyString(f["description"], `${
|
|
5114
|
+
requireNonEmptyString(f["id"], `${path43}.id`, errors);
|
|
5115
|
+
requireNonEmptyString(f["title"], `${path43}.title`, errors);
|
|
5116
|
+
requireNonEmptyString(f["description"], `${path43}.description`, errors);
|
|
5044
5117
|
}
|
|
5045
|
-
function validateModel(raw,
|
|
5118
|
+
function validateModel(raw, path43, errors) {
|
|
5046
5119
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5047
|
-
errors.push({ path:
|
|
5120
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5048
5121
|
return;
|
|
5049
5122
|
}
|
|
5050
5123
|
const m = raw;
|
|
5051
|
-
requireNonEmptyString(m["name"], `${
|
|
5124
|
+
requireNonEmptyString(m["name"], `${path43}.name`, errors);
|
|
5052
5125
|
if (!Array.isArray(m["fields"])) {
|
|
5053
|
-
errors.push({ path: `${
|
|
5126
|
+
errors.push({ path: `${path43}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
5054
5127
|
} else {
|
|
5055
5128
|
const fields = m["fields"];
|
|
5056
5129
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
5057
|
-
errors.push({ path: `${
|
|
5130
|
+
errors.push({ path: `${path43}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
5058
5131
|
}
|
|
5059
5132
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
5060
|
-
validateModelField(fields[j2], `${
|
|
5133
|
+
validateModelField(fields[j2], `${path43}.fields[${j2}]`, errors);
|
|
5061
5134
|
}
|
|
5062
5135
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
5063
5136
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -5066,7 +5139,7 @@ function validateModel(raw, path42, errors) {
|
|
|
5066
5139
|
const name = f["name"];
|
|
5067
5140
|
if (seenFieldNames.has(name)) {
|
|
5068
5141
|
errors.push({
|
|
5069
|
-
path: `${
|
|
5142
|
+
path: `${path43}.fields[${j2}].name`,
|
|
5070
5143
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
5071
5144
|
});
|
|
5072
5145
|
} else {
|
|
@@ -5077,184 +5150,184 @@ function validateModel(raw, path42, errors) {
|
|
|
5077
5150
|
}
|
|
5078
5151
|
if (m["relations"] !== void 0) {
|
|
5079
5152
|
if (!Array.isArray(m["relations"])) {
|
|
5080
|
-
errors.push({ path: `${
|
|
5153
|
+
errors.push({ path: `${path43}.relations`, message: "Must be an array of strings if present" });
|
|
5081
5154
|
} else {
|
|
5082
5155
|
const rels = m["relations"];
|
|
5083
5156
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
5084
5157
|
if (typeof rels[j2] !== "string") {
|
|
5085
|
-
errors.push({ path: `${
|
|
5158
|
+
errors.push({ path: `${path43}.relations[${j2}]`, message: "Must be a string" });
|
|
5086
5159
|
}
|
|
5087
5160
|
}
|
|
5088
5161
|
}
|
|
5089
5162
|
}
|
|
5090
5163
|
}
|
|
5091
|
-
function validateModelField(raw,
|
|
5164
|
+
function validateModelField(raw, path43, errors) {
|
|
5092
5165
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5093
|
-
errors.push({ path:
|
|
5166
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5094
5167
|
return;
|
|
5095
5168
|
}
|
|
5096
5169
|
const f = raw;
|
|
5097
|
-
requireNonEmptyString(f["name"], `${
|
|
5098
|
-
requireNonEmptyString(f["type"], `${
|
|
5170
|
+
requireNonEmptyString(f["name"], `${path43}.name`, errors);
|
|
5171
|
+
requireNonEmptyString(f["type"], `${path43}.type`, errors);
|
|
5099
5172
|
if (typeof f["required"] !== "boolean") {
|
|
5100
|
-
errors.push({ path: `${
|
|
5173
|
+
errors.push({ path: `${path43}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
5101
5174
|
}
|
|
5102
5175
|
}
|
|
5103
|
-
function validateEndpoint(raw,
|
|
5176
|
+
function validateEndpoint(raw, path43, errors) {
|
|
5104
5177
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5105
|
-
errors.push({ path:
|
|
5178
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5106
5179
|
return;
|
|
5107
5180
|
}
|
|
5108
5181
|
const e = raw;
|
|
5109
|
-
requireNonEmptyString(e["id"], `${
|
|
5110
|
-
requireNonEmptyString(e["description"], `${
|
|
5182
|
+
requireNonEmptyString(e["id"], `${path43}.id`, errors);
|
|
5183
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5111
5184
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
5112
5185
|
errors.push({
|
|
5113
|
-
path: `${
|
|
5186
|
+
path: `${path43}.method`,
|
|
5114
5187
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
5115
5188
|
});
|
|
5116
5189
|
}
|
|
5117
5190
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
5118
5191
|
errors.push({
|
|
5119
|
-
path: `${
|
|
5192
|
+
path: `${path43}.path`,
|
|
5120
5193
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
5121
5194
|
});
|
|
5122
5195
|
}
|
|
5123
5196
|
if (typeof e["auth"] !== "boolean") {
|
|
5124
|
-
errors.push({ path: `${
|
|
5197
|
+
errors.push({ path: `${path43}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
5125
5198
|
}
|
|
5126
5199
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
5127
5200
|
errors.push({
|
|
5128
|
-
path: `${
|
|
5201
|
+
path: `${path43}.successStatus`,
|
|
5129
5202
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
5130
5203
|
});
|
|
5131
5204
|
}
|
|
5132
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
5205
|
+
requireNonEmptyString(e["successDescription"], `${path43}.successDescription`, errors);
|
|
5133
5206
|
if (e["request"] !== void 0) {
|
|
5134
|
-
validateRequestSchema(e["request"], `${
|
|
5207
|
+
validateRequestSchema(e["request"], `${path43}.request`, errors);
|
|
5135
5208
|
}
|
|
5136
5209
|
if (e["errors"] !== void 0) {
|
|
5137
5210
|
if (!Array.isArray(e["errors"])) {
|
|
5138
|
-
errors.push({ path: `${
|
|
5211
|
+
errors.push({ path: `${path43}.errors`, message: "Must be an array if present" });
|
|
5139
5212
|
} else {
|
|
5140
5213
|
const errs = e["errors"];
|
|
5141
5214
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
5142
|
-
errors.push({ path: `${
|
|
5215
|
+
errors.push({ path: `${path43}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
5143
5216
|
}
|
|
5144
5217
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
5145
|
-
validateResponseError(errs[j2], `${
|
|
5218
|
+
validateResponseError(errs[j2], `${path43}.errors[${j2}]`, errors);
|
|
5146
5219
|
}
|
|
5147
5220
|
}
|
|
5148
5221
|
}
|
|
5149
5222
|
}
|
|
5150
|
-
function validateRequestSchema(raw,
|
|
5223
|
+
function validateRequestSchema(raw, path43, errors) {
|
|
5151
5224
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5152
|
-
errors.push({ path:
|
|
5225
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5153
5226
|
return;
|
|
5154
5227
|
}
|
|
5155
5228
|
const r = raw;
|
|
5156
5229
|
for (const key of ["body", "query", "params"]) {
|
|
5157
5230
|
if (r[key] !== void 0) {
|
|
5158
|
-
validateFieldMap(r[key], `${
|
|
5231
|
+
validateFieldMap(r[key], `${path43}.${key}`, errors);
|
|
5159
5232
|
}
|
|
5160
5233
|
}
|
|
5161
5234
|
}
|
|
5162
|
-
function validateFieldMap(raw,
|
|
5235
|
+
function validateFieldMap(raw, path43, errors) {
|
|
5163
5236
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5164
|
-
errors.push({ path:
|
|
5237
|
+
errors.push({ path: path43, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
5165
5238
|
return;
|
|
5166
5239
|
}
|
|
5167
5240
|
const map = raw;
|
|
5168
5241
|
for (const [k2, v2] of Object.entries(map)) {
|
|
5169
5242
|
if (typeof v2 !== "string") {
|
|
5170
|
-
errors.push({ path: `${
|
|
5243
|
+
errors.push({ path: `${path43}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
5171
5244
|
}
|
|
5172
5245
|
}
|
|
5173
5246
|
}
|
|
5174
|
-
function validateResponseError(raw,
|
|
5247
|
+
function validateResponseError(raw, path43, errors) {
|
|
5175
5248
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5176
|
-
errors.push({ path:
|
|
5249
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5177
5250
|
return;
|
|
5178
5251
|
}
|
|
5179
5252
|
const e = raw;
|
|
5180
5253
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5181
|
-
errors.push({ path: `${
|
|
5254
|
+
errors.push({ path: `${path43}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5182
5255
|
}
|
|
5183
|
-
requireNonEmptyString(e["code"], `${
|
|
5184
|
-
requireNonEmptyString(e["description"], `${
|
|
5256
|
+
requireNonEmptyString(e["code"], `${path43}.code`, errors);
|
|
5257
|
+
requireNonEmptyString(e["description"], `${path43}.description`, errors);
|
|
5185
5258
|
}
|
|
5186
|
-
function validateBehavior(raw,
|
|
5259
|
+
function validateBehavior(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 b = raw;
|
|
5192
|
-
requireNonEmptyString(b["id"], `${
|
|
5193
|
-
requireNonEmptyString(b["description"], `${
|
|
5265
|
+
requireNonEmptyString(b["id"], `${path43}.id`, errors);
|
|
5266
|
+
requireNonEmptyString(b["description"], `${path43}.description`, errors);
|
|
5194
5267
|
if (b["constraints"] !== void 0) {
|
|
5195
5268
|
if (!Array.isArray(b["constraints"])) {
|
|
5196
|
-
errors.push({ path: `${
|
|
5269
|
+
errors.push({ path: `${path43}.constraints`, message: "Must be an array of strings if present" });
|
|
5197
5270
|
} else {
|
|
5198
5271
|
const cs2 = b["constraints"];
|
|
5199
5272
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5200
5273
|
if (typeof cs2[j2] !== "string") {
|
|
5201
|
-
errors.push({ path: `${
|
|
5274
|
+
errors.push({ path: `${path43}.constraints[${j2}]`, message: "Must be a string" });
|
|
5202
5275
|
}
|
|
5203
5276
|
}
|
|
5204
5277
|
}
|
|
5205
5278
|
}
|
|
5206
5279
|
}
|
|
5207
|
-
function validateComponent(raw,
|
|
5280
|
+
function validateComponent(raw, path43, errors) {
|
|
5208
5281
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5209
|
-
errors.push({ path:
|
|
5282
|
+
errors.push({ path: path43, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5210
5283
|
return;
|
|
5211
5284
|
}
|
|
5212
5285
|
const c = raw;
|
|
5213
|
-
requireNonEmptyString(c["id"], `${
|
|
5214
|
-
requireNonEmptyString(c["name"], `${
|
|
5215
|
-
requireNonEmptyString(c["description"], `${
|
|
5286
|
+
requireNonEmptyString(c["id"], `${path43}.id`, errors);
|
|
5287
|
+
requireNonEmptyString(c["name"], `${path43}.name`, errors);
|
|
5288
|
+
requireNonEmptyString(c["description"], `${path43}.description`, errors);
|
|
5216
5289
|
if (c["props"] !== void 0) {
|
|
5217
5290
|
if (!Array.isArray(c["props"])) {
|
|
5218
|
-
errors.push({ path: `${
|
|
5291
|
+
errors.push({ path: `${path43}.props`, message: "Must be an array if present" });
|
|
5219
5292
|
} else {
|
|
5220
5293
|
const props = c["props"];
|
|
5221
5294
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5222
5295
|
const p = props[j2];
|
|
5223
5296
|
if (typeof p !== "object" || p === null) {
|
|
5224
|
-
errors.push({ path: `${
|
|
5297
|
+
errors.push({ path: `${path43}.props[${j2}]`, message: "Must be an object" });
|
|
5225
5298
|
continue;
|
|
5226
5299
|
}
|
|
5227
|
-
requireNonEmptyString(p["name"], `${
|
|
5228
|
-
requireNonEmptyString(p["type"], `${
|
|
5300
|
+
requireNonEmptyString(p["name"], `${path43}.props[${j2}].name`, errors);
|
|
5301
|
+
requireNonEmptyString(p["type"], `${path43}.props[${j2}].type`, errors);
|
|
5229
5302
|
if (typeof p["required"] !== "boolean") {
|
|
5230
|
-
errors.push({ path: `${
|
|
5303
|
+
errors.push({ path: `${path43}.props[${j2}].required`, message: "Must be boolean" });
|
|
5231
5304
|
}
|
|
5232
5305
|
}
|
|
5233
5306
|
}
|
|
5234
5307
|
}
|
|
5235
5308
|
if (c["events"] !== void 0) {
|
|
5236
5309
|
if (!Array.isArray(c["events"])) {
|
|
5237
|
-
errors.push({ path: `${
|
|
5310
|
+
errors.push({ path: `${path43}.events`, message: "Must be an array if present" });
|
|
5238
5311
|
} else {
|
|
5239
5312
|
const events = c["events"];
|
|
5240
5313
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5241
5314
|
const e = events[j2];
|
|
5242
5315
|
if (typeof e !== "object" || e === null) {
|
|
5243
|
-
errors.push({ path: `${
|
|
5316
|
+
errors.push({ path: `${path43}.events[${j2}]`, message: "Must be an object" });
|
|
5244
5317
|
continue;
|
|
5245
5318
|
}
|
|
5246
|
-
requireNonEmptyString(e["name"], `${
|
|
5319
|
+
requireNonEmptyString(e["name"], `${path43}.events[${j2}].name`, errors);
|
|
5247
5320
|
}
|
|
5248
5321
|
}
|
|
5249
5322
|
}
|
|
5250
5323
|
if (c["state"] !== void 0) {
|
|
5251
5324
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5252
|
-
errors.push({ path: `${
|
|
5325
|
+
errors.push({ path: `${path43}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5253
5326
|
}
|
|
5254
5327
|
}
|
|
5255
5328
|
if (c["apiCalls"] !== void 0) {
|
|
5256
5329
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5257
|
-
errors.push({ path: `${
|
|
5330
|
+
errors.push({ path: `${path43}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5258
5331
|
}
|
|
5259
5332
|
}
|
|
5260
5333
|
}
|
|
@@ -5316,10 +5389,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5316
5389
|
}
|
|
5317
5390
|
}
|
|
5318
5391
|
}
|
|
5319
|
-
function requireNonEmptyString(v2,
|
|
5392
|
+
function requireNonEmptyString(v2, path43, errors) {
|
|
5320
5393
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5321
5394
|
errors.push({
|
|
5322
|
-
path:
|
|
5395
|
+
path: path43,
|
|
5323
5396
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5324
5397
|
});
|
|
5325
5398
|
}
|
|
@@ -5530,13 +5603,18 @@ Output ONLY the corrected JSON. No explanation.`;
|
|
|
5530
5603
|
|
|
5531
5604
|
// core/token-budget.ts
|
|
5532
5605
|
var import_chalk5 = __toESM(require("chalk"));
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5606
|
+
|
|
5607
|
+
// core/config-defaults.ts
|
|
5608
|
+
var DEFAULT_LOG_DIR = ".ai-spec-logs";
|
|
5609
|
+
var DEFAULT_VCR_DIR = ".ai-spec-vcr";
|
|
5610
|
+
var DEFAULT_BACKUP_DIR = ".ai-spec-backup";
|
|
5611
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5612
|
+
var DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
|
|
5613
|
+
var DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 3e4;
|
|
5614
|
+
var DEFAULT_MAX_FIX_FILE_CHARS = 6e4;
|
|
5615
|
+
var DEFAULT_DSL_MAX_RETRIES = 2;
|
|
5616
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5617
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5540
5618
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5541
5619
|
gemini: 9e5,
|
|
5542
5620
|
claude: 18e4,
|
|
@@ -5544,8 +5622,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5544
5622
|
deepseek: 6e4,
|
|
5545
5623
|
default: 1e5
|
|
5546
5624
|
};
|
|
5625
|
+
|
|
5626
|
+
// core/token-budget.ts
|
|
5627
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5628
|
+
function estimateTokens(text) {
|
|
5629
|
+
if (!text) return 0;
|
|
5630
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5631
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5632
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5633
|
+
}
|
|
5634
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5547
5635
|
function getDefaultBudget(providerName) {
|
|
5548
|
-
return
|
|
5636
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5549
5637
|
}
|
|
5550
5638
|
|
|
5551
5639
|
// core/safe-json.ts
|
|
@@ -5616,7 +5704,7 @@ function sanitizeDsl(raw) {
|
|
|
5616
5704
|
}
|
|
5617
5705
|
return dsl;
|
|
5618
5706
|
}
|
|
5619
|
-
var MAX_RETRIES =
|
|
5707
|
+
var MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
|
|
5620
5708
|
var DEFAULT_MAX_SPEC_CHARS = 12e3;
|
|
5621
5709
|
function dslFilePath(specFilePath) {
|
|
5622
5710
|
const dir = path7.dirname(specFilePath);
|
|
@@ -6340,12 +6428,11 @@ Shared component structure patterns:`);
|
|
|
6340
6428
|
// core/run-snapshot.ts
|
|
6341
6429
|
var fs10 = __toESM(require("fs-extra"));
|
|
6342
6430
|
var path9 = __toESM(require("path"));
|
|
6343
|
-
var BACKUP_DIR = ".ai-spec-backup";
|
|
6344
6431
|
var RunSnapshot = class {
|
|
6345
6432
|
constructor(workingDir, runId) {
|
|
6346
6433
|
this.workingDir = workingDir;
|
|
6347
6434
|
this.runId = runId;
|
|
6348
|
-
this.backupRoot = path9.join(workingDir,
|
|
6435
|
+
this.backupRoot = path9.join(workingDir, DEFAULT_BACKUP_DIR, runId);
|
|
6349
6436
|
}
|
|
6350
6437
|
backupRoot;
|
|
6351
6438
|
snapshotted = /* @__PURE__ */ new Set();
|
|
@@ -6401,7 +6488,6 @@ function getActiveSnapshot() {
|
|
|
6401
6488
|
var fs11 = __toESM(require("fs-extra"));
|
|
6402
6489
|
var path10 = __toESM(require("path"));
|
|
6403
6490
|
var import_chalk7 = __toESM(require("chalk"));
|
|
6404
|
-
var LOG_DIR = ".ai-spec-logs";
|
|
6405
6491
|
function appendJsonlLine(filePath, record) {
|
|
6406
6492
|
try {
|
|
6407
6493
|
fs11.appendFileSync(filePath, JSON.stringify(record) + "\n");
|
|
@@ -6472,8 +6558,8 @@ var RunLogger = class {
|
|
|
6472
6558
|
this.workingDir = workingDir;
|
|
6473
6559
|
this.runId = runId;
|
|
6474
6560
|
this.startMs = Date.now();
|
|
6475
|
-
this.logPath = path10.join(workingDir,
|
|
6476
|
-
this.jsonlPath = path10.join(workingDir,
|
|
6561
|
+
this.logPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
|
|
6562
|
+
this.jsonlPath = path10.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
|
|
6477
6563
|
this.log = {
|
|
6478
6564
|
runId,
|
|
6479
6565
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7415,7 +7501,7 @@ ${context.routeSummary}
|
|
|
7415
7501
|
}
|
|
7416
7502
|
if (context.schema) {
|
|
7417
7503
|
parts.push(`=== Prisma Schema ===
|
|
7418
|
-
${context.schema.slice(0,
|
|
7504
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
7419
7505
|
`);
|
|
7420
7506
|
}
|
|
7421
7507
|
if (context.errorPatterns) {
|
|
@@ -7463,7 +7549,7 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
7463
7549
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
7464
7550
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
7465
7551
|
}
|
|
7466
|
-
var REVIEW_HISTORY_FILE =
|
|
7552
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
7467
7553
|
async function loadReviewHistory(projectRoot) {
|
|
7468
7554
|
const historyPath = path13.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
7469
7555
|
try {
|
|
@@ -7594,15 +7680,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
7594
7680
|
${codeContext}`;
|
|
7595
7681
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
7596
7682
|
console.log(import_chalk11.default.gray(" Pass 2/3: Implementation review..."));
|
|
7683
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
7597
7684
|
const history = await loadReviewHistory(this.projectRoot);
|
|
7598
7685
|
const historyContext = buildHistoryContext(history);
|
|
7599
7686
|
const implPrompt = `Review the implementation details of this change.
|
|
7600
7687
|
|
|
7601
|
-
=== Feature Spec ===
|
|
7602
|
-
${
|
|
7603
|
-
|
|
7604
|
-
=== Code ===
|
|
7605
|
-
${codeContext}
|
|
7688
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
7689
|
+
${specDigest}
|
|
7606
7690
|
|
|
7607
7691
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
7608
7692
|
${archReview}
|
|
@@ -7611,11 +7695,8 @@ ${historyContext}`;
|
|
|
7611
7695
|
console.log(import_chalk11.default.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
7612
7696
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
7613
7697
|
|
|
7614
|
-
=== Feature Spec ===
|
|
7615
|
-
${
|
|
7616
|
-
|
|
7617
|
-
=== Code ===
|
|
7618
|
-
${codeContext}
|
|
7698
|
+
=== Feature Spec (digest) ===
|
|
7699
|
+
${specDigest}
|
|
7619
7700
|
|
|
7620
7701
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
7621
7702
|
${archReview}
|
|
@@ -7689,8 +7770,8 @@ ${sep}
|
|
|
7689
7770
|
filesSection += `
|
|
7690
7771
|
|
|
7691
7772
|
=== ${filePath} ===
|
|
7692
|
-
${content.slice(0,
|
|
7693
|
-
if (content.length >
|
|
7773
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7774
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
7694
7775
|
... (truncated, ${content.length} chars total)`;
|
|
7695
7776
|
} catch {
|
|
7696
7777
|
filesSection += `
|
|
@@ -8295,8 +8376,8 @@ var import_child_process5 = require("child_process");
|
|
|
8295
8376
|
var fs18 = __toESM(require("fs-extra"));
|
|
8296
8377
|
var path17 = __toESM(require("path"));
|
|
8297
8378
|
init_cli_ui();
|
|
8298
|
-
var MAX_COMMAND_OUTPUT_CHARS =
|
|
8299
|
-
var MAX_FIX_FILE_CHARS =
|
|
8379
|
+
var MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
|
|
8380
|
+
var MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
|
|
8300
8381
|
function runCommand(cmd, cwd) {
|
|
8301
8382
|
try {
|
|
8302
8383
|
const output = (0, import_child_process5.execSync)(cmd, { cwd, encoding: "utf-8", timeout: 6e4 });
|
|
@@ -9505,8 +9586,8 @@ async function findLatestDslFile(projectDir) {
|
|
|
9505
9586
|
const entries = await fs22.readdir(dir);
|
|
9506
9587
|
for (const entry of entries) {
|
|
9507
9588
|
const abs = path21.join(dir, entry);
|
|
9508
|
-
const
|
|
9509
|
-
if (
|
|
9589
|
+
const stat5 = await fs22.stat(abs);
|
|
9590
|
+
if (stat5.isDirectory()) {
|
|
9510
9591
|
await scan(abs);
|
|
9511
9592
|
} else if (entry.endsWith(".dsl.json")) {
|
|
9512
9593
|
allFiles.push(abs);
|
|
@@ -11244,7 +11325,7 @@ ${optionsText}`;
|
|
|
11244
11325
|
var import_crypto2 = require("crypto");
|
|
11245
11326
|
var fs24 = __toESM(require("fs-extra"));
|
|
11246
11327
|
var path23 = __toESM(require("path"));
|
|
11247
|
-
var VCR_DIR =
|
|
11328
|
+
var VCR_DIR = DEFAULT_VCR_DIR;
|
|
11248
11329
|
var VcrRecordingProvider = class {
|
|
11249
11330
|
constructor(inner) {
|
|
11250
11331
|
this.inner = inner;
|
|
@@ -11942,7 +12023,147 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11942
12023
|
}
|
|
11943
12024
|
}
|
|
11944
12025
|
|
|
12026
|
+
// core/repo-store.ts
|
|
12027
|
+
var fs26 = __toESM(require("fs-extra"));
|
|
12028
|
+
var path25 = __toESM(require("path"));
|
|
12029
|
+
var os5 = __toESM(require("os"));
|
|
12030
|
+
var REPO_STORE_FILE = path25.join(os5.homedir(), ".ai-spec-repos.json");
|
|
12031
|
+
async function readStore2() {
|
|
12032
|
+
try {
|
|
12033
|
+
if (await fs26.pathExists(REPO_STORE_FILE)) {
|
|
12034
|
+
return await fs26.readJson(REPO_STORE_FILE);
|
|
12035
|
+
}
|
|
12036
|
+
} catch (err) {
|
|
12037
|
+
console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${err.message}.`);
|
|
12038
|
+
}
|
|
12039
|
+
return { repos: [] };
|
|
12040
|
+
}
|
|
12041
|
+
async function writeStore2(store) {
|
|
12042
|
+
await fs26.ensureFile(REPO_STORE_FILE);
|
|
12043
|
+
await fs26.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
|
|
12044
|
+
}
|
|
12045
|
+
async function getRegisteredRepos() {
|
|
12046
|
+
const store = await readStore2();
|
|
12047
|
+
return store.repos;
|
|
12048
|
+
}
|
|
12049
|
+
async function registerRepo(repo) {
|
|
12050
|
+
const store = await readStore2();
|
|
12051
|
+
const idx = store.repos.findIndex((r) => r.path === repo.path);
|
|
12052
|
+
if (idx >= 0) {
|
|
12053
|
+
store.repos[idx] = repo;
|
|
12054
|
+
} else {
|
|
12055
|
+
store.repos.push(repo);
|
|
12056
|
+
}
|
|
12057
|
+
await writeStore2(store);
|
|
12058
|
+
}
|
|
12059
|
+
|
|
11945
12060
|
// cli/commands/create.ts
|
|
12061
|
+
init_spec_generator();
|
|
12062
|
+
async function promptRepoRole() {
|
|
12063
|
+
return (0, import_prompts7.select)({
|
|
12064
|
+
message: "What type of repo is this?",
|
|
12065
|
+
choices: [
|
|
12066
|
+
{ name: "Frontend", value: "frontend" },
|
|
12067
|
+
{ name: "Backend", value: "backend" },
|
|
12068
|
+
{ name: "Mobile", value: "mobile" },
|
|
12069
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12070
|
+
]
|
|
12071
|
+
});
|
|
12072
|
+
}
|
|
12073
|
+
async function quickRegisterRepo(provider) {
|
|
12074
|
+
const roleOverride = await promptRepoRole();
|
|
12075
|
+
const roleLabels = {
|
|
12076
|
+
frontend: "frontend",
|
|
12077
|
+
backend: "backend",
|
|
12078
|
+
mobile: "mobile",
|
|
12079
|
+
shared: "shared"
|
|
12080
|
+
};
|
|
12081
|
+
const raw = await (0, import_prompts7.input)({
|
|
12082
|
+
message: `Enter your ${roleLabels[roleOverride]} repo path (absolute path):`,
|
|
12083
|
+
validate: (v2) => {
|
|
12084
|
+
const trimmed = v2.trim();
|
|
12085
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12086
|
+
if (!path26.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12087
|
+
return true;
|
|
12088
|
+
}
|
|
12089
|
+
});
|
|
12090
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12091
|
+
const resolved = path26.resolve(cleaned);
|
|
12092
|
+
if (!await fs27.pathExists(resolved)) {
|
|
12093
|
+
console.log(import_chalk26.default.red(` Path does not exist: ${resolved}`));
|
|
12094
|
+
return quickRegisterRepo(provider);
|
|
12095
|
+
}
|
|
12096
|
+
const { type, role: detectedRole } = await detectRepoType(resolved);
|
|
12097
|
+
const role = roleOverride ?? detectedRole;
|
|
12098
|
+
const repoName = path26.basename(resolved);
|
|
12099
|
+
console.log(import_chalk26.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12100
|
+
const constitutionPath = path26.join(resolved, CONSTITUTION_FILE);
|
|
12101
|
+
let hasConstitution = await fs27.pathExists(constitutionPath);
|
|
12102
|
+
if (!hasConstitution) {
|
|
12103
|
+
console.log(import_chalk26.default.blue(` Generating constitution for ${repoName}...`));
|
|
12104
|
+
try {
|
|
12105
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12106
|
+
const content = await gen.generate(resolved);
|
|
12107
|
+
await gen.saveConstitution(resolved, content);
|
|
12108
|
+
hasConstitution = true;
|
|
12109
|
+
console.log(import_chalk26.default.green(` \u2714 Constitution saved`));
|
|
12110
|
+
} catch (err) {
|
|
12111
|
+
console.log(import_chalk26.default.yellow(` \u26A0 Constitution failed: ${err.message}`));
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
const entry = {
|
|
12115
|
+
name: repoName,
|
|
12116
|
+
path: resolved,
|
|
12117
|
+
type,
|
|
12118
|
+
role,
|
|
12119
|
+
hasConstitution,
|
|
12120
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12121
|
+
};
|
|
12122
|
+
await registerRepo(entry);
|
|
12123
|
+
console.log(import_chalk26.default.green(` \u2714 Repo registered: ${repoName}`));
|
|
12124
|
+
return entry;
|
|
12125
|
+
}
|
|
12126
|
+
async function selectRepos(registeredRepos, provider) {
|
|
12127
|
+
const ADD_NEW = "__add_new__";
|
|
12128
|
+
const choices = [
|
|
12129
|
+
...registeredRepos.map((r) => ({
|
|
12130
|
+
name: `${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`,
|
|
12131
|
+
value: r.path
|
|
12132
|
+
})),
|
|
12133
|
+
{ name: import_chalk26.default.cyan("+ Add new repo"), value: ADD_NEW }
|
|
12134
|
+
];
|
|
12135
|
+
const selected = await (0, import_prompts7.checkbox)({
|
|
12136
|
+
message: "Select repo(s) for this feature (space to toggle, enter to confirm):",
|
|
12137
|
+
choices,
|
|
12138
|
+
required: true
|
|
12139
|
+
});
|
|
12140
|
+
const result = [];
|
|
12141
|
+
for (const val of selected) {
|
|
12142
|
+
if (val === ADD_NEW) {
|
|
12143
|
+
const newRepo = await quickRegisterRepo(provider);
|
|
12144
|
+
result.push(newRepo);
|
|
12145
|
+
} else {
|
|
12146
|
+
const repo = registeredRepos.find((r) => r.path === val);
|
|
12147
|
+
if (repo) result.push(repo);
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
if (result.length === 0) {
|
|
12151
|
+
console.log(import_chalk26.default.yellow(" No repos selected. Please select at least one."));
|
|
12152
|
+
return selectRepos(registeredRepos, provider);
|
|
12153
|
+
}
|
|
12154
|
+
return result;
|
|
12155
|
+
}
|
|
12156
|
+
function buildWorkspaceConfig(repos) {
|
|
12157
|
+
return {
|
|
12158
|
+
name: "ai-spec-workspace",
|
|
12159
|
+
repos: repos.map((r) => ({
|
|
12160
|
+
name: r.name,
|
|
12161
|
+
path: r.path,
|
|
12162
|
+
type: r.type,
|
|
12163
|
+
role: r.role
|
|
12164
|
+
}))
|
|
12165
|
+
};
|
|
12166
|
+
}
|
|
11946
12167
|
function registerCreate(program2) {
|
|
11947
12168
|
program2.command("create").description("Generate a feature spec and kick off code generation").argument("[idea]", "Feature idea in natural language (prompted if omitted)").option(
|
|
11948
12169
|
"--provider <name>",
|
|
@@ -11965,24 +12186,75 @@ function registerCreate(program2) {
|
|
|
11965
12186
|
});
|
|
11966
12187
|
}
|
|
11967
12188
|
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
11968
|
-
const
|
|
11969
|
-
if (
|
|
12189
|
+
const existingWorkspaceConfig = await workspaceLoader.load();
|
|
12190
|
+
if (existingWorkspaceConfig) {
|
|
11970
12191
|
console.log(import_chalk26.default.cyan(`
|
|
11971
|
-
[Workspace] Detected workspace: ${
|
|
11972
|
-
console.log(import_chalk26.default.gray(` Repos: ${
|
|
11973
|
-
const pipelineResults = await runMultiRepoPipeline(idea,
|
|
12192
|
+
[Workspace] Detected workspace: ${existingWorkspaceConfig.name}`));
|
|
12193
|
+
console.log(import_chalk26.default.gray(` Repos: ${existingWorkspaceConfig.repos.map((r) => r.name).join(", ")}`));
|
|
12194
|
+
const pipelineResults = await runMultiRepoPipeline(idea, existingWorkspaceConfig, opts, currentDir, config2);
|
|
11974
12195
|
if (opts.serve) {
|
|
11975
12196
|
await handleAutoServe(pipelineResults);
|
|
11976
12197
|
}
|
|
11977
12198
|
return;
|
|
11978
12199
|
}
|
|
11979
|
-
|
|
12200
|
+
const providerName = opts.provider || config2.provider || "gemini";
|
|
12201
|
+
const modelName = opts.model || config2.model || DEFAULT_MODELS[providerName];
|
|
12202
|
+
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
12203
|
+
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
12204
|
+
const registeredRepos = await getRegisteredRepos();
|
|
12205
|
+
if (registeredRepos.length === 0) {
|
|
12206
|
+
console.log(import_chalk26.default.yellow("\n No repos registered. Please register repos first."));
|
|
12207
|
+
console.log(import_chalk26.default.gray(" Run: ai-spec init"));
|
|
12208
|
+
console.log(import_chalk26.default.gray(" Or add a repo now:\n"));
|
|
12209
|
+
const addNow = await (0, import_prompts7.select)({
|
|
12210
|
+
message: "Add a repo now?",
|
|
12211
|
+
choices: [
|
|
12212
|
+
{ name: "Yes \u2014 register a repo and continue", value: "yes" },
|
|
12213
|
+
{ name: "No \u2014 exit", value: "no" }
|
|
12214
|
+
]
|
|
12215
|
+
});
|
|
12216
|
+
if (addNow === "no") {
|
|
12217
|
+
process.exit(0);
|
|
12218
|
+
}
|
|
12219
|
+
const newRepo = await quickRegisterRepo(specProvider);
|
|
12220
|
+
registeredRepos.push(newRepo);
|
|
12221
|
+
}
|
|
12222
|
+
const validRepos = [];
|
|
12223
|
+
for (const r of registeredRepos) {
|
|
12224
|
+
if (await fs27.pathExists(r.path)) {
|
|
12225
|
+
validRepos.push(r);
|
|
12226
|
+
} else {
|
|
12227
|
+
console.log(import_chalk26.default.yellow(` \u26A0 Skipping ${r.name}: path not found (${r.path})`));
|
|
12228
|
+
}
|
|
12229
|
+
}
|
|
12230
|
+
if (validRepos.length === 0) {
|
|
12231
|
+
console.log(import_chalk26.default.red(" No valid repos available. Run: ai-spec init"));
|
|
12232
|
+
process.exit(1);
|
|
12233
|
+
}
|
|
12234
|
+
const selectedRepos = await selectRepos(validRepos, specProvider);
|
|
12235
|
+
if (selectedRepos.length === 1) {
|
|
12236
|
+
const repo = selectedRepos[0];
|
|
12237
|
+
console.log(import_chalk26.default.cyan(`
|
|
12238
|
+
[Repo] ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12239
|
+
await runSingleRepoPipeline(idea, opts, repo.path, config2);
|
|
12240
|
+
} else {
|
|
12241
|
+
const workspaceConfig = buildWorkspaceConfig(selectedRepos);
|
|
12242
|
+
console.log(import_chalk26.default.cyan(`
|
|
12243
|
+
[Workspace] ${selectedRepos.length} repo(s) selected:`));
|
|
12244
|
+
for (const repo of selectedRepos) {
|
|
12245
|
+
console.log(import_chalk26.default.gray(` ${repo.name} (${repo.type}/${repo.role}) \u2192 ${repo.path}`));
|
|
12246
|
+
}
|
|
12247
|
+
const pipelineResults = await runMultiRepoPipeline(idea, workspaceConfig, opts, currentDir, config2);
|
|
12248
|
+
if (opts.serve) {
|
|
12249
|
+
await handleAutoServe(pipelineResults);
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
11980
12252
|
});
|
|
11981
12253
|
}
|
|
11982
12254
|
|
|
11983
12255
|
// cli/commands/review.ts
|
|
11984
|
-
var
|
|
11985
|
-
var
|
|
12256
|
+
var path27 = __toESM(require("path"));
|
|
12257
|
+
var fs28 = __toESM(require("fs-extra"));
|
|
11986
12258
|
var import_chalk27 = __toESM(require("chalk"));
|
|
11987
12259
|
init_spec_generator();
|
|
11988
12260
|
function registerReview(program2) {
|
|
@@ -12000,17 +12272,17 @@ function registerReview(program2) {
|
|
|
12000
12272
|
const reviewer = new CodeReviewer(provider, currentDir);
|
|
12001
12273
|
let specContent = "";
|
|
12002
12274
|
let resolvedSpecFile;
|
|
12003
|
-
if (specFile && await
|
|
12004
|
-
specContent = await
|
|
12275
|
+
if (specFile && await fs28.pathExists(specFile)) {
|
|
12276
|
+
specContent = await fs28.readFile(specFile, "utf-8");
|
|
12005
12277
|
resolvedSpecFile = specFile;
|
|
12006
12278
|
console.log(import_chalk27.default.gray(`Using spec: ${specFile}`));
|
|
12007
12279
|
} else {
|
|
12008
|
-
const specsDir =
|
|
12009
|
-
if (await
|
|
12010
|
-
const files = (await
|
|
12280
|
+
const specsDir = path27.join(currentDir, "specs");
|
|
12281
|
+
if (await fs28.pathExists(specsDir)) {
|
|
12282
|
+
const files = (await fs28.readdir(specsDir)).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
12011
12283
|
if (files.length > 0) {
|
|
12012
|
-
const latest =
|
|
12013
|
-
specContent = await
|
|
12284
|
+
const latest = path27.join(specsDir, files[0]);
|
|
12285
|
+
specContent = await fs28.readFile(latest, "utf-8");
|
|
12014
12286
|
resolvedSpecFile = latest;
|
|
12015
12287
|
console.log(import_chalk27.default.gray(`Auto-detected spec: specs/${files[0]}`));
|
|
12016
12288
|
}
|
|
@@ -12025,9 +12297,10 @@ function registerReview(program2) {
|
|
|
12025
12297
|
}
|
|
12026
12298
|
|
|
12027
12299
|
// cli/commands/init.ts
|
|
12028
|
-
var
|
|
12029
|
-
var
|
|
12300
|
+
var path28 = __toESM(require("path"));
|
|
12301
|
+
var fs29 = __toESM(require("fs-extra"));
|
|
12030
12302
|
var import_chalk28 = __toESM(require("chalk"));
|
|
12303
|
+
var import_prompts8 = require("@inquirer/prompts");
|
|
12031
12304
|
init_spec_generator();
|
|
12032
12305
|
|
|
12033
12306
|
// prompts/global-constitution.prompt.ts
|
|
@@ -12089,240 +12362,139 @@ ${summary}
|
|
|
12089
12362
|
return parts.join("\n");
|
|
12090
12363
|
}
|
|
12091
12364
|
|
|
12092
|
-
//
|
|
12093
|
-
var
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12365
|
+
// cli/commands/init.ts
|
|
12366
|
+
var ROLE_LABELS = {
|
|
12367
|
+
frontend: "frontend",
|
|
12368
|
+
backend: "backend",
|
|
12369
|
+
mobile: "mobile",
|
|
12370
|
+
shared: "shared"
|
|
12371
|
+
};
|
|
12372
|
+
async function promptRepoRole2() {
|
|
12373
|
+
return (0, import_prompts8.select)({
|
|
12374
|
+
message: "What type of repo is this?",
|
|
12375
|
+
choices: [
|
|
12376
|
+
{ name: "Frontend", value: "frontend" },
|
|
12377
|
+
{ name: "Backend", value: "backend" },
|
|
12378
|
+
{ name: "Mobile", value: "mobile" },
|
|
12379
|
+
{ name: "Shared / Other", value: "shared" }
|
|
12380
|
+
]
|
|
12381
|
+
});
|
|
12382
|
+
}
|
|
12383
|
+
async function promptRepoPath(role) {
|
|
12384
|
+
const label = ROLE_LABELS[role];
|
|
12385
|
+
const raw = await (0, import_prompts8.input)({
|
|
12386
|
+
message: `Enter your ${label} repo path (absolute path):`,
|
|
12387
|
+
validate: (v2) => {
|
|
12388
|
+
const trimmed = v2.trim();
|
|
12389
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
12390
|
+
if (!path28.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
12391
|
+
return true;
|
|
12392
|
+
}
|
|
12393
|
+
});
|
|
12394
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
12395
|
+
const resolved = path28.resolve(cleaned);
|
|
12396
|
+
if (!await fs29.pathExists(resolved)) {
|
|
12397
|
+
console.log(import_chalk28.default.red(` Path does not exist: ${resolved}`));
|
|
12398
|
+
return promptRepoPath(role);
|
|
12399
|
+
}
|
|
12400
|
+
const stat5 = await fs29.stat(resolved);
|
|
12401
|
+
if (!stat5.isDirectory()) {
|
|
12402
|
+
console.log(import_chalk28.default.red(` Not a directory: ${resolved}`));
|
|
12403
|
+
return promptRepoPath(role);
|
|
12404
|
+
}
|
|
12405
|
+
return resolved;
|
|
12406
|
+
}
|
|
12407
|
+
async function registerSingleRepo(repoPath, provider, roleOverride) {
|
|
12408
|
+
const { type, role: detectedRole } = await detectRepoType(repoPath);
|
|
12409
|
+
const role = roleOverride ?? detectedRole;
|
|
12410
|
+
const repoName = path28.basename(repoPath);
|
|
12411
|
+
console.log(import_chalk28.default.gray(` Detected: ${repoName} \u2192 ${type} (${role})`));
|
|
12412
|
+
const constitutionPath = path28.join(repoPath, CONSTITUTION_FILE);
|
|
12413
|
+
let hasConstitution = await fs29.pathExists(constitutionPath);
|
|
12414
|
+
if (!hasConstitution) {
|
|
12415
|
+
console.log(import_chalk28.default.blue(` Generating project constitution for ${repoName}...`));
|
|
12416
|
+
try {
|
|
12417
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12418
|
+
const content = await gen.generate(repoPath);
|
|
12419
|
+
await gen.saveConstitution(repoPath, content);
|
|
12420
|
+
hasConstitution = true;
|
|
12421
|
+
console.log(import_chalk28.default.green(` \u2714 Constitution saved: ${constitutionPath}`));
|
|
12422
|
+
} catch (err) {
|
|
12423
|
+
console.log(import_chalk28.default.yellow(` \u26A0 Constitution generation failed: ${err.message}`));
|
|
12424
|
+
}
|
|
12425
|
+
} else {
|
|
12426
|
+
console.log(import_chalk28.default.green(` \u2714 Constitution already exists: ${constitutionPath}`));
|
|
12427
|
+
}
|
|
12428
|
+
const entry = {
|
|
12429
|
+
name: repoName,
|
|
12430
|
+
path: repoPath,
|
|
12431
|
+
type,
|
|
12432
|
+
role,
|
|
12433
|
+
hasConstitution,
|
|
12434
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12435
|
+
};
|
|
12436
|
+
await registerRepo(entry);
|
|
12437
|
+
return entry;
|
|
12438
|
+
}
|
|
12439
|
+
async function buildProjectSummaries(repos) {
|
|
12440
|
+
const summaries = [];
|
|
12441
|
+
for (const repo of repos) {
|
|
12442
|
+
if (!await fs29.pathExists(repo.path)) continue;
|
|
12443
|
+
const lines = [
|
|
12444
|
+
`Type: ${repo.type} (${repo.role})`
|
|
12445
|
+
];
|
|
12446
|
+
try {
|
|
12447
|
+
const loader = new ContextLoader(repo.path);
|
|
12448
|
+
const ctx = await loader.loadProjectContext();
|
|
12449
|
+
lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
|
|
12450
|
+
lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
|
|
12451
|
+
} catch {
|
|
12452
|
+
lines.push(`Tech stack: ${repo.type}`);
|
|
12453
|
+
}
|
|
12454
|
+
if (repo.hasConstitution) {
|
|
12455
|
+
try {
|
|
12456
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12457
|
+
const raw = await fs29.readFile(constitutionPath, "utf-8");
|
|
12458
|
+
lines.push("", "Constitution excerpt:", raw.slice(0, 2e3));
|
|
12459
|
+
} catch {
|
|
12460
|
+
}
|
|
12461
|
+
}
|
|
12462
|
+
summaries.push({ name: repo.name, summary: lines.join("\n") });
|
|
12174
12463
|
}
|
|
12175
|
-
return
|
|
12464
|
+
return summaries;
|
|
12176
12465
|
}
|
|
12177
|
-
async function
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
if (
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
let
|
|
12466
|
+
async function generateGlobalConstitution(provider, repos, currentDir) {
|
|
12467
|
+
console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Generating Global Constitution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12468
|
+
console.log(import_chalk28.default.gray(` Based on ${repos.length} registered repo(s)`));
|
|
12469
|
+
const summaries = await buildProjectSummaries(repos);
|
|
12470
|
+
if (summaries.length === 0) {
|
|
12471
|
+
console.log(import_chalk28.default.yellow(" No valid repos found \u2014 skipping global constitution."));
|
|
12472
|
+
return;
|
|
12473
|
+
}
|
|
12474
|
+
const prompt = buildGlobalConstitutionPrompt(summaries);
|
|
12475
|
+
let globalConstitution;
|
|
12187
12476
|
try {
|
|
12188
|
-
|
|
12189
|
-
} catch {
|
|
12190
|
-
|
|
12477
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
12478
|
+
} catch (err) {
|
|
12479
|
+
console.error(import_chalk28.default.red(` \u2718 Failed to generate global constitution: ${err.message}`));
|
|
12480
|
+
return;
|
|
12191
12481
|
}
|
|
12192
|
-
const
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12482
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
12483
|
+
console.log(import_chalk28.default.green(` \u2714 Global constitution saved: ${saved}`));
|
|
12484
|
+
console.log(import_chalk28.default.gray(" Project constitutions will be merged with this at runtime."));
|
|
12485
|
+
const lines = globalConstitution.split("\n");
|
|
12486
|
+
console.log(import_chalk28.default.bold("\n Preview:"));
|
|
12487
|
+
console.log(import_chalk28.default.gray(lines.slice(0, 10).join("\n")));
|
|
12488
|
+
if (lines.length > 10) {
|
|
12489
|
+
console.log(import_chalk28.default.gray(` ... (${lines.length} lines total)`));
|
|
12199
12490
|
}
|
|
12200
|
-
return stack;
|
|
12201
12491
|
}
|
|
12202
|
-
async function discoverProjects(rootDir, maxDepth) {
|
|
12203
|
-
const found = [];
|
|
12204
|
-
async function walk(absDir, depth) {
|
|
12205
|
-
if (depth > maxDepth) return;
|
|
12206
|
-
let entries;
|
|
12207
|
-
try {
|
|
12208
|
-
entries = await fs27.readdir(absDir, { withFileTypes: true });
|
|
12209
|
-
} catch {
|
|
12210
|
-
return;
|
|
12211
|
-
}
|
|
12212
|
-
for (const entry of entries) {
|
|
12213
|
-
if (!entry.isDirectory()) continue;
|
|
12214
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
12215
|
-
const childAbs = path26.join(absDir, entry.name);
|
|
12216
|
-
const gitPath = path26.join(childAbs, ".git");
|
|
12217
|
-
if (await fs27.pathExists(gitPath)) {
|
|
12218
|
-
const gitStat = await fs27.stat(gitPath);
|
|
12219
|
-
if (gitStat.isFile()) continue;
|
|
12220
|
-
}
|
|
12221
|
-
if (await isProjectRoot(childAbs)) {
|
|
12222
|
-
found.push(path26.relative(rootDir, childAbs));
|
|
12223
|
-
} else {
|
|
12224
|
-
await walk(childAbs, depth + 1);
|
|
12225
|
-
}
|
|
12226
|
-
}
|
|
12227
|
-
}
|
|
12228
|
-
await walk(rootDir, 0);
|
|
12229
|
-
return found;
|
|
12230
|
-
}
|
|
12231
|
-
async function loadIndex(scanRoot) {
|
|
12232
|
-
const filePath = path26.join(scanRoot, INDEX_FILE);
|
|
12233
|
-
try {
|
|
12234
|
-
return await fs27.readJson(filePath);
|
|
12235
|
-
} catch {
|
|
12236
|
-
return null;
|
|
12237
|
-
}
|
|
12238
|
-
}
|
|
12239
|
-
async function saveIndex(scanRoot, index) {
|
|
12240
|
-
const filePath = path26.join(scanRoot, INDEX_FILE);
|
|
12241
|
-
await fs27.writeJson(filePath, index, { spaces: 2 });
|
|
12242
|
-
return filePath;
|
|
12243
|
-
}
|
|
12244
|
-
async function runScan(scanRoot, maxDepth = 2) {
|
|
12245
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12246
|
-
const existing = await loadIndex(scanRoot);
|
|
12247
|
-
const existingMap = new Map(
|
|
12248
|
-
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
12249
|
-
);
|
|
12250
|
-
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
12251
|
-
const added = [];
|
|
12252
|
-
const updated = [];
|
|
12253
|
-
const unchanged = [];
|
|
12254
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
12255
|
-
for (const relPath of discoveredPaths) {
|
|
12256
|
-
const absPath = path26.join(scanRoot, relPath);
|
|
12257
|
-
seenPaths.add(relPath);
|
|
12258
|
-
const { type, role } = await detectRepoType(absPath);
|
|
12259
|
-
const techStack = await extractTechStack(absPath, type);
|
|
12260
|
-
const hasConstitution = await fs27.pathExists(path26.join(absPath, CONSTITUTION_FILE));
|
|
12261
|
-
const hasWorkspace = await fs27.pathExists(path26.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
12262
|
-
const name = path26.basename(relPath);
|
|
12263
|
-
const prev = existingMap.get(relPath);
|
|
12264
|
-
if (!prev) {
|
|
12265
|
-
const entry = {
|
|
12266
|
-
name,
|
|
12267
|
-
path: relPath,
|
|
12268
|
-
type,
|
|
12269
|
-
role,
|
|
12270
|
-
techStack,
|
|
12271
|
-
hasConstitution,
|
|
12272
|
-
hasWorkspace,
|
|
12273
|
-
firstSeen: now,
|
|
12274
|
-
lastSeen: now
|
|
12275
|
-
};
|
|
12276
|
-
added.push(entry);
|
|
12277
|
-
existingMap.set(relPath, entry);
|
|
12278
|
-
} else {
|
|
12279
|
-
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
12280
|
-
const entry = {
|
|
12281
|
-
...prev,
|
|
12282
|
-
type,
|
|
12283
|
-
role,
|
|
12284
|
-
techStack,
|
|
12285
|
-
hasConstitution,
|
|
12286
|
-
hasWorkspace,
|
|
12287
|
-
lastSeen: now,
|
|
12288
|
-
missing: void 0
|
|
12289
|
-
// clear missing flag if it came back
|
|
12290
|
-
};
|
|
12291
|
-
existingMap.set(relPath, entry);
|
|
12292
|
-
if (changed) {
|
|
12293
|
-
updated.push(entry);
|
|
12294
|
-
} else {
|
|
12295
|
-
unchanged.push(entry);
|
|
12296
|
-
}
|
|
12297
|
-
}
|
|
12298
|
-
}
|
|
12299
|
-
const nowMissing = [];
|
|
12300
|
-
for (const [relPath, entry] of existingMap) {
|
|
12301
|
-
if (!seenPaths.has(relPath) && !entry.missing) {
|
|
12302
|
-
const gone = { ...entry, missing: true };
|
|
12303
|
-
existingMap.set(relPath, gone);
|
|
12304
|
-
nowMissing.push(gone);
|
|
12305
|
-
}
|
|
12306
|
-
}
|
|
12307
|
-
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
12308
|
-
const index = {
|
|
12309
|
-
scanRoot,
|
|
12310
|
-
lastScanned: now,
|
|
12311
|
-
projects
|
|
12312
|
-
};
|
|
12313
|
-
return { index, added, updated, unchanged, nowMissing };
|
|
12314
|
-
}
|
|
12315
|
-
|
|
12316
|
-
// cli/commands/init.ts
|
|
12317
12492
|
function registerInit(program2) {
|
|
12318
|
-
program2.command("init").description(
|
|
12493
|
+
program2.command("init").description("Setup workspace: register repos, generate constitutions").option(
|
|
12319
12494
|
"--provider <name>",
|
|
12320
12495
|
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
12321
12496
|
void 0
|
|
12322
|
-
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing
|
|
12323
|
-
"--global",
|
|
12324
|
-
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
12325
|
-
).option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules (prune & rebase)").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").action(async (opts) => {
|
|
12497
|
+
).option("--model <name>", "Model name").option("-k, --key <apiKey>", "API key").option("--force", "Overwrite existing constitutions").option("--consolidate", "Consolidate \xA79 accumulated lessons into \xA71\u2013\xA78 core rules").option("--dry-run", "Preview consolidation result without writing (use with --consolidate)").option("--add-repo", "Add a new repo to the registered list").action(async (opts) => {
|
|
12326
12498
|
const currentDir = process.cwd();
|
|
12327
12499
|
const config2 = await loadConfig(currentDir);
|
|
12328
12500
|
const providerName = opts.provider || config2.provider || "gemini";
|
|
@@ -12341,7 +12513,7 @@ function registerInit(program2) {
|
|
|
12341
12513
|
console.log(import_chalk28.default.gray(` Lines : ${result.before.totalLines} \u2192 ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
12342
12514
|
console.log(import_chalk28.default.gray(` \xA79 : ${result.before.lessonCount} \u2192 ${result.after.lessonCount} lessons remaining`));
|
|
12343
12515
|
if (result.backupPath) {
|
|
12344
|
-
console.log(import_chalk28.default.gray(` Backup: ${
|
|
12516
|
+
console.log(import_chalk28.default.gray(` Backup: ${path28.basename(result.backupPath)}`));
|
|
12345
12517
|
}
|
|
12346
12518
|
}
|
|
12347
12519
|
} catch (err) {
|
|
@@ -12350,119 +12522,125 @@ function registerInit(program2) {
|
|
|
12350
12522
|
}
|
|
12351
12523
|
return;
|
|
12352
12524
|
}
|
|
12353
|
-
if (opts.
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12525
|
+
if (opts.addRepo) {
|
|
12526
|
+
console.log(import_chalk28.default.blue("\n\u2500\u2500\u2500 Register New Repo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12527
|
+
const role = await promptRepoRole2();
|
|
12528
|
+
const repoPath = await promptRepoPath(role);
|
|
12529
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12530
|
+
console.log(import_chalk28.default.green(`
|
|
12531
|
+
\u2714 Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
|
|
12532
|
+
console.log(import_chalk28.default.gray(` Saved to: ${REPO_STORE_FILE}`));
|
|
12533
|
+
const updateGlobal = await (0, import_prompts8.confirm)({
|
|
12534
|
+
message: "Update global constitution with this repo's context?",
|
|
12535
|
+
default: true
|
|
12536
|
+
});
|
|
12537
|
+
if (updateGlobal) {
|
|
12538
|
+
const allRepos2 = await getRegisteredRepos();
|
|
12539
|
+
await generateGlobalConstitution(provider, allRepos2, currentDir);
|
|
12360
12540
|
}
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12541
|
+
return;
|
|
12542
|
+
}
|
|
12543
|
+
console.log(import_chalk28.default.blue("\n" + "\u2500".repeat(52)));
|
|
12544
|
+
console.log(import_chalk28.default.bold(" ai-spec init \u2014 Workspace Setup"));
|
|
12545
|
+
console.log(import_chalk28.default.blue("\u2500".repeat(52)));
|
|
12546
|
+
console.log(import_chalk28.default.gray(` Provider: ${providerName}/${modelName}
|
|
12547
|
+
`));
|
|
12548
|
+
const existingRepos = await getRegisteredRepos();
|
|
12549
|
+
if (existingRepos.length > 0) {
|
|
12550
|
+
console.log(import_chalk28.default.cyan(" Registered repos:"));
|
|
12551
|
+
for (const r of existingRepos) {
|
|
12552
|
+
const constitutionIcon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
|
|
12553
|
+
console.log(import_chalk28.default.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) \u2192 ${r.path}`));
|
|
12554
|
+
}
|
|
12555
|
+
console.log();
|
|
12556
|
+
}
|
|
12557
|
+
const action = existingRepos.length > 0 ? await (0, import_prompts8.select)({
|
|
12558
|
+
message: "What would you like to do?",
|
|
12559
|
+
choices: [
|
|
12560
|
+
{ name: "Add new repo(s)", value: "add" },
|
|
12561
|
+
{ name: "Re-generate constitutions for existing repos", value: "regen" },
|
|
12562
|
+
{ name: "Skip \u2014 proceed to global constitution", value: "skip" }
|
|
12563
|
+
]
|
|
12564
|
+
}) : "add";
|
|
12565
|
+
const newRepos = [];
|
|
12566
|
+
if (action === "add") {
|
|
12567
|
+
let addMore = true;
|
|
12568
|
+
while (addMore) {
|
|
12569
|
+
console.log(import_chalk28.default.blue(`
|
|
12570
|
+
\u2500\u2500 Register Repo #${existingRepos.length + newRepos.length + 1} \u2500\u2500`));
|
|
12571
|
+
const role = await promptRepoRole2();
|
|
12572
|
+
const repoPath = await promptRepoPath(role);
|
|
12573
|
+
const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
|
|
12574
|
+
if (alreadyRegistered) {
|
|
12575
|
+
console.log(import_chalk28.default.yellow(` Already registered: ${alreadyRegistered.name}`));
|
|
12576
|
+
} else {
|
|
12577
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
12578
|
+
newRepos.push(entry);
|
|
12579
|
+
console.log(import_chalk28.default.green(` \u2714 Registered: ${entry.name}`));
|
|
12384
12580
|
}
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
const loader = new ContextLoader(currentDir);
|
|
12389
|
-
const ctx = await loader.loadProjectContext();
|
|
12390
|
-
projectSummaries.push({
|
|
12391
|
-
name: path27.basename(currentDir),
|
|
12392
|
-
summary: [
|
|
12393
|
-
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
12394
|
-
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`
|
|
12395
|
-
].join("\n")
|
|
12581
|
+
addMore = await (0, import_prompts8.confirm)({
|
|
12582
|
+
message: "Add another repo?",
|
|
12583
|
+
default: false
|
|
12396
12584
|
});
|
|
12397
12585
|
}
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12586
|
+
}
|
|
12587
|
+
if (action === "regen") {
|
|
12588
|
+
console.log(import_chalk28.default.blue("\n Re-generating project constitutions..."));
|
|
12589
|
+
for (const repo of existingRepos) {
|
|
12590
|
+
if (!await fs29.pathExists(repo.path)) {
|
|
12591
|
+
console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: path not found \u2014 skipping`));
|
|
12592
|
+
continue;
|
|
12593
|
+
}
|
|
12594
|
+
const constitutionPath = path28.join(repo.path, CONSTITUTION_FILE);
|
|
12595
|
+
if (await fs29.pathExists(constitutionPath) && !opts.force) {
|
|
12596
|
+
console.log(import_chalk28.default.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
|
|
12597
|
+
continue;
|
|
12598
|
+
}
|
|
12599
|
+
console.log(import_chalk28.default.blue(` ${repo.name}: generating constitution...`));
|
|
12600
|
+
try {
|
|
12601
|
+
const gen = new ConstitutionGenerator(provider);
|
|
12602
|
+
const content = await gen.generate(repo.path);
|
|
12603
|
+
await gen.saveConstitution(repo.path, content);
|
|
12604
|
+
console.log(import_chalk28.default.green(` \u2714 ${repo.name}: constitution saved`));
|
|
12605
|
+
} catch (err) {
|
|
12606
|
+
console.log(import_chalk28.default.yellow(` \u26A0 ${repo.name}: failed \u2014 ${err.message}`));
|
|
12607
|
+
}
|
|
12416
12608
|
}
|
|
12417
|
-
return;
|
|
12418
12609
|
}
|
|
12419
|
-
const
|
|
12420
|
-
if (
|
|
12421
|
-
console.log(import_chalk28.default.yellow(`
|
|
12422
|
-
${CONSTITUTION_FILE} already exists.`));
|
|
12423
|
-
console.log(import_chalk28.default.gray(" Use --force to overwrite it."));
|
|
12424
|
-
console.log(import_chalk28.default.gray(` Or edit it directly: ${constitutionPath}`));
|
|
12610
|
+
const allRepos = await getRegisteredRepos();
|
|
12611
|
+
if (allRepos.length === 0) {
|
|
12612
|
+
console.log(import_chalk28.default.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
|
|
12425
12613
|
return;
|
|
12426
12614
|
}
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
}
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
}
|
|
12446
|
-
console.log(import_chalk28.default.green(`
|
|
12447
|
-
\u2714 Constitution saved: ${saved}`));
|
|
12448
|
-
console.log(import_chalk28.default.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
12449
|
-
console.log(import_chalk28.default.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
12450
|
-
console.log(import_chalk28.default.bold(" Preview:"));
|
|
12451
|
-
console.log(import_chalk28.default.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
12452
|
-
if (constitution.split("\n").length > 15) {
|
|
12453
|
-
console.log(import_chalk28.default.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
12454
|
-
}
|
|
12615
|
+
const existingGlobal = await loadGlobalConstitution([currentDir]);
|
|
12616
|
+
const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0 ? true : await (0, import_prompts8.confirm)({
|
|
12617
|
+
message: "Global constitution exists. Re-generate it?",
|
|
12618
|
+
default: false
|
|
12619
|
+
});
|
|
12620
|
+
if (shouldGenerateGlobal) {
|
|
12621
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
12622
|
+
}
|
|
12623
|
+
console.log(import_chalk28.default.bold.green("\n\u2714 Init complete!"));
|
|
12624
|
+
console.log(import_chalk28.default.gray(` Repos registered: ${allRepos.length}`));
|
|
12625
|
+
for (const r of allRepos) {
|
|
12626
|
+
const icon = r.hasConstitution ? import_chalk28.default.green("\u2714") : import_chalk28.default.gray("\u25CB");
|
|
12627
|
+
console.log(import_chalk28.default.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
|
|
12628
|
+
}
|
|
12629
|
+
console.log(import_chalk28.default.gray(`
|
|
12630
|
+
Repo store: ${REPO_STORE_FILE}`));
|
|
12631
|
+
console.log(import_chalk28.default.gray(` Next step: ai-spec create "your feature idea"`));
|
|
12632
|
+
process.exit(0);
|
|
12455
12633
|
});
|
|
12456
12634
|
}
|
|
12457
12635
|
|
|
12458
12636
|
// cli/commands/config.ts
|
|
12459
|
-
var
|
|
12460
|
-
var
|
|
12637
|
+
var path29 = __toESM(require("path"));
|
|
12638
|
+
var fs30 = __toESM(require("fs-extra"));
|
|
12461
12639
|
var import_chalk29 = __toESM(require("chalk"));
|
|
12462
12640
|
function registerConfig(program2) {
|
|
12463
12641
|
program2.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)").option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
12464
12642
|
const currentDir = process.cwd();
|
|
12465
|
-
const configPath =
|
|
12643
|
+
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12466
12644
|
if (opts.clearKeys) {
|
|
12467
12645
|
await clearAllKeys();
|
|
12468
12646
|
console.log(import_chalk29.default.green(`\u2714 All saved API keys cleared.`));
|
|
@@ -12474,7 +12652,7 @@ function registerConfig(program2) {
|
|
|
12474
12652
|
return;
|
|
12475
12653
|
}
|
|
12476
12654
|
if (opts.listKeys) {
|
|
12477
|
-
const store = await
|
|
12655
|
+
const store = await fs30.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
12478
12656
|
const providers = Object.keys(store);
|
|
12479
12657
|
if (providers.length === 0) {
|
|
12480
12658
|
console.log(import_chalk29.default.gray("No saved API keys."));
|
|
@@ -12490,7 +12668,7 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12490
12668
|
return;
|
|
12491
12669
|
}
|
|
12492
12670
|
if (opts.reset) {
|
|
12493
|
-
await
|
|
12671
|
+
await fs30.writeJson(configPath, {}, { spaces: 2 });
|
|
12494
12672
|
console.log(import_chalk29.default.green(`\u2714 Config reset: ${configPath}`));
|
|
12495
12673
|
return;
|
|
12496
12674
|
}
|
|
@@ -12534,22 +12712,18 @@ File: ${KEY_STORE_FILE}`));
|
|
|
12534
12712
|
}
|
|
12535
12713
|
updated.maxErrorCycles = cycles;
|
|
12536
12714
|
}
|
|
12537
|
-
await
|
|
12715
|
+
await fs30.writeJson(configPath, updated, { spaces: 2 });
|
|
12538
12716
|
console.log(import_chalk29.default.green(`\u2714 Config saved to ${configPath}`));
|
|
12539
12717
|
console.log(JSON.stringify(updated, null, 2));
|
|
12540
12718
|
});
|
|
12541
12719
|
}
|
|
12542
12720
|
|
|
12543
12721
|
// cli/commands/model.ts
|
|
12544
|
-
var path29 = __toESM(require("path"));
|
|
12545
|
-
var fs30 = __toESM(require("fs-extra"));
|
|
12546
12722
|
var import_chalk30 = __toESM(require("chalk"));
|
|
12547
|
-
var
|
|
12723
|
+
var import_prompts9 = require("@inquirer/prompts");
|
|
12548
12724
|
init_spec_generator();
|
|
12549
12725
|
function registerModel(program2) {
|
|
12550
12726
|
program2.command("model").description("Interactively switch the active AI provider/model and save to .ai-spec.json").option("--list", "List all available providers and models").action(async (opts) => {
|
|
12551
|
-
const currentDir = process.cwd();
|
|
12552
|
-
const configPath = path29.join(currentDir, CONFIG_FILE);
|
|
12553
12727
|
if (opts.list) {
|
|
12554
12728
|
console.log(import_chalk30.default.bold("\nAvailable providers & models:\n"));
|
|
12555
12729
|
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
@@ -12566,8 +12740,9 @@ function registerModel(program2) {
|
|
|
12566
12740
|
}
|
|
12567
12741
|
return;
|
|
12568
12742
|
}
|
|
12569
|
-
const existing = await
|
|
12743
|
+
const existing = await loadGlobalConfig();
|
|
12570
12744
|
console.log(import_chalk30.default.blue("\n\u2500\u2500\u2500 Model Switcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12745
|
+
console.log(import_chalk30.default.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
|
|
12571
12746
|
if (Object.keys(existing).length > 0) {
|
|
12572
12747
|
console.log(
|
|
12573
12748
|
import_chalk30.default.gray(
|
|
@@ -12576,7 +12751,7 @@ function registerModel(program2) {
|
|
|
12576
12751
|
);
|
|
12577
12752
|
}
|
|
12578
12753
|
console.log();
|
|
12579
|
-
const target = await (0,
|
|
12754
|
+
const target = await (0, import_prompts9.select)({
|
|
12580
12755
|
message: "Configure model for:",
|
|
12581
12756
|
choices: [
|
|
12582
12757
|
{ name: "Spec generation (used for spec writing & refinement)", value: "spec" },
|
|
@@ -12585,7 +12760,7 @@ function registerModel(program2) {
|
|
|
12585
12760
|
]
|
|
12586
12761
|
});
|
|
12587
12762
|
async function pickProviderAndModel(label) {
|
|
12588
|
-
const providerKey = await (0,
|
|
12763
|
+
const providerKey = await (0, import_prompts9.select)({
|
|
12589
12764
|
message: `${label} \u2014 select provider:`,
|
|
12590
12765
|
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta2]) => ({
|
|
12591
12766
|
name: `${meta2.displayName.padEnd(22)} ${import_chalk30.default.gray(meta2.description)}`,
|
|
@@ -12598,12 +12773,12 @@ function registerModel(program2) {
|
|
|
12598
12773
|
...meta.models.map((m) => ({ name: m, value: m })),
|
|
12599
12774
|
{ name: import_chalk30.default.italic("\u270E Enter custom model name..."), value: "__custom__" }
|
|
12600
12775
|
];
|
|
12601
|
-
let chosenModel = await (0,
|
|
12776
|
+
let chosenModel = await (0, import_prompts9.select)({
|
|
12602
12777
|
message: `${label} \u2014 select model (${meta.displayName}):`,
|
|
12603
12778
|
choices: modelChoices
|
|
12604
12779
|
});
|
|
12605
12780
|
if (chosenModel === "__custom__") {
|
|
12606
|
-
chosenModel = await (0,
|
|
12781
|
+
chosenModel = await (0, import_prompts9.input)({
|
|
12607
12782
|
message: "Enter model name:",
|
|
12608
12783
|
validate: (v2) => v2.trim().length > 0 || "Model name cannot be empty"
|
|
12609
12784
|
});
|
|
@@ -12648,14 +12823,14 @@ function registerModel(program2) {
|
|
|
12648
12823
|
)
|
|
12649
12824
|
);
|
|
12650
12825
|
}
|
|
12651
|
-
const ok = await (0,
|
|
12826
|
+
const ok = await (0, import_prompts9.confirm)({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
|
|
12652
12827
|
if (!ok) {
|
|
12653
12828
|
console.log(import_chalk30.default.gray(" Cancelled."));
|
|
12654
12829
|
return;
|
|
12655
12830
|
}
|
|
12656
|
-
await
|
|
12831
|
+
await saveGlobalConfig(updated);
|
|
12657
12832
|
console.log(import_chalk30.default.green(`
|
|
12658
|
-
\u2714 Saved to ${
|
|
12833
|
+
\u2714 Saved to ${GLOBAL_CONFIG_FILE}`));
|
|
12659
12834
|
const providerToCheck = updated.provider ?? "gemini";
|
|
12660
12835
|
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
12661
12836
|
if (envKey && !process.env[envKey]) {
|
|
@@ -12672,14 +12847,14 @@ function registerModel(program2) {
|
|
|
12672
12847
|
var path30 = __toESM(require("path"));
|
|
12673
12848
|
var fs31 = __toESM(require("fs-extra"));
|
|
12674
12849
|
var import_chalk31 = __toESM(require("chalk"));
|
|
12675
|
-
var
|
|
12850
|
+
var import_prompts10 = require("@inquirer/prompts");
|
|
12676
12851
|
function registerWorkspace(program2) {
|
|
12677
12852
|
const workspaceCmd = program2.command("workspace").description("Manage multi-repo workspace configuration");
|
|
12678
12853
|
workspaceCmd.command("init").description(`Interactive workspace setup \u2014 creates ${WORKSPACE_CONFIG_FILE}`).action(async () => {
|
|
12679
12854
|
const currentDir = process.cwd();
|
|
12680
12855
|
const configPath = path30.join(currentDir, WORKSPACE_CONFIG_FILE);
|
|
12681
12856
|
if (await fs31.pathExists(configPath)) {
|
|
12682
|
-
const overwrite = await (0,
|
|
12857
|
+
const overwrite = await (0, import_prompts10.confirm)({
|
|
12683
12858
|
message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
|
|
12684
12859
|
default: false
|
|
12685
12860
|
});
|
|
@@ -12689,12 +12864,12 @@ function registerWorkspace(program2) {
|
|
|
12689
12864
|
}
|
|
12690
12865
|
}
|
|
12691
12866
|
console.log(import_chalk31.default.blue("\n\u2500\u2500\u2500 Workspace Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
12692
|
-
const workspaceName = await (0,
|
|
12867
|
+
const workspaceName = await (0, import_prompts10.input)({
|
|
12693
12868
|
message: "Workspace name:",
|
|
12694
12869
|
validate: (v2) => v2.trim().length > 0 || "Name cannot be empty"
|
|
12695
12870
|
});
|
|
12696
12871
|
const repos = [];
|
|
12697
|
-
const useAutoScan = await (0,
|
|
12872
|
+
const useAutoScan = await (0, import_prompts10.confirm)({
|
|
12698
12873
|
message: "Auto-scan sibling directories for repos?",
|
|
12699
12874
|
default: true
|
|
12700
12875
|
});
|
|
@@ -12708,7 +12883,7 @@ function registerWorkspace(program2) {
|
|
|
12708
12883
|
for (const r of detected) {
|
|
12709
12884
|
console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12710
12885
|
}
|
|
12711
|
-
const keepAll = await (0,
|
|
12886
|
+
const keepAll = await (0, import_prompts10.confirm)({
|
|
12712
12887
|
message: `Include all ${detected.length} detected repo(s)?`,
|
|
12713
12888
|
default: true
|
|
12714
12889
|
});
|
|
@@ -12716,7 +12891,7 @@ function registerWorkspace(program2) {
|
|
|
12716
12891
|
repos.push(...detected);
|
|
12717
12892
|
} else {
|
|
12718
12893
|
for (const r of detected) {
|
|
12719
|
-
const keep = await (0,
|
|
12894
|
+
const keep = await (0, import_prompts10.confirm)({
|
|
12720
12895
|
message: `Include "${r.name}" (${r.role}, ${r.type})?`,
|
|
12721
12896
|
default: true
|
|
12722
12897
|
});
|
|
@@ -12740,14 +12915,14 @@ function registerWorkspace(program2) {
|
|
|
12740
12915
|
{ name: "react-native (React Native mobile)", value: "react-native" },
|
|
12741
12916
|
{ name: "unknown", value: "unknown" }
|
|
12742
12917
|
];
|
|
12743
|
-
let addMore = await (0,
|
|
12918
|
+
let addMore = await (0, import_prompts10.confirm)({
|
|
12744
12919
|
message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
|
|
12745
12920
|
default: repos.length === 0
|
|
12746
12921
|
});
|
|
12747
12922
|
while (addMore) {
|
|
12748
12923
|
console.log(import_chalk31.default.cyan(`
|
|
12749
12924
|
Adding repo #${repos.length + 1}`));
|
|
12750
|
-
const repoName = await (0,
|
|
12925
|
+
const repoName = await (0, import_prompts10.input)({
|
|
12751
12926
|
message: "Repo name (e.g. api, web, app):",
|
|
12752
12927
|
validate: (v2) => {
|
|
12753
12928
|
if (!v2.trim()) return "Name cannot be empty";
|
|
@@ -12755,7 +12930,7 @@ function registerWorkspace(program2) {
|
|
|
12755
12930
|
return true;
|
|
12756
12931
|
}
|
|
12757
12932
|
});
|
|
12758
|
-
const repoPath = await (0,
|
|
12933
|
+
const repoPath = await (0, import_prompts10.input)({
|
|
12759
12934
|
message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
|
|
12760
12935
|
default: `./${repoName}`
|
|
12761
12936
|
});
|
|
@@ -12770,12 +12945,12 @@ function registerWorkspace(program2) {
|
|
|
12770
12945
|
} else {
|
|
12771
12946
|
console.log(import_chalk31.default.yellow(` Path "${absPath}" not found \u2014 type/role will be manual.`));
|
|
12772
12947
|
}
|
|
12773
|
-
const repoType = await (0,
|
|
12948
|
+
const repoType = await (0, import_prompts10.select)({
|
|
12774
12949
|
message: `Repo type for "${repoName}":`,
|
|
12775
12950
|
choices: repoTypeChoices,
|
|
12776
12951
|
default: detectedType
|
|
12777
12952
|
});
|
|
12778
|
-
const repoRole = await (0,
|
|
12953
|
+
const repoRole = await (0, import_prompts10.select)({
|
|
12779
12954
|
message: `Repo role for "${repoName}":`,
|
|
12780
12955
|
choices: [
|
|
12781
12956
|
{ name: "backend", value: "backend" },
|
|
@@ -12792,7 +12967,7 @@ function registerWorkspace(program2) {
|
|
|
12792
12967
|
role: repoRole
|
|
12793
12968
|
});
|
|
12794
12969
|
console.log(import_chalk31.default.green(` \u2714 Added: ${repoName} (${repoRole}, ${repoType})`));
|
|
12795
|
-
addMore = await (0,
|
|
12970
|
+
addMore = await (0, import_prompts10.confirm)({
|
|
12796
12971
|
message: "Add another repo?",
|
|
12797
12972
|
default: false
|
|
12798
12973
|
});
|
|
@@ -12803,7 +12978,7 @@ function registerWorkspace(program2) {
|
|
|
12803
12978
|
for (const r of repos) {
|
|
12804
12979
|
console.log(import_chalk31.default.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
|
|
12805
12980
|
}
|
|
12806
|
-
const ok = await (0,
|
|
12981
|
+
const ok = await (0, import_prompts10.confirm)({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
|
|
12807
12982
|
if (!ok) {
|
|
12808
12983
|
console.log(import_chalk31.default.gray(" Cancelled."));
|
|
12809
12984
|
return;
|
|
@@ -12847,7 +13022,7 @@ Workspace: ${config2.name}`));
|
|
|
12847
13022
|
var path32 = __toESM(require("path"));
|
|
12848
13023
|
var fs33 = __toESM(require("fs-extra"));
|
|
12849
13024
|
var import_chalk33 = __toESM(require("chalk"));
|
|
12850
|
-
var
|
|
13025
|
+
var import_prompts11 = require("@inquirer/prompts");
|
|
12851
13026
|
init_spec_generator();
|
|
12852
13027
|
|
|
12853
13028
|
// core/spec-updater.ts
|
|
@@ -13082,7 +13257,7 @@ function registerUpdate(program2) {
|
|
|
13082
13257
|
const currentDir = process.cwd();
|
|
13083
13258
|
const config2 = await loadConfig(currentDir);
|
|
13084
13259
|
if (!change) {
|
|
13085
|
-
change = await (0,
|
|
13260
|
+
change = await (0, import_prompts11.input)({
|
|
13086
13261
|
message: "Describe the change you want to make:",
|
|
13087
13262
|
validate: (v2) => v2.trim().length > 0 || "Change description cannot be empty"
|
|
13088
13263
|
});
|
|
@@ -13400,7 +13575,7 @@ function buildYamlDoc(obj) {
|
|
|
13400
13575
|
return `${k2}: ${valStr}`;
|
|
13401
13576
|
}).join("\n") + "\n";
|
|
13402
13577
|
}
|
|
13403
|
-
function dslToOpenApi(dsl, serverUrl =
|
|
13578
|
+
function dslToOpenApi(dsl, serverUrl = DEFAULT_OPENAPI_SERVER_URL) {
|
|
13404
13579
|
const info = {
|
|
13405
13580
|
title: dsl.feature.title,
|
|
13406
13581
|
description: dsl.feature.description,
|
|
@@ -13447,7 +13622,7 @@ function dslToOpenApi(dsl, serverUrl = "http://localhost:3000") {
|
|
|
13447
13622
|
}
|
|
13448
13623
|
async function exportOpenApi(dsl, projectDir, opts = {}) {
|
|
13449
13624
|
const format = opts.format ?? "yaml";
|
|
13450
|
-
const serverUrl = opts.serverUrl ??
|
|
13625
|
+
const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
|
|
13451
13626
|
const defaultName = `openapi.${format}`;
|
|
13452
13627
|
const outputPath = opts.outputPath ? path33.isAbsolute(opts.outputPath) ? opts.outputPath : path33.join(projectDir, opts.outputPath) : path33.join(projectDir, defaultName);
|
|
13453
13628
|
const doc = dslToOpenApi(dsl, serverUrl);
|
|
@@ -13643,12 +13818,12 @@ function registerMock(program2) {
|
|
|
13643
13818
|
|
|
13644
13819
|
// cli/commands/learn.ts
|
|
13645
13820
|
var import_chalk36 = __toESM(require("chalk"));
|
|
13646
|
-
var
|
|
13821
|
+
var import_prompts12 = require("@inquirer/prompts");
|
|
13647
13822
|
function registerLearn(program2) {
|
|
13648
13823
|
program2.command("learn").description("Append a lesson or engineering decision directly to constitution \xA79").argument("[lesson]", "The lesson or decision to record (prompted if omitted)").action(async (lesson) => {
|
|
13649
13824
|
const currentDir = process.cwd();
|
|
13650
13825
|
if (!lesson) {
|
|
13651
|
-
lesson = await (0,
|
|
13826
|
+
lesson = await (0, import_prompts12.input)({
|
|
13652
13827
|
message: "What lesson or engineering decision should be recorded?",
|
|
13653
13828
|
validate: (v2) => v2.trim().length > 0 || "Please enter a lesson"
|
|
13654
13829
|
});
|
|
@@ -13690,9 +13865,8 @@ var import_chalk39 = __toESM(require("chalk"));
|
|
|
13690
13865
|
var fs37 = __toESM(require("fs-extra"));
|
|
13691
13866
|
var path36 = __toESM(require("path"));
|
|
13692
13867
|
var import_chalk38 = __toESM(require("chalk"));
|
|
13693
|
-
var LOG_DIR2 = ".ai-spec-logs";
|
|
13694
13868
|
async function loadRunLogs(workingDir) {
|
|
13695
|
-
const logDir = path36.join(workingDir,
|
|
13869
|
+
const logDir = path36.join(workingDir, DEFAULT_LOG_DIR);
|
|
13696
13870
|
if (!await fs37.pathExists(logDir)) return [];
|
|
13697
13871
|
const files = await fs37.readdir(logDir);
|
|
13698
13872
|
const jsonFiles = new Set(files.filter((f) => f.endsWith(".json")));
|
|
@@ -13837,7 +14011,7 @@ function printTrendReport(report, workingDir) {
|
|
|
13837
14011
|
` ${import_chalk38.default.gray(formatDate(e.startedAt))} ${bar}${scoreStr} ${hash} ${dur}${errMark} ${spec}`
|
|
13838
14012
|
);
|
|
13839
14013
|
}
|
|
13840
|
-
const logRelDir = path36.relative(workingDir, path36.join(workingDir,
|
|
14014
|
+
const logRelDir = path36.relative(workingDir, path36.join(workingDir, DEFAULT_LOG_DIR));
|
|
13841
14015
|
console.log(import_chalk38.default.gray(`
|
|
13842
14016
|
${entries.length} run(s) shown \xB7 logs: ${logRelDir}/`));
|
|
13843
14017
|
console.log(import_chalk38.default.cyan("\u2500".repeat(63)));
|
|
@@ -14496,7 +14670,233 @@ function registerVcr(program2) {
|
|
|
14496
14670
|
|
|
14497
14671
|
// cli/commands/scan.ts
|
|
14498
14672
|
var import_chalk44 = __toESM(require("chalk"));
|
|
14673
|
+
var path42 = __toESM(require("path"));
|
|
14674
|
+
|
|
14675
|
+
// core/project-index.ts
|
|
14676
|
+
var fs42 = __toESM(require("fs-extra"));
|
|
14499
14677
|
var path41 = __toESM(require("path"));
|
|
14678
|
+
var INDEX_FILE = ".ai-spec-index.json";
|
|
14679
|
+
var KEY_DEPS = [
|
|
14680
|
+
// Frameworks
|
|
14681
|
+
"express",
|
|
14682
|
+
"fastify",
|
|
14683
|
+
"koa",
|
|
14684
|
+
"@nestjs/core",
|
|
14685
|
+
"hapi",
|
|
14686
|
+
"next",
|
|
14687
|
+
"react",
|
|
14688
|
+
"vue",
|
|
14689
|
+
"nuxt",
|
|
14690
|
+
"svelte",
|
|
14691
|
+
"react-native",
|
|
14692
|
+
"expo",
|
|
14693
|
+
// DB / ORM
|
|
14694
|
+
"prisma",
|
|
14695
|
+
"@prisma/client",
|
|
14696
|
+
"mongoose",
|
|
14697
|
+
"typeorm",
|
|
14698
|
+
"sequelize",
|
|
14699
|
+
"drizzle-orm",
|
|
14700
|
+
// Auth
|
|
14701
|
+
"jsonwebtoken",
|
|
14702
|
+
"passport",
|
|
14703
|
+
"next-auth",
|
|
14704
|
+
"@clerk/nextjs",
|
|
14705
|
+
// Build / Lang
|
|
14706
|
+
"typescript",
|
|
14707
|
+
"vite",
|
|
14708
|
+
"webpack",
|
|
14709
|
+
"esbuild",
|
|
14710
|
+
"turbo",
|
|
14711
|
+
// Testing
|
|
14712
|
+
"jest",
|
|
14713
|
+
"vitest",
|
|
14714
|
+
"mocha",
|
|
14715
|
+
"cypress",
|
|
14716
|
+
"playwright",
|
|
14717
|
+
// Infra
|
|
14718
|
+
"redis",
|
|
14719
|
+
"bull",
|
|
14720
|
+
"socket.io",
|
|
14721
|
+
"graphql",
|
|
14722
|
+
"@trpc/server"
|
|
14723
|
+
];
|
|
14724
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14725
|
+
"node_modules",
|
|
14726
|
+
".git",
|
|
14727
|
+
".svn",
|
|
14728
|
+
"dist",
|
|
14729
|
+
"build",
|
|
14730
|
+
"out",
|
|
14731
|
+
".next",
|
|
14732
|
+
".nuxt",
|
|
14733
|
+
"coverage",
|
|
14734
|
+
".turbo",
|
|
14735
|
+
".cache",
|
|
14736
|
+
"__pycache__",
|
|
14737
|
+
"vendor",
|
|
14738
|
+
".ai-spec-vcr",
|
|
14739
|
+
".ai-spec-logs",
|
|
14740
|
+
"specs"
|
|
14741
|
+
]);
|
|
14742
|
+
var MANIFEST_FILES = [
|
|
14743
|
+
"package.json",
|
|
14744
|
+
"go.mod",
|
|
14745
|
+
"Cargo.toml",
|
|
14746
|
+
"pom.xml",
|
|
14747
|
+
"build.gradle",
|
|
14748
|
+
"build.gradle.kts",
|
|
14749
|
+
"requirements.txt",
|
|
14750
|
+
"pyproject.toml",
|
|
14751
|
+
"setup.py",
|
|
14752
|
+
"composer.json"
|
|
14753
|
+
];
|
|
14754
|
+
async function isProjectRoot(absPath) {
|
|
14755
|
+
for (const manifest of MANIFEST_FILES) {
|
|
14756
|
+
if (await fs42.pathExists(path41.join(absPath, manifest))) return true;
|
|
14757
|
+
}
|
|
14758
|
+
return false;
|
|
14759
|
+
}
|
|
14760
|
+
async function extractTechStack(absPath, type) {
|
|
14761
|
+
const stack = [];
|
|
14762
|
+
if (type === "go") stack.push("go");
|
|
14763
|
+
if (type === "rust") stack.push("rust");
|
|
14764
|
+
if (type === "java") stack.push("java");
|
|
14765
|
+
if (type === "python") stack.push("python");
|
|
14766
|
+
if (type === "php") stack.push("php");
|
|
14767
|
+
const pkgPath = path41.join(absPath, "package.json");
|
|
14768
|
+
if (!await fs42.pathExists(pkgPath)) return stack;
|
|
14769
|
+
let pkg = {};
|
|
14770
|
+
try {
|
|
14771
|
+
pkg = await fs42.readJson(pkgPath);
|
|
14772
|
+
} catch {
|
|
14773
|
+
return stack;
|
|
14774
|
+
}
|
|
14775
|
+
const allDeps = {
|
|
14776
|
+
...pkg.dependencies ?? {},
|
|
14777
|
+
...pkg.devDependencies ?? {}
|
|
14778
|
+
};
|
|
14779
|
+
const depKeys = new Set(Object.keys(allDeps));
|
|
14780
|
+
for (const dep of KEY_DEPS) {
|
|
14781
|
+
if (depKeys.has(dep)) stack.push(dep);
|
|
14782
|
+
}
|
|
14783
|
+
return stack;
|
|
14784
|
+
}
|
|
14785
|
+
async function discoverProjects(rootDir, maxDepth) {
|
|
14786
|
+
const found = [];
|
|
14787
|
+
async function walk(absDir, depth) {
|
|
14788
|
+
if (depth > maxDepth) return;
|
|
14789
|
+
let entries;
|
|
14790
|
+
try {
|
|
14791
|
+
entries = await fs42.readdir(absDir, { withFileTypes: true });
|
|
14792
|
+
} catch {
|
|
14793
|
+
return;
|
|
14794
|
+
}
|
|
14795
|
+
for (const entry of entries) {
|
|
14796
|
+
if (!entry.isDirectory()) continue;
|
|
14797
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
14798
|
+
const childAbs = path41.join(absDir, entry.name);
|
|
14799
|
+
const gitPath = path41.join(childAbs, ".git");
|
|
14800
|
+
if (await fs42.pathExists(gitPath)) {
|
|
14801
|
+
const gitStat = await fs42.stat(gitPath);
|
|
14802
|
+
if (gitStat.isFile()) continue;
|
|
14803
|
+
}
|
|
14804
|
+
if (await isProjectRoot(childAbs)) {
|
|
14805
|
+
found.push(path41.relative(rootDir, childAbs));
|
|
14806
|
+
} else {
|
|
14807
|
+
await walk(childAbs, depth + 1);
|
|
14808
|
+
}
|
|
14809
|
+
}
|
|
14810
|
+
}
|
|
14811
|
+
await walk(rootDir, 0);
|
|
14812
|
+
return found;
|
|
14813
|
+
}
|
|
14814
|
+
async function loadIndex(scanRoot) {
|
|
14815
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14816
|
+
try {
|
|
14817
|
+
return await fs42.readJson(filePath);
|
|
14818
|
+
} catch {
|
|
14819
|
+
return null;
|
|
14820
|
+
}
|
|
14821
|
+
}
|
|
14822
|
+
async function saveIndex(scanRoot, index) {
|
|
14823
|
+
const filePath = path41.join(scanRoot, INDEX_FILE);
|
|
14824
|
+
await fs42.writeJson(filePath, index, { spaces: 2 });
|
|
14825
|
+
return filePath;
|
|
14826
|
+
}
|
|
14827
|
+
async function runScan(scanRoot, maxDepth = 2) {
|
|
14828
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14829
|
+
const existing = await loadIndex(scanRoot);
|
|
14830
|
+
const existingMap = new Map(
|
|
14831
|
+
(existing?.projects ?? []).map((p) => [p.path, p])
|
|
14832
|
+
);
|
|
14833
|
+
const discoveredPaths = await discoverProjects(scanRoot, maxDepth);
|
|
14834
|
+
const added = [];
|
|
14835
|
+
const updated = [];
|
|
14836
|
+
const unchanged = [];
|
|
14837
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
14838
|
+
for (const relPath of discoveredPaths) {
|
|
14839
|
+
const absPath = path41.join(scanRoot, relPath);
|
|
14840
|
+
seenPaths.add(relPath);
|
|
14841
|
+
const { type, role } = await detectRepoType(absPath);
|
|
14842
|
+
const techStack = await extractTechStack(absPath, type);
|
|
14843
|
+
const hasConstitution = await fs42.pathExists(path41.join(absPath, CONSTITUTION_FILE));
|
|
14844
|
+
const hasWorkspace = await fs42.pathExists(path41.join(absPath, WORKSPACE_CONFIG_FILE));
|
|
14845
|
+
const name = path41.basename(relPath);
|
|
14846
|
+
const prev = existingMap.get(relPath);
|
|
14847
|
+
if (!prev) {
|
|
14848
|
+
const entry = {
|
|
14849
|
+
name,
|
|
14850
|
+
path: relPath,
|
|
14851
|
+
type,
|
|
14852
|
+
role,
|
|
14853
|
+
techStack,
|
|
14854
|
+
hasConstitution,
|
|
14855
|
+
hasWorkspace,
|
|
14856
|
+
firstSeen: now,
|
|
14857
|
+
lastSeen: now
|
|
14858
|
+
};
|
|
14859
|
+
added.push(entry);
|
|
14860
|
+
existingMap.set(relPath, entry);
|
|
14861
|
+
} else {
|
|
14862
|
+
const changed = prev.type !== type || prev.role !== role || prev.hasConstitution !== hasConstitution || prev.hasWorkspace !== hasWorkspace || JSON.stringify(prev.techStack.sort()) !== JSON.stringify(techStack.sort());
|
|
14863
|
+
const entry = {
|
|
14864
|
+
...prev,
|
|
14865
|
+
type,
|
|
14866
|
+
role,
|
|
14867
|
+
techStack,
|
|
14868
|
+
hasConstitution,
|
|
14869
|
+
hasWorkspace,
|
|
14870
|
+
lastSeen: now,
|
|
14871
|
+
missing: void 0
|
|
14872
|
+
// clear missing flag if it came back
|
|
14873
|
+
};
|
|
14874
|
+
existingMap.set(relPath, entry);
|
|
14875
|
+
if (changed) {
|
|
14876
|
+
updated.push(entry);
|
|
14877
|
+
} else {
|
|
14878
|
+
unchanged.push(entry);
|
|
14879
|
+
}
|
|
14880
|
+
}
|
|
14881
|
+
}
|
|
14882
|
+
const nowMissing = [];
|
|
14883
|
+
for (const [relPath, entry] of existingMap) {
|
|
14884
|
+
if (!seenPaths.has(relPath) && !entry.missing) {
|
|
14885
|
+
const gone = { ...entry, missing: true };
|
|
14886
|
+
existingMap.set(relPath, gone);
|
|
14887
|
+
nowMissing.push(gone);
|
|
14888
|
+
}
|
|
14889
|
+
}
|
|
14890
|
+
const projects = [...existingMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
14891
|
+
const index = {
|
|
14892
|
+
scanRoot,
|
|
14893
|
+
lastScanned: now,
|
|
14894
|
+
projects
|
|
14895
|
+
};
|
|
14896
|
+
return { index, added, updated, unchanged, nowMissing };
|
|
14897
|
+
}
|
|
14898
|
+
|
|
14899
|
+
// cli/commands/scan.ts
|
|
14500
14900
|
var ROLE_COLOR = {
|
|
14501
14901
|
backend: import_chalk44.default.blue,
|
|
14502
14902
|
frontend: import_chalk44.default.green,
|
|
@@ -14570,7 +14970,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14570
14970
|
}
|
|
14571
14971
|
console.log(import_chalk44.default.cyan("\n\u2500".repeat(52)));
|
|
14572
14972
|
console.log(import_chalk44.default.gray(" \xA7C = has constitution W = workspace root"));
|
|
14573
|
-
console.log(import_chalk44.default.gray(` Index saved : ${
|
|
14973
|
+
console.log(import_chalk44.default.gray(` Index saved : ${path42.relative(cwd, path42.join(cwd, INDEX_FILE))}`));
|
|
14574
14974
|
console.log(import_chalk44.default.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
|
|
14575
14975
|
});
|
|
14576
14976
|
}
|
|
@@ -14578,7 +14978,7 @@ Scanning ${cwd} (depth: ${maxDepth})...`));
|
|
|
14578
14978
|
// cli/index.ts
|
|
14579
14979
|
dotenv.config();
|
|
14580
14980
|
var program = new import_commander.Command();
|
|
14581
|
-
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(
|
|
14981
|
+
program.name("ai-spec").description("AI-driven Development Orchestrator \u2014 spec, generate, review").version(require_package().version);
|
|
14582
14982
|
registerCreate(program);
|
|
14583
14983
|
registerReview(program);
|
|
14584
14984
|
registerInit(program);
|