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.
Files changed (37) hide show
  1. package/.ai-spec-workspace.json +17 -0
  2. package/.ai-spec.json +7 -0
  3. package/cli/pipeline/single-repo.ts +19 -10
  4. package/core/cli-ui.ts +136 -0
  5. package/core/code-generator.ts +4 -2
  6. package/core/error-feedback.ts +4 -2
  7. package/core/provider-utils.ts +8 -7
  8. package/demo-backend/.ai-spec-constitution.md +65 -0
  9. package/demo-backend/package.json +21 -0
  10. package/demo-backend/prisma/schema.prisma +22 -0
  11. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +186 -0
  12. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +211 -0
  13. package/demo-backend/src/controllers/bookmark.controller.test.ts +255 -0
  14. package/demo-backend/src/controllers/bookmark.controller.ts +187 -0
  15. package/demo-backend/src/index.ts +17 -0
  16. package/demo-backend/src/routes/bookmark.routes.test.ts +264 -0
  17. package/demo-backend/src/routes/bookmark.routes.ts +11 -0
  18. package/demo-backend/src/routes/index.ts +8 -0
  19. package/demo-backend/src/services/bookmark.service.test.ts +433 -0
  20. package/demo-backend/src/services/bookmark.service.ts +261 -0
  21. package/demo-backend/tsconfig.json +12 -0
  22. package/demo-frontend/.ai-spec-constitution.md +95 -0
  23. package/demo-frontend/package.json +23 -0
  24. package/demo-frontend/src/App.tsx +12 -0
  25. package/demo-frontend/src/main.tsx +9 -0
  26. package/demo-frontend/tsconfig.json +13 -0
  27. package/dist/cli/index.js +130 -21
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/index.mjs +130 -21
  30. package/dist/cli/index.mjs.map +1 -1
  31. package/dist/index.js +80 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +80 -8
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +1 -1
  36. package/RELEASE_LOG.md +0 -2962
  37. package/purpose.md +0 -1434
@@ -126,8 +126,101 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
126
126
  }
127
127
  });
128
128
 
129
- // core/provider-utils.ts
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 sleep(waitMs);
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 sleep, ProviderError;
311
+ var ProviderError;
216
312
  var init_provider_utils = __esm({
217
313
  "core/provider-utils.ts"() {
218
314
  "use strict";
219
- sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
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
- console.log(`${prefix}${existingContent ? chalk9.yellow("~") : chalk9.green("+")} ${chalk9.bold(item.file)} ${chalk9.green("\u2714")}`);
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
- console.log(`${prefix}${chalk9.red("\u2718")} ${chalk9.bold(item.file)} \u2014 ${chalk9.red(err.message)}`);
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
- console.log(chalk15.green(` \u2714 Auto-fixed: ${file}`));
8475
+ fixSpinner.succeed(`Auto-fixed: ${file}`);
8376
8476
  } catch (err) {
8377
8477
  results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
8378
- console.log(chalk15.yellow(` \u26A0 Could not auto-fix: ${file}`));
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
- console.log(chalk25.yellow(" Constitution : not found \u2014 auto-generating..."));
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
- console.log(chalk25.green(` Constitution : \u2714 generated and saved (.ai-spec-constitution.md)`));
11425
+ constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
11325
11426
  } catch (err) {
11326
- console.log(chalk25.yellow(` Constitution : \u26A0 auto-generation failed (${err.message}), continuing without it.`));
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
- console.log(chalk25.green(" \u2714 Spec generated."));
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
- console.log(chalk25.green(` \u2714 Spec generated.`));
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) console.log(chalk25.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
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
- console.log(chalk25.green(" \u2714 DSL extracted and validated."));
11605
+ dslSpinner.succeed("DSL extracted and validated.");
11499
11606
  } else {
11500
11607
  runLogger.stageEnd("dsl_extract", { skipped: true });
11501
- console.log(chalk25.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
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
- console.log(chalk25.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
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);