@aibloom/cli 0.2.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 +11 -0
- package/dist/adapters.d.ts +17 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +38 -0
- package/dist/adapters.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +847 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +165 -0
- package/dist/markdown.js.map +1 -0
- package/dist/page-planner.d.ts +8 -0
- package/dist/page-planner.d.ts.map +1 -0
- package/dist/page-planner.js +170 -0
- package/dist/page-planner.js.map +1 -0
- package/dist/verification.d.ts +11 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +119 -0
- package/dist/verification.js.map +1 -0
- package/package.json +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { cac } from "cac";
|
|
5
|
+
import { ConflictDetector, DesignDocParser, DesignSystemSynthesizer, FileChangeApplier, FusionOptionGenerator, ProjectStateStore, RunRecordStore, StyleFingerprintExtractor, TaskStore, VibeUIError, readJsonFile, writeJsonFile, } from "@aibloom/core";
|
|
6
|
+
import { DesignReferenceDocumentSchema, FusionDecisionSchema, FusionOptionSchema, HostTaskResultSchema, HostTaskSchema, SCHEMA_VERSION, StyleConflictReportSchema, StyleFingerprintSchema, VibeUIConfigSchema, } from "@aibloom/schemas";
|
|
7
|
+
import { allDetections, adapterById, detectFramework } from "./adapters.js";
|
|
8
|
+
import { createImplementationPlan, createPageShape, createProductSpec, pageArtifactDir, slugify, } from "./page-planner.js";
|
|
9
|
+
import { renderImplementationPlanMarkdown, renderImplementationPrompt, renderPageShapeMarkdown, renderProductMarkdown, } from "./markdown.js";
|
|
10
|
+
import { verifyPage } from "./verification.js";
|
|
11
|
+
const cli = cac("vibeui");
|
|
12
|
+
const DEFAULT_VIEWPORTS = [
|
|
13
|
+
{ name: "mobile", width: 390, height: 844 },
|
|
14
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
15
|
+
{ name: "desktop", width: 1440, height: 1000 },
|
|
16
|
+
];
|
|
17
|
+
function now() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
async function recordRun(projectRoot, run) {
|
|
21
|
+
const runStore = new RunRecordStore(projectRoot);
|
|
22
|
+
await runStore.append({ schemaVersion: SCHEMA_VERSION, ...run });
|
|
23
|
+
const store = new ProjectStateStore(projectRoot);
|
|
24
|
+
await store.update((state) => ({ ...state, runs: [...state.runs.filter((id) => id !== run.id), run.id] }));
|
|
25
|
+
}
|
|
26
|
+
function defaultConfig(framework) {
|
|
27
|
+
return VibeUIConfigSchema.parse({
|
|
28
|
+
schemaVersion: SCHEMA_VERSION,
|
|
29
|
+
framework,
|
|
30
|
+
packageManager: "pnpm",
|
|
31
|
+
verification: {
|
|
32
|
+
lint: true,
|
|
33
|
+
typecheck: true,
|
|
34
|
+
unitTest: true,
|
|
35
|
+
build: true,
|
|
36
|
+
playwright: true,
|
|
37
|
+
accessibility: true,
|
|
38
|
+
viewports: DEFAULT_VIEWPORTS,
|
|
39
|
+
},
|
|
40
|
+
safety: {
|
|
41
|
+
allowedWriteRoots: ["app", "src", "components", "pages", "."],
|
|
42
|
+
confirmDependencyInstall: true,
|
|
43
|
+
maxRepairAttempts: 3,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function readConfig(projectRoot) {
|
|
48
|
+
const configPath = path.join(projectRoot, ".vibeui", "config.json");
|
|
49
|
+
try {
|
|
50
|
+
return await readJsonFile(configPath, VibeUIConfigSchema);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
const state = await new ProjectStateStore(projectRoot).read();
|
|
54
|
+
const config = defaultConfig(state.framework);
|
|
55
|
+
await writeJsonFile(configPath, config);
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function handleError(error) {
|
|
60
|
+
if (error instanceof VibeUIError) {
|
|
61
|
+
console.error(`${error.code}: ${error.message}`);
|
|
62
|
+
if (error.details)
|
|
63
|
+
console.error(error.details);
|
|
64
|
+
}
|
|
65
|
+
else if (error instanceof Error) {
|
|
66
|
+
console.error(error.message);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error(String(error));
|
|
70
|
+
}
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
73
|
+
async function ensureInitialized(projectRoot) {
|
|
74
|
+
const store = new ProjectStateStore(projectRoot);
|
|
75
|
+
const state = await store.read();
|
|
76
|
+
if (state.projectStage === "uninitialized") {
|
|
77
|
+
throw new VibeUIError("NOT_INITIALIZED", "No Aibloom project initialized. Run `aibloom init` first.");
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
function pageBySlug(pages, slugOrId) {
|
|
82
|
+
if (!slugOrId)
|
|
83
|
+
return pages[pages.length - 1];
|
|
84
|
+
return pages.find((page) => page.slug === slugOrId || page.id === slugOrId);
|
|
85
|
+
}
|
|
86
|
+
function optionList(value) {
|
|
87
|
+
if (!value)
|
|
88
|
+
return [];
|
|
89
|
+
return Array.isArray(value) ? value : [value];
|
|
90
|
+
}
|
|
91
|
+
function fusionAlias(value) {
|
|
92
|
+
if (value === "primary-accent")
|
|
93
|
+
return "primary-plus-accent";
|
|
94
|
+
if (value === "derived-system")
|
|
95
|
+
return "new-derived-system";
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
async function initializeProject(projectRoot, options) {
|
|
99
|
+
const detection = await detectFramework(projectRoot, options.framework).catch(() => null);
|
|
100
|
+
const framework = detection?.context.framework ?? null;
|
|
101
|
+
await fs.mkdir(path.join(projectRoot, ".vibeui", "artifacts", "design-references"), { recursive: true });
|
|
102
|
+
await fs.mkdir(path.join(projectRoot, ".vibeui", "tasks"), { recursive: true });
|
|
103
|
+
await fs.mkdir(path.join(projectRoot, "docs"), { recursive: true });
|
|
104
|
+
await writeJsonFile(path.join(projectRoot, ".vibeui", "config.json"), defaultConfig(framework));
|
|
105
|
+
const state = await new ProjectStateStore(projectRoot).initialize(options.name, framework);
|
|
106
|
+
await recordRun(projectRoot, {
|
|
107
|
+
id: `run-${Date.now()}`,
|
|
108
|
+
command: "vibeui init",
|
|
109
|
+
projectStage: state.projectStage,
|
|
110
|
+
status: "completed",
|
|
111
|
+
startedAt: now(),
|
|
112
|
+
completedAt: now(),
|
|
113
|
+
artifacts: [".vibeui/state.json", ".vibeui/config.json"],
|
|
114
|
+
errors: [],
|
|
115
|
+
});
|
|
116
|
+
return state;
|
|
117
|
+
}
|
|
118
|
+
async function ensureProjectReady(projectRoot, framework) {
|
|
119
|
+
const store = new ProjectStateStore(projectRoot);
|
|
120
|
+
const state = await store.read();
|
|
121
|
+
if (state.projectStage === "uninitialized") {
|
|
122
|
+
return initializeProject(projectRoot, { framework });
|
|
123
|
+
}
|
|
124
|
+
return state;
|
|
125
|
+
}
|
|
126
|
+
async function importDesignReference(projectRoot, file, name) {
|
|
127
|
+
await ensureInitialized(projectRoot);
|
|
128
|
+
const parser = new DesignDocParser();
|
|
129
|
+
const doc = await parser.parse({
|
|
130
|
+
path: path.resolve(projectRoot, file),
|
|
131
|
+
name,
|
|
132
|
+
sourceType: "manual",
|
|
133
|
+
});
|
|
134
|
+
const artifactsDir = path.join(projectRoot, ".vibeui", "artifacts", "design-references");
|
|
135
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
136
|
+
const docPath = path.join(artifactsDir, `${doc.id}.json`);
|
|
137
|
+
await writeJsonFile(docPath, doc);
|
|
138
|
+
await new ProjectStateStore(projectRoot).addDesignReference({
|
|
139
|
+
id: doc.id,
|
|
140
|
+
name: doc.name,
|
|
141
|
+
sourceType: "manual",
|
|
142
|
+
role: "candidate",
|
|
143
|
+
status: "parsed",
|
|
144
|
+
importedAt: now(),
|
|
145
|
+
});
|
|
146
|
+
return { doc, docPath };
|
|
147
|
+
}
|
|
148
|
+
async function analyzeDesignReferences(projectRoot) {
|
|
149
|
+
const state = await ensureInitialized(projectRoot);
|
|
150
|
+
const extractor = new StyleFingerprintExtractor();
|
|
151
|
+
const fingerprints = [];
|
|
152
|
+
for (const ref of state.designReferences) {
|
|
153
|
+
const doc = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "design-references", `${ref.id}.json`), DesignReferenceDocumentSchema);
|
|
154
|
+
fingerprints.push(StyleFingerprintSchema.parse(extractor.extract(doc)));
|
|
155
|
+
}
|
|
156
|
+
await writeJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "style-fingerprints.json"), fingerprints);
|
|
157
|
+
await new ProjectStateStore(projectRoot).update((current) => ({
|
|
158
|
+
...current,
|
|
159
|
+
projectStage: "design_references_analyzed",
|
|
160
|
+
designReferences: current.designReferences.map((ref) => ({ ...ref, status: "analyzed" })),
|
|
161
|
+
}));
|
|
162
|
+
return fingerprints;
|
|
163
|
+
}
|
|
164
|
+
async function detectDesignConflicts(projectRoot) {
|
|
165
|
+
await ensureInitialized(projectRoot);
|
|
166
|
+
const fingerprints = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "style-fingerprints.json"), StyleFingerprintSchema.array());
|
|
167
|
+
const detector = new ConflictDetector();
|
|
168
|
+
const report = StyleConflictReportSchema.parse(detector.detect(fingerprints));
|
|
169
|
+
const options = new FusionOptionGenerator().generate(fingerprints, report);
|
|
170
|
+
await writeJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "style-conflict-report.json"), report);
|
|
171
|
+
await writeJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "fusion-options.json"), options);
|
|
172
|
+
if (report.conflicts.length > 0 && fingerprints.length > 1) {
|
|
173
|
+
await new ProjectStateStore(projectRoot).setProjectStage("fusion_decision_required");
|
|
174
|
+
}
|
|
175
|
+
return { fingerprints, report, options };
|
|
176
|
+
}
|
|
177
|
+
function createFusionDecision(fingerprints, selected) {
|
|
178
|
+
const primary = selected?.primaryReference
|
|
179
|
+
? fingerprints.find((fingerprint) => fingerprint.id === selected.primaryReference)
|
|
180
|
+
: fingerprints[0];
|
|
181
|
+
const primaryId = primary?.id ?? fingerprints[0]?.id ?? "unknown";
|
|
182
|
+
return FusionDecisionSchema.parse({
|
|
183
|
+
schemaVersion: SCHEMA_VERSION,
|
|
184
|
+
selectedStrategy: selected?.strategy ?? "primary-plus-accent",
|
|
185
|
+
selectedOptionId: selected?.id ?? "single-reference",
|
|
186
|
+
userInstruction: selected?.description ?? `Use ${primary?.name ?? "the only design reference"} as the project design system.`,
|
|
187
|
+
timestamp: now(),
|
|
188
|
+
finalReferenceRoles: Object.fromEntries(fingerprints.map((fingerprint) => [
|
|
189
|
+
fingerprint.id,
|
|
190
|
+
fingerprint.id === primaryId ? "primary" : "secondary",
|
|
191
|
+
])),
|
|
192
|
+
rejectedRules: selected?.rejectedRules ?? [],
|
|
193
|
+
conditionalRules: selected?.risks ?? [],
|
|
194
|
+
notes: selected
|
|
195
|
+
? [`Selected option: ${selected.label}`]
|
|
196
|
+
: [`Primary default: ${primary?.name ?? "n/a"}`],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function recordFusionDecision(projectRoot, decision) {
|
|
200
|
+
await writeJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "fusion-decision.json"), decision);
|
|
201
|
+
await new ProjectStateStore(projectRoot).update((state) => ({
|
|
202
|
+
...state,
|
|
203
|
+
fusionDecision: {
|
|
204
|
+
strategy: decision.selectedStrategy,
|
|
205
|
+
selectedOptionId: decision.selectedOptionId,
|
|
206
|
+
recordedAt: now(),
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
function selectFusionOption(options, strategy) {
|
|
211
|
+
if (!strategy)
|
|
212
|
+
return options[0];
|
|
213
|
+
const normalized = fusionAlias(strategy);
|
|
214
|
+
return options.find((item) => item.id === normalized || item.strategy === normalized);
|
|
215
|
+
}
|
|
216
|
+
async function synthesizeProjectDesign(projectRoot) {
|
|
217
|
+
await ensureInitialized(projectRoot);
|
|
218
|
+
const fingerprints = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "style-fingerprints.json"), StyleFingerprintSchema.array());
|
|
219
|
+
const decision = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "fusion-decision.json"), FusionDecisionSchema);
|
|
220
|
+
const synthesized = new DesignSystemSynthesizer().synthesize(fingerprints, decision);
|
|
221
|
+
await fs.writeFile(path.join(projectRoot, "DESIGN.md"), synthesized.content, "utf-8");
|
|
222
|
+
const brief = `# Design Fusion Brief
|
|
223
|
+
|
|
224
|
+
## Selected Strategy
|
|
225
|
+
|
|
226
|
+
${decision.selectedStrategy}
|
|
227
|
+
|
|
228
|
+
## User Instruction
|
|
229
|
+
|
|
230
|
+
${decision.userInstruction}
|
|
231
|
+
|
|
232
|
+
## Conditional Rules
|
|
233
|
+
|
|
234
|
+
${decision.conditionalRules.map((rule) => `- ${rule}`).join("\n") || "- None"}
|
|
235
|
+
|
|
236
|
+
## Source Decisions
|
|
237
|
+
|
|
238
|
+
${synthesized.decisions.map((item) => `- ${item.decisionId}: ${item.dimension} -> ${item.finalRule}`).join("\n")}
|
|
239
|
+
`;
|
|
240
|
+
const briefPath = path.join(projectRoot, "DESIGN_FUSION_BRIEF.md");
|
|
241
|
+
const sourceMapPath = path.join(projectRoot, "docs", "ui", "design-source-map.json");
|
|
242
|
+
await fs.mkdir(path.dirname(sourceMapPath), { recursive: true });
|
|
243
|
+
await fs.writeFile(briefPath, brief, "utf-8");
|
|
244
|
+
await writeJsonFile(sourceMapPath, {
|
|
245
|
+
schemaVersion: SCHEMA_VERSION,
|
|
246
|
+
projectDesignPath: "DESIGN.md",
|
|
247
|
+
decisions: synthesized.decisions,
|
|
248
|
+
});
|
|
249
|
+
await new ProjectStateStore(projectRoot).update((state) => ({
|
|
250
|
+
...state,
|
|
251
|
+
activeDesignSystem: "DESIGN.md",
|
|
252
|
+
projectStage: "design_system_synthesized",
|
|
253
|
+
}));
|
|
254
|
+
return {
|
|
255
|
+
designPath: "DESIGN.md",
|
|
256
|
+
fusionBriefPath: "DESIGN_FUSION_BRIEF.md",
|
|
257
|
+
sourceMapPath: "docs/ui/design-source-map.json",
|
|
258
|
+
decisions: synthesized.decisions,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function prepareDesignSystem(projectRoot, input) {
|
|
262
|
+
await ensureProjectReady(projectRoot, input.framework);
|
|
263
|
+
const importedDesigns = [];
|
|
264
|
+
for (const [index, designPath] of input.designs.entries()) {
|
|
265
|
+
const imported = await importDesignReference(projectRoot, designPath, `design-${index + 1}`);
|
|
266
|
+
importedDesigns.push(path.relative(projectRoot, imported.docPath));
|
|
267
|
+
}
|
|
268
|
+
const state = await ensureInitialized(projectRoot);
|
|
269
|
+
if (state.designReferences.length === 0) {
|
|
270
|
+
throw new VibeUIError("DESIGN_SYSTEM_REQUIRED", "Pass at least one --design path or synthesize a DESIGN.md first.");
|
|
271
|
+
}
|
|
272
|
+
const fingerprints = await analyzeDesignReferences(projectRoot);
|
|
273
|
+
const { report, options } = await detectDesignConflicts(projectRoot);
|
|
274
|
+
const requiresChoice = fingerprints.length > 1 && report.conflicts.length > 0 && !input.fusionStrategy;
|
|
275
|
+
if (requiresChoice) {
|
|
276
|
+
return {
|
|
277
|
+
status: "needs_fusion_choice",
|
|
278
|
+
nextAction: "select-fusion-strategy",
|
|
279
|
+
importedDesigns,
|
|
280
|
+
conflictReport: report,
|
|
281
|
+
fusionOptions: options,
|
|
282
|
+
artifacts: [
|
|
283
|
+
".vibeui/artifacts/style-fingerprints.json",
|
|
284
|
+
".vibeui/artifacts/style-conflict-report.json",
|
|
285
|
+
".vibeui/artifacts/fusion-options.json",
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const selected = selectFusionOption(options, input.fusionStrategy);
|
|
290
|
+
if (input.fusionStrategy && !selected) {
|
|
291
|
+
throw new VibeUIError("INVALID_STATE", `Unknown fusion strategy: ${input.fusionStrategy}`);
|
|
292
|
+
}
|
|
293
|
+
const decision = createFusionDecision(fingerprints, selected);
|
|
294
|
+
await recordFusionDecision(projectRoot, decision);
|
|
295
|
+
const synthesized = await synthesizeProjectDesign(projectRoot);
|
|
296
|
+
return {
|
|
297
|
+
status: "completed",
|
|
298
|
+
nextAction: "create-page",
|
|
299
|
+
importedDesigns,
|
|
300
|
+
conflictReport: report,
|
|
301
|
+
fusionOptions: options,
|
|
302
|
+
selectedFusion: decision,
|
|
303
|
+
artifacts: [
|
|
304
|
+
...importedDesigns,
|
|
305
|
+
".vibeui/artifacts/style-fingerprints.json",
|
|
306
|
+
".vibeui/artifacts/style-conflict-report.json",
|
|
307
|
+
".vibeui/artifacts/fusion-options.json",
|
|
308
|
+
".vibeui/artifacts/fusion-decision.json",
|
|
309
|
+
synthesized.designPath,
|
|
310
|
+
synthesized.fusionBriefPath,
|
|
311
|
+
synthesized.sourceMapPath,
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function createPageTask(projectRoot, input) {
|
|
316
|
+
const state = await ensureInitialized(projectRoot);
|
|
317
|
+
if (!state.activeDesignSystem) {
|
|
318
|
+
throw new VibeUIError("DESIGN_SYSTEM_REQUIRED", "Run `aibloom prepare-design --design <path>` before creating pages.");
|
|
319
|
+
}
|
|
320
|
+
const { adapter, context } = await detectFramework(projectRoot, input.framework);
|
|
321
|
+
const slug = slugify(input.prompt);
|
|
322
|
+
const pageId = `page-${slug}`;
|
|
323
|
+
const artifactDir = pageArtifactDir(slug);
|
|
324
|
+
const pageDir = path.join(projectRoot, artifactDir);
|
|
325
|
+
await fs.mkdir(pageDir, { recursive: true });
|
|
326
|
+
const product = createProductSpec(input.prompt, slug);
|
|
327
|
+
const shape = await createPageShape(pageId, input.prompt, context.framework);
|
|
328
|
+
const plan = await createImplementationPlan(shape, adapter, context);
|
|
329
|
+
const pageShapePath = path.join(artifactDir, "PAGE_SHAPE.md");
|
|
330
|
+
const implementationPlanPath = path.join(artifactDir, "IMPLEMENTATION_PLAN.md");
|
|
331
|
+
const implementationPromptPath = path.join(artifactDir, "IMPLEMENTATION_PROMPT.md");
|
|
332
|
+
await writeJsonFile(path.join(pageDir, "product.json"), product);
|
|
333
|
+
await fs.writeFile(path.join(projectRoot, "PRODUCT.md"), renderProductMarkdown(product), "utf-8");
|
|
334
|
+
await writeJsonFile(path.join(pageDir, "page-shape.json"), shape);
|
|
335
|
+
await fs.writeFile(path.join(projectRoot, pageShapePath), renderPageShapeMarkdown(shape), "utf-8");
|
|
336
|
+
await writeJsonFile(path.join(pageDir, "implementation-plan.json"), plan);
|
|
337
|
+
await fs.writeFile(path.join(projectRoot, implementationPlanPath), renderImplementationPlanMarkdown(plan), "utf-8");
|
|
338
|
+
await fs.writeFile(path.join(projectRoot, implementationPromptPath), renderImplementationPrompt(pageShapePath, implementationPlanPath), "utf-8");
|
|
339
|
+
const taskId = `task-${Date.now()}-${slug}`;
|
|
340
|
+
const page = {
|
|
341
|
+
id: pageId,
|
|
342
|
+
name: shape.pageName,
|
|
343
|
+
slug,
|
|
344
|
+
route: shape.route,
|
|
345
|
+
framework: context.framework,
|
|
346
|
+
stage: "implementation_pending",
|
|
347
|
+
prompt: input.prompt,
|
|
348
|
+
artifactDir,
|
|
349
|
+
shapePath: pageShapePath,
|
|
350
|
+
implementationPlanPath,
|
|
351
|
+
implementationPromptPath,
|
|
352
|
+
activeTaskId: taskId,
|
|
353
|
+
repairAttempts: 0,
|
|
354
|
+
lastVerificationReport: null,
|
|
355
|
+
createdAt: now(),
|
|
356
|
+
updatedAt: now(),
|
|
357
|
+
};
|
|
358
|
+
const taskInputs = [
|
|
359
|
+
{ kind: "product", path: "PRODUCT.md", required: true },
|
|
360
|
+
{ kind: "design-system", path: "DESIGN.md", required: true },
|
|
361
|
+
{ kind: "page-shape", path: pageShapePath, required: true },
|
|
362
|
+
{ kind: "implementation-plan", path: implementationPlanPath, required: true },
|
|
363
|
+
];
|
|
364
|
+
if (input.prototype) {
|
|
365
|
+
taskInputs.push({ kind: "prototype", path: input.prototype, required: false });
|
|
366
|
+
}
|
|
367
|
+
const task = HostTaskSchema.parse({
|
|
368
|
+
schemaVersion: SCHEMA_VERSION,
|
|
369
|
+
id: taskId,
|
|
370
|
+
type: "implement-page",
|
|
371
|
+
pageId,
|
|
372
|
+
status: "pending",
|
|
373
|
+
createdAt: now(),
|
|
374
|
+
inputs: taskInputs,
|
|
375
|
+
constraints: {
|
|
376
|
+
projectRoot,
|
|
377
|
+
allowedWritePaths: plan.targetFiles.map((file) => file.path),
|
|
378
|
+
forbiddenPaths: [".git", "node_modules", ".env", ".env.local"],
|
|
379
|
+
maxRepairAttempts: 3,
|
|
380
|
+
requirePlanBeforeWrite: true,
|
|
381
|
+
},
|
|
382
|
+
expectedOutputs: [
|
|
383
|
+
{ kind: "file-change", description: "Implement all target files in IMPLEMENTATION_PLAN.md" },
|
|
384
|
+
{ kind: "task-result", description: "Write a HostTaskResult for `aibloom task complete`" },
|
|
385
|
+
{ kind: "verification-pass", description: "Aibloom verify passes after implementation" },
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
await new TaskStore(projectRoot).create(task);
|
|
389
|
+
await new ProjectStateStore(projectRoot).upsertPage(page);
|
|
390
|
+
return { page, task, pageShapePath, implementationPlanPath, implementationPromptPath };
|
|
391
|
+
}
|
|
392
|
+
cli.help().version("0.2.0");
|
|
393
|
+
cli
|
|
394
|
+
.command("init", "Initialize or update an Aibloom project")
|
|
395
|
+
.option("--name <name>", "Project name")
|
|
396
|
+
.option("--framework <framework>", "next, vite-react, or auto", { default: "auto" })
|
|
397
|
+
.action(async (options) => {
|
|
398
|
+
const projectRoot = process.cwd();
|
|
399
|
+
try {
|
|
400
|
+
const state = await initializeProject(projectRoot, options);
|
|
401
|
+
console.log("Aibloom initialized.");
|
|
402
|
+
console.log(` Project: ${state.projectName ?? "(unnamed)"}`);
|
|
403
|
+
console.log(` Framework: ${state.framework ?? "(not detected)"}`);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
handleError(error);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
cli
|
|
410
|
+
.command("prepare-design", "Prepare a project DESIGN.md from one or more design references")
|
|
411
|
+
.option("--design <path>", "DESIGN.md reference path; repeat for multiple references")
|
|
412
|
+
.option("--fusion-strategy <strategy>", "Fusion option id or strategy")
|
|
413
|
+
.option("--framework <framework>", "next, vite-react, or auto", { default: "auto" })
|
|
414
|
+
.option("--json", "Print JSON")
|
|
415
|
+
.action(async (options) => {
|
|
416
|
+
const projectRoot = process.cwd();
|
|
417
|
+
try {
|
|
418
|
+
const result = await prepareDesignSystem(projectRoot, {
|
|
419
|
+
designs: optionList(options.design),
|
|
420
|
+
fusionStrategy: options.fusionStrategy,
|
|
421
|
+
framework: options.framework,
|
|
422
|
+
});
|
|
423
|
+
if (options.json) {
|
|
424
|
+
console.log(JSON.stringify(result, null, 2));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (result.status === "needs_fusion_choice") {
|
|
428
|
+
console.log("Design references need a fusion choice.");
|
|
429
|
+
console.log(` Compatibility score: ${result.conflictReport.compatibilityScore}`);
|
|
430
|
+
console.log("Fusion options:");
|
|
431
|
+
for (const option of result.fusionOptions) {
|
|
432
|
+
console.log(` ${option.id}. ${option.label} (${option.strategy})`);
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
console.log("Project design system prepared.");
|
|
437
|
+
for (const artifact of result.artifacts)
|
|
438
|
+
console.log(` ${artifact}`);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
handleError(error);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
cli
|
|
445
|
+
.command("prepare-page <prompt>", "Prepare design artifacts and a host task for a page")
|
|
446
|
+
.option("--design <path>", "DESIGN.md reference path; repeat for multiple references")
|
|
447
|
+
.option("--prototype <path>", "Optional prototype UI image path for the host agent")
|
|
448
|
+
.option("--fusion-strategy <strategy>", "Fusion option id or strategy")
|
|
449
|
+
.option("--framework <framework>", "next, vite-react, or auto", { default: "auto" })
|
|
450
|
+
.option("--json", "Print JSON")
|
|
451
|
+
.action(async (prompt, options) => {
|
|
452
|
+
const projectRoot = process.cwd();
|
|
453
|
+
try {
|
|
454
|
+
await ensureProjectReady(projectRoot, options.framework);
|
|
455
|
+
const state = await ensureInitialized(projectRoot);
|
|
456
|
+
const designs = optionList(options.design);
|
|
457
|
+
const rootDesignPath = path.join(projectRoot, "DESIGN.md");
|
|
458
|
+
const rootDesignExists = await fs.access(rootDesignPath).then(() => true, () => false);
|
|
459
|
+
if (!state.activeDesignSystem) {
|
|
460
|
+
const designInputs = designs.length > 0 ? designs : rootDesignExists ? ["DESIGN.md"] : [];
|
|
461
|
+
const designResult = await prepareDesignSystem(projectRoot, {
|
|
462
|
+
designs: designInputs,
|
|
463
|
+
fusionStrategy: options.fusionStrategy,
|
|
464
|
+
framework: options.framework,
|
|
465
|
+
});
|
|
466
|
+
if (designResult.status === "needs_fusion_choice") {
|
|
467
|
+
const response = {
|
|
468
|
+
...designResult,
|
|
469
|
+
pageSlug: null,
|
|
470
|
+
taskId: null,
|
|
471
|
+
taskPath: null,
|
|
472
|
+
requiredInputs: [],
|
|
473
|
+
allowedWritePaths: [],
|
|
474
|
+
};
|
|
475
|
+
if (options.json)
|
|
476
|
+
console.log(JSON.stringify(response, null, 2));
|
|
477
|
+
else {
|
|
478
|
+
console.log("Design references need a fusion choice before page planning.");
|
|
479
|
+
for (const option of designResult.fusionOptions) {
|
|
480
|
+
console.log(` ${option.id}. ${option.label} (${option.strategy})`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else if (designs.length > 0) {
|
|
487
|
+
const designResult = await prepareDesignSystem(projectRoot, {
|
|
488
|
+
designs,
|
|
489
|
+
fusionStrategy: options.fusionStrategy,
|
|
490
|
+
framework: options.framework,
|
|
491
|
+
});
|
|
492
|
+
if (designResult.status === "needs_fusion_choice") {
|
|
493
|
+
const response = {
|
|
494
|
+
...designResult,
|
|
495
|
+
pageSlug: null,
|
|
496
|
+
taskId: null,
|
|
497
|
+
taskPath: null,
|
|
498
|
+
requiredInputs: [],
|
|
499
|
+
allowedWritePaths: [],
|
|
500
|
+
};
|
|
501
|
+
if (options.json)
|
|
502
|
+
console.log(JSON.stringify(response, null, 2));
|
|
503
|
+
else
|
|
504
|
+
console.log("Design references need a fusion choice before page planning.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const created = await createPageTask(projectRoot, {
|
|
509
|
+
prompt,
|
|
510
|
+
framework: options.framework,
|
|
511
|
+
prototype: options.prototype,
|
|
512
|
+
});
|
|
513
|
+
const response = {
|
|
514
|
+
status: "completed",
|
|
515
|
+
nextAction: "run-host-task",
|
|
516
|
+
pageSlug: created.page.slug,
|
|
517
|
+
taskId: created.task.id,
|
|
518
|
+
taskPath: path.join(".vibeui", "tasks", created.task.id, "task.json"),
|
|
519
|
+
requiredInputs: created.task.inputs,
|
|
520
|
+
allowedWritePaths: created.task.constraints.allowedWritePaths,
|
|
521
|
+
artifacts: [
|
|
522
|
+
"PRODUCT.md",
|
|
523
|
+
created.pageShapePath,
|
|
524
|
+
created.implementationPlanPath,
|
|
525
|
+
created.implementationPromptPath,
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
if (options.json) {
|
|
529
|
+
console.log(JSON.stringify(response, null, 2));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.log(`Page prepared: ${created.page.slug}`);
|
|
533
|
+
console.log(` Task: ${created.task.id}`);
|
|
534
|
+
console.log(` Next: vibeui task next --json`);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
handleError(error);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
cli
|
|
541
|
+
.command("doctor", "Inspect Aibloom, framework, package manager, and browser dependencies")
|
|
542
|
+
.action(async () => {
|
|
543
|
+
const projectRoot = process.cwd();
|
|
544
|
+
try {
|
|
545
|
+
const detections = await allDetections(projectRoot);
|
|
546
|
+
const checks = [];
|
|
547
|
+
checks.push({ checkType: "manual_review_required", status: "pass", message: `Node ${process.version}` });
|
|
548
|
+
for (const item of detections) {
|
|
549
|
+
checks.push({
|
|
550
|
+
checkType: "manual_review_required",
|
|
551
|
+
status: item.detection.detected ? "pass" : "skip",
|
|
552
|
+
message: `${item.adapter}: confidence ${item.detection.confidence}`,
|
|
553
|
+
details: item.detection.evidence.join("\n"),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
await Function("specifier", "return import(specifier)")("@playwright/test");
|
|
558
|
+
checks.push({ checkType: "playwright", status: "pass", message: "@playwright/test importable" });
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
checks.push({ checkType: "playwright", status: "skip", message: "@playwright/test not installed" });
|
|
562
|
+
}
|
|
563
|
+
console.log("Aibloom doctor:");
|
|
564
|
+
for (const check of checks) {
|
|
565
|
+
console.log(` ${check.status.toUpperCase()} ${check.message}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
handleError(error);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
cli
|
|
573
|
+
.command("report", "View current project status")
|
|
574
|
+
.option("--json", "Print JSON")
|
|
575
|
+
.action(async (options) => {
|
|
576
|
+
const projectRoot = process.cwd();
|
|
577
|
+
try {
|
|
578
|
+
const state = await new ProjectStateStore(projectRoot).read();
|
|
579
|
+
const tasks = await new TaskStore(projectRoot).list();
|
|
580
|
+
if (options.json) {
|
|
581
|
+
console.log(JSON.stringify({ state, tasks }, null, 2));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (state.projectStage === "uninitialized") {
|
|
585
|
+
console.log("Aibloom project not initialized. Run `aibloom init`.");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
console.log("Aibloom Project Report");
|
|
589
|
+
console.log(` Project: ${state.projectName ?? "(unnamed)"}`);
|
|
590
|
+
console.log(` Project stage: ${state.projectStage}`);
|
|
591
|
+
console.log(` Framework: ${state.framework ?? "(not detected)"}`);
|
|
592
|
+
console.log(` Design system: ${state.activeDesignSystem ?? "(missing)"}`);
|
|
593
|
+
console.log(` Design references: ${state.designReferences.length}`);
|
|
594
|
+
console.log(` Pages: ${state.pages.length}`);
|
|
595
|
+
for (const page of state.pages) {
|
|
596
|
+
console.log(` ${page.slug}: ${page.stage} (${page.framework})`);
|
|
597
|
+
}
|
|
598
|
+
const nextTask = tasks.find((task) => task.status === "pending");
|
|
599
|
+
console.log(` Next task: ${nextTask ? nextTask.id : "(none)"}`);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
handleError(error);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
cli
|
|
606
|
+
.command("import-design <file>", "Advanced: import a DESIGN.md reference document")
|
|
607
|
+
.option("--name <name>", "Reference name")
|
|
608
|
+
.action(async (file, options) => {
|
|
609
|
+
const projectRoot = process.cwd();
|
|
610
|
+
try {
|
|
611
|
+
const { doc, docPath } = await importDesignReference(projectRoot, file, options.name);
|
|
612
|
+
console.log(`Imported design reference: ${doc.name}`);
|
|
613
|
+
console.log(` ${docPath}`);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
handleError(error);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
cli
|
|
620
|
+
.command("analyze-designs", "Advanced: extract style fingerprints from imported DESIGN.md references")
|
|
621
|
+
.action(async () => {
|
|
622
|
+
const projectRoot = process.cwd();
|
|
623
|
+
try {
|
|
624
|
+
const fingerprints = await analyzeDesignReferences(projectRoot);
|
|
625
|
+
console.log(`Analyzed ${fingerprints.length} design reference(s).`);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
handleError(error);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
cli
|
|
632
|
+
.command("design-conflicts", "Advanced: detect conflicts between imported design references")
|
|
633
|
+
.action(async () => {
|
|
634
|
+
const projectRoot = process.cwd();
|
|
635
|
+
try {
|
|
636
|
+
const { report, options } = await detectDesignConflicts(projectRoot);
|
|
637
|
+
console.log(`Compatibility score: ${report.compatibilityScore}`);
|
|
638
|
+
console.log(`Conflicts: ${report.conflicts.length}`);
|
|
639
|
+
console.log("Fusion options:");
|
|
640
|
+
for (const option of options) {
|
|
641
|
+
console.log(` ${option.id}. ${option.label} (${option.strategy})`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
handleError(error);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
cli
|
|
649
|
+
.command("fuse-designs", "Advanced: select a design fusion strategy")
|
|
650
|
+
.option("--strategy <strategy>", "Fusion strategy or option id")
|
|
651
|
+
.action(async (options) => {
|
|
652
|
+
const projectRoot = process.cwd();
|
|
653
|
+
try {
|
|
654
|
+
await ensureInitialized(projectRoot);
|
|
655
|
+
if (!options.strategy) {
|
|
656
|
+
throw new VibeUIError("INVALID_STATE", "Pass --strategy with an option id or strategy name.");
|
|
657
|
+
}
|
|
658
|
+
const fingerprints = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "style-fingerprints.json"), StyleFingerprintSchema.array());
|
|
659
|
+
const fusionOptions = await readJsonFile(path.join(projectRoot, ".vibeui", "artifacts", "fusion-options.json"), FusionOptionSchema.array());
|
|
660
|
+
const selected = selectFusionOption(fusionOptions, options.strategy);
|
|
661
|
+
if (!selected)
|
|
662
|
+
throw new VibeUIError("INVALID_STATE", `Unknown fusion strategy: ${options.strategy}`);
|
|
663
|
+
const decision = createFusionDecision(fingerprints, selected);
|
|
664
|
+
await recordFusionDecision(projectRoot, decision);
|
|
665
|
+
console.log(`Fusion decision recorded: ${selected.label}`);
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
handleError(error);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
cli
|
|
672
|
+
.command("synthesize-design", "Advanced: generate project-level DESIGN.md from fusion decision")
|
|
673
|
+
.action(async () => {
|
|
674
|
+
const projectRoot = process.cwd();
|
|
675
|
+
try {
|
|
676
|
+
await synthesizeProjectDesign(projectRoot);
|
|
677
|
+
console.log("Project design system synthesized.");
|
|
678
|
+
console.log(" DESIGN.md");
|
|
679
|
+
console.log(" DESIGN_FUSION_BRIEF.md");
|
|
680
|
+
console.log(" docs/ui/design-source-map.json");
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
handleError(error);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
cli
|
|
687
|
+
.command("create-page <prompt>", "Advanced: create structured page artifacts and a host implementation task")
|
|
688
|
+
.option("--framework <framework>", "next, vite-react, or auto", { default: "auto" })
|
|
689
|
+
.action(async (prompt, options) => {
|
|
690
|
+
const projectRoot = process.cwd();
|
|
691
|
+
try {
|
|
692
|
+
const created = await createPageTask(projectRoot, { prompt, framework: options.framework });
|
|
693
|
+
console.log(`Page planned: ${created.page.slug}`);
|
|
694
|
+
console.log(` Task: ${created.task.id}`);
|
|
695
|
+
console.log(` ${created.pageShapePath}`);
|
|
696
|
+
console.log(` ${created.implementationPlanPath}`);
|
|
697
|
+
console.log(` ${created.implementationPromptPath}`);
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
handleError(error);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
cli
|
|
704
|
+
.command("task <subcommand> [taskId]", "Inspect or complete host tasks")
|
|
705
|
+
.option("--json", "Print JSON for task next")
|
|
706
|
+
.option("--result <path>", "Path to HostTaskResult JSON for task complete")
|
|
707
|
+
.action(async (subcommand, taskId, options) => {
|
|
708
|
+
const projectRoot = process.cwd();
|
|
709
|
+
try {
|
|
710
|
+
await ensureInitialized(projectRoot);
|
|
711
|
+
const taskStore = new TaskStore(projectRoot);
|
|
712
|
+
if (subcommand === "next") {
|
|
713
|
+
const task = await taskStore.next();
|
|
714
|
+
if (options.json)
|
|
715
|
+
console.log(JSON.stringify(task, null, 2));
|
|
716
|
+
else
|
|
717
|
+
console.log(task ? `${task.id} ${task.type} ${task.status}` : "No pending task.");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (subcommand === "complete") {
|
|
721
|
+
if (!taskId)
|
|
722
|
+
throw new VibeUIError("TASK_NOT_FOUND", "Pass a task id.");
|
|
723
|
+
if (!options.result)
|
|
724
|
+
throw new VibeUIError("TASK_NOT_FOUND", "Pass --result <path>.");
|
|
725
|
+
const task = await taskStore.get(taskId);
|
|
726
|
+
const result = await readJsonFile(path.resolve(projectRoot, options.result), HostTaskResultSchema);
|
|
727
|
+
if (result.taskId !== task.id) {
|
|
728
|
+
throw new VibeUIError("TASK_NOT_FOUND", `Result taskId ${result.taskId} does not match ${task.id}`);
|
|
729
|
+
}
|
|
730
|
+
await new FileChangeApplier(projectRoot).apply(task.id, result.changedFiles, task.constraints.allowedWritePaths);
|
|
731
|
+
await taskStore.saveResult(result);
|
|
732
|
+
await new ProjectStateStore(projectRoot).update((state) => ({
|
|
733
|
+
...state,
|
|
734
|
+
pages: state.pages.map((page) => page.id === task.pageId
|
|
735
|
+
? { ...page, stage: result.status === "completed" ? "implemented" : "blocked", activeTaskId: null, updatedAt: now() }
|
|
736
|
+
: page),
|
|
737
|
+
}));
|
|
738
|
+
console.log(`Task completed: ${task.id}`);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
throw new VibeUIError("TASK_NOT_FOUND", `Unknown task subcommand: ${subcommand}`);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
handleError(error);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
cli
|
|
748
|
+
.command("verify", "Run verification checks on current project state")
|
|
749
|
+
.option("--page <slug>", "Verify a specific page")
|
|
750
|
+
.action(async (options) => {
|
|
751
|
+
const projectRoot = process.cwd();
|
|
752
|
+
try {
|
|
753
|
+
const state = await ensureInitialized(projectRoot);
|
|
754
|
+
const config = await readConfig(projectRoot);
|
|
755
|
+
const page = pageBySlug(state.pages, options.page);
|
|
756
|
+
const framework = page?.framework ?? state.framework;
|
|
757
|
+
if (!framework)
|
|
758
|
+
throw new VibeUIError("FRAMEWORK_NOT_DETECTED", "No framework known. Run `aibloom init --framework auto`.");
|
|
759
|
+
const adapter = adapterById(framework);
|
|
760
|
+
const context = await adapter.inspect(projectRoot);
|
|
761
|
+
const report = await verifyPage({
|
|
762
|
+
projectRoot,
|
|
763
|
+
adapter,
|
|
764
|
+
context,
|
|
765
|
+
page,
|
|
766
|
+
viewports: config.verification.viewports,
|
|
767
|
+
reportDir: path.join(projectRoot, ".vibeui", "reports"),
|
|
768
|
+
});
|
|
769
|
+
await new ProjectStateStore(projectRoot).update((current) => ({
|
|
770
|
+
...current,
|
|
771
|
+
pages: current.pages.map((item) => page && item.id === page.id
|
|
772
|
+
? {
|
|
773
|
+
...item,
|
|
774
|
+
stage: report.requiredChecksPassed ? "completed" : "verification_failed",
|
|
775
|
+
lastVerificationReport: path.join(".vibeui", "reports", `${report.runId}.json`),
|
|
776
|
+
updatedAt: now(),
|
|
777
|
+
}
|
|
778
|
+
: item),
|
|
779
|
+
}));
|
|
780
|
+
console.log(`Verification: ${report.status.toUpperCase()}`);
|
|
781
|
+
console.log(` ${report.summary}`);
|
|
782
|
+
if (report.issues.length > 0) {
|
|
783
|
+
for (const issue of report.issues)
|
|
784
|
+
console.log(` - ${issue}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
handleError(error);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
cli
|
|
792
|
+
.command("repair", "Create a structured repair task from the latest failed verification")
|
|
793
|
+
.option("--page <slug>", "Page slug")
|
|
794
|
+
.action(async (options) => {
|
|
795
|
+
const projectRoot = process.cwd();
|
|
796
|
+
try {
|
|
797
|
+
const state = await ensureInitialized(projectRoot);
|
|
798
|
+
const page = pageBySlug(state.pages, options.page);
|
|
799
|
+
if (!page)
|
|
800
|
+
throw new VibeUIError("INVALID_STATE", "No page available to repair.");
|
|
801
|
+
if (page.repairAttempts >= 3) {
|
|
802
|
+
throw new VibeUIError("INVALID_STATE", `Repair limit reached for ${page.slug}`);
|
|
803
|
+
}
|
|
804
|
+
const taskId = `task-${Date.now()}-repair-${page.slug}`;
|
|
805
|
+
const task = HostTaskSchema.parse({
|
|
806
|
+
schemaVersion: SCHEMA_VERSION,
|
|
807
|
+
id: taskId,
|
|
808
|
+
type: "repair",
|
|
809
|
+
pageId: page.id,
|
|
810
|
+
status: "pending",
|
|
811
|
+
createdAt: now(),
|
|
812
|
+
inputs: [
|
|
813
|
+
{ kind: "verification-report", path: page.lastVerificationReport ?? ".vibeui/reports/VERIFICATION_REPORT.md", required: true },
|
|
814
|
+
{ kind: "page-shape", path: page.shapePath, required: true },
|
|
815
|
+
{ kind: "implementation-plan", path: page.implementationPlanPath, required: true },
|
|
816
|
+
],
|
|
817
|
+
constraints: {
|
|
818
|
+
projectRoot,
|
|
819
|
+
allowedWritePaths: [page.artifactDir, "app", "src", "components"],
|
|
820
|
+
forbiddenPaths: [".git", "node_modules", ".env", ".env.local"],
|
|
821
|
+
maxRepairAttempts: 3 - page.repairAttempts,
|
|
822
|
+
requirePlanBeforeWrite: true,
|
|
823
|
+
},
|
|
824
|
+
expectedOutputs: [
|
|
825
|
+
{ kind: "file-change", description: "Apply minimal changes that address the verification failures" },
|
|
826
|
+
{ kind: "task-result", description: "Write a HostTaskResult for `aibloom task complete`" },
|
|
827
|
+
],
|
|
828
|
+
});
|
|
829
|
+
await new TaskStore(projectRoot).create(task);
|
|
830
|
+
await new ProjectStateStore(projectRoot).update((current) => ({
|
|
831
|
+
...current,
|
|
832
|
+
pages: current.pages.map((item) => item.id === page.id
|
|
833
|
+
? { ...item, stage: "repair_pending", repairAttempts: item.repairAttempts + 1, activeTaskId: taskId, updatedAt: now() }
|
|
834
|
+
: item),
|
|
835
|
+
}));
|
|
836
|
+
console.log(`Repair task created: ${taskId}`);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
handleError(error);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
cli.on("command:*", () => {
|
|
843
|
+
console.error("Unknown command. Run `aibloom --help` for available commands.");
|
|
844
|
+
process.exitCode = 1;
|
|
845
|
+
});
|
|
846
|
+
cli.parse();
|
|
847
|
+
//# sourceMappingURL=index.js.map
|