ai-spec-dev 0.46.0 → 0.56.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 (41) hide show
  1. package/README.md +60 -30
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +14 -0
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +36 -1
  6. package/cli/index.ts +2 -6
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +300 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +23 -0
  11. package/core/code-generator.ts +63 -14
  12. package/core/cross-stack-verifier.ts +482 -0
  13. package/core/fix-history.ts +333 -0
  14. package/core/import-fixer.ts +827 -0
  15. package/core/import-verifier.ts +569 -0
  16. package/core/knowledge-memory.ts +55 -6
  17. package/core/self-evaluator.ts +44 -7
  18. package/core/spec-generator.ts +3 -3
  19. package/core/types-generator.ts +2 -2
  20. package/dist/cli/index.js +3968 -2353
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/index.mjs +3810 -2195
  23. package/dist/cli/index.mjs.map +1 -1
  24. package/dist/index.d.mts +14 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.js +249 -128
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +249 -128
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +2 -2
  31. package/tests/cross-stack-verifier.test.ts +402 -0
  32. package/tests/fix-history.test.ts +335 -0
  33. package/tests/import-fixer.test.ts +944 -0
  34. package/tests/import-verifier.test.ts +420 -0
  35. package/tests/knowledge-memory.test.ts +40 -0
  36. package/tests/self-evaluator.test.ts +97 -0
  37. package/.ai-spec-workspace.json +0 -17
  38. package/.ai-spec.json +0 -7
  39. package/cli/commands/model.ts +0 -152
  40. package/cli/commands/scan.ts +0 -99
  41. package/cli/commands/workspace.ts +0 -219
@@ -1,152 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import { input, select, confirm } from "@inquirer/prompts";
4
- import {
5
- DEFAULT_MODELS,
6
- ENV_KEY_MAP,
7
- PROVIDER_CATALOG,
8
- } from "../../core/spec-generator";
9
- import { AiSpecGlobalConfig, GLOBAL_CONFIG_FILE, loadGlobalConfig, saveGlobalConfig } from "../utils";
10
-
11
- export function registerModel(program: Command): void {
12
- program
13
- .command("model")
14
- .description("Interactively switch the active AI provider/model and save to .ai-spec.json")
15
- .option("--list", "List all available providers and models")
16
- .action(async (opts) => {
17
- // ── --list ──────────────────────────────────────────────────────────────
18
- if (opts.list) {
19
- console.log(chalk.bold("\nAvailable providers & models:\n"));
20
- for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
21
- console.log(
22
- ` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`
23
- );
24
- console.log(chalk.gray(` ${meta.description}`));
25
- console.log(
26
- chalk.gray(
27
- ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
28
- )
29
- );
30
- console.log();
31
- }
32
- return;
33
- }
34
-
35
- const existing: AiSpecGlobalConfig = await loadGlobalConfig();
36
-
37
- console.log(chalk.blue("\n─── Model Switcher ─────────────────────────────"));
38
- console.log(chalk.gray(` Config: ${GLOBAL_CONFIG_FILE}`));
39
- if (Object.keys(existing).length > 0) {
40
- console.log(
41
- chalk.gray(
42
- ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
43
- (existing.codegenProvider
44
- ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}`
45
- : "")
46
- )
47
- );
48
- }
49
- console.log();
50
-
51
- const target = await select({
52
- message: "Configure model for:",
53
- choices: [
54
- { name: "Spec generation (used for spec writing & refinement)", value: "spec" },
55
- { name: "Code generation (used when --codegen api is active)", value: "codegen" },
56
- { name: "Both (same provider/model for all tasks)", value: "both" },
57
- ],
58
- });
59
-
60
- async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
61
- const providerKey = await select({
62
- message: `${label} — select provider:`,
63
- choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
64
- name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
65
- value: key,
66
- short: meta.displayName,
67
- })),
68
- });
69
-
70
- const meta = PROVIDER_CATALOG[providerKey];
71
- const modelChoices = [
72
- ...meta.models.map((m) => ({ name: m, value: m })),
73
- { name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
74
- ];
75
-
76
- let chosenModel = await select({
77
- message: `${label} — select model (${meta.displayName}):`,
78
- choices: modelChoices,
79
- });
80
-
81
- if (chosenModel === "__custom__") {
82
- chosenModel = await input({
83
- message: "Enter model name:",
84
- validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
85
- });
86
- }
87
-
88
- return { provider: providerKey, model: chosenModel };
89
- }
90
-
91
- const updated: AiSpecGlobalConfig = { ...existing };
92
-
93
- if (target === "spec" || target === "both") {
94
- const { provider, model } = await pickProviderAndModel("Spec");
95
- updated.provider = provider;
96
- updated.model = model;
97
- }
98
-
99
- if (target === "codegen" || target === "both") {
100
- if (target === "both") {
101
- updated.codegenProvider = updated.provider;
102
- updated.codegenModel = updated.model;
103
- } else {
104
- const { provider, model } = await pickProviderAndModel("Codegen");
105
- updated.codegenProvider = provider;
106
- updated.codegenModel = model;
107
- }
108
-
109
- const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
110
- if (effectiveCodegenProvider !== "claude") {
111
- if (!updated.codegen || updated.codegen === "claude-code") {
112
- updated.codegen = "api";
113
- console.log(
114
- chalk.yellow(
115
- `\n ⚠ provider "${effectiveCodegenProvider}" 不支持 "claude-code" 模式。`
116
- )
117
- );
118
- console.log(chalk.gray(` 已自动将 codegen 模式设为 "api"。`));
119
- }
120
- }
121
- }
122
-
123
- console.log(chalk.blue("\n Preview:"));
124
- console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
125
- if (updated.codegenProvider) {
126
- console.log(
127
- chalk.gray(
128
- ` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
129
- )
130
- );
131
- }
132
-
133
- const ok = await confirm({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
134
- if (!ok) {
135
- console.log(chalk.gray(" Cancelled."));
136
- return;
137
- }
138
-
139
- await saveGlobalConfig(updated);
140
- console.log(chalk.green(`\n ✔ Saved to ${GLOBAL_CONFIG_FILE}`));
141
-
142
- const providerToCheck = updated.provider ?? "gemini";
143
- const envKey = ENV_KEY_MAP[providerToCheck];
144
- if (envKey && !process.env[envKey]) {
145
- console.log(
146
- chalk.yellow(
147
- ` ⚠ Remember to set ${envKey} in your environment or .env file.`
148
- )
149
- );
150
- }
151
- });
152
- }
@@ -1,99 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import * as path from "path";
4
- import { runScan, saveIndex, loadIndex, INDEX_FILE, ProjectEntry } from "../../core/project-index";
5
-
6
- const ROLE_COLOR: Record<string, (s: string) => string> = {
7
- backend: chalk.blue,
8
- frontend: chalk.green,
9
- mobile: chalk.magenta,
10
- shared: chalk.gray,
11
- };
12
-
13
- function formatEntry(entry: ProjectEntry): string {
14
- const roleColor = ROLE_COLOR[entry.role] ?? chalk.white;
15
- const role = roleColor(entry.role.padEnd(8));
16
- const type = chalk.gray(entry.type.padEnd(14));
17
- const name = (entry.missing ? chalk.strikethrough.gray : chalk.white)(entry.path.padEnd(30));
18
- const badges: string[] = [];
19
- if (entry.hasConstitution) badges.push(chalk.cyan("§C"));
20
- if (entry.hasWorkspace) badges.push(chalk.yellow("W"));
21
- if (entry.missing) badges.push(chalk.red("missing"));
22
- const stack = chalk.gray(entry.techStack.slice(0, 5).join(", "));
23
- return ` ${name} ${role} ${type} ${badges.join(" ")} ${stack}`;
24
- }
25
-
26
- export function registerScan(program: Command): void {
27
- program
28
- .command("scan")
29
- .description("Discover and index all projects under the current directory")
30
- .option("-d, --depth <n>", "Max directory depth to search", "2")
31
- .option("--list", "Just print the current index without rescanning")
32
- .action(async (opts) => {
33
- const cwd = process.cwd();
34
-
35
- // ── List mode ─────────────────────────────────────────────────────────
36
- if (opts.list) {
37
- const existing = await loadIndex(cwd);
38
- if (!existing || existing.projects.length === 0) {
39
- console.log(chalk.gray("No index found. Run: ai-spec scan"));
40
- return;
41
- }
42
-
43
- console.log(chalk.cyan(`\n─── Project Index (${existing.projects.length} projects) ─────────────────────────────`));
44
- console.log(chalk.gray(` Last scanned : ${existing.lastScanned.slice(0, 19).replace("T", " ")}`));
45
- console.log(chalk.gray(` Root : ${existing.scanRoot}\n`));
46
-
47
- const active = existing.projects.filter((p) => !p.missing);
48
- const missing = existing.projects.filter((p) => p.missing);
49
-
50
- for (const entry of active) {
51
- console.log(formatEntry(entry));
52
- }
53
- if (missing.length > 0) {
54
- console.log(chalk.gray(`\n (${missing.length} previously seen, now missing)`));
55
- for (const entry of missing) {
56
- console.log(formatEntry(entry));
57
- }
58
- }
59
-
60
- console.log(chalk.cyan("\n─".repeat(52)));
61
- console.log(chalk.gray(" §C = has constitution W = workspace root"));
62
- console.log(chalk.gray(` Index file: ${INDEX_FILE}`));
63
- return;
64
- }
65
-
66
- // ── Scan mode ─────────────────────────────────────────────────────────
67
- const maxDepth = parseInt(opts.depth, 10);
68
- console.log(chalk.blue(`\nScanning ${cwd} (depth: ${maxDepth})...`));
69
-
70
- const { index, added, updated, unchanged, nowMissing } = await runScan(cwd, maxDepth);
71
- await saveIndex(cwd, index);
72
-
73
- const active = index.projects.filter((p) => !p.missing);
74
-
75
- // ── Summary ───────────────────────────────────────────────────────────
76
- console.log(chalk.cyan(`\n─── Scan Results ────────────────────────────────────`));
77
- if (added.length > 0) console.log(chalk.green(` + ${added.length} new project(s) added`));
78
- if (updated.length > 0) console.log(chalk.yellow(` ~ ${updated.length} project(s) updated`));
79
- if (unchanged.length > 0) console.log(chalk.gray(` · ${unchanged.length} project(s) unchanged`));
80
- if (nowMissing.length > 0) console.log(chalk.red(` ✘ ${nowMissing.length} project(s) no longer found (marked missing)`));
81
-
82
- if (added.length === 0 && updated.length === 0 && nowMissing.length === 0) {
83
- console.log(chalk.gray(" Nothing changed."));
84
- }
85
-
86
- // ── Full listing ──────────────────────────────────────────────────────
87
- if (active.length > 0) {
88
- console.log(chalk.cyan(`\n Projects (${active.length}):`));
89
- for (const entry of active) {
90
- console.log(formatEntry(entry));
91
- }
92
- }
93
-
94
- console.log(chalk.cyan("\n─".repeat(52)));
95
- console.log(chalk.gray(" §C = has constitution W = workspace root"));
96
- console.log(chalk.gray(` Index saved : ${path.relative(cwd, path.join(cwd, INDEX_FILE))}`));
97
- console.log(chalk.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
98
- });
99
- }
@@ -1,219 +0,0 @@
1
- import { Command } from "commander";
2
- import * as path from "path";
3
- import * as fs from "fs-extra";
4
- import chalk from "chalk";
5
- import { input, select, confirm } from "@inquirer/prompts";
6
- import {
7
- WorkspaceLoader,
8
- WorkspaceConfig,
9
- RepoConfig,
10
- WORKSPACE_CONFIG_FILE,
11
- detectRepoType,
12
- } from "../../core/workspace-loader";
13
-
14
- export function registerWorkspace(program: Command): void {
15
- const workspaceCmd = program
16
- .command("workspace")
17
- .description("Manage multi-repo workspace configuration");
18
-
19
- // ── workspace init ──────────────────────────────────────────────────────────
20
- workspaceCmd
21
- .command("init")
22
- .description(`Interactive workspace setup — creates ${WORKSPACE_CONFIG_FILE}`)
23
- .action(async () => {
24
- const currentDir = process.cwd();
25
- const configPath = path.join(currentDir, WORKSPACE_CONFIG_FILE);
26
-
27
- if (await fs.pathExists(configPath)) {
28
- const overwrite = await confirm({
29
- message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
30
- default: false,
31
- });
32
- if (!overwrite) {
33
- console.log(chalk.gray(" Cancelled."));
34
- return;
35
- }
36
- }
37
-
38
- console.log(chalk.blue("\n─── Workspace Setup ────────────────────────────"));
39
-
40
- const workspaceName = await input({
41
- message: "Workspace name:",
42
- validate: (v) => v.trim().length > 0 || "Name cannot be empty",
43
- });
44
-
45
- const repos: RepoConfig[] = [];
46
-
47
- const useAutoScan = await confirm({
48
- message: "Auto-scan sibling directories for repos?",
49
- default: true,
50
- });
51
-
52
- if (useAutoScan) {
53
- const workspaceLoader = new WorkspaceLoader(currentDir);
54
- const detected = await workspaceLoader.autoDetect();
55
-
56
- if (detected.length === 0) {
57
- console.log(chalk.yellow(" No recognizable repos found in sibling directories."));
58
- } else {
59
- console.log(chalk.cyan("\n Detected repos:"));
60
- for (const r of detected) {
61
- console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
62
- }
63
-
64
- const keepAll = await confirm({
65
- message: `Include all ${detected.length} detected repo(s)?`,
66
- default: true,
67
- });
68
-
69
- if (keepAll) {
70
- repos.push(...detected);
71
- } else {
72
- for (const r of detected) {
73
- const keep = await confirm({
74
- message: `Include "${r.name}" (${r.role}, ${r.type})?`,
75
- default: true,
76
- });
77
- if (keep) repos.push(r);
78
- }
79
- }
80
- console.log(chalk.green(` ✔ ${repos.length} repo(s) added from auto-scan.`));
81
- }
82
- }
83
-
84
- const repoTypeChoices = [
85
- { name: "node-express (Node.js/Express backend)", value: "node-express" },
86
- { name: "node-koa (Node.js/Koa backend)", value: "node-koa" },
87
- { name: "go (Go backend)", value: "go" },
88
- { name: "python (Python backend)", value: "python" },
89
- { name: "java (Java/Spring backend)", value: "java" },
90
- { name: "rust (Rust backend)", value: "rust" },
91
- { name: "php (PHP/Lumen/Laravel backend)", value: "php" },
92
- { name: "react (React frontend)", value: "react" },
93
- { name: "next (Next.js)", value: "next" },
94
- { name: "vue (Vue frontend)", value: "vue" },
95
- { name: "react-native (React Native mobile)", value: "react-native" },
96
- { name: "unknown", value: "unknown" },
97
- ];
98
-
99
- let addMore = await confirm({
100
- message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
101
- default: repos.length === 0,
102
- });
103
-
104
- while (addMore) {
105
- console.log(chalk.cyan(`\n Adding repo #${repos.length + 1}`));
106
-
107
- const repoName = await input({
108
- message: "Repo name (e.g. api, web, app):",
109
- validate: (v) => {
110
- if (!v.trim()) return "Name cannot be empty";
111
- if (repos.some((r) => r.name === v.trim())) return "Name already used";
112
- return true;
113
- },
114
- });
115
-
116
- const repoPath = await input({
117
- message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
118
- default: `./${repoName}`,
119
- });
120
-
121
- const absPath = path.resolve(currentDir, repoPath);
122
- let detectedType = "unknown";
123
- let detectedRole = "shared";
124
-
125
- if (await fs.pathExists(absPath)) {
126
- const { type, role } = await detectRepoType(absPath);
127
- detectedType = type;
128
- detectedRole = role;
129
- console.log(chalk.gray(` Auto-detected: type=${type}, role=${role}`));
130
- } else {
131
- console.log(chalk.yellow(` Path "${absPath}" not found — type/role will be manual.`));
132
- }
133
-
134
- const repoType = await select({
135
- message: `Repo type for "${repoName}":`,
136
- choices: repoTypeChoices,
137
- default: detectedType,
138
- });
139
-
140
- const repoRole = await select({
141
- message: `Repo role for "${repoName}":`,
142
- choices: [
143
- { name: "backend", value: "backend" },
144
- { name: "frontend", value: "frontend" },
145
- { name: "mobile", value: "mobile" },
146
- { name: "shared", value: "shared" },
147
- ],
148
- default: detectedRole,
149
- });
150
-
151
- repos.push({
152
- name: repoName,
153
- path: repoPath,
154
- type: repoType as RepoConfig["type"],
155
- role: repoRole as RepoConfig["role"],
156
- });
157
-
158
- console.log(chalk.green(` ✔ Added: ${repoName} (${repoRole}, ${repoType})`));
159
-
160
- addMore = await confirm({
161
- message: "Add another repo?",
162
- default: false,
163
- });
164
- }
165
-
166
- const workspaceConfig: WorkspaceConfig = { name: workspaceName, repos };
167
-
168
- console.log(chalk.cyan("\n Workspace summary:"));
169
- console.log(chalk.gray(` Name: ${workspaceName}`));
170
- for (const r of repos) {
171
- console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
172
- }
173
-
174
- const ok = await confirm({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
175
- if (!ok) {
176
- console.log(chalk.gray(" Cancelled."));
177
- return;
178
- }
179
-
180
- const loader = new WorkspaceLoader(currentDir);
181
- const saved = await loader.save(workspaceConfig);
182
- console.log(chalk.green(`\n ✔ Workspace saved: ${saved}`));
183
- console.log(chalk.gray(` Run \`ai-spec create "your feature"\` — workspace mode will activate automatically.`));
184
- });
185
-
186
- // ── workspace status ────────────────────────────────────────────────────────
187
- workspaceCmd
188
- .command("status")
189
- .description("Show current workspace configuration")
190
- .action(async () => {
191
- const currentDir = process.cwd();
192
- const loader = new WorkspaceLoader(currentDir);
193
- const config = await loader.load();
194
-
195
- if (!config) {
196
- console.log(chalk.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
197
- console.log(chalk.gray(" Run `ai-spec workspace init` to create one."));
198
- return;
199
- }
200
-
201
- console.log(chalk.bold(`\nWorkspace: ${config.name}`));
202
- console.log(chalk.gray(` Config: ${path.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
203
- console.log(chalk.gray(` Repos (${config.repos.length}):\n`));
204
-
205
- for (const repo of config.repos) {
206
- const absPath = loader.resolveAbsPath(repo);
207
- const exists = await fs.pathExists(absPath);
208
- const status = exists ? chalk.green("found") : chalk.red("not found");
209
-
210
- console.log(
211
- ` ${chalk.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
212
- );
213
- console.log(chalk.gray(` path: ${absPath}`));
214
- if (repo.constitution) {
215
- console.log(chalk.green(` constitution: found`));
216
- }
217
- }
218
- });
219
- }