ai-spec-dev 0.42.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -40
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +246 -11
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +344 -106
- package/cli/index.ts +3 -7
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +291 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +95 -4
- package/core/code-generator.ts +63 -14
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/cross-stack-verifier.ts +395 -0
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +3 -2
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/openapi-exporter.ts +3 -2
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +30 -45
- package/core/token-budget.ts +3 -8
- package/core/types-generator.ts +2 -2
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +3889 -1937
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3888 -1936
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +292 -181
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -181
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +301 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/cli/commands/model.ts +0 -156
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- package/demo-frontend/tsconfig.json +0 -13
package/cli/commands/init.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as fs from "fs-extra";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import {
|
|
5
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
6
|
+
import { RepoRole } from "../../core/workspace-loader";
|
|
7
|
+
import { createProvider, DEFAULT_MODELS, SUPPORTED_PROVIDERS, AIProvider } from "../../core/spec-generator";
|
|
6
8
|
import { ContextLoader } from "../../core/context-loader";
|
|
7
9
|
import { ConstitutionGenerator, CONSTITUTION_FILE } from "../../core/constitution-generator";
|
|
8
10
|
import { ConstitutionConsolidator } from "../../core/constitution-consolidator";
|
|
@@ -16,12 +18,201 @@ import {
|
|
|
16
18
|
buildGlobalConstitutionPrompt,
|
|
17
19
|
} from "../../prompts/global-constitution.prompt";
|
|
18
20
|
import { loadConfig, resolveApiKey } from "../utils";
|
|
19
|
-
import { loadIndex, ProjectEntry } from "../../core/project-index";
|
|
21
|
+
import { loadIndex, runScan, saveIndex, ProjectEntry } from "../../core/project-index";
|
|
22
|
+
import { detectRepoType } from "../../core/workspace-loader";
|
|
23
|
+
import {
|
|
24
|
+
RegisteredRepo,
|
|
25
|
+
getRegisteredRepos,
|
|
26
|
+
registerRepo,
|
|
27
|
+
REPO_STORE_FILE,
|
|
28
|
+
} from "../../core/repo-store";
|
|
29
|
+
|
|
30
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const ROLE_LABELS: Record<RepoRole, string> = {
|
|
33
|
+
frontend: "frontend",
|
|
34
|
+
backend: "backend",
|
|
35
|
+
mobile: "mobile",
|
|
36
|
+
shared: "shared",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Prompt user to select a repo role.
|
|
41
|
+
*/
|
|
42
|
+
async function promptRepoRole(): Promise<RepoRole> {
|
|
43
|
+
return select<RepoRole>({
|
|
44
|
+
message: "What type of repo is this?",
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: "Frontend", value: "frontend" },
|
|
47
|
+
{ name: "Backend", value: "backend" },
|
|
48
|
+
{ name: "Mobile", value: "mobile" },
|
|
49
|
+
{ name: "Shared / Other", value: "shared" },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prompt user for a repo absolute path with validation.
|
|
56
|
+
*/
|
|
57
|
+
async function promptRepoPath(role: RepoRole): Promise<string> {
|
|
58
|
+
const label = ROLE_LABELS[role];
|
|
59
|
+
const raw = await input({
|
|
60
|
+
message: `Enter your ${label} repo path (absolute path):`,
|
|
61
|
+
validate: (v) => {
|
|
62
|
+
const trimmed = v.trim();
|
|
63
|
+
if (trimmed.length === 0) return "Path cannot be empty";
|
|
64
|
+
if (!path.isAbsolute(trimmed)) return "Please provide an absolute path";
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Strip shell escape backslashes (e.g. "文稿\ -\ hongzhong" → "文稿 - hongzhong")
|
|
70
|
+
const cleaned = raw.trim().replace(/\\ /g, " ");
|
|
71
|
+
const resolved = path.resolve(cleaned);
|
|
72
|
+
if (!(await fs.pathExists(resolved))) {
|
|
73
|
+
console.log(chalk.red(` Path does not exist: ${resolved}`));
|
|
74
|
+
return promptRepoPath(role);
|
|
75
|
+
}
|
|
76
|
+
const stat = await fs.stat(resolved);
|
|
77
|
+
if (!stat.isDirectory()) {
|
|
78
|
+
console.log(chalk.red(` Not a directory: ${resolved}`));
|
|
79
|
+
return promptRepoPath(role);
|
|
80
|
+
}
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a single repo: detect type, generate project constitution, return entry.
|
|
86
|
+
* @param roleOverride — user-selected role (takes priority over auto-detection)
|
|
87
|
+
*/
|
|
88
|
+
async function registerSingleRepo(
|
|
89
|
+
repoPath: string,
|
|
90
|
+
provider: AIProvider,
|
|
91
|
+
roleOverride?: RepoRole
|
|
92
|
+
): Promise<RegisteredRepo> {
|
|
93
|
+
const { type, role: detectedRole } = await detectRepoType(repoPath);
|
|
94
|
+
const role = roleOverride ?? detectedRole;
|
|
95
|
+
const repoName = path.basename(repoPath);
|
|
96
|
+
|
|
97
|
+
console.log(chalk.gray(` Detected: ${repoName} → ${type} (${role})`));
|
|
98
|
+
|
|
99
|
+
// Generate project constitution
|
|
100
|
+
const constitutionPath = path.join(repoPath, CONSTITUTION_FILE);
|
|
101
|
+
let hasConstitution = await fs.pathExists(constitutionPath);
|
|
102
|
+
|
|
103
|
+
if (!hasConstitution) {
|
|
104
|
+
console.log(chalk.blue(` Generating project constitution for ${repoName}...`));
|
|
105
|
+
try {
|
|
106
|
+
const gen = new ConstitutionGenerator(provider);
|
|
107
|
+
const content = await gen.generate(repoPath);
|
|
108
|
+
await gen.saveConstitution(repoPath, content);
|
|
109
|
+
hasConstitution = true;
|
|
110
|
+
console.log(chalk.green(` ✔ Constitution saved: ${constitutionPath}`));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.log(chalk.yellow(` ⚠ Constitution generation failed: ${(err as Error).message}`));
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.green(` ✔ Constitution already exists: ${constitutionPath}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entry: RegisteredRepo = {
|
|
119
|
+
name: repoName,
|
|
120
|
+
path: repoPath,
|
|
121
|
+
type,
|
|
122
|
+
role,
|
|
123
|
+
hasConstitution,
|
|
124
|
+
registeredAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
await registerRepo(entry);
|
|
128
|
+
return entry;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build per-project summaries for global constitution generation from registered repos.
|
|
133
|
+
*/
|
|
134
|
+
async function buildProjectSummaries(
|
|
135
|
+
repos: RegisteredRepo[]
|
|
136
|
+
): Promise<Array<{ name: string; summary: string }>> {
|
|
137
|
+
const summaries: Array<{ name: string; summary: string }> = [];
|
|
138
|
+
|
|
139
|
+
for (const repo of repos) {
|
|
140
|
+
if (!(await fs.pathExists(repo.path))) continue;
|
|
141
|
+
|
|
142
|
+
const lines: string[] = [
|
|
143
|
+
`Type: ${repo.type} (${repo.role})`,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Load tech stack from context
|
|
147
|
+
try {
|
|
148
|
+
const loader = new ContextLoader(repo.path);
|
|
149
|
+
const ctx = await loader.loadProjectContext();
|
|
150
|
+
lines.push(`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`);
|
|
151
|
+
lines.push(`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`);
|
|
152
|
+
} catch {
|
|
153
|
+
lines.push(`Tech stack: ${repo.type}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Include constitution excerpt if available
|
|
157
|
+
if (repo.hasConstitution) {
|
|
158
|
+
try {
|
|
159
|
+
const constitutionPath = path.join(repo.path, CONSTITUTION_FILE);
|
|
160
|
+
const raw = await fs.readFile(constitutionPath, "utf-8");
|
|
161
|
+
lines.push("", "Constitution excerpt:", raw.slice(0, 2000));
|
|
162
|
+
} catch { /* skip */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
summaries.push({ name: repo.name, summary: lines.join("\n") });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return summaries;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate or update global constitution based on all registered repos.
|
|
173
|
+
*/
|
|
174
|
+
async function generateGlobalConstitution(
|
|
175
|
+
provider: AIProvider,
|
|
176
|
+
repos: RegisteredRepo[],
|
|
177
|
+
currentDir: string
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
console.log(chalk.blue("\n─── Generating Global Constitution ──────────────"));
|
|
180
|
+
console.log(chalk.gray(` Based on ${repos.length} registered repo(s)`));
|
|
181
|
+
|
|
182
|
+
const summaries = await buildProjectSummaries(repos);
|
|
183
|
+
if (summaries.length === 0) {
|
|
184
|
+
console.log(chalk.yellow(" No valid repos found — skipping global constitution."));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const prompt = buildGlobalConstitutionPrompt(summaries);
|
|
189
|
+
let globalConstitution: string;
|
|
190
|
+
try {
|
|
191
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(chalk.red(` ✘ Failed to generate global constitution: ${(err as Error).message}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
198
|
+
console.log(chalk.green(` ✔ Global constitution saved: ${saved}`));
|
|
199
|
+
console.log(chalk.gray(" Project constitutions will be merged with this at runtime."));
|
|
200
|
+
|
|
201
|
+
// Preview
|
|
202
|
+
const lines = globalConstitution.split("\n");
|
|
203
|
+
console.log(chalk.bold("\n Preview:"));
|
|
204
|
+
console.log(chalk.gray(lines.slice(0, 10).join("\n")));
|
|
205
|
+
if (lines.length > 10) {
|
|
206
|
+
console.log(chalk.gray(` ... (${lines.length} lines total)`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Command: init ───────────────────────────────────────────────────────────
|
|
20
211
|
|
|
21
212
|
export function registerInit(program: Command): void {
|
|
22
213
|
program
|
|
23
214
|
.command("init")
|
|
24
|
-
.description(
|
|
215
|
+
.description("Setup workspace: register repos, generate constitutions")
|
|
25
216
|
.option(
|
|
26
217
|
"--provider <name>",
|
|
27
218
|
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
@@ -29,15 +220,34 @@ export function registerInit(program: Command): void {
|
|
|
29
220
|
)
|
|
30
221
|
.option("--model <name>", "Model name")
|
|
31
222
|
.option("-k, --key <apiKey>", "API key")
|
|
32
|
-
.option("--force", "Overwrite existing
|
|
33
|
-
.option(
|
|
34
|
-
"--global",
|
|
35
|
-
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
36
|
-
)
|
|
37
|
-
.option("--consolidate", "Consolidate §9 accumulated lessons into §1–§8 core rules (prune & rebase)")
|
|
223
|
+
.option("--force", "Overwrite existing constitutions")
|
|
224
|
+
.option("--consolidate", "Consolidate §9 accumulated lessons into §1–§8 core rules")
|
|
38
225
|
.option("--dry-run", "Preview consolidation result without writing (use with --consolidate)")
|
|
226
|
+
.option("--add-repo", "Add a new repo to the registered list")
|
|
227
|
+
.option("--status", "Show registered repos and constitution status (no changes made)")
|
|
39
228
|
.action(async (opts) => {
|
|
40
229
|
const currentDir = process.cwd();
|
|
230
|
+
|
|
231
|
+
// ── --status: show registered repos and constitution health ───────────
|
|
232
|
+
if (opts.status) {
|
|
233
|
+
const repos = await getRegisteredRepos();
|
|
234
|
+
if (repos.length === 0) {
|
|
235
|
+
console.log(chalk.yellow("\nNo repos registered. Run `ai-spec init` to add repos."));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log(chalk.bold(`\n─── Registered Repos (${repos.length}) ──────────────────────`));
|
|
239
|
+
for (const r of repos) {
|
|
240
|
+
const constitutionIcon = r.hasConstitution ? chalk.green("✔ §C") : chalk.gray("○ §C");
|
|
241
|
+
const roleColor = r.role === "frontend" ? chalk.green : r.role === "backend" ? chalk.blue : r.role === "mobile" ? chalk.magenta : chalk.gray;
|
|
242
|
+
const pathExists = await fs.pathExists(r.path);
|
|
243
|
+
const pathStatus = pathExists ? chalk.gray(r.path) : chalk.red(`${r.path} (not found)`);
|
|
244
|
+
console.log(` ${constitutionIcon} ${roleColor(r.role.padEnd(9))} ${chalk.white(r.name.padEnd(20))} ${pathStatus}`);
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.gray(`\n Store: ${REPO_STORE_FILE}`));
|
|
247
|
+
console.log(chalk.gray(" Run `ai-spec init` to add repos or regenerate constitutions."));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
41
251
|
const config = await loadConfig(currentDir);
|
|
42
252
|
|
|
43
253
|
const providerName = opts.provider || config.provider || "gemini";
|
|
@@ -68,123 +278,151 @@ export function registerInit(program: Command): void {
|
|
|
68
278
|
return;
|
|
69
279
|
}
|
|
70
280
|
|
|
71
|
-
// ──
|
|
72
|
-
if (opts.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
281
|
+
// ── Add-repo shortcut ──────────────────────────────────────────────────
|
|
282
|
+
if (opts.addRepo) {
|
|
283
|
+
console.log(chalk.blue("\n─── Register New Repo ──────────────────────────"));
|
|
284
|
+
const role = await promptRepoRole();
|
|
285
|
+
const repoPath = await promptRepoPath(role);
|
|
286
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
287
|
+
console.log(chalk.green(`\n ✔ Repo registered: ${entry.name} (${entry.type} / ${entry.role})`));
|
|
288
|
+
console.log(chalk.gray(` Saved to: ${REPO_STORE_FILE}`));
|
|
289
|
+
|
|
290
|
+
// Ask if user wants to update global constitution
|
|
291
|
+
const updateGlobal = await confirm({
|
|
292
|
+
message: "Update global constitution with this repo's context?",
|
|
293
|
+
default: true,
|
|
294
|
+
});
|
|
295
|
+
if (updateGlobal) {
|
|
296
|
+
const allRepos = await getRegisteredRepos();
|
|
297
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Full init flow ─────────────────────────────────────────────────────
|
|
303
|
+
console.log(chalk.blue("\n" + "─".repeat(52)));
|
|
304
|
+
console.log(chalk.bold(" ai-spec init — Workspace Setup"));
|
|
305
|
+
console.log(chalk.blue("─".repeat(52)));
|
|
306
|
+
console.log(chalk.gray(` Provider: ${providerName}/${modelName}\n`));
|
|
307
|
+
|
|
308
|
+
const existingRepos = await getRegisteredRepos();
|
|
309
|
+
|
|
310
|
+
// ── Step 1: Show existing repos if any ─────────────────────────────────
|
|
311
|
+
if (existingRepos.length > 0) {
|
|
312
|
+
console.log(chalk.cyan(" Registered repos:"));
|
|
313
|
+
for (const r of existingRepos) {
|
|
314
|
+
const constitutionIcon = r.hasConstitution ? chalk.green("✔") : chalk.gray("○");
|
|
315
|
+
console.log(chalk.gray(` ${constitutionIcon} ${r.name} (${r.type} / ${r.role}) → ${r.path}`));
|
|
78
316
|
}
|
|
317
|
+
console.log();
|
|
318
|
+
}
|
|
79
319
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const lines: string[] = [
|
|
94
|
-
`Type: ${entry.type} (${entry.role})`,
|
|
95
|
-
`Tech stack: ${entry.techStack.join(", ") || "unknown"}`,
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
// Include §1–§6 of project constitution if available (skip §9 lessons)
|
|
99
|
-
if (entry.hasConstitution) {
|
|
100
|
-
try {
|
|
101
|
-
const constitutionPath = path.join(absPath, CONSTITUTION_FILE);
|
|
102
|
-
const raw = await fs.readFile(constitutionPath, "utf-8");
|
|
103
|
-
// Take up to first 2000 chars (covers §1–§6 without §9 noise)
|
|
104
|
-
const excerpt = raw.slice(0, 2000);
|
|
105
|
-
lines.push("", "Constitution excerpt:", excerpt);
|
|
106
|
-
} catch { /* skip if unreadable */ }
|
|
107
|
-
}
|
|
320
|
+
// ── Step 2: Register repos ─────────────────────────────────────────────
|
|
321
|
+
const action = existingRepos.length > 0
|
|
322
|
+
? await select({
|
|
323
|
+
message: "What would you like to do?",
|
|
324
|
+
choices: [
|
|
325
|
+
{ name: "Add new repo(s)", value: "add" as const },
|
|
326
|
+
{ name: "Re-generate constitutions for existing repos", value: "regen" as const },
|
|
327
|
+
{ name: "Skip — proceed to global constitution", value: "skip" as const },
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
: "add" as const;
|
|
331
|
+
|
|
332
|
+
const newRepos: RegisteredRepo[] = [];
|
|
108
333
|
|
|
109
|
-
|
|
334
|
+
if (action === "add") {
|
|
335
|
+
let addMore = true;
|
|
336
|
+
while (addMore) {
|
|
337
|
+
console.log(chalk.blue(`\n ── Register Repo #${existingRepos.length + newRepos.length + 1} ──`));
|
|
338
|
+
const role = await promptRepoRole();
|
|
339
|
+
const repoPath = await promptRepoPath(role);
|
|
340
|
+
|
|
341
|
+
// Check if already registered
|
|
342
|
+
const alreadyRegistered = [...existingRepos, ...newRepos].find((r) => r.path === repoPath);
|
|
343
|
+
if (alreadyRegistered) {
|
|
344
|
+
console.log(chalk.yellow(` Already registered: ${alreadyRegistered.name}`));
|
|
345
|
+
} else {
|
|
346
|
+
const entry = await registerSingleRepo(repoPath, provider, role);
|
|
347
|
+
newRepos.push(entry);
|
|
348
|
+
console.log(chalk.green(` ✔ Registered: ${entry.name}`));
|
|
110
349
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const loader = new ContextLoader(currentDir);
|
|
116
|
-
const ctx = await loader.loadProjectContext();
|
|
117
|
-
projectSummaries.push({
|
|
118
|
-
name: path.basename(currentDir),
|
|
119
|
-
summary: [
|
|
120
|
-
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
121
|
-
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`,
|
|
122
|
-
].join("\n"),
|
|
350
|
+
|
|
351
|
+
addMore = await confirm({
|
|
352
|
+
message: "Add another repo?",
|
|
353
|
+
default: false,
|
|
123
354
|
});
|
|
124
355
|
}
|
|
356
|
+
}
|
|
125
357
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
358
|
+
if (action === "regen") {
|
|
359
|
+
console.log(chalk.blue("\n Re-generating project constitutions..."));
|
|
360
|
+
for (const repo of existingRepos) {
|
|
361
|
+
if (!(await fs.pathExists(repo.path))) {
|
|
362
|
+
console.log(chalk.yellow(` ⚠ ${repo.name}: path not found — skipping`));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const constitutionPath = path.join(repo.path, CONSTITUTION_FILE);
|
|
367
|
+
if ((await fs.pathExists(constitutionPath)) && !opts.force) {
|
|
368
|
+
console.log(chalk.gray(` ${repo.name}: constitution exists (use --force to overwrite)`));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
135
371
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
372
|
+
console.log(chalk.blue(` ${repo.name}: generating constitution...`));
|
|
373
|
+
try {
|
|
374
|
+
const gen = new ConstitutionGenerator(provider);
|
|
375
|
+
const content = await gen.generate(repo.path);
|
|
376
|
+
await gen.saveConstitution(repo.path, content);
|
|
377
|
+
console.log(chalk.green(` ✔ ${repo.name}: constitution saved`));
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.log(chalk.yellow(` ⚠ ${repo.name}: failed — ${(err as Error).message}`));
|
|
380
|
+
}
|
|
144
381
|
}
|
|
145
|
-
return;
|
|
146
382
|
}
|
|
147
383
|
|
|
148
|
-
// ──
|
|
149
|
-
const
|
|
384
|
+
// ── Step 3: Generate/update global constitution ────────────────────────
|
|
385
|
+
const allRepos = await getRegisteredRepos();
|
|
150
386
|
|
|
151
|
-
if (
|
|
152
|
-
console.log(chalk.yellow(
|
|
153
|
-
console.log(chalk.gray(" Use --force to overwrite it."));
|
|
154
|
-
console.log(chalk.gray(` Or edit it directly: ${constitutionPath}`));
|
|
387
|
+
if (allRepos.length === 0) {
|
|
388
|
+
console.log(chalk.yellow("\n No repos registered. Run `ai-spec init` again to add repos."));
|
|
155
389
|
return;
|
|
156
390
|
}
|
|
157
391
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
392
|
+
const existingGlobal = await loadGlobalConstitution([currentDir]);
|
|
393
|
+
const shouldGenerateGlobal = !existingGlobal || opts.force || newRepos.length > 0
|
|
394
|
+
? true
|
|
395
|
+
: await confirm({
|
|
396
|
+
message: "Global constitution exists. Re-generate it?",
|
|
397
|
+
default: false,
|
|
398
|
+
});
|
|
163
399
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
constitution = await generator.generate(currentDir);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.error(chalk.red(" ✘ Failed to generate constitution:"), err);
|
|
169
|
-
process.exit(1);
|
|
400
|
+
if (shouldGenerateGlobal) {
|
|
401
|
+
await generateGlobalConstitution(provider, allRepos, currentDir);
|
|
170
402
|
}
|
|
171
403
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.log(chalk.gray(
|
|
178
|
-
console.log(chalk.gray(" Project rules take priority over global rules."));
|
|
404
|
+
// ── Done ───────────────────────────────────────────────────────────────
|
|
405
|
+
console.log(chalk.bold.green("\n✔ Init complete!"));
|
|
406
|
+
console.log(chalk.gray(` Repos registered: ${allRepos.length}`));
|
|
407
|
+
for (const r of allRepos) {
|
|
408
|
+
const icon = r.hasConstitution ? chalk.green("✔") : chalk.gray("○");
|
|
409
|
+
console.log(chalk.gray(` ${icon} ${r.name} (${r.type}/${r.role})`));
|
|
179
410
|
}
|
|
411
|
+
console.log(chalk.gray(`\n Repo store: ${REPO_STORE_FILE}`));
|
|
412
|
+
console.log(chalk.gray(` Next step: ai-spec create "your feature idea"`));
|
|
180
413
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
414
|
+
// ── Auto-scan: silently update project index ───────────────────────────
|
|
415
|
+
try {
|
|
416
|
+
const { index, added, updated: upd, nowMissing } = await runScan(currentDir, 2);
|
|
417
|
+
await saveIndex(currentDir, index);
|
|
418
|
+
const changes = added.length + upd.length + nowMissing.length;
|
|
419
|
+
if (changes > 0) {
|
|
420
|
+
console.log(chalk.gray(` Project index updated (${index.projects.filter((p: ProjectEntry) => !p.missing).length} projects found).`));
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// scan failure is non-blocking
|
|
188
424
|
}
|
|
425
|
+
|
|
426
|
+
process.exit(0);
|
|
189
427
|
});
|
|
190
428
|
}
|
package/cli/index.ts
CHANGED
|
@@ -8,8 +8,6 @@ import { registerCreate } from "./commands/create";
|
|
|
8
8
|
import { registerReview } from "./commands/review";
|
|
9
9
|
import { registerInit } from "./commands/init";
|
|
10
10
|
import { registerConfig } from "./commands/config";
|
|
11
|
-
import { registerModel } from "./commands/model";
|
|
12
|
-
import { registerWorkspace } from "./commands/workspace";
|
|
13
11
|
import { registerUpdate } from "./commands/update";
|
|
14
12
|
import { registerExport } from "./commands/export";
|
|
15
13
|
import { registerMock } from "./commands/mock";
|
|
@@ -20,21 +18,19 @@ import { registerLogs } from "./commands/logs";
|
|
|
20
18
|
import { registerTypes } from "./commands/types";
|
|
21
19
|
import { registerDashboard } from "./commands/dashboard";
|
|
22
20
|
import { registerVcr } from "./commands/vcr";
|
|
23
|
-
import {
|
|
21
|
+
import { registerFixHistory } from "./commands/fix-history";
|
|
24
22
|
|
|
25
23
|
const program = new Command();
|
|
26
24
|
|
|
27
25
|
program
|
|
28
26
|
.name("ai-spec")
|
|
29
27
|
.description("AI-driven Development Orchestrator — spec, generate, review")
|
|
30
|
-
.version("
|
|
28
|
+
.version(require("../package.json").version);
|
|
31
29
|
|
|
32
30
|
registerCreate(program);
|
|
33
31
|
registerReview(program);
|
|
34
32
|
registerInit(program);
|
|
35
33
|
registerConfig(program);
|
|
36
|
-
registerModel(program);
|
|
37
|
-
registerWorkspace(program);
|
|
38
34
|
registerUpdate(program);
|
|
39
35
|
registerExport(program);
|
|
40
36
|
registerMock(program);
|
|
@@ -45,6 +41,6 @@ registerLogs(program);
|
|
|
45
41
|
registerTypes(program);
|
|
46
42
|
registerDashboard(program);
|
|
47
43
|
registerVcr(program);
|
|
48
|
-
|
|
44
|
+
registerFixHistory(program);
|
|
49
45
|
|
|
50
46
|
program.parse();
|
package/cli/pipeline/helpers.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type MultiRepoResult = {
|
|
|
10
10
|
dsl: SpecDSL | null;
|
|
11
11
|
repoAbsPath: string;
|
|
12
12
|
role: string;
|
|
13
|
+
/** Files written by codegen during this run. Empty when codegen failed or never ran. */
|
|
14
|
+
generatedFiles: string[];
|
|
15
|
+
/** Human-readable reason when status === "failed". */
|
|
16
|
+
failureReason?: string;
|
|
17
|
+
/** Per-repo run ID (when RunLogger was created for this repo). */
|
|
18
|
+
runId?: string;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
// ─── Banner ──────────────────────────────────────────────────────────────────
|