ai-spec-dev 0.33.0 → 0.35.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/.claude/commands/add-lesson.md +34 -0
- package/.claude/commands/check-layers.md +65 -0
- package/.claude/commands/installed-deps.md +35 -0
- package/.claude/commands/recall-lessons.md +40 -0
- package/.claude/commands/scan-singletons.md +45 -0
- package/.claude/commands/verify-imports.md +48 -0
- package/.claude/settings.local.json +11 -1
- package/README.md +531 -213
- package/RELEASE_LOG.md +305 -0
- package/cli/commands/create.ts +1233 -0
- package/cli/commands/dashboard.ts +62 -0
- package/cli/commands/init.ts +45 -8
- package/cli/commands/mock.ts +175 -0
- package/cli/commands/scan.ts +99 -0
- package/cli/commands/types.ts +69 -0
- package/cli/commands/vcr.ts +70 -0
- package/cli/index.ts +34 -2517
- package/core/combined-generator.ts +13 -3
- package/core/dashboard-generator.ts +340 -0
- package/core/design-dialogue.ts +124 -0
- package/core/dsl-feedback.ts +34 -4
- package/core/error-feedback.ts +46 -2
- package/core/project-index.ts +301 -0
- package/core/reviewer.ts +84 -6
- package/core/run-logger.ts +109 -3
- package/core/run-trend.ts +24 -4
- package/core/self-evaluator.ts +39 -11
- package/core/spec-generator.ts +14 -8
- package/core/task-generator.ts +17 -0
- package/core/types-generator.ts +219 -0
- package/core/vcr.ts +210 -0
- package/dist/cli/index.js +7297 -5640
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +8728 -7071
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +420 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -224
- package/dist/index.mjs.map +1 -1
- package/docs-assets/purpose/architecture-overview.svg +64 -0
- package/docs-assets/purpose/create-pipeline.svg +113 -0
- package/docs-assets/purpose/task-layering.svg +74 -0
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +97 -9
- package/prompts/design.prompt.ts +59 -0
- package/prompts/spec.prompt.ts +8 -1
- package/prompts/tasks.prompt.ts +27 -2
- package/purpose.md +600 -174
|
@@ -0,0 +1,62 @@
|
|
|
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 { execSync } from "child_process";
|
|
6
|
+
import { loadRunLogs } from "../../core/run-trend";
|
|
7
|
+
import { generateDashboard } from "../../core/dashboard-generator";
|
|
8
|
+
|
|
9
|
+
export function registerDashboard(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("dashboard")
|
|
12
|
+
.description("Generate an HTML Harness Dashboard from run logs")
|
|
13
|
+
.option("--output <path>", "Output file path (default: .ai-spec/dashboard.html)")
|
|
14
|
+
.option("--open", "Auto-open the dashboard in the default browser after generation")
|
|
15
|
+
.option("--last <n>", "Limit to the last N runs (default: all)", "0")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const currentDir = process.cwd();
|
|
18
|
+
|
|
19
|
+
// ── Load run logs ────────────────────────────────────────────────────────
|
|
20
|
+
let logs = await loadRunLogs(currentDir);
|
|
21
|
+
if (logs.length === 0) {
|
|
22
|
+
console.log(chalk.yellow("\n No run logs found. Run `ai-spec create` at least once first.\n"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const last = parseInt(opts.last, 10);
|
|
27
|
+
if (last > 0) logs = logs.slice(0, last);
|
|
28
|
+
|
|
29
|
+
// ── Generate HTML ────────────────────────────────────────────────────────
|
|
30
|
+
const html = generateDashboard(logs);
|
|
31
|
+
|
|
32
|
+
// ── Write file ───────────────────────────────────────────────────────────
|
|
33
|
+
const outputPath = opts.output
|
|
34
|
+
? path.resolve(opts.output)
|
|
35
|
+
: path.join(currentDir, ".ai-spec", "dashboard.html");
|
|
36
|
+
|
|
37
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
38
|
+
await fs.writeFile(outputPath, html, "utf-8");
|
|
39
|
+
const relPath = path.relative(currentDir, outputPath);
|
|
40
|
+
|
|
41
|
+
console.log(chalk.green(`\n ✔ Dashboard generated: ${relPath}`));
|
|
42
|
+
console.log(chalk.gray(` Runs analyzed : ${logs.length}`));
|
|
43
|
+
console.log(chalk.gray(` Size : ${Math.round(html.length / 1024)}KB`));
|
|
44
|
+
console.log(chalk.blue(`\n Open in browser:`));
|
|
45
|
+
console.log(chalk.gray(` open ${relPath}\n`));
|
|
46
|
+
|
|
47
|
+
// ── Auto-open ────────────────────────────────────────────────────────────
|
|
48
|
+
if (opts.open) {
|
|
49
|
+
try {
|
|
50
|
+
const cmd =
|
|
51
|
+
process.platform === "darwin"
|
|
52
|
+
? `open "${outputPath}"`
|
|
53
|
+
: process.platform === "win32"
|
|
54
|
+
? `start "" "${outputPath}"`
|
|
55
|
+
: `xdg-open "${outputPath}"`;
|
|
56
|
+
execSync(cmd);
|
|
57
|
+
} catch {
|
|
58
|
+
// Non-fatal — file was already written
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
package/cli/commands/init.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildGlobalConstitutionPrompt,
|
|
17
17
|
} from "../../prompts/global-constitution.prompt";
|
|
18
18
|
import { loadConfig, resolveApiKey } from "../utils";
|
|
19
|
+
import { loadIndex, ProjectEntry } from "../../core/project-index";
|
|
19
20
|
|
|
20
21
|
export function registerInit(program: Command): void {
|
|
21
22
|
program
|
|
@@ -78,16 +79,52 @@ export function registerInit(program: Command): void {
|
|
|
78
79
|
|
|
79
80
|
console.log(chalk.blue("\n─── Generating Global Constitution ──────────────"));
|
|
80
81
|
console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
|
|
81
|
-
console.log(chalk.gray(" Scanning repos in workspace..."));
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
// ── Build per-project summaries ────────────────────────────────────
|
|
84
|
+
const projectSummaries: Array<{ name: string; summary: string }> = [];
|
|
85
|
+
const index = await loadIndex(currentDir);
|
|
86
|
+
|
|
87
|
+
if (index && index.projects.length > 0) {
|
|
88
|
+
const active = index.projects.filter((p: ProjectEntry) => !p.missing);
|
|
89
|
+
console.log(chalk.gray(` Found project index: ${active.length} project(s) — reading constitutions...`));
|
|
90
|
+
|
|
91
|
+
for (const entry of active) {
|
|
92
|
+
const absPath = path.join(currentDir, entry.path);
|
|
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
|
+
}
|
|
108
|
+
|
|
109
|
+
projectSummaries.push({ name: entry.name, summary: lines.join("\n") });
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// No index — fall back to scanning just the current directory
|
|
113
|
+
console.log(chalk.yellow(" No project index found. Run `ai-spec scan` first for better results."));
|
|
114
|
+
console.log(chalk.gray(" Falling back: scanning current directory only..."));
|
|
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"),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
89
125
|
|
|
90
|
-
|
|
126
|
+
console.log(chalk.gray(` Generating from ${projectSummaries.length} project(s)...`));
|
|
127
|
+
const prompt = buildGlobalConstitutionPrompt(projectSummaries);
|
|
91
128
|
let globalConstitution: string;
|
|
92
129
|
try {
|
|
93
130
|
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
@@ -0,0 +1,175 @@
|
|
|
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 {
|
|
6
|
+
WorkspaceLoader,
|
|
7
|
+
WORKSPACE_CONFIG_FILE,
|
|
8
|
+
} from "../../core/workspace-loader";
|
|
9
|
+
import { SpecDSL } from "../../core/dsl-types";
|
|
10
|
+
import {
|
|
11
|
+
generateMockAssets,
|
|
12
|
+
findLatestDslFile,
|
|
13
|
+
applyMockProxy,
|
|
14
|
+
restoreMockProxy,
|
|
15
|
+
startMockServerBackground,
|
|
16
|
+
saveMockServerPid,
|
|
17
|
+
} from "../../core/mock-server-generator";
|
|
18
|
+
|
|
19
|
+
export function registerMock(program: Command): void {
|
|
20
|
+
program
|
|
21
|
+
.command("mock")
|
|
22
|
+
.description("Generate a standalone mock server + proxy config from the latest DSL")
|
|
23
|
+
.option("--port <n>", "Mock server port (default: 3001)", "3001")
|
|
24
|
+
.option("--msw", "Also generate MSW (Mock Service Worker) handlers at src/mocks/")
|
|
25
|
+
.option("--proxy", "Also generate frontend proxy config snippet")
|
|
26
|
+
.option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)")
|
|
27
|
+
.option("--workspace", "Generate mock assets for all backend repos in the workspace")
|
|
28
|
+
.option("--serve", "Start mock server in background + patch frontend proxy (use with --frontend)")
|
|
29
|
+
.option("--frontend <path>", "Path to frontend project for proxy patching (used with --serve/--restore)")
|
|
30
|
+
.option("--restore", "Undo proxy changes and stop mock server (requires --frontend or auto-detects)")
|
|
31
|
+
.action(async (opts) => {
|
|
32
|
+
const currentDir = process.cwd();
|
|
33
|
+
const port = parseInt(opts.port, 10) || 3001;
|
|
34
|
+
|
|
35
|
+
console.log(chalk.blue("\n─── ai-spec mock ───────────────────────────────"));
|
|
36
|
+
|
|
37
|
+
// ── Restore mode ────────────────────────────────────────────────────────
|
|
38
|
+
if (opts.restore) {
|
|
39
|
+
const frontendDir = opts.frontend ? path.resolve(opts.frontend) : currentDir;
|
|
40
|
+
const r = await restoreMockProxy(frontendDir);
|
|
41
|
+
if (r.restored) {
|
|
42
|
+
console.log(chalk.green(" ✔ Proxy restored and mock server stopped."));
|
|
43
|
+
} else {
|
|
44
|
+
console.log(chalk.yellow(` ${r.note ?? "Nothing to restore."}`));
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Workspace mode ──────────────────────────────────────────────────────
|
|
50
|
+
if (opts.workspace) {
|
|
51
|
+
const workspaceLoader = new WorkspaceLoader(currentDir);
|
|
52
|
+
const workspaceConfig = await workspaceLoader.load();
|
|
53
|
+
if (!workspaceConfig) {
|
|
54
|
+
console.error(chalk.red(` No ${WORKSPACE_CONFIG_FILE} found. Run \`ai-spec workspace init\` first.`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const backendRepos = workspaceConfig.repos.filter((r) => r.role === "backend");
|
|
59
|
+
if (backendRepos.length === 0) {
|
|
60
|
+
console.log(chalk.yellow(" No backend repos found in workspace."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const repo of backendRepos) {
|
|
65
|
+
const repoAbsPath = workspaceLoader.resolveAbsPath(repo);
|
|
66
|
+
console.log(chalk.cyan(`\n Repo: ${repo.name} (${repoAbsPath})`));
|
|
67
|
+
|
|
68
|
+
const dslFile = await findLatestDslFile(repoAbsPath);
|
|
69
|
+
if (!dslFile) {
|
|
70
|
+
console.log(chalk.yellow(` No DSL file found — skipping.`));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const dsl: SpecDSL = await fs.readJson(dslFile);
|
|
75
|
+
const result = await generateMockAssets(dsl, repoAbsPath, {
|
|
76
|
+
port,
|
|
77
|
+
msw: opts.msw,
|
|
78
|
+
proxy: opts.proxy,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const f of result.files) {
|
|
82
|
+
console.log(chalk.green(` ✔ ${f.path}`));
|
|
83
|
+
console.log(chalk.gray(` ${f.description}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Single-repo mode ────────────────────────────────────────────────────
|
|
90
|
+
let dslPath: string | null = opts.dsl ?? null;
|
|
91
|
+
|
|
92
|
+
if (!dslPath) {
|
|
93
|
+
dslPath = await findLatestDslFile(currentDir);
|
|
94
|
+
if (!dslPath) {
|
|
95
|
+
console.error(
|
|
96
|
+
chalk.red(
|
|
97
|
+
" No .dsl.json file found in .ai-spec/. Run `ai-spec create` first or use --dsl <path>."
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.gray(` Using DSL: ${path.relative(currentDir, dslPath)}`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let dsl: SpecDSL;
|
|
106
|
+
try {
|
|
107
|
+
dsl = await fs.readJson(dslPath);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(chalk.red(` Failed to read DSL file: ${(err as Error).message}`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = await generateMockAssets(dsl, currentDir, {
|
|
114
|
+
port,
|
|
115
|
+
msw: opts.msw,
|
|
116
|
+
proxy: opts.proxy,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(chalk.green(`\n ✔ Mock assets generated (${result.files.length} file(s)):`));
|
|
120
|
+
for (const f of result.files) {
|
|
121
|
+
console.log(chalk.green(` ${f.path}`));
|
|
122
|
+
console.log(chalk.gray(` ${f.description}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Serve mode: start mock server + patch frontend proxy ────────────────
|
|
126
|
+
if (opts.serve) {
|
|
127
|
+
const serverJsPath = path.join(currentDir, "mock", "server.js");
|
|
128
|
+
if (!(await fs.pathExists(serverJsPath))) {
|
|
129
|
+
console.error(chalk.red(" mock/server.js not found — generation may have failed."));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pid = startMockServerBackground(serverJsPath, port);
|
|
134
|
+
console.log(chalk.green(`\n ✔ Mock server started (PID ${pid}) → http://localhost:${port}`));
|
|
135
|
+
|
|
136
|
+
if (opts.frontend) {
|
|
137
|
+
const frontendDir = path.resolve(opts.frontend);
|
|
138
|
+
const proxyResult = await applyMockProxy(frontendDir, port, dsl.endpoints);
|
|
139
|
+
await saveMockServerPid(frontendDir, pid);
|
|
140
|
+
|
|
141
|
+
if (proxyResult.applied) {
|
|
142
|
+
console.log(chalk.green(` ✔ Frontend proxy patched (${proxyResult.framework})`));
|
|
143
|
+
console.log(chalk.bold.cyan(`\n Ready! Open a new terminal and run:`));
|
|
144
|
+
console.log(chalk.white(` cd ${frontendDir}`));
|
|
145
|
+
console.log(chalk.white(` ${proxyResult.devCommand}`));
|
|
146
|
+
console.log(chalk.gray(`\n When done: ai-spec mock --restore --frontend ${frontendDir}`));
|
|
147
|
+
} else {
|
|
148
|
+
console.log(chalk.yellow(` ⚠ Auto-patch not available for ${proxyResult.framework}.`));
|
|
149
|
+
if (proxyResult.note) console.log(chalk.gray(` ${proxyResult.note}`));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
console.log(chalk.gray(` Tip: use --frontend <path> to also auto-patch your frontend proxy config.`));
|
|
153
|
+
console.log(chalk.gray(` Mock server: http://localhost:${port}`));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(chalk.blue("\n─── Quick start ────────────────────────────────"));
|
|
159
|
+
console.log(chalk.white(` 1. Install express (if not already):`));
|
|
160
|
+
console.log(chalk.gray(` npm install --save-dev express`));
|
|
161
|
+
console.log(chalk.white(` 2. Start mock server:`));
|
|
162
|
+
console.log(chalk.gray(` node mock/server.js`));
|
|
163
|
+
console.log(chalk.gray(` # or: ai-spec mock --serve --frontend <path-to-frontend>`));
|
|
164
|
+
console.log(chalk.white(` 3. Configure your frontend to proxy API calls to:`));
|
|
165
|
+
console.log(chalk.gray(` http://localhost:${port}`));
|
|
166
|
+
if (opts.proxy) {
|
|
167
|
+
console.log(chalk.gray(` (See the generated proxy config file for framework-specific instructions)`));
|
|
168
|
+
}
|
|
169
|
+
if (opts.msw) {
|
|
170
|
+
console.log(chalk.white(` 4. MSW: import and start the worker in your app entry:`));
|
|
171
|
+
console.log(chalk.gray(` import { worker } from './mocks/browser';`));
|
|
172
|
+
console.log(chalk.gray(` if (process.env.NODE_ENV === 'development') worker.start();`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { findLatestDslFile } from "../../core/mock-server-generator";
|
|
6
|
+
import { SpecDSL } from "../../core/dsl-types";
|
|
7
|
+
import { saveTypescriptTypes, generateTypescriptTypes } from "../../core/types-generator";
|
|
8
|
+
|
|
9
|
+
export function registerTypes(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("types")
|
|
12
|
+
.description("Generate TypeScript types from the latest DSL (models + endpoint request types)")
|
|
13
|
+
.option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)")
|
|
14
|
+
.option("--output <path>", "Output file path (default: .ai-spec/<feature>.types.ts)")
|
|
15
|
+
.option("--stdout", "Print generated types to stdout instead of writing a file")
|
|
16
|
+
.option("--no-endpoint-types", "Skip endpoint request/response type generation")
|
|
17
|
+
.option("--no-endpoint-map", "Skip the API_ENDPOINTS constant map")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const currentDir = process.cwd();
|
|
20
|
+
|
|
21
|
+
// ── Resolve DSL ──────────────────────────────────────────────────────────
|
|
22
|
+
let dslPath: string | null = opts.dsl ?? null;
|
|
23
|
+
if (!dslPath) {
|
|
24
|
+
dslPath = await findLatestDslFile(currentDir);
|
|
25
|
+
if (!dslPath) {
|
|
26
|
+
console.error(
|
|
27
|
+
chalk.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>.")
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let dsl: SpecDSL;
|
|
34
|
+
try {
|
|
35
|
+
dsl = await fs.readJson(dslPath);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(chalk.red(` Failed to read DSL: ${(err as Error).message}`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const genOpts = {
|
|
42
|
+
includeEndpointTypes: opts.endpointTypes !== false,
|
|
43
|
+
includeEndpointMap: opts.endpointMap !== false,
|
|
44
|
+
outputPath: opts.output,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── Stdout mode ──────────────────────────────────────────────────────────
|
|
48
|
+
if (opts.stdout) {
|
|
49
|
+
const content = generateTypescriptTypes(dsl, genOpts);
|
|
50
|
+
process.stdout.write(content);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── File mode ────────────────────────────────────────────────────────────
|
|
55
|
+
const outputPath = await saveTypescriptTypes(dsl, currentDir, genOpts);
|
|
56
|
+
const relPath = path.relative(currentDir, outputPath);
|
|
57
|
+
|
|
58
|
+
console.log(chalk.green(`\n ✔ TypeScript types generated: ${relPath}`));
|
|
59
|
+
console.log(chalk.gray(` Feature : ${dsl.feature.title}`));
|
|
60
|
+
console.log(chalk.gray(` Models : ${dsl.models.length}`));
|
|
61
|
+
console.log(chalk.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
62
|
+
if (dsl.components?.length) {
|
|
63
|
+
console.log(chalk.gray(` Components: ${dsl.components.length}`));
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk.blue(`\n Usage:`));
|
|
66
|
+
console.log(chalk.gray(` import type { ${dsl.models.slice(0, 3).map((m) => m.name).join(", ")}${dsl.models.length > 3 ? ", ..." : ""} } from './${relPath}';`));
|
|
67
|
+
console.log(chalk.gray(` import { API_ENDPOINTS } from './${relPath}';\n`));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { listVcrRecordings, loadVcrRecording } from "../../core/vcr";
|
|
4
|
+
|
|
5
|
+
export function registerVcr(program: Command): void {
|
|
6
|
+
const vcr = program
|
|
7
|
+
.command("vcr")
|
|
8
|
+
.description("Manage VCR recordings for offline pipeline replay");
|
|
9
|
+
|
|
10
|
+
// ── ai-spec vcr list ──────────────────────────────────────────────────────
|
|
11
|
+
vcr
|
|
12
|
+
.command("list")
|
|
13
|
+
.description("List available VCR recordings in .ai-spec-vcr/")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const recordings = await listVcrRecordings(cwd);
|
|
17
|
+
|
|
18
|
+
if (recordings.length === 0) {
|
|
19
|
+
console.log(chalk.gray("No VCR recordings found."));
|
|
20
|
+
console.log(chalk.gray("Record a run with: ai-spec create --vcr-record <idea>"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(chalk.cyan("\n─── VCR Recordings ─────────────────────────────"));
|
|
25
|
+
for (const r of recordings) {
|
|
26
|
+
console.log(
|
|
27
|
+
" " + chalk.white(r.runId) +
|
|
28
|
+
chalk.gray(` · ${r.entryCount} AI calls · ${r.providers.join(", ")}`) +
|
|
29
|
+
chalk.gray(` · ${r.recordedAt.slice(0, 10)}`)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk.cyan("─".repeat(49)));
|
|
33
|
+
console.log(chalk.gray("\nInspect : ai-spec vcr show <runId>"));
|
|
34
|
+
console.log(chalk.gray("Replay : ai-spec create --vcr-replay <runId> <idea>"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── ai-spec vcr show <runId> ──────────────────────────────────────────────
|
|
38
|
+
vcr
|
|
39
|
+
.command("show <runId>")
|
|
40
|
+
.description("Show call-by-call details of a VCR recording")
|
|
41
|
+
.action(async (runId: string) => {
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const recording = await loadVcrRecording(cwd, runId);
|
|
44
|
+
|
|
45
|
+
if (!recording) {
|
|
46
|
+
console.log(chalk.red(`Recording not found: ${runId}`));
|
|
47
|
+
console.log(chalk.gray(`Expected: .ai-spec-vcr/${runId}.json`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.cyan(`\n─── VCR: ${recording.runId} ──────────────────────────`));
|
|
52
|
+
console.log(chalk.gray(` Recorded at : ${recording.recordedAt}`));
|
|
53
|
+
console.log(chalk.gray(` Providers : ${recording.providers.join(", ")}`));
|
|
54
|
+
console.log(chalk.gray(` Total calls : ${recording.entryCount}`));
|
|
55
|
+
console.log(chalk.cyan("\n Calls:"));
|
|
56
|
+
|
|
57
|
+
for (const entry of recording.entries) {
|
|
58
|
+
const idx = String(entry.index).padStart(2, "0");
|
|
59
|
+
const preview = entry.promptPreview.slice(0, 90).replace(/\s+/g, " ");
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.gray(` [${idx}]`) + " " +
|
|
62
|
+
chalk.white(`${entry.providerName}/${entry.modelName}`) +
|
|
63
|
+
chalk.gray(` ${entry.durationMs}ms hash:${entry.callHash}`)
|
|
64
|
+
);
|
|
65
|
+
console.log(chalk.gray(` "${preview}..."`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.cyan("─".repeat(49)));
|
|
69
|
+
});
|
|
70
|
+
}
|