ai-spec-dev 0.41.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/pipeline/single-repo.ts +19 -10
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +4 -2
- package/core/error-feedback.ts +4 -2
- package/core/provider-utils.ts +8 -7
- 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 +130 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +130 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js +80 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/RELEASE_LOG.md +0 -2962
- package/purpose.md +0 -1434
package/dist/cli/index.mjs
CHANGED
|
@@ -126,8 +126,101 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
|
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
// core/
|
|
129
|
+
// core/cli-ui.ts
|
|
130
130
|
import chalk from "chalk";
|
|
131
|
+
function startSpinner(text) {
|
|
132
|
+
const isTTY = process.stderr.isTTY;
|
|
133
|
+
let frame = 0;
|
|
134
|
+
let currentText = text;
|
|
135
|
+
let stopped = false;
|
|
136
|
+
function render() {
|
|
137
|
+
if (stopped) return;
|
|
138
|
+
const symbol = chalk.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
|
|
139
|
+
if (isTTY) {
|
|
140
|
+
process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
|
|
141
|
+
}
|
|
142
|
+
frame++;
|
|
143
|
+
}
|
|
144
|
+
if (!isTTY) {
|
|
145
|
+
process.stderr.write(` \u2026 ${currentText}
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
const timer = setInterval(render, 80);
|
|
149
|
+
render();
|
|
150
|
+
return {
|
|
151
|
+
update(newText) {
|
|
152
|
+
currentText = newText;
|
|
153
|
+
},
|
|
154
|
+
stop(finalText) {
|
|
155
|
+
if (stopped) return;
|
|
156
|
+
stopped = true;
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
if (isTTY) {
|
|
159
|
+
process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
|
|
160
|
+
}
|
|
161
|
+
if (finalText) {
|
|
162
|
+
process.stderr.write(` ${finalText}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
succeed(successText) {
|
|
167
|
+
this.stop(chalk.green(`\u2714 ${successText}`));
|
|
168
|
+
},
|
|
169
|
+
fail(failText) {
|
|
170
|
+
this.stop(chalk.red(`\u2718 ${failText}`));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function retryCountdown(opts) {
|
|
175
|
+
const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
|
|
176
|
+
const isTTY = process.stderr.isTTY;
|
|
177
|
+
const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
|
|
178
|
+
process.stderr.write("\n");
|
|
179
|
+
process.stderr.write(chalk.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + chalk.gray(`[${label}]`) + chalk.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
|
|
180
|
+
`));
|
|
181
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.white(shortErr) + "\n");
|
|
182
|
+
process.stderr.write(chalk.yellow(` \u2502 `) + chalk.gray(`Waiting before retry...`) + "\n");
|
|
183
|
+
const totalSeconds = Math.ceil(waitMs / 1e3);
|
|
184
|
+
for (let s = totalSeconds; s > 0; s--) {
|
|
185
|
+
const bar = chalk.green("\u2588".repeat(totalSeconds - s)) + chalk.gray("\u2591".repeat(s));
|
|
186
|
+
const line = chalk.yellow(` \u2502 `) + `${bar} ${chalk.bold.white(`${s}s`)}`;
|
|
187
|
+
if (isTTY) {
|
|
188
|
+
process.stderr.write(`\r${line}${" ".repeat(10)}`);
|
|
189
|
+
}
|
|
190
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
191
|
+
}
|
|
192
|
+
if (isTTY) {
|
|
193
|
+
process.stderr.write(`\r${" ".repeat(70)}\r`);
|
|
194
|
+
}
|
|
195
|
+
process.stderr.write(chalk.yellow(` \u2514\u2500 `) + chalk.cyan(`Retrying now...`) + "\n\n");
|
|
196
|
+
}
|
|
197
|
+
function startStage(stageKey, label) {
|
|
198
|
+
const icon = STAGE_ICONS[stageKey] ?? "\u25B8";
|
|
199
|
+
return startSpinner(`${icon} ${label}`);
|
|
200
|
+
}
|
|
201
|
+
var SPINNER_FRAMES, STAGE_ICONS;
|
|
202
|
+
var init_cli_ui = __esm({
|
|
203
|
+
"core/cli-ui.ts"() {
|
|
204
|
+
"use strict";
|
|
205
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
206
|
+
STAGE_ICONS = {
|
|
207
|
+
context_load: "\u{1F4C2}",
|
|
208
|
+
design_dialogue: "\u{1F4AC}",
|
|
209
|
+
spec_gen: "\u{1F4DD}",
|
|
210
|
+
spec_refine: "\u270F\uFE0F ",
|
|
211
|
+
spec_assess: "\u{1F4CA}",
|
|
212
|
+
dsl_extract: "\u{1F517}",
|
|
213
|
+
dsl_gap_feedback: "\u{1F50D}",
|
|
214
|
+
codegen: "\u2699\uFE0F ",
|
|
215
|
+
test_gen: "\u{1F9EA}",
|
|
216
|
+
error_feedback: "\u{1F527}",
|
|
217
|
+
review: "\u{1F50E}",
|
|
218
|
+
self_eval: "\u{1F4C8}"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// core/provider-utils.ts
|
|
131
224
|
function classifyError(err, label) {
|
|
132
225
|
const e = err;
|
|
133
226
|
const status = e.status ?? e.response?.status;
|
|
@@ -203,20 +296,23 @@ async function withReliability(fn, opts) {
|
|
|
203
296
|
throw classifyError(err, label);
|
|
204
297
|
}
|
|
205
298
|
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
206
|
-
console.warn(
|
|
207
|
-
chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
|
|
208
|
-
);
|
|
209
299
|
onRetry?.(attempt + 1, err);
|
|
210
|
-
await
|
|
300
|
+
await retryCountdown({
|
|
301
|
+
attempt: attempt + 1,
|
|
302
|
+
maxAttempts: retries + 1,
|
|
303
|
+
waitMs,
|
|
304
|
+
errorMessage: err.message ?? String(err),
|
|
305
|
+
label
|
|
306
|
+
});
|
|
211
307
|
}
|
|
212
308
|
}
|
|
213
309
|
throw new Error("unreachable");
|
|
214
310
|
}
|
|
215
|
-
var
|
|
311
|
+
var ProviderError;
|
|
216
312
|
var init_provider_utils = __esm({
|
|
217
313
|
"core/provider-utils.ts"() {
|
|
218
314
|
"use strict";
|
|
219
|
-
|
|
315
|
+
init_cli_ui();
|
|
220
316
|
ProviderError = class extends Error {
|
|
221
317
|
constructor(message, kind, originalError) {
|
|
222
318
|
super(message);
|
|
@@ -6690,6 +6786,7 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6690
6786
|
}
|
|
6691
6787
|
|
|
6692
6788
|
// core/code-generator.ts
|
|
6789
|
+
init_cli_ui();
|
|
6693
6790
|
var CodeGenerator = class {
|
|
6694
6791
|
constructor(provider, mode = "claude-code") {
|
|
6695
6792
|
this.provider = provider;
|
|
@@ -7138,6 +7235,7 @@ ${spec}
|
|
|
7138
7235
|
${constitutionSection}
|
|
7139
7236
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
7140
7237
|
${existingContent || "Output only the complete file content."}`;
|
|
7238
|
+
const fileSpinner = startSpinner(`${prefix}Generating ${chalk9.bold(item.file)}...`);
|
|
7141
7239
|
try {
|
|
7142
7240
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
7143
7241
|
const fileContent = stripCodeFences(raw);
|
|
@@ -7145,11 +7243,11 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
7145
7243
|
await fs12.ensureDir(path11.dirname(fullPath));
|
|
7146
7244
|
await fs12.writeFile(fullPath, fileContent, "utf-8");
|
|
7147
7245
|
getActiveLogger()?.fileWritten(item.file);
|
|
7148
|
-
|
|
7246
|
+
fileSpinner.succeed(`${existingContent ? chalk9.yellow("~") : chalk9.green("+")} ${chalk9.bold(item.file)}`);
|
|
7149
7247
|
successCount++;
|
|
7150
7248
|
writtenFiles.push(item.file);
|
|
7151
7249
|
} catch (err) {
|
|
7152
|
-
|
|
7250
|
+
fileSpinner.fail(`${chalk9.bold(item.file)} \u2014 ${err.message}`);
|
|
7153
7251
|
}
|
|
7154
7252
|
}
|
|
7155
7253
|
if (!taskLabel) {
|
|
@@ -8175,6 +8273,7 @@ import chalk15 from "chalk";
|
|
|
8175
8273
|
import { execSync as execSync5 } from "child_process";
|
|
8176
8274
|
import * as fs18 from "fs-extra";
|
|
8177
8275
|
import * as path17 from "path";
|
|
8276
|
+
init_cli_ui();
|
|
8178
8277
|
var MAX_COMMAND_OUTPUT_CHARS = 5e4;
|
|
8179
8278
|
var MAX_FIX_FILE_CHARS = 6e4;
|
|
8180
8279
|
function runCommand(cmd, cwd) {
|
|
@@ -8366,16 +8465,17 @@ ${errorSummary}
|
|
|
8366
8465
|
${fileContent}
|
|
8367
8466
|
|
|
8368
8467
|
Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
|
|
8468
|
+
const fixSpinner = startSpinner(`Fixing ${chalk15.bold(file)} (${fileErrors.length} error(s))...`);
|
|
8369
8469
|
try {
|
|
8370
8470
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
8371
8471
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
8372
8472
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
8373
8473
|
await fs18.writeFile(fullPath, fixed, "utf-8");
|
|
8374
8474
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
8375
|
-
|
|
8475
|
+
fixSpinner.succeed(`Auto-fixed: ${file}`);
|
|
8376
8476
|
} catch (err) {
|
|
8377
8477
|
results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
|
|
8378
|
-
|
|
8478
|
+
fixSpinner.fail(`Could not auto-fix: ${file}`);
|
|
8379
8479
|
}
|
|
8380
8480
|
}
|
|
8381
8481
|
return results;
|
|
@@ -11251,6 +11351,7 @@ async function listVcrRecordings(workingDir) {
|
|
|
11251
11351
|
}
|
|
11252
11352
|
|
|
11253
11353
|
// cli/pipeline/single-repo.ts
|
|
11354
|
+
init_cli_ui();
|
|
11254
11355
|
async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
11255
11356
|
const specProviderName = opts.provider || config2.provider || "gemini";
|
|
11256
11357
|
const specModelName = opts.model || config2.model || DEFAULT_MODELS[specProviderName];
|
|
@@ -11313,7 +11414,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11313
11414
|
console.log(chalk25.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11314
11415
|
}
|
|
11315
11416
|
} else {
|
|
11316
|
-
|
|
11417
|
+
const constitutionSpinner = startSpinner("Constitution not found \u2014 auto-generating...");
|
|
11317
11418
|
try {
|
|
11318
11419
|
const constitutionGen = new ConstitutionGenerator(
|
|
11319
11420
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -11321,9 +11422,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11321
11422
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
11322
11423
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
11323
11424
|
context.constitution = constitutionContent;
|
|
11324
|
-
|
|
11425
|
+
constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
|
|
11325
11426
|
} catch (err) {
|
|
11326
|
-
|
|
11427
|
+
constitutionSpinner.fail(`Constitution auto-generation failed (${err.message}), continuing without it.`);
|
|
11327
11428
|
}
|
|
11328
11429
|
}
|
|
11329
11430
|
let architectureDecision;
|
|
@@ -11354,17 +11455,18 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11354
11455
|
let initialSpec;
|
|
11355
11456
|
let initialTasks = [];
|
|
11356
11457
|
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
11458
|
+
const specSpinner = startStage("spec_gen", `Generating spec with ${specProviderName}/${specModelName}...`);
|
|
11357
11459
|
try {
|
|
11358
11460
|
if (opts.skipTasks) {
|
|
11359
11461
|
const { SpecGenerator: SpecGenerator2 } = await Promise.resolve().then(() => (init_spec_generator(), spec_generator_exports));
|
|
11360
11462
|
const generator = new SpecGenerator2(specProvider);
|
|
11361
11463
|
initialSpec = await generator.generateSpec(idea, context, architectureDecision);
|
|
11362
|
-
|
|
11464
|
+
specSpinner.succeed("Spec generated.");
|
|
11363
11465
|
} else {
|
|
11364
11466
|
const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
|
|
11365
11467
|
initialSpec = result.spec;
|
|
11366
11468
|
initialTasks = result.tasks;
|
|
11367
|
-
|
|
11469
|
+
specSpinner.succeed("Spec generated.");
|
|
11368
11470
|
if (initialTasks.length > 0) {
|
|
11369
11471
|
console.log(chalk25.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
11370
11472
|
} else {
|
|
@@ -11373,8 +11475,8 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11373
11475
|
}
|
|
11374
11476
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
11375
11477
|
} catch (err) {
|
|
11478
|
+
specSpinner.fail(`Spec generation failed: ${err.message}`);
|
|
11376
11479
|
runLogger.stageFail("spec_gen", err.message);
|
|
11377
|
-
console.error(chalk25.red(" \u2718 Spec generation failed:"), err);
|
|
11378
11480
|
process.exit(1);
|
|
11379
11481
|
}
|
|
11380
11482
|
let finalSpec;
|
|
@@ -11396,7 +11498,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11396
11498
|
console.log(chalk25.blue("\n[3.4/6] Spec quality assessment..."));
|
|
11397
11499
|
}
|
|
11398
11500
|
runLogger.stageStart("spec_assess");
|
|
11501
|
+
const assessSpinner = startStage("spec_assess", "Evaluating spec quality...");
|
|
11399
11502
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
11503
|
+
assessSpinner.stop();
|
|
11400
11504
|
if (assessment) {
|
|
11401
11505
|
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
11402
11506
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
@@ -11488,21 +11592,24 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11488
11592
|
console.log(chalk25.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
11489
11593
|
console.log(chalk25.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
11490
11594
|
runLogger.stageStart("dsl_extract");
|
|
11595
|
+
const dslSpinner = startStage("dsl_extract", "Extracting DSL from spec...");
|
|
11491
11596
|
try {
|
|
11492
11597
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
11493
|
-
if (isFrontend)
|
|
11598
|
+
if (isFrontend) {
|
|
11599
|
+
dslSpinner.update("\u{1F517} Extracting DSL (frontend ComponentSpec mode)...");
|
|
11600
|
+
}
|
|
11494
11601
|
const dslExtractor = new DslExtractor(specProvider);
|
|
11495
11602
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
11496
11603
|
if (extractedDsl) {
|
|
11497
11604
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
11498
|
-
|
|
11605
|
+
dslSpinner.succeed("DSL extracted and validated.");
|
|
11499
11606
|
} else {
|
|
11500
11607
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
11501
|
-
|
|
11608
|
+
dslSpinner.fail("DSL skipped \u2014 codegen will use Spec + Tasks only.");
|
|
11502
11609
|
}
|
|
11503
11610
|
} catch (err) {
|
|
11504
11611
|
runLogger.stageFail("dsl_extract", err.message);
|
|
11505
|
-
|
|
11612
|
+
dslSpinner.fail(`DSL extraction error: ${err.message} \u2014 continuing without DSL.`);
|
|
11506
11613
|
}
|
|
11507
11614
|
}
|
|
11508
11615
|
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
@@ -11677,6 +11784,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11677
11784
|
if (!opts.skipReview) {
|
|
11678
11785
|
console.log(chalk25.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
11679
11786
|
runLogger.stageStart("review");
|
|
11787
|
+
const reviewSpinner = startStage("review", "Running 3-pass code review...");
|
|
11680
11788
|
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
11681
11789
|
const savedSpec = await fs25.readFile(specFile, "utf-8");
|
|
11682
11790
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
@@ -11684,6 +11792,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11684
11792
|
} else {
|
|
11685
11793
|
reviewResult = await reviewer.reviewCode(savedSpec, specFile);
|
|
11686
11794
|
}
|
|
11795
|
+
reviewSpinner.succeed("Code review complete.");
|
|
11687
11796
|
runLogger.stageEnd("review");
|
|
11688
11797
|
const complianceScore = extractComplianceScore(reviewResult);
|
|
11689
11798
|
const missingCount = extractMissingCount(reviewResult);
|