ai-spec-dev 0.38.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/cli/commands/create.ts +9 -1176
- package/cli/commands/dashboard.ts +1 -1
- package/cli/pipeline/helpers.ts +34 -0
- package/cli/pipeline/multi-repo.ts +483 -0
- package/cli/pipeline/single-repo.ts +764 -0
- package/cli/utils.ts +2 -0
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +56 -343
- package/core/codegen/helpers.ts +219 -0
- package/core/codegen/topo-sort.ts +98 -0
- package/core/constitution-consolidator.ts +2 -2
- package/core/dsl-coverage-checker.ts +298 -0
- package/core/dsl-extractor.ts +19 -46
- package/core/dsl-feedback.ts +1 -1
- package/core/dsl-validator.ts +74 -0
- package/core/error-feedback.ts +99 -13
- package/core/frontend-context-loader.ts +27 -5
- package/core/knowledge-memory.ts +52 -0
- package/core/mock/fixtures.ts +89 -0
- package/core/mock/proxy.ts +380 -0
- package/core/mock-server-generator.ts +12 -460
- package/core/provider-utils.ts +8 -7
- package/core/requirement-decomposer.ts +4 -28
- package/core/reviewer.ts +1 -1
- package/core/safe-json.ts +76 -0
- package/core/spec-updater.ts +5 -21
- package/core/token-budget.ts +124 -0
- package/core/vcr.ts +20 -1
- package/demo-backend/.ai-spec-constitution.md +65 -0
- package/demo-backend/package.json +21 -0
- package/demo-backend/prisma/schema.prisma +22 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +186 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +211 -0
- package/demo-backend/src/controllers/bookmark.controller.test.ts +255 -0
- package/demo-backend/src/controllers/bookmark.controller.ts +187 -0
- package/demo-backend/src/index.ts +17 -0
- package/demo-backend/src/routes/bookmark.routes.test.ts +264 -0
- package/demo-backend/src/routes/bookmark.routes.ts +11 -0
- package/demo-backend/src/routes/index.ts +8 -0
- package/demo-backend/src/services/bookmark.service.test.ts +433 -0
- package/demo-backend/src/services/bookmark.service.ts +261 -0
- package/demo-backend/tsconfig.json +12 -0
- package/demo-frontend/.ai-spec-constitution.md +95 -0
- package/demo-frontend/package.json +23 -0
- package/demo-frontend/src/App.tsx +12 -0
- package/demo-frontend/src/main.tsx +9 -0
- package/demo-frontend/tsconfig.json +13 -0
- package/dist/cli/index.js +4351 -3666
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3997 -3312
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +388 -188
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +386 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/auto-consolidation.test.ts +109 -0
- package/tests/combined-generator.test.ts +81 -0
- package/tests/constitution-consolidator.test.ts +161 -0
- package/tests/constitution-generator.test.ts +94 -0
- package/tests/contract-bridge.test.ts +201 -0
- package/tests/design-dialogue.test.ts +108 -0
- package/tests/dsl-coverage-checker.test.ts +230 -0
- package/tests/dsl-feedback.test.ts +45 -0
- package/tests/dsl-validator-xref.test.ts +99 -0
- package/tests/error-feedback-repair.test.ts +319 -0
- package/tests/error-feedback-validation.test.ts +91 -0
- package/tests/frontend-context-loader.test.ts +609 -0
- package/tests/global-constitution.test.ts +110 -0
- package/tests/key-store.test.ts +73 -0
- package/tests/knowledge-memory.test.ts +327 -0
- package/tests/project-index.test.ts +206 -0
- package/tests/prompt-hasher.test.ts +19 -0
- package/tests/requirement-decomposer.test.ts +171 -0
- package/tests/reviewer.test.ts +4 -1
- package/tests/run-logger.test.ts +289 -0
- package/tests/run-snapshot.test.ts +113 -0
- package/tests/safe-json.test.ts +63 -0
- package/tests/spec-updater.test.ts +161 -0
- package/tests/test-generator.test.ts +146 -0
- package/tests/token-budget.test.ts +124 -0
- package/tests/vcr-hash.test.ts +101 -0
- package/tests/workspace-loader.test.ts +277 -0
- package/RELEASE_LOG.md +0 -2731
- package/purpose.md +0 -1294
package/dist/index.mjs
CHANGED
|
@@ -116,9 +116,77 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
|
|
|
116
116
|
3. \u5BF9\u4E8E\u6BCF\u6761\u76F4\u63A5\u76F8\u5173\u7684\u6559\u8BAD\uFF0C\u5728 \xA78 \u5B9E\u65BD\u8981\u70B9\u672B\u5C3E\u8FFD\u52A0\u4E00\u884C\uFF1A\u300C\u26A0 \u57FA\u4E8E\u5386\u53F2\u6559\u8BAD\uFF1A[\u7B80\u8FF0\u672C\u6B21 spec \u5982\u4F55\u89C4\u907F\u8BE5\u95EE\u9898]\u300D
|
|
117
117
|
4. \u5982\u65E0\u76F8\u5173\u6559\u8BAD\uFF0C\xA78 \u4E0D\u5FC5\u8FFD\u52A0\u4EFB\u4F55\u5185\u5BB9`;
|
|
118
118
|
|
|
119
|
-
// core/
|
|
119
|
+
// core/cli-ui.ts
|
|
120
120
|
import chalk from "chalk";
|
|
121
|
-
var
|
|
121
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
122
|
+
function startSpinner(text) {
|
|
123
|
+
const isTTY = process.stderr.isTTY;
|
|
124
|
+
let frame = 0;
|
|
125
|
+
let currentText = text;
|
|
126
|
+
let stopped = false;
|
|
127
|
+
function render() {
|
|
128
|
+
if (stopped) return;
|
|
129
|
+
const symbol = chalk.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
|
|
130
|
+
if (isTTY) {
|
|
131
|
+
process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
|
|
132
|
+
}
|
|
133
|
+
frame++;
|
|
134
|
+
}
|
|
135
|
+
if (!isTTY) {
|
|
136
|
+
process.stderr.write(` \u2026 ${currentText}
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
const timer = setInterval(render, 80);
|
|
140
|
+
render();
|
|
141
|
+
return {
|
|
142
|
+
update(newText) {
|
|
143
|
+
currentText = newText;
|
|
144
|
+
},
|
|
145
|
+
stop(finalText) {
|
|
146
|
+
if (stopped) return;
|
|
147
|
+
stopped = true;
|
|
148
|
+
clearInterval(timer);
|
|
149
|
+
if (isTTY) {
|
|
150
|
+
process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
|
|
151
|
+
}
|
|
152
|
+
if (finalText) {
|
|
153
|
+
process.stderr.write(` ${finalText}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
succeed(successText) {
|
|
158
|
+
this.stop(chalk.green(`\u2714 ${successText}`));
|
|
159
|
+
},
|
|
160
|
+
fail(failText) {
|
|
161
|
+
this.stop(chalk.red(`\u2718 ${failText}`));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function retryCountdown(opts) {
|
|
166
|
+
const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
|
|
167
|
+
const isTTY = process.stderr.isTTY;
|
|
168
|
+
const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
|
|
169
|
+
process.stderr.write("\n");
|
|
170
|
+
process.stderr.write(chalk.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + chalk.gray(`[${label}]`) + chalk.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
|
|
171
|
+
`));
|
|
172
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.white(shortErr) + "\n");
|
|
173
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.gray(`Waiting before retry...`) + "\n");
|
|
174
|
+
const totalSeconds = Math.ceil(waitMs / 1e3);
|
|
175
|
+
for (let s = totalSeconds; s > 0; s--) {
|
|
176
|
+
const bar = chalk.green("\u2588".repeat(totalSeconds - s)) + chalk.gray("\u2591".repeat(s));
|
|
177
|
+
const line = chalk.yellow(` \u2502 `) + `${bar} ${chalk.bold.white(`${s}s`)}`;
|
|
178
|
+
if (isTTY) {
|
|
179
|
+
process.stderr.write(`\r${line}${" ".repeat(10)}`);
|
|
180
|
+
}
|
|
181
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
182
|
+
}
|
|
183
|
+
if (isTTY) {
|
|
184
|
+
process.stderr.write(`\r${" ".repeat(70)}\r`);
|
|
185
|
+
}
|
|
186
|
+
process.stderr.write(chalk.yellow(` \u2514\u2500 `) + chalk.cyan(`Retrying now...`) + "\n\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// core/provider-utils.ts
|
|
122
190
|
var ProviderError = class extends Error {
|
|
123
191
|
constructor(message, kind, originalError) {
|
|
124
192
|
super(message);
|
|
@@ -202,11 +270,14 @@ async function withReliability(fn, opts) {
|
|
|
202
270
|
throw classifyError(err, label);
|
|
203
271
|
}
|
|
204
272
|
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
205
|
-
console.warn(
|
|
206
|
-
chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
|
|
207
|
-
);
|
|
208
273
|
onRetry?.(attempt + 1, err);
|
|
209
|
-
await
|
|
274
|
+
await retryCountdown({
|
|
275
|
+
attempt: attempt + 1,
|
|
276
|
+
maxAttempts: retries + 1,
|
|
277
|
+
waitMs,
|
|
278
|
+
errorMessage: err.message ?? String(err),
|
|
279
|
+
label
|
|
280
|
+
});
|
|
210
281
|
}
|
|
211
282
|
}
|
|
212
283
|
throw new Error("unreachable");
|
|
@@ -4107,8 +4178,8 @@ ${currentSpec}`,
|
|
|
4107
4178
|
};
|
|
4108
4179
|
|
|
4109
4180
|
// core/code-generator.ts
|
|
4110
|
-
import
|
|
4111
|
-
import { execSync, spawnSync } from "child_process";
|
|
4181
|
+
import chalk10 from "chalk";
|
|
4182
|
+
import { execSync as execSync2, spawnSync } from "child_process";
|
|
4112
4183
|
import * as path6 from "path";
|
|
4113
4184
|
import * as fs10 from "fs-extra";
|
|
4114
4185
|
|
|
@@ -4685,7 +4756,7 @@ async function updateTaskStatus(specFilePath, taskId, status) {
|
|
|
4685
4756
|
}
|
|
4686
4757
|
|
|
4687
4758
|
// core/dsl-extractor.ts
|
|
4688
|
-
import
|
|
4759
|
+
import chalk7 from "chalk";
|
|
4689
4760
|
import * as fs6 from "fs-extra";
|
|
4690
4761
|
import * as path4 from "path";
|
|
4691
4762
|
import { select as select2 } from "@inquirer/prompts";
|
|
@@ -4774,6 +4845,7 @@ function validateDsl(raw) {
|
|
|
4774
4845
|
}
|
|
4775
4846
|
}
|
|
4776
4847
|
}
|
|
4848
|
+
crossReferenceChecks(obj, errors);
|
|
4777
4849
|
if (errors.length > 0) {
|
|
4778
4850
|
return { valid: false, errors };
|
|
4779
4851
|
}
|
|
@@ -5005,6 +5077,64 @@ function validateComponent(raw, path10, errors) {
|
|
|
5005
5077
|
}
|
|
5006
5078
|
}
|
|
5007
5079
|
}
|
|
5080
|
+
function crossReferenceChecks(obj, errors) {
|
|
5081
|
+
const models = Array.isArray(obj["models"]) ? obj["models"] : [];
|
|
5082
|
+
const endpoints = Array.isArray(obj["endpoints"]) ? obj["endpoints"] : [];
|
|
5083
|
+
const components = Array.isArray(obj["components"]) ? obj["components"] : [];
|
|
5084
|
+
const seenRoutes = /* @__PURE__ */ new Map();
|
|
5085
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
5086
|
+
const ep = endpoints[i];
|
|
5087
|
+
if (typeof ep?.["method"] === "string" && typeof ep?.["path"] === "string") {
|
|
5088
|
+
const route = `${ep["method"].toUpperCase()} ${ep["path"]}`;
|
|
5089
|
+
if (seenRoutes.has(route)) {
|
|
5090
|
+
errors.push({
|
|
5091
|
+
path: `endpoints[${i}]`,
|
|
5092
|
+
message: `Duplicate route "${route}" \u2014 also defined at endpoints[${seenRoutes.get(route)}]`
|
|
5093
|
+
});
|
|
5094
|
+
} else {
|
|
5095
|
+
seenRoutes.set(route, i);
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
const modelNames = new Set(
|
|
5100
|
+
models.filter((m) => typeof m?.["name"] === "string").map((m) => m["name"])
|
|
5101
|
+
);
|
|
5102
|
+
for (let i = 0; i < models.length; i++) {
|
|
5103
|
+
const m = models[i];
|
|
5104
|
+
if (!Array.isArray(m?.["relations"])) continue;
|
|
5105
|
+
for (const rel of m["relations"]) {
|
|
5106
|
+
if (typeof rel !== "string") continue;
|
|
5107
|
+
const refMatch = rel.match(/(?:hasMany|hasOne|belongsTo|manyToMany)\s+(\w+)/i);
|
|
5108
|
+
if (refMatch) {
|
|
5109
|
+
const refName = refMatch[1];
|
|
5110
|
+
if (!modelNames.has(refName)) {
|
|
5111
|
+
errors.push({
|
|
5112
|
+
path: `models[${i}].relations`,
|
|
5113
|
+
message: `Relation references model "${refName}" which is not defined in models[]`
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
const endpointIds = new Set(
|
|
5120
|
+
endpoints.filter((e) => typeof e?.["id"] === "string").map((e) => e["id"])
|
|
5121
|
+
);
|
|
5122
|
+
if (endpointIds.size > 0) {
|
|
5123
|
+
for (let i = 0; i < components.length; i++) {
|
|
5124
|
+
const c = components[i];
|
|
5125
|
+
if (!Array.isArray(c?.["apiCalls"])) continue;
|
|
5126
|
+
for (const call of c["apiCalls"]) {
|
|
5127
|
+
if (typeof call !== "string") continue;
|
|
5128
|
+
if (!endpointIds.has(call)) {
|
|
5129
|
+
errors.push({
|
|
5130
|
+
path: `components[${i}].apiCalls`,
|
|
5131
|
+
message: `References endpoint "${call}" which is not defined in endpoints[]`
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5008
5138
|
function requireNonEmptyString(v2, path10, errors) {
|
|
5009
5139
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5010
5140
|
errors.push({
|
|
@@ -5019,6 +5149,26 @@ function typeLabel(v2) {
|
|
|
5019
5149
|
return typeof v2;
|
|
5020
5150
|
}
|
|
5021
5151
|
|
|
5152
|
+
// core/token-budget.ts
|
|
5153
|
+
import chalk6 from "chalk";
|
|
5154
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5155
|
+
function estimateTokens(text) {
|
|
5156
|
+
if (!text) return 0;
|
|
5157
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5158
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5159
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5160
|
+
}
|
|
5161
|
+
var DEFAULT_TOKEN_BUDGETS = {
|
|
5162
|
+
gemini: 9e5,
|
|
5163
|
+
claude: 18e4,
|
|
5164
|
+
openai: 12e4,
|
|
5165
|
+
deepseek: 6e4,
|
|
5166
|
+
default: 1e5
|
|
5167
|
+
};
|
|
5168
|
+
function getDefaultBudget(providerName) {
|
|
5169
|
+
return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5022
5172
|
// core/dsl-extractor.ts
|
|
5023
5173
|
function dslFilePath(specFilePath) {
|
|
5024
5174
|
const dir = path4.dirname(specFilePath);
|
|
@@ -5177,7 +5327,9 @@ var ROUTING_LIBS = [
|
|
|
5177
5327
|
["@tanstack/react-router", "tanstack-router"],
|
|
5178
5328
|
["react-navigation", "react-navigation"],
|
|
5179
5329
|
["expo-router", "expo-router"],
|
|
5180
|
-
["vue-router", "vue-router"]
|
|
5330
|
+
["vue-router", "vue-router"],
|
|
5331
|
+
["@solidjs/router", "solid-router"],
|
|
5332
|
+
["@builder.io/qwik-city", "qwik-city"]
|
|
5181
5333
|
];
|
|
5182
5334
|
async function loadFrontendContext(projectRoot) {
|
|
5183
5335
|
const ctx = {
|
|
@@ -5209,6 +5361,18 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5209
5361
|
const has = (name) => depKeys.includes(name);
|
|
5210
5362
|
if (has("react-native") || has("expo")) {
|
|
5211
5363
|
ctx.framework = "react-native";
|
|
5364
|
+
} else if (has("@sveltejs/kit")) {
|
|
5365
|
+
ctx.framework = "sveltekit";
|
|
5366
|
+
} else if (has("svelte")) {
|
|
5367
|
+
ctx.framework = "svelte";
|
|
5368
|
+
} else if (has("@builder.io/qwik")) {
|
|
5369
|
+
ctx.framework = "qwik";
|
|
5370
|
+
} else if (has("@remix-run/react") || has("@remix-run/node")) {
|
|
5371
|
+
ctx.framework = "remix";
|
|
5372
|
+
} else if (has("astro")) {
|
|
5373
|
+
ctx.framework = "astro";
|
|
5374
|
+
} else if (has("solid-js")) {
|
|
5375
|
+
ctx.framework = "solid";
|
|
5212
5376
|
} else if (has("next")) {
|
|
5213
5377
|
ctx.framework = "next";
|
|
5214
5378
|
} else if (has("react")) {
|
|
@@ -5235,6 +5399,12 @@ async function loadFrontendContext(projectRoot) {
|
|
|
5235
5399
|
if (ctx.framework === "next") {
|
|
5236
5400
|
const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
|
|
5237
5401
|
ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
|
|
5402
|
+
} else if (ctx.framework === "sveltekit") {
|
|
5403
|
+
ctx.routingPattern = "sveltekit-file-router";
|
|
5404
|
+
} else if (ctx.framework === "remix") {
|
|
5405
|
+
ctx.routingPattern = "remix-file-router";
|
|
5406
|
+
} else if (ctx.framework === "astro") {
|
|
5407
|
+
ctx.routingPattern = "astro-file-router";
|
|
5238
5408
|
} else {
|
|
5239
5409
|
for (const [lib, label] of ROUTING_LIBS) {
|
|
5240
5410
|
if (has(lib)) {
|
|
@@ -5620,13 +5790,14 @@ function getActiveSnapshot() {
|
|
|
5620
5790
|
|
|
5621
5791
|
// core/run-logger.ts
|
|
5622
5792
|
import * as fs9 from "fs-extra";
|
|
5623
|
-
import
|
|
5793
|
+
import chalk8 from "chalk";
|
|
5624
5794
|
var _activeLogger = null;
|
|
5625
5795
|
function getActiveLogger() {
|
|
5626
5796
|
return _activeLogger;
|
|
5627
5797
|
}
|
|
5628
5798
|
|
|
5629
|
-
// core/
|
|
5799
|
+
// core/codegen/helpers.ts
|
|
5800
|
+
import { execSync } from "child_process";
|
|
5630
5801
|
function buildSharedConfigSection(context) {
|
|
5631
5802
|
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
5632
5803
|
const lines = [
|
|
@@ -5758,7 +5929,7 @@ function buildGeneratedFilesSection(cache) {
|
|
|
5758
5929
|
}
|
|
5759
5930
|
function isRtkAvailable() {
|
|
5760
5931
|
try {
|
|
5761
|
-
execSync("rtk --version", { stdio: "ignore" });
|
|
5932
|
+
execSync("rtk --version", { stdio: "ignore", timeout: 1e4 });
|
|
5762
5933
|
return true;
|
|
5763
5934
|
} catch {
|
|
5764
5935
|
return false;
|
|
@@ -5782,6 +5953,73 @@ function parseJsonArray(text) {
|
|
|
5782
5953
|
}
|
|
5783
5954
|
return [];
|
|
5784
5955
|
}
|
|
5956
|
+
|
|
5957
|
+
// core/codegen/topo-sort.ts
|
|
5958
|
+
import chalk9 from "chalk";
|
|
5959
|
+
function topoSortLayerTasks(tasks) {
|
|
5960
|
+
if (tasks.length <= 1) return [tasks];
|
|
5961
|
+
const idSet = new Set(tasks.map((t) => t.id));
|
|
5962
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
5963
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
5964
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
5965
|
+
for (const task of tasks) {
|
|
5966
|
+
inDegree.set(task.id, 0);
|
|
5967
|
+
dependents.set(task.id, []);
|
|
5968
|
+
}
|
|
5969
|
+
for (const task of tasks) {
|
|
5970
|
+
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
5971
|
+
inDegree.set(task.id, intraDeps.length);
|
|
5972
|
+
for (const dep of intraDeps) {
|
|
5973
|
+
dependents.get(dep).push(task.id);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
const batches = [];
|
|
5977
|
+
const remaining = new Set(tasks.map((t) => t.id));
|
|
5978
|
+
while (remaining.size > 0) {
|
|
5979
|
+
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
5980
|
+
if (batch.length === 0) {
|
|
5981
|
+
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
5982
|
+
break;
|
|
5983
|
+
}
|
|
5984
|
+
batches.push(batch);
|
|
5985
|
+
for (const task of batch) {
|
|
5986
|
+
remaining.delete(task.id);
|
|
5987
|
+
for (const dependent of dependents.get(task.id)) {
|
|
5988
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
return batches;
|
|
5993
|
+
}
|
|
5994
|
+
var LAYER_ICONS = {
|
|
5995
|
+
data: "\u{1F4BE}",
|
|
5996
|
+
infra: "\u2699\uFE0F ",
|
|
5997
|
+
service: "\u{1F527}",
|
|
5998
|
+
api: "\u{1F310}",
|
|
5999
|
+
view: "\u{1F5A5}\uFE0F ",
|
|
6000
|
+
route: "\u{1F5FA}\uFE0F ",
|
|
6001
|
+
test: "\u{1F9EA}"
|
|
6002
|
+
};
|
|
6003
|
+
function printTaskProgress(completed, total, task, mode) {
|
|
6004
|
+
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6005
|
+
const barWidth = 20;
|
|
6006
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
6007
|
+
const bar = chalk9.green("\u2588".repeat(filled)) + chalk9.gray("\u2591".repeat(barWidth - filled));
|
|
6008
|
+
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6009
|
+
if (mode === "skip") {
|
|
6010
|
+
console.log(
|
|
6011
|
+
chalk9.gray(`
|
|
6012
|
+
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6013
|
+
);
|
|
6014
|
+
} else {
|
|
6015
|
+
console.log(
|
|
6016
|
+
chalk9.bold(`
|
|
6017
|
+
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
// core/code-generator.ts
|
|
5785
6023
|
var CodeGenerator = class {
|
|
5786
6024
|
constructor(provider, mode = "claude-code") {
|
|
5787
6025
|
this.provider = provider;
|
|
@@ -5792,13 +6030,13 @@ var CodeGenerator = class {
|
|
|
5792
6030
|
let effectiveMode = this.mode;
|
|
5793
6031
|
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5794
6032
|
console.log(
|
|
5795
|
-
|
|
6033
|
+
chalk10.yellow(
|
|
5796
6034
|
`
|
|
5797
6035
|
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5798
6036
|
)
|
|
5799
6037
|
);
|
|
5800
|
-
console.log(
|
|
5801
|
-
console.log(
|
|
6038
|
+
console.log(chalk10.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
6039
|
+
console.log(chalk10.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5802
6040
|
`));
|
|
5803
6041
|
effectiveMode = "api";
|
|
5804
6042
|
}
|
|
@@ -5816,23 +6054,23 @@ var CodeGenerator = class {
|
|
|
5816
6054
|
// ── Mode: claude-code ──────────────────────────────────────────────────────
|
|
5817
6055
|
isClaudeCLIAvailable() {
|
|
5818
6056
|
try {
|
|
5819
|
-
|
|
6057
|
+
execSync2("claude --version", { stdio: "ignore", timeout: 1e4 });
|
|
5820
6058
|
return true;
|
|
5821
6059
|
} catch {
|
|
5822
6060
|
return false;
|
|
5823
6061
|
}
|
|
5824
6062
|
}
|
|
5825
6063
|
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5826
|
-
console.log(
|
|
6064
|
+
console.log(chalk10.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5827
6065
|
if (!this.isClaudeCLIAvailable()) {
|
|
5828
|
-
console.log(
|
|
5829
|
-
console.log(
|
|
6066
|
+
console.log(chalk10.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
6067
|
+
console.log(chalk10.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
5830
6068
|
return this.runPlanMode(specFilePath);
|
|
5831
6069
|
}
|
|
5832
6070
|
const rtkAvailable = isRtkAvailable();
|
|
5833
6071
|
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5834
6072
|
if (rtkAvailable) {
|
|
5835
|
-
console.log(
|
|
6073
|
+
console.log(chalk10.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5836
6074
|
}
|
|
5837
6075
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5838
6076
|
if (options.auto && tasks && tasks.length > 0) {
|
|
@@ -5848,28 +6086,28 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5848
6086
|
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5849
6087
|
await fs10.writeFile(promptFile, promptContent, "utf-8");
|
|
5850
6088
|
if (options.auto) {
|
|
5851
|
-
console.log(
|
|
5852
|
-
console.log(
|
|
6089
|
+
console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6090
|
+
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
5853
6091
|
try {
|
|
5854
6092
|
spawnSync(claudeCmd, ["-p", promptContent], {
|
|
5855
6093
|
cwd: workingDir,
|
|
5856
6094
|
stdio: "inherit",
|
|
5857
6095
|
shell: false
|
|
5858
6096
|
});
|
|
5859
|
-
console.log(
|
|
6097
|
+
console.log(chalk10.green("\n \u2714 Claude Code completed."));
|
|
5860
6098
|
} catch {
|
|
5861
|
-
console.log(
|
|
6099
|
+
console.log(chalk10.yellow("\n Claude Code exited. Check output above."));
|
|
5862
6100
|
}
|
|
5863
6101
|
} else {
|
|
5864
|
-
console.log(
|
|
5865
|
-
console.log(
|
|
5866
|
-
if (tasks) console.log(
|
|
5867
|
-
console.log(
|
|
6102
|
+
console.log(chalk10.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
6103
|
+
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
6104
|
+
if (tasks) console.log(chalk10.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
6105
|
+
console.log(chalk10.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
5868
6106
|
try {
|
|
5869
|
-
|
|
5870
|
-
console.log(
|
|
6107
|
+
execSync2(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
6108
|
+
console.log(chalk10.green("\n \u2714 Claude Code session completed."));
|
|
5871
6109
|
} catch {
|
|
5872
|
-
console.log(
|
|
6110
|
+
console.log(chalk10.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
5873
6111
|
}
|
|
5874
6112
|
}
|
|
5875
6113
|
}
|
|
@@ -5882,10 +6120,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
5882
6120
|
const pending = tasks.filter((t) => t.status !== "done");
|
|
5883
6121
|
const doneCount = tasks.length - pending.length;
|
|
5884
6122
|
if (options.resume && doneCount > 0) {
|
|
5885
|
-
console.log(
|
|
6123
|
+
console.log(chalk10.cyan(`
|
|
5886
6124
|
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5887
6125
|
} else {
|
|
5888
|
-
console.log(
|
|
6126
|
+
console.log(chalk10.cyan(`
|
|
5889
6127
|
Incremental mode: ${tasks.length} tasks`));
|
|
5890
6128
|
}
|
|
5891
6129
|
let completed = doneCount;
|
|
@@ -5914,33 +6152,33 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
5914
6152
|
completed++;
|
|
5915
6153
|
} catch {
|
|
5916
6154
|
taskStatus = "failed";
|
|
5917
|
-
console.log(
|
|
6155
|
+
console.log(chalk10.yellow(`
|
|
5918
6156
|
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5919
6157
|
}
|
|
5920
6158
|
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5921
6159
|
}
|
|
5922
6160
|
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5923
6161
|
console.log(
|
|
5924
|
-
|
|
6162
|
+
chalk10.bold(
|
|
5925
6163
|
`
|
|
5926
|
-
${successCount === tasks.length ?
|
|
6164
|
+
${successCount === tasks.length ? chalk10.green("\u2714") : chalk10.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5927
6165
|
)
|
|
5928
6166
|
);
|
|
5929
6167
|
}
|
|
5930
6168
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
5931
6169
|
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
5932
6170
|
console.log(
|
|
5933
|
-
|
|
6171
|
+
chalk10.blue(
|
|
5934
6172
|
`
|
|
5935
6173
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
5936
6174
|
)
|
|
5937
6175
|
);
|
|
5938
6176
|
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
5939
6177
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
5940
|
-
console.log(
|
|
6178
|
+
console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
5941
6179
|
}
|
|
5942
6180
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
5943
|
-
|
|
6181
|
+
let constitutionSection = context?.constitution ? `
|
|
5944
6182
|
=== Project Constitution (MUST follow) ===
|
|
5945
6183
|
${context.constitution}
|
|
5946
6184
|
` : "";
|
|
@@ -5955,7 +6193,7 @@ ${buildDslContextSection(dsl)}
|
|
|
5955
6193
|
if (dsl) {
|
|
5956
6194
|
const cmpCount = dsl.components?.length ?? 0;
|
|
5957
6195
|
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
5958
|
-
console.log(
|
|
6196
|
+
console.log(chalk10.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
5959
6197
|
}
|
|
5960
6198
|
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
5961
6199
|
let frontendSection = "";
|
|
@@ -5964,13 +6202,30 @@ ${buildDslContextSection(dsl)}
|
|
|
5964
6202
|
frontendSection = `
|
|
5965
6203
|
${buildFrontendContextSection(fctx)}
|
|
5966
6204
|
`;
|
|
5967
|
-
console.log(
|
|
6205
|
+
console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6206
|
+
}
|
|
6207
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
|
|
6208
|
+
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6209
|
+
const budget = getDefaultBudget(this.provider.providerName);
|
|
6210
|
+
if (estimatedTokenCount > budget * 0.7) {
|
|
6211
|
+
console.log(
|
|
6212
|
+
chalk10.yellow(
|
|
6213
|
+
` \u26A0 Context size: ~${Math.round(estimatedTokenCount / 1e3)}K tokens (budget: ${Math.round(budget / 1e3)}K for ${this.provider.providerName})`
|
|
6214
|
+
)
|
|
6215
|
+
);
|
|
6216
|
+
if (constitutionSection.length > 4e3) {
|
|
6217
|
+
const s9Start = constitutionSection.indexOf("## 9.");
|
|
6218
|
+
if (s9Start > 0) {
|
|
6219
|
+
constitutionSection = constitutionSection.slice(0, s9Start) + "## 9. \u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons)\n[Trimmed for context budget \u2014 run `ai-spec init --consolidate` to prune]\n";
|
|
6220
|
+
console.log(chalk10.gray(" \u2192 \xA79 trimmed from constitution to save tokens."));
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
5968
6223
|
}
|
|
5969
6224
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5970
6225
|
if (tasks && tasks.length > 0) {
|
|
5971
6226
|
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
5972
6227
|
}
|
|
5973
|
-
console.log(
|
|
6228
|
+
console.log(chalk10.gray(" [1/2] Planning implementation files..."));
|
|
5974
6229
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
5975
6230
|
|
|
5976
6231
|
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
@@ -5993,18 +6248,18 @@ Output ONLY a valid JSON array:
|
|
|
5993
6248
|
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
5994
6249
|
filePlan = parseJsonArray(planResponse);
|
|
5995
6250
|
} catch (err) {
|
|
5996
|
-
console.error(
|
|
6251
|
+
console.error(chalk10.red(" Failed to generate file plan:"), err);
|
|
5997
6252
|
}
|
|
5998
6253
|
if (filePlan.length === 0) {
|
|
5999
|
-
console.log(
|
|
6254
|
+
console.log(chalk10.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
6000
6255
|
await this.runPlanMode(specFilePath);
|
|
6001
6256
|
return [];
|
|
6002
6257
|
}
|
|
6003
|
-
console.log(
|
|
6258
|
+
console.log(chalk10.cyan(`
|
|
6004
6259
|
Plan: ${filePlan.length} file(s) to process`));
|
|
6005
6260
|
filePlan.forEach((item) => {
|
|
6006
|
-
const icon = item.action === "create" ?
|
|
6007
|
-
console.log(` ${icon} ${item.file}: ${
|
|
6261
|
+
const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
|
|
6262
|
+
console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
|
|
6008
6263
|
});
|
|
6009
6264
|
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6010
6265
|
return files;
|
|
@@ -6013,13 +6268,13 @@ Output ONLY a valid JSON array:
|
|
|
6013
6268
|
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
6014
6269
|
const doneCount = tasks.length - pendingTasks.length;
|
|
6015
6270
|
if (options.resume && doneCount > 0) {
|
|
6016
|
-
console.log(
|
|
6017
|
-
Task-based generation (resume): ${tasks.length} tasks (${
|
|
6271
|
+
console.log(chalk10.cyan(`
|
|
6272
|
+
Task-based generation (resume): ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, skipping)`));
|
|
6018
6273
|
} else if (doneCount > 0) {
|
|
6019
|
-
console.log(
|
|
6020
|
-
Task-based generation: ${tasks.length} tasks (${
|
|
6274
|
+
console.log(chalk10.cyan(`
|
|
6275
|
+
Task-based generation: ${tasks.length} tasks (${chalk10.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
6021
6276
|
} else {
|
|
6022
|
-
console.log(
|
|
6277
|
+
console.log(chalk10.cyan(`
|
|
6023
6278
|
Task-based generation: ${tasks.length} tasks`));
|
|
6024
6279
|
}
|
|
6025
6280
|
const sharedConfigPaths = new Set(
|
|
@@ -6051,9 +6306,9 @@ Output ONLY a valid JSON array:
|
|
|
6051
6306
|
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
6052
6307
|
const barWidth = 20;
|
|
6053
6308
|
const filled = Math.round(pct / 100 * barWidth);
|
|
6054
|
-
const bar =
|
|
6309
|
+
const bar = chalk10.green("\u2588".repeat(filled)) + chalk10.gray("\u2591".repeat(barWidth - filled));
|
|
6055
6310
|
console.log(
|
|
6056
|
-
|
|
6311
|
+
chalk10.bold(`
|
|
6057
6312
|
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
6058
6313
|
);
|
|
6059
6314
|
} else {
|
|
@@ -6061,7 +6316,7 @@ Output ONLY a valid JSON array:
|
|
|
6061
6316
|
}
|
|
6062
6317
|
const executeTask = async (task, batchIsParallel) => {
|
|
6063
6318
|
if (task.filesToTouch.length === 0) {
|
|
6064
|
-
if (!batchIsParallel) console.log(
|
|
6319
|
+
if (!batchIsParallel) console.log(chalk10.gray(" No files specified, skipping."));
|
|
6065
6320
|
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
6066
6321
|
}
|
|
6067
6322
|
const filePlan = await Promise.all(
|
|
@@ -6118,13 +6373,19 @@ ${taskContext}`,
|
|
|
6118
6373
|
const layerResults = [];
|
|
6119
6374
|
for (const batch of taskBatches) {
|
|
6120
6375
|
const batchIsParallel = batch.length > 1;
|
|
6121
|
-
const batchResultPromises = batch.map(
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6376
|
+
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6377
|
+
const settled = await Promise.allSettled(batchResultPromises);
|
|
6378
|
+
const batchResults = [];
|
|
6379
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6380
|
+
const outcome = settled[i];
|
|
6381
|
+
if (outcome.status === "fulfilled") {
|
|
6382
|
+
batchResults.push(outcome.value);
|
|
6383
|
+
} else {
|
|
6384
|
+
const task = batch[i];
|
|
6385
|
+
console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6386
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6128
6389
|
layerResults.push(...batchResults);
|
|
6129
6390
|
await updateCacheFromBatch(batchResults);
|
|
6130
6391
|
}
|
|
@@ -6136,14 +6397,14 @@ ${taskContext}`,
|
|
|
6136
6397
|
totalFiles += result.total;
|
|
6137
6398
|
allGeneratedFiles.push(...result.files);
|
|
6138
6399
|
if (isParallel) {
|
|
6139
|
-
const icon = result.success === result.total ?
|
|
6400
|
+
const icon = result.success === result.total ? chalk10.green("\u2714") : chalk10.yellow("!");
|
|
6140
6401
|
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
6141
6402
|
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
6142
6403
|
}
|
|
6143
6404
|
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
6144
6405
|
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
6145
6406
|
if (taskStatus === "failed") {
|
|
6146
|
-
console.log(
|
|
6407
|
+
console.log(chalk10.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
6147
6408
|
}
|
|
6148
6409
|
}
|
|
6149
6410
|
completedTasks += layerTasks.length;
|
|
@@ -6158,7 +6419,7 @@ ${taskContext}`,
|
|
|
6158
6419
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
6159
6420
|
purpose = `Add to this file: import ${newModuleNames.join(", ")} from their respective paths and register them in the export/default array. Do NOT remove any existing imports.`;
|
|
6160
6421
|
}
|
|
6161
|
-
console.log(
|
|
6422
|
+
console.log(chalk10.gray(`
|
|
6162
6423
|
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
6163
6424
|
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
6164
6425
|
await this.generateFiles(
|
|
@@ -6176,17 +6437,17 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6176
6437
|
}
|
|
6177
6438
|
}
|
|
6178
6439
|
console.log(
|
|
6179
|
-
|
|
6440
|
+
chalk10.bold(
|
|
6180
6441
|
`
|
|
6181
|
-
${totalSuccess === totalFiles ?
|
|
6442
|
+
${totalSuccess === totalFiles ? chalk10.green("\u2714") : chalk10.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
6182
6443
|
)
|
|
6183
6444
|
);
|
|
6184
6445
|
return allGeneratedFiles;
|
|
6185
6446
|
}
|
|
6186
6447
|
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
6187
|
-
const prefix = taskLabel ? ` [${
|
|
6448
|
+
const prefix = taskLabel ? ` [${chalk10.cyan(taskLabel)}] ` : " ";
|
|
6188
6449
|
if (!taskLabel) {
|
|
6189
|
-
console.log(
|
|
6450
|
+
console.log(chalk10.gray(`
|
|
6190
6451
|
Generating ${filePlan.length} file(s)...`));
|
|
6191
6452
|
}
|
|
6192
6453
|
let successCount = 0;
|
|
@@ -6207,6 +6468,7 @@ ${spec}
|
|
|
6207
6468
|
${constitutionSection}
|
|
6208
6469
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
6209
6470
|
${existingContent || "Output only the complete file content."}`;
|
|
6471
|
+
const fileSpinner = startSpinner(`${prefix}Generating ${chalk10.bold(item.file)}...`);
|
|
6210
6472
|
try {
|
|
6211
6473
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
6212
6474
|
const fileContent = stripCodeFences(raw);
|
|
@@ -6214,17 +6476,17 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6214
6476
|
await fs10.ensureDir(path6.dirname(fullPath));
|
|
6215
6477
|
await fs10.writeFile(fullPath, fileContent, "utf-8");
|
|
6216
6478
|
getActiveLogger()?.fileWritten(item.file);
|
|
6217
|
-
|
|
6479
|
+
fileSpinner.succeed(`${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)}`);
|
|
6218
6480
|
successCount++;
|
|
6219
6481
|
writtenFiles.push(item.file);
|
|
6220
6482
|
} catch (err) {
|
|
6221
|
-
|
|
6483
|
+
fileSpinner.fail(`${chalk10.bold(item.file)} \u2014 ${err.message}`);
|
|
6222
6484
|
}
|
|
6223
6485
|
}
|
|
6224
6486
|
if (!taskLabel) {
|
|
6225
6487
|
console.log(
|
|
6226
|
-
|
|
6227
|
-
` ${successCount === filePlan.length ?
|
|
6488
|
+
chalk10.bold(
|
|
6489
|
+
` ${successCount === filePlan.length ? chalk10.green("\u2714") : chalk10.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
6228
6490
|
)
|
|
6229
6491
|
);
|
|
6230
6492
|
}
|
|
@@ -6232,7 +6494,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6232
6494
|
}
|
|
6233
6495
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6234
6496
|
async runPlanMode(specFilePath) {
|
|
6235
|
-
console.log(
|
|
6497
|
+
console.log(chalk10.blue("\n\u2500\u2500\u2500 Implementation Plan \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"));
|
|
6236
6498
|
const spec = await fs10.readFile(specFilePath, "utf-8");
|
|
6237
6499
|
const plan = await this.provider.generate(
|
|
6238
6500
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
@@ -6245,80 +6507,18 @@ Be specific about:
|
|
|
6245
6507
|
${spec}`,
|
|
6246
6508
|
"You are a senior developer creating an actionable implementation guide."
|
|
6247
6509
|
);
|
|
6248
|
-
console.log(
|
|
6249
|
-
}
|
|
6250
|
-
};
|
|
6251
|
-
function topoSortLayerTasks(tasks) {
|
|
6252
|
-
if (tasks.length <= 1) return [tasks];
|
|
6253
|
-
const idSet = new Set(tasks.map((t) => t.id));
|
|
6254
|
-
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
6255
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
6256
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
6257
|
-
for (const task of tasks) {
|
|
6258
|
-
inDegree.set(task.id, 0);
|
|
6259
|
-
dependents.set(task.id, []);
|
|
6260
|
-
}
|
|
6261
|
-
for (const task of tasks) {
|
|
6262
|
-
const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
|
|
6263
|
-
inDegree.set(task.id, intraDeps.length);
|
|
6264
|
-
for (const dep of intraDeps) {
|
|
6265
|
-
dependents.get(dep).push(task.id);
|
|
6266
|
-
}
|
|
6510
|
+
console.log(chalk10.cyan("\n") + plan);
|
|
6267
6511
|
}
|
|
6268
|
-
const batches = [];
|
|
6269
|
-
const remaining = new Set(tasks.map((t) => t.id));
|
|
6270
|
-
while (remaining.size > 0) {
|
|
6271
|
-
const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
|
|
6272
|
-
if (batch.length === 0) {
|
|
6273
|
-
batches.push([...remaining].map((id) => taskById.get(id)));
|
|
6274
|
-
break;
|
|
6275
|
-
}
|
|
6276
|
-
batches.push(batch);
|
|
6277
|
-
for (const task of batch) {
|
|
6278
|
-
remaining.delete(task.id);
|
|
6279
|
-
for (const dependent of dependents.get(task.id)) {
|
|
6280
|
-
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
6281
|
-
}
|
|
6282
|
-
}
|
|
6283
|
-
}
|
|
6284
|
-
return batches;
|
|
6285
|
-
}
|
|
6286
|
-
var LAYER_ICONS = {
|
|
6287
|
-
data: "\u{1F4BE}",
|
|
6288
|
-
infra: "\u2699\uFE0F ",
|
|
6289
|
-
service: "\u{1F527}",
|
|
6290
|
-
api: "\u{1F310}",
|
|
6291
|
-
view: "\u{1F5A5}\uFE0F ",
|
|
6292
|
-
route: "\u{1F5FA}\uFE0F ",
|
|
6293
|
-
test: "\u{1F9EA}"
|
|
6294
6512
|
};
|
|
6295
|
-
function printTaskProgress(completed, total, task, mode) {
|
|
6296
|
-
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
6297
|
-
const barWidth = 20;
|
|
6298
|
-
const filled = Math.round(pct / 100 * barWidth);
|
|
6299
|
-
const bar = chalk8.green("\u2588".repeat(filled)) + chalk8.gray("\u2591".repeat(barWidth - filled));
|
|
6300
|
-
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
6301
|
-
if (mode === "skip") {
|
|
6302
|
-
console.log(
|
|
6303
|
-
chalk8.gray(`
|
|
6304
|
-
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
6305
|
-
);
|
|
6306
|
-
} else {
|
|
6307
|
-
console.log(
|
|
6308
|
-
chalk8.bold(`
|
|
6309
|
-
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
6310
|
-
);
|
|
6311
|
-
}
|
|
6312
|
-
}
|
|
6313
6513
|
|
|
6314
6514
|
// core/reviewer.ts
|
|
6315
|
-
import
|
|
6316
|
-
import { execSync as
|
|
6515
|
+
import chalk12 from "chalk";
|
|
6516
|
+
import { execSync as execSync3 } from "child_process";
|
|
6317
6517
|
import * as path8 from "path";
|
|
6318
6518
|
import * as fs12 from "fs-extra";
|
|
6319
6519
|
|
|
6320
6520
|
// core/constitution-generator.ts
|
|
6321
|
-
import
|
|
6521
|
+
import chalk11 from "chalk";
|
|
6322
6522
|
import * as fs11 from "fs-extra";
|
|
6323
6523
|
import * as path7 from "path";
|
|
6324
6524
|
|
|
@@ -6468,7 +6668,7 @@ async function loadConstitution(projectRoot) {
|
|
|
6468
6668
|
function printConstitutionHint(exists) {
|
|
6469
6669
|
if (!exists) {
|
|
6470
6670
|
console.log(
|
|
6471
|
-
|
|
6671
|
+
chalk11.yellow(
|
|
6472
6672
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
6473
6673
|
)
|
|
6474
6674
|
);
|
|
@@ -6552,16 +6752,16 @@ var CodeReviewer = class {
|
|
|
6552
6752
|
this.projectRoot = projectRoot;
|
|
6553
6753
|
}
|
|
6554
6754
|
getGitDiff() {
|
|
6555
|
-
const silent = { encoding: "utf-8", stdio: "pipe" };
|
|
6755
|
+
const silent = { encoding: "utf-8", stdio: "pipe", cwd: this.projectRoot, timeout: 3e4 };
|
|
6556
6756
|
try {
|
|
6557
|
-
|
|
6757
|
+
execSync3("git rev-parse --is-inside-work-tree", silent);
|
|
6558
6758
|
} catch {
|
|
6559
6759
|
return "";
|
|
6560
6760
|
}
|
|
6561
6761
|
try {
|
|
6562
|
-
let diff =
|
|
6563
|
-
if (!diff.trim()) diff =
|
|
6564
|
-
if (!diff.trim()) diff =
|
|
6762
|
+
let diff = execSync3("git diff --cached", silent);
|
|
6763
|
+
if (!diff.trim()) diff = execSync3("git diff HEAD", silent);
|
|
6764
|
+
if (!diff.trim()) diff = execSync3("git diff", silent);
|
|
6565
6765
|
return diff;
|
|
6566
6766
|
} catch {
|
|
6567
6767
|
return "";
|
|
@@ -6586,7 +6786,7 @@ var CodeReviewer = class {
|
|
|
6586
6786
|
async runThreePassReview(specContent, codeContext, specFile) {
|
|
6587
6787
|
let complianceReview = "";
|
|
6588
6788
|
if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
|
|
6589
|
-
console.log(
|
|
6789
|
+
console.log(chalk12.gray(" Pass 0/3: Spec compliance check..."));
|
|
6590
6790
|
const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
|
|
6591
6791
|
|
|
6592
6792
|
=== Feature Spec ===
|
|
@@ -6598,13 +6798,13 @@ ${codeContext}`;
|
|
|
6598
6798
|
const complianceScore2 = extractComplianceScore(complianceReview);
|
|
6599
6799
|
const missingCount = extractMissingCount(complianceReview);
|
|
6600
6800
|
if (complianceScore2 > 0) {
|
|
6601
|
-
const scoreColor = complianceScore2 >= 8 ?
|
|
6801
|
+
const scoreColor = complianceScore2 >= 8 ? chalk12.green : complianceScore2 >= 6 ? chalk12.yellow : chalk12.red;
|
|
6602
6802
|
console.log(
|
|
6603
|
-
|
|
6803
|
+
chalk12.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? chalk12.red(` \xB7 ${missingCount} missing requirement(s)`) : chalk12.green(" \xB7 all requirements covered"))
|
|
6604
6804
|
);
|
|
6605
6805
|
}
|
|
6606
6806
|
}
|
|
6607
|
-
console.log(
|
|
6807
|
+
console.log(chalk12.gray(` Pass 1/3: Architecture review...`));
|
|
6608
6808
|
const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
|
|
6609
6809
|
const archPrompt = `Review the architecture of this change.
|
|
6610
6810
|
${complianceReview ? `
|
|
@@ -6621,7 +6821,7 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6621
6821
|
=== Code ===
|
|
6622
6822
|
${codeContext}`;
|
|
6623
6823
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6624
|
-
console.log(
|
|
6824
|
+
console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
|
|
6625
6825
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6626
6826
|
const historyContext = buildHistoryContext(history);
|
|
6627
6827
|
const implPrompt = `Review the implementation details of this change.
|
|
@@ -6636,7 +6836,7 @@ ${codeContext}
|
|
|
6636
6836
|
${archReview}
|
|
6637
6837
|
${historyContext}`;
|
|
6638
6838
|
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
6639
|
-
console.log(
|
|
6839
|
+
console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6640
6840
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6641
6841
|
|
|
6642
6842
|
=== Feature Spec ===
|
|
@@ -6677,37 +6877,37 @@ ${sep}
|
|
|
6677
6877
|
return combined;
|
|
6678
6878
|
}
|
|
6679
6879
|
async reviewCode(specContent, specFile) {
|
|
6680
|
-
console.log(
|
|
6880
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6681
6881
|
const diff = this.getGitDiff();
|
|
6682
6882
|
if (!diff.trim()) {
|
|
6683
6883
|
console.log(
|
|
6684
|
-
|
|
6884
|
+
chalk12.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
6685
6885
|
);
|
|
6686
|
-
console.log(
|
|
6886
|
+
console.log(chalk12.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
6687
6887
|
return "No changes";
|
|
6688
6888
|
}
|
|
6689
6889
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
6690
6890
|
console.log(
|
|
6691
|
-
|
|
6891
|
+
chalk12.gray(` Diff: ${files} file(s), ${chalk12.green("+" + added)} ${chalk12.red("-" + removed)}`)
|
|
6692
6892
|
);
|
|
6693
6893
|
console.log(
|
|
6694
|
-
|
|
6894
|
+
chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6695
6895
|
);
|
|
6696
6896
|
const codeContext = diff.slice(0, 1e4);
|
|
6697
6897
|
const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
|
|
6698
|
-
console.log(
|
|
6898
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6699
6899
|
console.log(reviewResult);
|
|
6700
|
-
console.log(
|
|
6900
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6701
6901
|
return reviewResult;
|
|
6702
6902
|
}
|
|
6703
6903
|
/**
|
|
6704
6904
|
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
6705
6905
|
*/
|
|
6706
6906
|
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
6707
|
-
console.log(
|
|
6708
|
-
console.log(
|
|
6907
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6908
|
+
console.log(chalk12.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
6709
6909
|
console.log(
|
|
6710
|
-
|
|
6910
|
+
chalk12.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
6711
6911
|
);
|
|
6712
6912
|
let filesSection = "";
|
|
6713
6913
|
for (const filePath of filePaths) {
|
|
@@ -6728,28 +6928,28 @@ ${content.slice(0, 3e3)}`;
|
|
|
6728
6928
|
}
|
|
6729
6929
|
}
|
|
6730
6930
|
const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
|
|
6731
|
-
console.log(
|
|
6931
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6732
6932
|
console.log(reviewResult);
|
|
6733
|
-
console.log(
|
|
6933
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
6734
6934
|
return reviewResult;
|
|
6735
6935
|
}
|
|
6736
6936
|
/** Print score trend from history (last N reviews) */
|
|
6737
6937
|
async printScoreTrend(limit = 5) {
|
|
6738
6938
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6739
6939
|
if (history.length === 0) {
|
|
6740
|
-
console.log(
|
|
6940
|
+
console.log(chalk12.gray(" No review history yet."));
|
|
6741
6941
|
return;
|
|
6742
6942
|
}
|
|
6743
6943
|
const recent = history.slice(-limit);
|
|
6744
|
-
console.log(
|
|
6944
|
+
console.log(chalk12.cyan("\n\u2500\u2500\u2500 Review Score Trend \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"));
|
|
6745
6945
|
for (const entry of recent) {
|
|
6746
6946
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
6747
|
-
const color = entry.score >= 8 ?
|
|
6748
|
-
const impactTag = entry.impactLevel ?
|
|
6749
|
-
const complexityTag = entry.complexityLevel ?
|
|
6947
|
+
const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
|
|
6948
|
+
const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
|
|
6949
|
+
const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
|
|
6750
6950
|
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
|
|
6751
6951
|
}
|
|
6752
|
-
console.log(
|
|
6952
|
+
console.log(chalk12.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6753
6953
|
}
|
|
6754
6954
|
};
|
|
6755
6955
|
|
|
@@ -6799,17 +6999,17 @@ function parseSpecAndTasks(raw) {
|
|
|
6799
6999
|
}
|
|
6800
7000
|
|
|
6801
7001
|
// git/worktree.ts
|
|
6802
|
-
import { execSync as
|
|
7002
|
+
import { execSync as execSync4 } from "child_process";
|
|
6803
7003
|
import * as path9 from "path";
|
|
6804
7004
|
import * as fs13 from "fs-extra";
|
|
6805
|
-
import
|
|
7005
|
+
import chalk13 from "chalk";
|
|
6806
7006
|
var GitWorktreeManager = class {
|
|
6807
7007
|
constructor(baseDir) {
|
|
6808
7008
|
this.baseDir = baseDir;
|
|
6809
7009
|
}
|
|
6810
7010
|
isGitRepo() {
|
|
6811
7011
|
try {
|
|
6812
|
-
|
|
7012
|
+
execSync4("git rev-parse --is-inside-work-tree", { cwd: this.baseDir, stdio: "ignore" });
|
|
6813
7013
|
return true;
|
|
6814
7014
|
} catch {
|
|
6815
7015
|
return false;
|
|
@@ -6833,58 +7033,58 @@ var GitWorktreeManager = class {
|
|
|
6833
7033
|
if (await fs13.pathExists(dest)) continue;
|
|
6834
7034
|
try {
|
|
6835
7035
|
await fs13.ensureSymlink(src, dest, "dir");
|
|
6836
|
-
console.log(
|
|
7036
|
+
console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6837
7037
|
} catch (err) {
|
|
6838
|
-
console.log(
|
|
6839
|
-
console.log(
|
|
7038
|
+
console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
7039
|
+
console.log(chalk13.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6840
7040
|
}
|
|
6841
7041
|
}
|
|
6842
7042
|
}
|
|
6843
7043
|
async createWorktree(idea) {
|
|
6844
7044
|
if (!this.isGitRepo()) {
|
|
6845
|
-
console.log(
|
|
7045
|
+
console.log(chalk13.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
6846
7046
|
return null;
|
|
6847
7047
|
}
|
|
6848
7048
|
const featureName = this.sanitizeFeatureName(idea);
|
|
6849
7049
|
const branchName = `feature/${featureName}`;
|
|
6850
7050
|
const repoName = path9.basename(this.baseDir);
|
|
6851
7051
|
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6852
|
-
console.log(
|
|
7052
|
+
console.log(chalk13.cyan(`
|
|
6853
7053
|
--- Setting up Git Worktree ---`));
|
|
6854
7054
|
if (await fs13.pathExists(worktreePath)) {
|
|
6855
|
-
console.log(
|
|
7055
|
+
console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6856
7056
|
await this.linkDependencies(worktreePath);
|
|
6857
7057
|
return worktreePath;
|
|
6858
7058
|
}
|
|
6859
7059
|
try {
|
|
6860
7060
|
let branchExists = false;
|
|
6861
7061
|
try {
|
|
6862
|
-
|
|
7062
|
+
execSync4(`git show-ref --verify refs/heads/${branchName}`, {
|
|
6863
7063
|
cwd: this.baseDir,
|
|
6864
7064
|
stdio: "ignore"
|
|
6865
7065
|
});
|
|
6866
7066
|
branchExists = true;
|
|
6867
7067
|
} catch {
|
|
6868
7068
|
}
|
|
6869
|
-
console.log(
|
|
7069
|
+
console.log(chalk13.gray(`Creating worktree at: ${worktreePath}`));
|
|
6870
7070
|
if (branchExists) {
|
|
6871
|
-
|
|
7071
|
+
execSync4(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6872
7072
|
cwd: this.baseDir,
|
|
6873
7073
|
stdio: "inherit"
|
|
6874
7074
|
});
|
|
6875
7075
|
} else {
|
|
6876
|
-
|
|
7076
|
+
execSync4(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
6877
7077
|
cwd: this.baseDir,
|
|
6878
7078
|
stdio: "inherit"
|
|
6879
7079
|
});
|
|
6880
7080
|
}
|
|
6881
7081
|
console.log(
|
|
6882
|
-
|
|
7082
|
+
chalk13.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6883
7083
|
);
|
|
6884
7084
|
await this.linkDependencies(worktreePath);
|
|
6885
7085
|
return worktreePath;
|
|
6886
7086
|
} catch (error) {
|
|
6887
|
-
console.error(
|
|
7087
|
+
console.error(chalk13.red("Failed to create git worktree:"), error);
|
|
6888
7088
|
return null;
|
|
6889
7089
|
}
|
|
6890
7090
|
}
|