forgecraft 1.0.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/CHANGELOG.md +105 -0
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/chunk-2HFEPXBV.js +2808 -0
- package/dist/chunk-2HFEPXBV.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1405 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/forge.config.json +11 -0
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AutoPipeline,
|
|
4
|
+
GitManager,
|
|
5
|
+
Orchestrator,
|
|
6
|
+
Pipeline,
|
|
7
|
+
Worker,
|
|
8
|
+
getAdapter,
|
|
9
|
+
listAdapters,
|
|
10
|
+
loadAndValidateConfig,
|
|
11
|
+
stateManager
|
|
12
|
+
} from "../chunk-2HFEPXBV.js";
|
|
13
|
+
|
|
14
|
+
// src/cli/index.ts
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
import chalk20 from "chalk";
|
|
17
|
+
|
|
18
|
+
// src/cli/commands/init.ts
|
|
19
|
+
import fs from "fs/promises";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import ora from "ora";
|
|
23
|
+
import inquirer from "inquirer";
|
|
24
|
+
var DEFAULT_CONFIG = {
|
|
25
|
+
framework: "nextjs",
|
|
26
|
+
model: "sonnet",
|
|
27
|
+
designPreview: "storybook",
|
|
28
|
+
githubSync: false,
|
|
29
|
+
githubRepo: null,
|
|
30
|
+
autoCommit: true,
|
|
31
|
+
storybook: {
|
|
32
|
+
port: 6006
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
async function initCommand(options) {
|
|
36
|
+
console.log(
|
|
37
|
+
chalk.bold("\n\u26A1 Forge") + " \u2014 Initializing project\n"
|
|
38
|
+
);
|
|
39
|
+
const forgeDir = path.join(process.cwd(), ".forge");
|
|
40
|
+
const exists = await fs.access(forgeDir).then(() => true).catch(() => false);
|
|
41
|
+
if (exists) {
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.yellow(" .forge/ directory already exists. Reinitialize? ")
|
|
44
|
+
);
|
|
45
|
+
const { confirm } = await inquirer.prompt([
|
|
46
|
+
{
|
|
47
|
+
type: "confirm",
|
|
48
|
+
name: "confirm",
|
|
49
|
+
message: "Overwrite existing Forge config?",
|
|
50
|
+
default: false
|
|
51
|
+
}
|
|
52
|
+
]);
|
|
53
|
+
if (!confirm) {
|
|
54
|
+
console.log(chalk.dim(" Aborted."));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const answers = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: "list",
|
|
61
|
+
name: "framework",
|
|
62
|
+
message: "What framework are you using?",
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: "Next.js (TypeScript + Tailwind + App Router)", value: "nextjs" },
|
|
65
|
+
{ name: "React + Vite (TypeScript SPA)", value: "react" },
|
|
66
|
+
{ name: "Django (Python + DRF)", value: "django" },
|
|
67
|
+
{ name: "Flutter (coming soon)", value: "flutter", disabled: true }
|
|
68
|
+
],
|
|
69
|
+
default: options.framework || "nextjs"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "list",
|
|
73
|
+
name: "model",
|
|
74
|
+
message: "Which Claude model should agents use?",
|
|
75
|
+
choices: [
|
|
76
|
+
{ name: "Sonnet (fast, cost-effective)", value: "sonnet" },
|
|
77
|
+
{ name: "Opus (highest quality, slower)", value: "opus" },
|
|
78
|
+
{ name: "Haiku (fastest, basic tasks)", value: "haiku" }
|
|
79
|
+
],
|
|
80
|
+
default: "sonnet"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: "confirm",
|
|
84
|
+
name: "githubSync",
|
|
85
|
+
message: "Sync sprint board to GitHub Issues/Projects?",
|
|
86
|
+
default: false
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
let githubRepo = null;
|
|
90
|
+
if (answers.githubSync) {
|
|
91
|
+
const { repo } = await inquirer.prompt([
|
|
92
|
+
{
|
|
93
|
+
type: "input",
|
|
94
|
+
name: "repo",
|
|
95
|
+
message: "GitHub repo (owner/repo):",
|
|
96
|
+
validate: (input) => input.includes("/") || "Use format: owner/repo"
|
|
97
|
+
}
|
|
98
|
+
]);
|
|
99
|
+
githubRepo = repo;
|
|
100
|
+
}
|
|
101
|
+
const spinner = ora("Creating .forge directory...").start();
|
|
102
|
+
const config = {
|
|
103
|
+
...DEFAULT_CONFIG,
|
|
104
|
+
framework: answers.framework,
|
|
105
|
+
model: answers.model,
|
|
106
|
+
githubSync: answers.githubSync,
|
|
107
|
+
githubRepo
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
await fs.mkdir(forgeDir, { recursive: true });
|
|
111
|
+
await fs.mkdir(path.join(forgeDir, "snapshots"), { recursive: true });
|
|
112
|
+
await fs.mkdir(path.join(forgeDir, "designs"), { recursive: true });
|
|
113
|
+
await fs.writeFile(
|
|
114
|
+
path.join(process.cwd(), "forge.config.json"),
|
|
115
|
+
JSON.stringify(config, null, 2)
|
|
116
|
+
);
|
|
117
|
+
await fs.writeFile(
|
|
118
|
+
path.join(forgeDir, "state.json"),
|
|
119
|
+
JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
currentPhase: "init",
|
|
122
|
+
currentStory: null,
|
|
123
|
+
workerMode: null,
|
|
124
|
+
queue: [],
|
|
125
|
+
history: []
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
132
|
+
const gitignoreExists = await fs.access(gitignorePath).then(() => true).catch(() => false);
|
|
133
|
+
if (gitignoreExists) {
|
|
134
|
+
const content = await fs.readFile(gitignorePath, "utf-8");
|
|
135
|
+
if (!content.includes(".forge/snapshots")) {
|
|
136
|
+
await fs.appendFile(gitignorePath, "\n# Forge snapshots\n.forge/snapshots/\n");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
spinner.succeed("Created .forge/ directory");
|
|
140
|
+
console.log(
|
|
141
|
+
chalk.green("\n \u2705 Forge initialized!\n")
|
|
142
|
+
);
|
|
143
|
+
console.log(" Created:");
|
|
144
|
+
console.log(chalk.dim(" .forge/state.json \u2014 Sprint state"));
|
|
145
|
+
console.log(chalk.dim(" .forge/snapshots/ \u2014 Action snapshots"));
|
|
146
|
+
console.log(chalk.dim(" .forge/designs/ \u2014 Design metadata"));
|
|
147
|
+
console.log(chalk.dim(" forge.config.json \u2014 Project config"));
|
|
148
|
+
if (answers.githubSync) {
|
|
149
|
+
console.log(
|
|
150
|
+
chalk.cyan(
|
|
151
|
+
`
|
|
152
|
+
\u{1F517} GitHub sync enabled for ${githubRepo}`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.dim(
|
|
158
|
+
"\n \u{1F4A1} Tip: Run " + chalk.white("forge init --github") + " later to enable GitHub sync"
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
console.log(
|
|
163
|
+
chalk.bold(
|
|
164
|
+
"\n Next step: " + chalk.cyan('forge plan "describe your app"') + "\n"
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
spinner.fail("Failed to initialize Forge");
|
|
169
|
+
console.error(chalk.red(` ${error}`));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/cli/commands/plan.ts
|
|
175
|
+
import chalk2 from "chalk";
|
|
176
|
+
import inquirer2 from "inquirer";
|
|
177
|
+
async function planCommand(description, options) {
|
|
178
|
+
const config = await stateManager.getConfig();
|
|
179
|
+
if (!config) {
|
|
180
|
+
console.log(
|
|
181
|
+
chalk2.red("\n Forge not initialized. Run: forge init\n")
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!description) {
|
|
186
|
+
const { desc } = await inquirer2.prompt([
|
|
187
|
+
{
|
|
188
|
+
type: "input",
|
|
189
|
+
name: "desc",
|
|
190
|
+
message: "Describe your application:",
|
|
191
|
+
validate: (input) => input.length > 10 || "Please provide a more detailed description"
|
|
192
|
+
}
|
|
193
|
+
]);
|
|
194
|
+
description = desc;
|
|
195
|
+
}
|
|
196
|
+
const pipeline = new Pipeline(config);
|
|
197
|
+
await pipeline.runPlanPhase(description);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/cli/commands/design.ts
|
|
201
|
+
import chalk3 from "chalk";
|
|
202
|
+
import fs2 from "fs/promises";
|
|
203
|
+
import path2 from "path";
|
|
204
|
+
import ora2 from "ora";
|
|
205
|
+
async function designCommand(options) {
|
|
206
|
+
const config = await stateManager.getConfig();
|
|
207
|
+
if (!config) {
|
|
208
|
+
console.log(chalk3.red("\n Forge not initialized. Run: forge init\n"));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const plan = await stateManager.getPlan();
|
|
212
|
+
if (!plan) {
|
|
213
|
+
console.log(
|
|
214
|
+
chalk3.red('\n No sprint plan found. Run: forge plan "description"\n')
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (options?.import) {
|
|
219
|
+
await importDesigns(options.import);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const pipeline = new Pipeline(config);
|
|
223
|
+
await pipeline.runDesignPhase(plan);
|
|
224
|
+
}
|
|
225
|
+
async function importDesigns(importPath) {
|
|
226
|
+
const spinner = ora2({ text: "Importing designs...", indent: 2 }).start();
|
|
227
|
+
const absPath = path2.resolve(importPath);
|
|
228
|
+
try {
|
|
229
|
+
const stat = await fs2.stat(absPath);
|
|
230
|
+
let files = [];
|
|
231
|
+
if (stat.isDirectory()) {
|
|
232
|
+
const entries = await fs2.readdir(absPath);
|
|
233
|
+
const imageExts = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
|
|
234
|
+
files = entries.filter((f) => imageExts.includes(path2.extname(f).toLowerCase())).map((f) => path2.join(absPath, f));
|
|
235
|
+
} else {
|
|
236
|
+
files = [absPath];
|
|
237
|
+
}
|
|
238
|
+
if (files.length === 0) {
|
|
239
|
+
spinner.warn("No image files found (png, jpg, svg, webp, gif)");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await stateManager.saveDesignReferences(files);
|
|
243
|
+
spinner.succeed(`Imported ${files.length} design reference${files.length > 1 ? "s" : ""}`);
|
|
244
|
+
console.log("");
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
console.log(chalk3.dim(` ${path2.basename(file)}`));
|
|
247
|
+
}
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(
|
|
250
|
+
chalk3.dim(" These references will be used in the design and build phases.")
|
|
251
|
+
);
|
|
252
|
+
console.log(
|
|
253
|
+
chalk3.dim(" The AI agent will read them and match the visual style.\n")
|
|
254
|
+
);
|
|
255
|
+
} catch {
|
|
256
|
+
spinner.fail(`Path not found: ${absPath}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/cli/commands/build.ts
|
|
261
|
+
import chalk4 from "chalk";
|
|
262
|
+
async function buildCommand(options) {
|
|
263
|
+
const config = await stateManager.getConfig();
|
|
264
|
+
if (!config) {
|
|
265
|
+
console.log(chalk4.red("\n Forge not initialized. Run: forge init\n"));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const plan = await stateManager.getPlan();
|
|
269
|
+
if (!plan) {
|
|
270
|
+
console.log(
|
|
271
|
+
chalk4.red('\n No sprint plan found. Run: forge plan "description"\n')
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (options?.story) {
|
|
276
|
+
const story = plan.epics.flatMap((e) => e.stories).find((s) => s.id === options.story);
|
|
277
|
+
if (!story) {
|
|
278
|
+
console.log(chalk4.red(`
|
|
279
|
+
Story "${options.story}" not found.
|
|
280
|
+
`));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (story.status === "done") {
|
|
284
|
+
console.log(chalk4.yellow(`
|
|
285
|
+
Story "${story.title}" is already done.
|
|
286
|
+
`));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const pipeline = new Pipeline(config);
|
|
291
|
+
await pipeline.runBuildPhase(plan);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/cli/commands/review.ts
|
|
295
|
+
import chalk5 from "chalk";
|
|
296
|
+
async function reviewCommand(options) {
|
|
297
|
+
const config = await stateManager.getConfig();
|
|
298
|
+
if (!config) {
|
|
299
|
+
console.log(chalk5.red("\n Forge not initialized. Run: forge init\n"));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const plan = await stateManager.getPlan();
|
|
303
|
+
if (!plan) {
|
|
304
|
+
console.log(
|
|
305
|
+
chalk5.red('\n No sprint plan found. Run: forge plan "description"\n')
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const reviewable = plan.epics.flatMap((e) => e.stories).filter((s) => s.status === "reviewing");
|
|
310
|
+
if (reviewable.length === 0) {
|
|
311
|
+
console.log(
|
|
312
|
+
chalk5.yellow("\n No stories ready for review. Run: forge build\n")
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const pipeline = new Pipeline(config);
|
|
317
|
+
await pipeline.runReviewPhase(plan);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/cli/commands/sprint.ts
|
|
321
|
+
import chalk6 from "chalk";
|
|
322
|
+
import inquirer3 from "inquirer";
|
|
323
|
+
async function sprintCommand(description) {
|
|
324
|
+
const config = await stateManager.getConfig();
|
|
325
|
+
if (!config) {
|
|
326
|
+
console.log(chalk6.red("\n Forge not initialized. Run: forge init\n"));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!description) {
|
|
330
|
+
const { desc } = await inquirer3.prompt([
|
|
331
|
+
{
|
|
332
|
+
type: "input",
|
|
333
|
+
name: "desc",
|
|
334
|
+
message: "What do you want to build?",
|
|
335
|
+
validate: (input) => input.length > 10 || "Please provide a more detailed description"
|
|
336
|
+
}
|
|
337
|
+
]);
|
|
338
|
+
description = desc;
|
|
339
|
+
}
|
|
340
|
+
const pipeline = new Pipeline(config);
|
|
341
|
+
await pipeline.runSprint(description);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/cli/commands/status.ts
|
|
345
|
+
import chalk7 from "chalk";
|
|
346
|
+
var STATUS_ICONS = {
|
|
347
|
+
planned: "\u{1F4CB}",
|
|
348
|
+
designing: "\u{1F3A8}",
|
|
349
|
+
"design-approved": "\u2705",
|
|
350
|
+
building: "\u{1F527}",
|
|
351
|
+
reviewing: "\u{1F50D}",
|
|
352
|
+
done: "\u2705",
|
|
353
|
+
blocked: "\u{1F6AB}"
|
|
354
|
+
};
|
|
355
|
+
async function statusCommand() {
|
|
356
|
+
const config = await stateManager.getConfig();
|
|
357
|
+
if (!config) {
|
|
358
|
+
console.log(chalk7.red("\n Forge not initialized. Run: forge init\n"));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const plan = await stateManager.getPlan();
|
|
362
|
+
const state = await stateManager.getState();
|
|
363
|
+
const git = new GitManager();
|
|
364
|
+
console.log(chalk7.bold("\n\u26A1 Forge Status\n"));
|
|
365
|
+
if (plan) {
|
|
366
|
+
console.log(chalk7.bold(` Project: ${plan.project}`));
|
|
367
|
+
console.log(chalk7.dim(` Framework: ${plan.framework}`));
|
|
368
|
+
console.log(chalk7.dim(` Phase: ${state.currentPhase}`));
|
|
369
|
+
const currentBranch = await git.getCurrentBranch();
|
|
370
|
+
console.log(chalk7.dim(` Branch: ${currentBranch}`));
|
|
371
|
+
console.log("");
|
|
372
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
373
|
+
const done = allStories.filter((s) => s.status === "done").length;
|
|
374
|
+
const total = allStories.length;
|
|
375
|
+
const percent = total > 0 ? Math.round(done / total * 100) : 0;
|
|
376
|
+
const barWidth = 30;
|
|
377
|
+
const filled = Math.round(percent / 100 * barWidth);
|
|
378
|
+
const bar = chalk7.green("\u2588".repeat(filled)) + chalk7.dim("\u2591".repeat(barWidth - filled));
|
|
379
|
+
console.log(` Progress: ${bar} ${percent}% (${done}/${total} stories)`);
|
|
380
|
+
console.log("");
|
|
381
|
+
for (const epic of plan.epics) {
|
|
382
|
+
console.log(chalk7.bold(` ${epic.title}`));
|
|
383
|
+
for (const story of epic.stories) {
|
|
384
|
+
const icon = STATUS_ICONS[story.status] || "\u2753";
|
|
385
|
+
const statusText = story.status === "done" ? chalk7.green(story.status) : story.status === "building" || story.status === "reviewing" ? chalk7.yellow(story.status) : story.status === "blocked" ? chalk7.red(story.status) : chalk7.dim(story.status);
|
|
386
|
+
const branchInfo = story.branch ? chalk7.dim(` (${story.branch})`) : "";
|
|
387
|
+
console.log(
|
|
388
|
+
` ${icon} ${story.title} \u2014 ${statusText}${branchInfo}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
console.log("");
|
|
392
|
+
}
|
|
393
|
+
if (state.queue.length > 0) {
|
|
394
|
+
console.log(chalk7.bold(" Queued Changes:"));
|
|
395
|
+
for (const change of state.queue) {
|
|
396
|
+
console.log(chalk7.yellow(` \u{1F4DD} ${change.message}`));
|
|
397
|
+
}
|
|
398
|
+
console.log("");
|
|
399
|
+
}
|
|
400
|
+
const tags = await git.listTags();
|
|
401
|
+
if (tags.length > 0) {
|
|
402
|
+
console.log(chalk7.bold(" Checkpoints:"));
|
|
403
|
+
for (const tag of tags) {
|
|
404
|
+
console.log(chalk7.dim(` \u{1F3F7}\uFE0F ${tag}`));
|
|
405
|
+
}
|
|
406
|
+
console.log("");
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
console.log(chalk7.dim(" No sprint plan yet."));
|
|
410
|
+
console.log(
|
|
411
|
+
chalk7.dim(
|
|
412
|
+
" Run: " + chalk7.white('forge plan "describe your app"') + "\n"
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/cli/commands/fix.ts
|
|
419
|
+
import chalk8 from "chalk";
|
|
420
|
+
import ora3 from "ora";
|
|
421
|
+
import { existsSync } from "fs";
|
|
422
|
+
async function fixCommand(description, options) {
|
|
423
|
+
const config = await stateManager.getConfig();
|
|
424
|
+
if (!config) {
|
|
425
|
+
console.log(chalk8.red("\n Forge not initialized. Run: forge init\n"));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!description || description.trim().length === 0) {
|
|
429
|
+
console.log(chalk8.red('\n Please describe the fix: forge fix "description"\n'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (options?.image && !existsSync(options.image)) {
|
|
433
|
+
console.log(chalk8.red(`
|
|
434
|
+
Image not found: ${options.image}
|
|
435
|
+
`));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
console.log(chalk8.bold("\n forge fix\n"));
|
|
439
|
+
console.log(chalk8.dim(` "${description}"`));
|
|
440
|
+
if (options?.image) {
|
|
441
|
+
console.log(chalk8.dim(` image: ${options.image}`));
|
|
442
|
+
}
|
|
443
|
+
console.log("");
|
|
444
|
+
const orchestrator = new Orchestrator(config);
|
|
445
|
+
const worker = new Worker(config, {});
|
|
446
|
+
const git = new GitManager();
|
|
447
|
+
const state = await stateManager.getState();
|
|
448
|
+
const spinner = ora3({ text: "Analyzing request...", indent: 2 }).start();
|
|
449
|
+
let decision;
|
|
450
|
+
try {
|
|
451
|
+
decision = await orchestrator.routeUserInput(description, state);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
spinner.fail("Failed to analyze request");
|
|
454
|
+
console.log(chalk8.red(` ${err instanceof Error ? err.message : err}`));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
switch (decision.action) {
|
|
458
|
+
case "route-to-worker": {
|
|
459
|
+
const mode = decision.workerMode || "fix";
|
|
460
|
+
spinner.text = `Worker (${mode}) applying fix...`;
|
|
461
|
+
const headBefore = await git.getHead();
|
|
462
|
+
await stateManager.saveSnapshot({
|
|
463
|
+
action: "fix",
|
|
464
|
+
storyId: state.currentStory,
|
|
465
|
+
branch: "main",
|
|
466
|
+
commitBefore: headBefore
|
|
467
|
+
});
|
|
468
|
+
let fixPrompt = decision.prompt || description;
|
|
469
|
+
if (options?.image) {
|
|
470
|
+
fixPrompt += `
|
|
471
|
+
|
|
472
|
+
A screenshot has been saved at: ${options.image}
|
|
473
|
+
Read this image file to see the visual issue the user is referring to.`;
|
|
474
|
+
}
|
|
475
|
+
const result = await worker.run(mode, fixPrompt, {
|
|
476
|
+
onProgress: (event) => {
|
|
477
|
+
if (event.type === "tool_use") {
|
|
478
|
+
spinner.text = event.content;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
if (result.success) {
|
|
483
|
+
await git.commitAll(`fix: ${description}`);
|
|
484
|
+
spinner.succeed("Fix applied");
|
|
485
|
+
for (const file of result.filesModified) {
|
|
486
|
+
console.log(chalk8.dim(` modified: ${file}`));
|
|
487
|
+
}
|
|
488
|
+
for (const file of result.filesCreated) {
|
|
489
|
+
console.log(chalk8.dim(` created: ${file}`));
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
spinner.fail("Fix failed");
|
|
493
|
+
for (const error of result.errors) {
|
|
494
|
+
console.log(chalk8.red(` ${error}`));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
await stateManager.addHistoryEntry({
|
|
498
|
+
action: "fix",
|
|
499
|
+
storyId: null,
|
|
500
|
+
details: description
|
|
501
|
+
});
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "add-story": {
|
|
505
|
+
spinner.succeed("New feature detected");
|
|
506
|
+
const plan = await stateManager.getPlan();
|
|
507
|
+
if (plan && plan.epics.length > 0) {
|
|
508
|
+
const newStory = {
|
|
509
|
+
id: `story-${Date.now()}`,
|
|
510
|
+
title: decision.story?.title || description,
|
|
511
|
+
description: decision.story?.description || description,
|
|
512
|
+
type: decision.story?.type || "fullstack",
|
|
513
|
+
status: "planned",
|
|
514
|
+
branch: null,
|
|
515
|
+
designApproved: false,
|
|
516
|
+
tags: [],
|
|
517
|
+
priority: 99,
|
|
518
|
+
dependencies: []
|
|
519
|
+
};
|
|
520
|
+
plan.epics[plan.epics.length - 1].stories.push(newStory);
|
|
521
|
+
await stateManager.savePlan(plan);
|
|
522
|
+
await git.commitAll(`forge: add story "${newStory.title}"`);
|
|
523
|
+
console.log(chalk8.green(` Added: ${newStory.title}`));
|
|
524
|
+
console.log(chalk8.dim(` Run ${chalk8.white("forge build")} or ${chalk8.white("forge auto")} to build it.
|
|
525
|
+
`));
|
|
526
|
+
} else {
|
|
527
|
+
console.log(chalk8.yellow(" No plan found. Run forge plan first to create a sprint plan.\n"));
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "answer": {
|
|
532
|
+
spinner.stop();
|
|
533
|
+
console.log(` ${decision.response}
|
|
534
|
+
`);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "queue-change": {
|
|
538
|
+
spinner.succeed("Change queued (Worker is busy)");
|
|
539
|
+
console.log(chalk8.dim(" Will apply after the current task completes.\n"));
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
default: {
|
|
543
|
+
spinner.fail("Could not process request");
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/cli/commands/undo.ts
|
|
550
|
+
import chalk9 from "chalk";
|
|
551
|
+
import inquirer4 from "inquirer";
|
|
552
|
+
async function undoCommand(options) {
|
|
553
|
+
const config = await stateManager.getConfig();
|
|
554
|
+
if (!config) {
|
|
555
|
+
console.log(chalk9.red("\n Forge not initialized. Run: forge init\n"));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const git = new GitManager();
|
|
559
|
+
console.log(chalk9.bold("\n Forge Undo\n"));
|
|
560
|
+
const log = await git.getForgeLog(parseInt(options?.steps || "10", 10));
|
|
561
|
+
const forgeCommits = log.filter(
|
|
562
|
+
(c) => c.message.startsWith("feat:") || c.message.startsWith("fix:") || c.message.startsWith("docs:")
|
|
563
|
+
);
|
|
564
|
+
if (forgeCommits.length === 0) {
|
|
565
|
+
console.log(chalk9.dim(" No actions to undo.\n"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const choices = forgeCommits.map((commit) => {
|
|
569
|
+
const date = new Date(commit.date);
|
|
570
|
+
const timeStr = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
571
|
+
const hash = commit.hash.slice(0, 7);
|
|
572
|
+
return {
|
|
573
|
+
name: ` ${chalk9.dim(hash)} ${commit.message} ${chalk9.dim(timeStr)}`,
|
|
574
|
+
value: commit
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
const { selected } = await inquirer4.prompt([
|
|
578
|
+
{
|
|
579
|
+
type: "list",
|
|
580
|
+
name: "selected",
|
|
581
|
+
message: "Which action to undo?",
|
|
582
|
+
choices: [
|
|
583
|
+
...choices,
|
|
584
|
+
new inquirer4.Separator(),
|
|
585
|
+
{ name: chalk9.dim(" Cancel"), value: null }
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
]);
|
|
589
|
+
if (!selected) {
|
|
590
|
+
console.log(chalk9.dim(" Cancelled.\n"));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const { confirm } = await inquirer4.prompt([
|
|
594
|
+
{
|
|
595
|
+
type: "confirm",
|
|
596
|
+
name: "confirm",
|
|
597
|
+
message: `Revert "${selected.message}"? This creates a new commit that undoes those changes.`,
|
|
598
|
+
default: false
|
|
599
|
+
}
|
|
600
|
+
]);
|
|
601
|
+
if (!confirm) {
|
|
602
|
+
console.log(chalk9.dim(" Cancelled.\n"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
await git.revertCommit(selected.hash);
|
|
607
|
+
console.log(chalk9.green(`
|
|
608
|
+
Reverted: ${selected.message}`));
|
|
609
|
+
console.log(chalk9.dim(` Commit ${selected.hash.slice(0, 7)} has been undone.
|
|
610
|
+
`));
|
|
611
|
+
await stateManager.addHistoryEntry({
|
|
612
|
+
action: "undo",
|
|
613
|
+
storyId: null,
|
|
614
|
+
details: `Reverted: ${selected.message} (${selected.hash.slice(0, 7)})`
|
|
615
|
+
});
|
|
616
|
+
const plan = await stateManager.getPlan();
|
|
617
|
+
if (plan && selected.message.startsWith("feat:")) {
|
|
618
|
+
const storyTitle = selected.message.replace("feat: ", "");
|
|
619
|
+
const story = plan.epics.flatMap((e) => e.stories).find((s) => s.title === storyTitle);
|
|
620
|
+
if (story && (story.status === "reviewing" || story.status === "done")) {
|
|
621
|
+
story.status = "planned";
|
|
622
|
+
await stateManager.savePlan(plan);
|
|
623
|
+
console.log(chalk9.dim(` Story "${story.title}" reset to planned.
|
|
624
|
+
`));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
629
|
+
if (msg.includes("conflict")) {
|
|
630
|
+
console.log(chalk9.red("\n Revert has conflicts."));
|
|
631
|
+
console.log(chalk9.dim(" Resolve them manually, then: git revert --continue\n"));
|
|
632
|
+
} else {
|
|
633
|
+
console.log(chalk9.red(`
|
|
634
|
+
Failed to revert: ${msg}`));
|
|
635
|
+
console.log(chalk9.dim(" Try manually: " + chalk9.white(`git revert ${selected.hash.slice(0, 7)}`) + "\n"));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/cli/commands/auto.ts
|
|
641
|
+
import chalk10 from "chalk";
|
|
642
|
+
import inquirer5 from "inquirer";
|
|
643
|
+
import { execSync } from "child_process";
|
|
644
|
+
import { existsSync as existsSync2 } from "fs";
|
|
645
|
+
import { homedir } from "os";
|
|
646
|
+
import path3 from "path";
|
|
647
|
+
function checkClaudeAuth() {
|
|
648
|
+
try {
|
|
649
|
+
execSync("which claude", { stdio: "ignore" });
|
|
650
|
+
} catch {
|
|
651
|
+
return { ok: false, reason: "Claude Code CLI is not installed." };
|
|
652
|
+
}
|
|
653
|
+
const claudeDir = path3.join(homedir(), ".claude");
|
|
654
|
+
if (!existsSync2(claudeDir)) {
|
|
655
|
+
return { ok: false, reason: "Claude Code is installed but not logged in." };
|
|
656
|
+
}
|
|
657
|
+
return { ok: true, reason: "" };
|
|
658
|
+
}
|
|
659
|
+
async function autoCommand(description, options) {
|
|
660
|
+
const auth = checkClaudeAuth();
|
|
661
|
+
if (!auth.ok) {
|
|
662
|
+
console.log(chalk10.red(`
|
|
663
|
+
${auth.reason}
|
|
664
|
+
`));
|
|
665
|
+
console.log(chalk10.bold(" Forge requires Claude Code to run.\n"));
|
|
666
|
+
console.log(chalk10.dim(" Who can use it:"));
|
|
667
|
+
console.log(chalk10.dim(" Anyone with a Claude Max, Team, or Enterprise subscription.\n"));
|
|
668
|
+
console.log(chalk10.dim(" Setup:"));
|
|
669
|
+
console.log(chalk10.dim(" 1. npm install -g @anthropic-ai/claude-code"));
|
|
670
|
+
console.log(chalk10.dim(" 2. claude login"));
|
|
671
|
+
console.log(chalk10.dim(' 3. forge auto "your app idea"\n'));
|
|
672
|
+
console.log(chalk10.dim(" Diagnose: forge doctor\n"));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const rawConfig = await stateManager.getConfig();
|
|
676
|
+
if (!rawConfig) {
|
|
677
|
+
console.log(chalk10.red("\n Forge not initialized. Run: forge init\n"));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const config = loadAndValidateConfig(rawConfig);
|
|
681
|
+
if (!config) return;
|
|
682
|
+
const adapter = getAdapter(config.framework);
|
|
683
|
+
const skipDesign = options.skipDesign || !adapter.designSupport;
|
|
684
|
+
if (!description) {
|
|
685
|
+
const { desc } = await inquirer5.prompt([
|
|
686
|
+
{
|
|
687
|
+
type: "input",
|
|
688
|
+
name: "desc",
|
|
689
|
+
message: "What do you want to build?",
|
|
690
|
+
validate: (input) => input.length > 10 || "Please provide a more detailed description"
|
|
691
|
+
}
|
|
692
|
+
]);
|
|
693
|
+
description = desc;
|
|
694
|
+
}
|
|
695
|
+
const allowedDomains = options.allowNetwork ? options.allowNetwork.split(",").map((d) => d.trim()) : void 0;
|
|
696
|
+
const pipeline = new AutoPipeline(config, {
|
|
697
|
+
sandbox: options.sandbox !== false,
|
|
698
|
+
quiet: options.quiet ?? false,
|
|
699
|
+
mute: options.mute ?? false,
|
|
700
|
+
deploy: options.deploy ?? false,
|
|
701
|
+
skipDesign,
|
|
702
|
+
allowedDomains
|
|
703
|
+
});
|
|
704
|
+
const result = await pipeline.run(description);
|
|
705
|
+
if (!result.success) {
|
|
706
|
+
process.exitCode = 1;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/cli/commands/resume.ts
|
|
711
|
+
import chalk11 from "chalk";
|
|
712
|
+
import inquirer6 from "inquirer";
|
|
713
|
+
async function resumeCommand(options) {
|
|
714
|
+
let workingDir;
|
|
715
|
+
try {
|
|
716
|
+
workingDir = process.cwd();
|
|
717
|
+
} catch {
|
|
718
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
719
|
+
process.chdir(home);
|
|
720
|
+
console.log(chalk11.yellow("\n Working directory no longer exists."));
|
|
721
|
+
console.log(chalk11.dim(` Falling back to: ${home}`));
|
|
722
|
+
console.log(chalk11.dim(" Please cd to your project directory and retry.\n"));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const config = await stateManager.getConfig();
|
|
726
|
+
if (!config) {
|
|
727
|
+
console.log(chalk11.red("\n Forge not initialized. Run: forge init\n"));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const plan = await stateManager.getPlan();
|
|
731
|
+
if (!plan) {
|
|
732
|
+
console.log(chalk11.red("\n No sprint plan found. Nothing to resume.\n"));
|
|
733
|
+
console.log(chalk11.dim(' Start a new sprint: forge auto "description"\n'));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const state = await stateManager.getState();
|
|
737
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
738
|
+
const done = allStories.filter((s) => s.status === "done").length;
|
|
739
|
+
const blocked = allStories.filter((s) => s.status === "blocked").length;
|
|
740
|
+
const remaining = allStories.filter(
|
|
741
|
+
(s) => s.status !== "done" && s.status !== "blocked"
|
|
742
|
+
);
|
|
743
|
+
if (remaining.length === 0 && blocked === 0) {
|
|
744
|
+
console.log(chalk11.green("\n Sprint is already complete. All stories done.\n"));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
console.log(chalk11.bold("\n Resuming sprint"));
|
|
748
|
+
console.log(chalk11.dim(` ${plan.project} \xB7 ${plan.framework}`));
|
|
749
|
+
console.log(chalk11.dim(` Phase: ${state.currentPhase}`));
|
|
750
|
+
console.log(
|
|
751
|
+
chalk11.dim(` Progress: ${done}/${allStories.length} done`) + (blocked > 0 ? chalk11.red(` \xB7 ${blocked} blocked`) : "")
|
|
752
|
+
);
|
|
753
|
+
console.log("");
|
|
754
|
+
if (remaining.length > 0) {
|
|
755
|
+
console.log(chalk11.dim(" Remaining stories:"));
|
|
756
|
+
for (const story of remaining) {
|
|
757
|
+
const icon = story.status === "building" ? chalk11.yellow("\u25D1") : story.status === "reviewing" ? chalk11.magenta("\u25D5") : story.status === "designing" ? chalk11.blue("\u25D0") : chalk11.dim("\u25CB");
|
|
758
|
+
console.log(` ${icon} ${story.title} ${chalk11.dim(`(${story.status})`)}`);
|
|
759
|
+
}
|
|
760
|
+
console.log("");
|
|
761
|
+
}
|
|
762
|
+
if (blocked > 0) {
|
|
763
|
+
const blockedStories = allStories.filter((s) => s.status === "blocked");
|
|
764
|
+
console.log(chalk11.red(" Blocked stories:"));
|
|
765
|
+
for (const story of blockedStories) {
|
|
766
|
+
console.log(` ${chalk11.red("\u2715")} ${story.title}`);
|
|
767
|
+
}
|
|
768
|
+
console.log("");
|
|
769
|
+
}
|
|
770
|
+
const { action } = await inquirer6.prompt([
|
|
771
|
+
{
|
|
772
|
+
type: "list",
|
|
773
|
+
name: "action",
|
|
774
|
+
message: "What would you like to do?",
|
|
775
|
+
choices: [
|
|
776
|
+
{ name: `Resume (${remaining.length} stories left)`, value: "resume" },
|
|
777
|
+
...blocked > 0 ? [{ name: `Retry blocked stories (${blocked})`, value: "retry" }] : [],
|
|
778
|
+
{ name: "Cancel", value: "cancel" }
|
|
779
|
+
]
|
|
780
|
+
}
|
|
781
|
+
]);
|
|
782
|
+
if (action === "cancel") {
|
|
783
|
+
console.log(chalk11.dim(" Cancelled.\n"));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (action === "retry") {
|
|
787
|
+
for (const story of allStories) {
|
|
788
|
+
if (story.status === "blocked") {
|
|
789
|
+
story.status = "planned";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
await stateManager.savePlan(plan);
|
|
793
|
+
}
|
|
794
|
+
for (const story of allStories) {
|
|
795
|
+
if (story.status === "building") {
|
|
796
|
+
story.status = story.designApproved ? "design-approved" : "planned";
|
|
797
|
+
}
|
|
798
|
+
if (story.status === "designing") {
|
|
799
|
+
story.status = "planned";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
await stateManager.savePlan(plan);
|
|
803
|
+
const pipeline = new AutoPipeline(config, {
|
|
804
|
+
sandbox: options.sandbox !== false,
|
|
805
|
+
quiet: options.quiet ?? false,
|
|
806
|
+
mute: options.mute ?? false,
|
|
807
|
+
skipDesign: options.skipDesign ?? false
|
|
808
|
+
});
|
|
809
|
+
const result = await pipeline.resume(plan);
|
|
810
|
+
if (!result.success) {
|
|
811
|
+
process.exitCode = 1;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/cli/commands/map.ts
|
|
816
|
+
import chalk12 from "chalk";
|
|
817
|
+
var STATUS_ICON = {
|
|
818
|
+
planned: chalk12.dim("\u25CB"),
|
|
819
|
+
designing: chalk12.blue("\u25D0"),
|
|
820
|
+
"design-approved": chalk12.cyan("\u25D1"),
|
|
821
|
+
building: chalk12.yellow("\u25D1"),
|
|
822
|
+
reviewing: chalk12.magenta("\u25D5"),
|
|
823
|
+
done: chalk12.green("\u25CF"),
|
|
824
|
+
blocked: chalk12.red("\u2715")
|
|
825
|
+
};
|
|
826
|
+
var STATUS_COLOR = {
|
|
827
|
+
planned: chalk12.dim,
|
|
828
|
+
designing: chalk12.blue,
|
|
829
|
+
"design-approved": chalk12.cyan,
|
|
830
|
+
building: chalk12.yellow,
|
|
831
|
+
reviewing: chalk12.magenta,
|
|
832
|
+
done: chalk12.green,
|
|
833
|
+
blocked: chalk12.red
|
|
834
|
+
};
|
|
835
|
+
async function mapCommand() {
|
|
836
|
+
const plan = await stateManager.getPlan();
|
|
837
|
+
if (!plan) {
|
|
838
|
+
console.log(chalk12.red("\n No sprint plan found. Run: forge plan\n"));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
842
|
+
const done = allStories.filter((s) => s.status === "done").length;
|
|
843
|
+
const total = allStories.length;
|
|
844
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
845
|
+
console.log("");
|
|
846
|
+
console.log(chalk12.bold(` ${plan.project}`) + chalk12.dim(` \xB7 ${plan.framework}`));
|
|
847
|
+
console.log(chalk12.dim(` ${plan.description}`));
|
|
848
|
+
console.log("");
|
|
849
|
+
const barWidth = 30;
|
|
850
|
+
const filled = Math.round(done / total * barWidth);
|
|
851
|
+
const bar = chalk12.green("\u2588".repeat(filled)) + chalk12.dim("\u2591".repeat(barWidth - filled));
|
|
852
|
+
console.log(` ${bar} ${chalk12.bold(`${pct}%`)} ${chalk12.dim(`(${done}/${total} stories)`)}`);
|
|
853
|
+
console.log("");
|
|
854
|
+
for (const epic of plan.epics) {
|
|
855
|
+
const epicDone = epic.stories.filter((s) => s.status === "done").length;
|
|
856
|
+
const epicTotal = epic.stories.length;
|
|
857
|
+
const epicStatus = epicDone === epicTotal ? chalk12.green("done") : epicDone > 0 ? chalk12.yellow("in progress") : chalk12.dim("planned");
|
|
858
|
+
console.log(` ${chalk12.bold(epic.title)} ${chalk12.dim(`(${epicDone}/${epicTotal})`)} ${epicStatus}`);
|
|
859
|
+
for (let i = 0; i < epic.stories.length; i++) {
|
|
860
|
+
const story = epic.stories[i];
|
|
861
|
+
const isLast = i === epic.stories.length - 1;
|
|
862
|
+
const connector = isLast ? "\u2514" : "\u251C";
|
|
863
|
+
const icon = STATUS_ICON[story.status] || chalk12.dim("?");
|
|
864
|
+
const colorFn = STATUS_COLOR[story.status] || chalk12.dim;
|
|
865
|
+
const typeTag = chalk12.dim(`[${story.type === "ui" ? "ui" : story.type === "backend" ? "api" : "full"}]`);
|
|
866
|
+
let line = ` ${chalk12.dim(connector + "\u2500")} ${icon} ${colorFn(story.title)} ${typeTag}`;
|
|
867
|
+
if (story.tags.length > 0) {
|
|
868
|
+
line += " " + chalk12.dim(story.tags[story.tags.length - 1]);
|
|
869
|
+
}
|
|
870
|
+
console.log(line);
|
|
871
|
+
if (story.dependencies.length > 0) {
|
|
872
|
+
const depPrefix = isLast ? " " : " \u2502";
|
|
873
|
+
const depNames = story.dependencies.map((depId) => {
|
|
874
|
+
const dep = allStories.find((s) => s.id === depId);
|
|
875
|
+
if (!dep) return chalk12.dim(depId);
|
|
876
|
+
const depIcon = STATUS_ICON[dep.status] || "?";
|
|
877
|
+
return `${depIcon} ${chalk12.dim(dep.title)}`;
|
|
878
|
+
}).join(", ");
|
|
879
|
+
console.log(chalk12.dim(`${depPrefix} \u2190 depends on: ${depNames}`));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
console.log("");
|
|
883
|
+
}
|
|
884
|
+
console.log(
|
|
885
|
+
chalk12.dim(" Legend: ") + `${chalk12.dim("\u25CB")} planned ${chalk12.blue("\u25D0")} designing ${chalk12.yellow("\u25D1")} building ${chalk12.magenta("\u25D5")} reviewing ${chalk12.green("\u25CF")} done ${chalk12.red("\u2715")} blocked`
|
|
886
|
+
);
|
|
887
|
+
console.log("");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/cli/commands/diff.ts
|
|
891
|
+
import chalk13 from "chalk";
|
|
892
|
+
async function diffCommand(v1, v2) {
|
|
893
|
+
const git = new GitManager();
|
|
894
|
+
const resolveRef = async (ref) => {
|
|
895
|
+
const tags = await git.listTags();
|
|
896
|
+
if (tags.includes(ref)) return ref;
|
|
897
|
+
const prefixed = `forge/${ref}`;
|
|
898
|
+
if (tags.includes(prefixed)) return prefixed;
|
|
899
|
+
const match = tags.find((t) => t.includes(ref));
|
|
900
|
+
if (match) return match;
|
|
901
|
+
return ref;
|
|
902
|
+
};
|
|
903
|
+
const ref1 = await resolveRef(v1);
|
|
904
|
+
const ref2 = v2 ? await resolveRef(v2) : "HEAD";
|
|
905
|
+
console.log(chalk13.bold("\n forge diff"));
|
|
906
|
+
console.log(chalk13.dim(` ${ref1} \u2192 ${ref2}
|
|
907
|
+
`));
|
|
908
|
+
try {
|
|
909
|
+
const diff = await git.getDiff2(ref1, ref2);
|
|
910
|
+
if (!diff.trim()) {
|
|
911
|
+
console.log(chalk13.dim(" No differences found.\n"));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const lines = diff.split("\n");
|
|
915
|
+
for (const line of lines) {
|
|
916
|
+
if (line.startsWith("diff --git")) {
|
|
917
|
+
console.log(chalk13.bold(chalk13.white(` ${line}`)));
|
|
918
|
+
} else if (line.startsWith("+++") || line.startsWith("---")) {
|
|
919
|
+
console.log(chalk13.dim(` ${line}`));
|
|
920
|
+
} else if (line.startsWith("+")) {
|
|
921
|
+
console.log(chalk13.green(` ${line}`));
|
|
922
|
+
} else if (line.startsWith("-")) {
|
|
923
|
+
console.log(chalk13.red(` ${line}`));
|
|
924
|
+
} else if (line.startsWith("@@")) {
|
|
925
|
+
console.log(chalk13.cyan(` ${line}`));
|
|
926
|
+
} else {
|
|
927
|
+
console.log(chalk13.dim(` ${line}`));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const filesChanged = lines.filter((l) => l.startsWith("diff --git")).length;
|
|
931
|
+
const additions = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
932
|
+
const deletions = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
933
|
+
console.log("");
|
|
934
|
+
console.log(
|
|
935
|
+
chalk13.dim(` ${filesChanged} files changed, `) + chalk13.green(`${additions} additions`) + chalk13.dim(", ") + chalk13.red(`${deletions} deletions`) + "\n"
|
|
936
|
+
);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.log(chalk13.red(` Could not compute diff: ${err instanceof Error ? err.message : err}`));
|
|
939
|
+
console.log(chalk13.dim(" Make sure both references exist (use forge history to see tags).\n"));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/cli/commands/doctor.ts
|
|
944
|
+
import chalk14 from "chalk";
|
|
945
|
+
import { execSync as execSync2 } from "child_process";
|
|
946
|
+
import { existsSync as existsSync3 } from "fs";
|
|
947
|
+
import { homedir as homedir2 } from "os";
|
|
948
|
+
import path4 from "path";
|
|
949
|
+
function checkCommand(cmd) {
|
|
950
|
+
try {
|
|
951
|
+
execSync2(`which ${cmd}`, { stdio: "ignore" });
|
|
952
|
+
return true;
|
|
953
|
+
} catch {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function getVersion(cmd) {
|
|
958
|
+
try {
|
|
959
|
+
return execSync2(`${cmd} --version`, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
960
|
+
} catch {
|
|
961
|
+
return "unknown";
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function doctorCommand() {
|
|
965
|
+
console.log(chalk14.bold("\n forge doctor\n"));
|
|
966
|
+
const checks = [];
|
|
967
|
+
const nodeVersion = process.version;
|
|
968
|
+
const nodeMajor = parseInt(nodeVersion.slice(1));
|
|
969
|
+
checks.push({
|
|
970
|
+
name: "Node.js",
|
|
971
|
+
status: nodeMajor >= 18 ? "pass" : "fail",
|
|
972
|
+
detail: nodeVersion,
|
|
973
|
+
fix: nodeMajor < 18 ? "Upgrade to Node.js 18+: https://nodejs.org" : void 0
|
|
974
|
+
});
|
|
975
|
+
const hasGit = checkCommand("git");
|
|
976
|
+
checks.push({
|
|
977
|
+
name: "Git",
|
|
978
|
+
status: hasGit ? "pass" : "fail",
|
|
979
|
+
detail: hasGit ? getVersion("git") : "not found",
|
|
980
|
+
fix: !hasGit ? "Install git: https://git-scm.com" : void 0
|
|
981
|
+
});
|
|
982
|
+
const hasClaude = checkCommand("claude");
|
|
983
|
+
checks.push({
|
|
984
|
+
name: "Claude Code CLI",
|
|
985
|
+
status: hasClaude ? "pass" : "fail",
|
|
986
|
+
detail: hasClaude ? "installed" : "not found",
|
|
987
|
+
fix: !hasClaude ? "npm install -g @anthropic-ai/claude-code" : void 0
|
|
988
|
+
});
|
|
989
|
+
const claudeDir = path4.join(homedir2(), ".claude");
|
|
990
|
+
const hasAuth = existsSync3(claudeDir);
|
|
991
|
+
checks.push({
|
|
992
|
+
name: "Claude auth",
|
|
993
|
+
status: hasClaude && hasAuth ? "pass" : hasClaude ? "fail" : "warn",
|
|
994
|
+
detail: hasAuth ? "logged in" : "not logged in",
|
|
995
|
+
fix: !hasAuth ? "claude login" : void 0
|
|
996
|
+
});
|
|
997
|
+
const config = await stateManager.getConfig();
|
|
998
|
+
checks.push({
|
|
999
|
+
name: "Forge project",
|
|
1000
|
+
status: config ? "pass" : "warn",
|
|
1001
|
+
detail: config ? `${config.framework} \xB7 ${config.model}` : "not initialized in this directory",
|
|
1002
|
+
fix: !config ? "forge init" : void 0
|
|
1003
|
+
});
|
|
1004
|
+
const plan = await stateManager.getPlan();
|
|
1005
|
+
checks.push({
|
|
1006
|
+
name: "Sprint plan",
|
|
1007
|
+
status: plan ? "pass" : "warn",
|
|
1008
|
+
detail: plan ? `${plan.project} \xB7 ${plan.epics.flatMap((e) => e.stories).length} stories` : "no plan",
|
|
1009
|
+
fix: !plan ? 'forge plan "description"' : void 0
|
|
1010
|
+
});
|
|
1011
|
+
if (config) {
|
|
1012
|
+
const framework = config.framework;
|
|
1013
|
+
if (framework === "nextjs" || framework === "react") {
|
|
1014
|
+
const hasNpm = checkCommand("npm");
|
|
1015
|
+
checks.push({
|
|
1016
|
+
name: "npm",
|
|
1017
|
+
status: hasNpm ? "pass" : "fail",
|
|
1018
|
+
detail: hasNpm ? getVersion("npm").replace("npm ", "") : "not found"
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (framework === "django") {
|
|
1022
|
+
const hasPython = checkCommand("python3");
|
|
1023
|
+
checks.push({
|
|
1024
|
+
name: "Python 3",
|
|
1025
|
+
status: hasPython ? "pass" : "fail",
|
|
1026
|
+
detail: hasPython ? getVersion("python3") : "not found",
|
|
1027
|
+
fix: !hasPython ? "Install Python 3.10+: https://python.org" : void 0
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const hasGh = checkCommand("gh");
|
|
1032
|
+
checks.push({
|
|
1033
|
+
name: "GitHub CLI",
|
|
1034
|
+
status: hasGh ? "pass" : "warn",
|
|
1035
|
+
detail: hasGh ? "installed" : "not installed (optional, for GitHub sync)",
|
|
1036
|
+
fix: !hasGh ? "https://cli.github.com (optional)" : void 0
|
|
1037
|
+
});
|
|
1038
|
+
let hasFailure = false;
|
|
1039
|
+
for (const check of checks) {
|
|
1040
|
+
const icon = check.status === "pass" ? chalk14.green("OK") : check.status === "fail" ? chalk14.red("FAIL") : chalk14.yellow("WARN");
|
|
1041
|
+
console.log(` ${icon} ${chalk14.bold(check.name)} ${chalk14.dim(check.detail)}`);
|
|
1042
|
+
if (check.fix) {
|
|
1043
|
+
console.log(chalk14.dim(` fix: ${check.fix}`));
|
|
1044
|
+
}
|
|
1045
|
+
if (check.status === "fail") hasFailure = true;
|
|
1046
|
+
}
|
|
1047
|
+
console.log("");
|
|
1048
|
+
console.log(chalk14.dim(" Supported frameworks:"));
|
|
1049
|
+
for (const adapter of listAdapters()) {
|
|
1050
|
+
console.log(chalk14.dim(` ${adapter.name} (${adapter.language})`));
|
|
1051
|
+
}
|
|
1052
|
+
console.log("");
|
|
1053
|
+
if (hasFailure) {
|
|
1054
|
+
console.log(chalk14.red(" Some checks failed. Fix the issues above before running Forge.\n"));
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log(chalk14.green(" All checks passed. Forge is ready.\n"));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/cli/commands/clean.ts
|
|
1061
|
+
import chalk15 from "chalk";
|
|
1062
|
+
import fs3 from "fs/promises";
|
|
1063
|
+
import path5 from "path";
|
|
1064
|
+
import inquirer7 from "inquirer";
|
|
1065
|
+
async function cleanCommand(options) {
|
|
1066
|
+
const forgeDir = path5.join(process.cwd(), ".forge");
|
|
1067
|
+
try {
|
|
1068
|
+
await fs3.access(forgeDir);
|
|
1069
|
+
} catch {
|
|
1070
|
+
console.log(chalk15.red("\n No .forge/ directory found. Nothing to clean.\n"));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (options.snapshots) {
|
|
1074
|
+
const snapshotsDir = path5.join(forgeDir, "snapshots");
|
|
1075
|
+
try {
|
|
1076
|
+
const files = await fs3.readdir(snapshotsDir);
|
|
1077
|
+
if (files.length === 0) {
|
|
1078
|
+
console.log(chalk15.dim("\n No snapshots to clean.\n"));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
for (const file of files) {
|
|
1082
|
+
await fs3.unlink(path5.join(snapshotsDir, file));
|
|
1083
|
+
}
|
|
1084
|
+
console.log(chalk15.green(`
|
|
1085
|
+
Cleaned ${files.length} snapshots.
|
|
1086
|
+
`));
|
|
1087
|
+
} catch {
|
|
1088
|
+
console.log(chalk15.dim("\n No snapshots directory found.\n"));
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
console.log(chalk15.bold("\n forge clean\n"));
|
|
1093
|
+
console.log(chalk15.dim(" This will remove:"));
|
|
1094
|
+
console.log(chalk15.dim(" .forge/plan.json \u2014 Sprint plan"));
|
|
1095
|
+
console.log(chalk15.dim(" .forge/state.json \u2014 Sprint state"));
|
|
1096
|
+
console.log(chalk15.dim(" .forge/snapshots/ \u2014 All snapshots"));
|
|
1097
|
+
console.log(chalk15.dim(" .forge/designs/ \u2014 Design metadata"));
|
|
1098
|
+
console.log(chalk15.dim("\n forge.config.json will be kept.\n"));
|
|
1099
|
+
if (!options.force) {
|
|
1100
|
+
const { confirm } = await inquirer7.prompt([
|
|
1101
|
+
{
|
|
1102
|
+
type: "confirm",
|
|
1103
|
+
name: "confirm",
|
|
1104
|
+
message: "Reset sprint state? This cannot be undone.",
|
|
1105
|
+
default: false
|
|
1106
|
+
}
|
|
1107
|
+
]);
|
|
1108
|
+
if (!confirm) {
|
|
1109
|
+
console.log(chalk15.dim(" Cancelled.\n"));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const toRemove = ["plan.json", "state.json"];
|
|
1114
|
+
for (const file of toRemove) {
|
|
1115
|
+
try {
|
|
1116
|
+
await fs3.unlink(path5.join(forgeDir, file));
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const dirsToClean = ["snapshots", "designs"];
|
|
1121
|
+
for (const dir of dirsToClean) {
|
|
1122
|
+
const dirPath = path5.join(forgeDir, dir);
|
|
1123
|
+
try {
|
|
1124
|
+
await fs3.rm(dirPath, { recursive: true });
|
|
1125
|
+
await fs3.mkdir(dirPath, { recursive: true });
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
await fs3.writeFile(
|
|
1130
|
+
path5.join(forgeDir, "state.json"),
|
|
1131
|
+
JSON.stringify({
|
|
1132
|
+
currentPhase: "init",
|
|
1133
|
+
currentStory: null,
|
|
1134
|
+
workerMode: null,
|
|
1135
|
+
queue: [],
|
|
1136
|
+
history: []
|
|
1137
|
+
}, null, 2)
|
|
1138
|
+
);
|
|
1139
|
+
console.log(chalk15.green(" Sprint state reset. Config preserved.\n"));
|
|
1140
|
+
console.log(chalk15.dim(' Start fresh: forge plan "description"\n'));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/cli/commands/export.ts
|
|
1144
|
+
import chalk16 from "chalk";
|
|
1145
|
+
import fs4 from "fs/promises";
|
|
1146
|
+
var STATUS_LABEL = {
|
|
1147
|
+
planned: "[ ]",
|
|
1148
|
+
designing: "[~]",
|
|
1149
|
+
"design-approved": "[~]",
|
|
1150
|
+
building: "[~]",
|
|
1151
|
+
reviewing: "[~]",
|
|
1152
|
+
done: "[x]",
|
|
1153
|
+
blocked: "[!]"
|
|
1154
|
+
};
|
|
1155
|
+
async function exportCommand(options) {
|
|
1156
|
+
const plan = await stateManager.getPlan();
|
|
1157
|
+
if (!plan) {
|
|
1158
|
+
console.log(chalk16.red("\n No sprint plan found. Run: forge plan\n"));
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
1162
|
+
const done = allStories.filter((s) => s.status === "done").length;
|
|
1163
|
+
let md = `# ${plan.project}
|
|
1164
|
+
|
|
1165
|
+
`;
|
|
1166
|
+
md += `> ${plan.description}
|
|
1167
|
+
|
|
1168
|
+
`;
|
|
1169
|
+
md += `**Framework:** ${plan.framework}
|
|
1170
|
+
`;
|
|
1171
|
+
md += `**Created:** ${plan.created}
|
|
1172
|
+
`;
|
|
1173
|
+
md += `**Progress:** ${done}/${allStories.length} stories complete
|
|
1174
|
+
|
|
1175
|
+
`;
|
|
1176
|
+
md += `---
|
|
1177
|
+
|
|
1178
|
+
`;
|
|
1179
|
+
for (const epic of plan.epics) {
|
|
1180
|
+
const epicDone = epic.stories.filter((s) => s.status === "done").length;
|
|
1181
|
+
md += `## ${epic.title} (${epicDone}/${epic.stories.length})
|
|
1182
|
+
|
|
1183
|
+
`;
|
|
1184
|
+
for (const story of epic.stories) {
|
|
1185
|
+
const check = STATUS_LABEL[story.status] || "[ ]";
|
|
1186
|
+
const type = story.type === "ui" ? "UI" : story.type === "backend" ? "API" : "Full";
|
|
1187
|
+
md += `- ${check} **${story.title}** \`${type}\`
|
|
1188
|
+
`;
|
|
1189
|
+
md += ` ${story.description}
|
|
1190
|
+
`;
|
|
1191
|
+
if (story.dependencies.length > 0) {
|
|
1192
|
+
const depNames = story.dependencies.map((depId) => {
|
|
1193
|
+
const dep = allStories.find((s) => s.id === depId);
|
|
1194
|
+
return dep ? dep.title : depId;
|
|
1195
|
+
});
|
|
1196
|
+
md += ` *Depends on: ${depNames.join(", ")}*
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
if (story.tags.length > 0) {
|
|
1200
|
+
md += ` Tags: ${story.tags.join(", ")}
|
|
1201
|
+
`;
|
|
1202
|
+
}
|
|
1203
|
+
md += `
|
|
1204
|
+
`;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
md += `---
|
|
1208
|
+
|
|
1209
|
+
`;
|
|
1210
|
+
md += `*Exported from [ForgeAI](https://github.com/joeljohn159/forgeai)*
|
|
1211
|
+
`;
|
|
1212
|
+
const outputPath = options.output || "sprint-plan.md";
|
|
1213
|
+
await fs4.writeFile(outputPath, md);
|
|
1214
|
+
console.log(chalk16.green(`
|
|
1215
|
+
Exported to ${outputPath}
|
|
1216
|
+
`));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/cli/commands/history.ts
|
|
1220
|
+
import chalk17 from "chalk";
|
|
1221
|
+
async function historyCommand() {
|
|
1222
|
+
const config = await stateManager.getConfig();
|
|
1223
|
+
if (!config) {
|
|
1224
|
+
console.log(chalk17.red("\n Forge not initialized. Run: forge init\n"));
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const git = new GitManager();
|
|
1228
|
+
const state = await stateManager.getState();
|
|
1229
|
+
const plan = await stateManager.getPlan();
|
|
1230
|
+
console.log(chalk17.bold("\n Forge History\n"));
|
|
1231
|
+
const tags = await git.listTags();
|
|
1232
|
+
if (tags.length > 0) {
|
|
1233
|
+
console.log(chalk17.bold(" Checkpoints\n"));
|
|
1234
|
+
for (const tag of [...tags].reverse()) {
|
|
1235
|
+
const label = tag.replace("forge/", "");
|
|
1236
|
+
console.log(` ${chalk17.cyan(tag)} ${chalk17.dim(label)}`);
|
|
1237
|
+
}
|
|
1238
|
+
console.log("");
|
|
1239
|
+
}
|
|
1240
|
+
console.log(chalk17.bold(" Activity\n"));
|
|
1241
|
+
const log = await git.getForgeLog(30);
|
|
1242
|
+
if (log.length === 0) {
|
|
1243
|
+
console.log(chalk17.dim(" No activity yet.\n"));
|
|
1244
|
+
} else {
|
|
1245
|
+
for (const entry of log) {
|
|
1246
|
+
const date = new Date(entry.date);
|
|
1247
|
+
const timeStr = date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1248
|
+
const hash = entry.hash.slice(0, 7);
|
|
1249
|
+
let msgColor = chalk17.white;
|
|
1250
|
+
if (entry.message.startsWith("feat:")) msgColor = chalk17.green;
|
|
1251
|
+
else if (entry.message.startsWith("fix:")) msgColor = chalk17.yellow;
|
|
1252
|
+
else if (entry.message.startsWith("forge:")) msgColor = chalk17.cyan;
|
|
1253
|
+
else if (entry.message.startsWith("docs:")) msgColor = chalk17.blue;
|
|
1254
|
+
else if (entry.message.startsWith("ci:")) msgColor = chalk17.magenta;
|
|
1255
|
+
const commitTags = await git.getTagsAtCommit(entry.hash);
|
|
1256
|
+
const tagStr = commitTags.length > 0 ? " " + commitTags.map((t) => chalk17.cyan(`[${t}]`)).join(" ") : "";
|
|
1257
|
+
console.log(
|
|
1258
|
+
chalk17.dim(` ${hash}`) + ` ${msgColor(entry.message)}` + tagStr + chalk17.dim(` ${timeStr}`)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
console.log("");
|
|
1262
|
+
}
|
|
1263
|
+
if (plan) {
|
|
1264
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
1265
|
+
const done = allStories.filter((s) => s.status === "done").length;
|
|
1266
|
+
const blocked = allStories.filter((s) => s.status === "blocked").length;
|
|
1267
|
+
const total = allStories.length;
|
|
1268
|
+
console.log(chalk17.bold(" Sprint\n"));
|
|
1269
|
+
console.log(chalk17.dim(` Phase: ${state.currentPhase}`));
|
|
1270
|
+
console.log(chalk17.dim(` Stories: ${done}/${total} done${blocked > 0 ? `, ${blocked} blocked` : ""}`));
|
|
1271
|
+
console.log(chalk17.dim(` History entries: ${state.history.length}`));
|
|
1272
|
+
console.log("");
|
|
1273
|
+
}
|
|
1274
|
+
if (tags.length > 0) {
|
|
1275
|
+
console.log(chalk17.dim(` Tip: ${chalk17.white("forge checkout <tag>")} to jump to a checkpoint
|
|
1276
|
+
`));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async function checkoutCommand(version) {
|
|
1280
|
+
const git = new GitManager();
|
|
1281
|
+
const tags = await git.listTags();
|
|
1282
|
+
const match = tags.find((t) => t === version || t === `forge/${version}`);
|
|
1283
|
+
if (match) {
|
|
1284
|
+
await git.checkoutTag(match);
|
|
1285
|
+
console.log(chalk17.green(`
|
|
1286
|
+
Checked out ${match}
|
|
1287
|
+
`));
|
|
1288
|
+
console.log(chalk17.dim(" You are in detached HEAD state."));
|
|
1289
|
+
console.log(chalk17.dim(" To go back: " + chalk17.white("git checkout main") + "\n"));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
await git.checkout(version);
|
|
1294
|
+
console.log(chalk17.green(`
|
|
1295
|
+
Checked out ${version}
|
|
1296
|
+
`));
|
|
1297
|
+
} catch {
|
|
1298
|
+
console.log(chalk17.red(`
|
|
1299
|
+
Version "${version}" not found.`));
|
|
1300
|
+
console.log(chalk17.dim(" Run: " + chalk17.white("forge history") + " to see available versions.\n"));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/cli/commands/start.ts
|
|
1305
|
+
import chalk18 from "chalk";
|
|
1306
|
+
import { spawn } from "child_process";
|
|
1307
|
+
async function startCommand() {
|
|
1308
|
+
const config = await stateManager.getConfig();
|
|
1309
|
+
if (!config) {
|
|
1310
|
+
console.log(chalk18.red("\n Forge not initialized. Run: forge init\n"));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const adapter = getAdapter(config.framework);
|
|
1314
|
+
const [cmd, ...args] = adapter.devCommand.split(" ");
|
|
1315
|
+
console.log(chalk18.bold("\n forge") + chalk18.dim(" start"));
|
|
1316
|
+
console.log(chalk18.dim(` ${adapter.name} dev server on port ${adapter.devPort}
|
|
1317
|
+
`));
|
|
1318
|
+
console.log(chalk18.dim(` Running: ${adapter.devCommand}
|
|
1319
|
+
`));
|
|
1320
|
+
const child = spawn(cmd, args, {
|
|
1321
|
+
cwd: process.cwd(),
|
|
1322
|
+
stdio: "inherit",
|
|
1323
|
+
shell: true
|
|
1324
|
+
});
|
|
1325
|
+
child.on("error", (err) => {
|
|
1326
|
+
console.log(chalk18.red(`
|
|
1327
|
+
Failed to start: ${err.message}`));
|
|
1328
|
+
console.log(chalk18.dim(` Try running manually: ${adapter.devCommand}
|
|
1329
|
+
`));
|
|
1330
|
+
});
|
|
1331
|
+
child.on("exit", (code) => {
|
|
1332
|
+
if (code && code !== 0) {
|
|
1333
|
+
console.log(chalk18.red(`
|
|
1334
|
+
Dev server exited with code ${code}
|
|
1335
|
+
`));
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/cli/commands/push.ts
|
|
1341
|
+
import chalk19 from "chalk";
|
|
1342
|
+
import ora4 from "ora";
|
|
1343
|
+
async function pushCommand() {
|
|
1344
|
+
const config = await stateManager.getConfig();
|
|
1345
|
+
if (!config) {
|
|
1346
|
+
console.log(chalk19.red("\n Forge not initialized. Run: forge init\n"));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const git = new GitManager();
|
|
1350
|
+
const hasRemote = await git.hasRemote();
|
|
1351
|
+
if (!hasRemote) {
|
|
1352
|
+
console.log(chalk19.red("\n No remote 'origin' found."));
|
|
1353
|
+
console.log(chalk19.dim(" Add one with: git remote add origin <url>\n"));
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const spinner = ora4({ text: "Pushing to origin...", indent: 2 }).start();
|
|
1357
|
+
try {
|
|
1358
|
+
const hasChanges = await git.hasUncommittedChanges();
|
|
1359
|
+
if (hasChanges) {
|
|
1360
|
+
await git.commitAll("forge: save progress before push");
|
|
1361
|
+
spinner.text = "Committed uncommitted changes, pushing...";
|
|
1362
|
+
}
|
|
1363
|
+
await git.push();
|
|
1364
|
+
spinner.text = "Pushing tags...";
|
|
1365
|
+
await git.pushTags();
|
|
1366
|
+
spinner.succeed("Pushed to origin (commits + tags)");
|
|
1367
|
+
const branch = await git.getCurrentBranch();
|
|
1368
|
+
console.log(chalk19.dim(` Branch: ${branch}
|
|
1369
|
+
`));
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
spinner.fail("Push failed");
|
|
1372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1373
|
+
console.log(chalk19.red(` ${msg}
|
|
1374
|
+
`));
|
|
1375
|
+
console.log(chalk19.dim(" Make sure you have push access and the remote is configured.\n"));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/cli/index.ts
|
|
1380
|
+
var program = new Command();
|
|
1381
|
+
program.name("forge").description(
|
|
1382
|
+
chalk20.bold("Forge") + " \u2014 AI Development Orchestration Framework\n Structured multi-agent pipeline: plan \u2192 design \u2192 build \u2192 review"
|
|
1383
|
+
).version("1.0.0");
|
|
1384
|
+
program.command("auto [description]").description("Fully autonomous mode \u2014 plan, build, and review in one command").option("--no-sandbox", "Disable sandbox (not recommended)").option("-q, --quiet", "Hide live agent output, show spinners only").option("--allow-network <domains>", "Comma-separated allowed network domains").option("--mute", "Suppress notification sounds").option("--deploy", "Configure GitHub Pages deployment after build").option("--skip-design", "Skip design phase (faster, no Storybook previews)").action(autoCommand);
|
|
1385
|
+
program.command("resume").description("Resume an interrupted sprint from where it left off").option("--no-sandbox", "Disable sandbox").option("-q, --quiet", "Spinners only").option("--mute", "Suppress sounds").option("--skip-design", "Skip design phase").action(resumeCommand);
|
|
1386
|
+
program.command("sprint [description]").description("Run full pipeline with human gates between phases").action(sprintCommand);
|
|
1387
|
+
program.command("init").description("Initialize Forge in the current project").option("-f, --framework <framework>", "Framework (nextjs, react, django)").option("--no-storybook", "Skip Storybook setup").action(initCommand);
|
|
1388
|
+
program.command("plan [description]").description("Generate a sprint plan from a project description").option("--regen", "Regenerate plan from scratch").action(planCommand);
|
|
1389
|
+
program.command("design").description("Generate and review design previews in Storybook").option("-s, --story <storyId>", "Design a specific story only").option("--import <path>", "Import design references (screenshots, mockups)").action(designCommand);
|
|
1390
|
+
program.command("build").description("Build stories sequentially with the Worker agent").option("-s, --story <storyId>", "Build a specific story only").action(buildCommand);
|
|
1391
|
+
program.command("review").description("Run QA review on completed stories").option("-s, --story <storyId>", "Review a specific story only").action(reviewCommand);
|
|
1392
|
+
program.command("start").description("Start the dev server for the current project").action(startCommand);
|
|
1393
|
+
program.command("push").description("Push project to GitHub (commits + tags)").action(pushCommand);
|
|
1394
|
+
program.command("status").description("Show current sprint state and progress").action(statusCommand);
|
|
1395
|
+
program.command("map").description("Visual sprint map with story status and dependencies").action(mapCommand);
|
|
1396
|
+
program.command("diff <v1> [v2]").description("Show changes between two versions or tags").action(diffCommand);
|
|
1397
|
+
program.command("fix <description>").description("Fix a bug or make a small change").option("-i, --image <path>", "Attach a screenshot for visual context").action(fixCommand);
|
|
1398
|
+
program.command("undo").description("Revert the last agent action").option("-n, --steps <number>", "Number of commits to show", "10").action(undoCommand);
|
|
1399
|
+
program.command("history").description("Show version timeline, checkpoints, and activity log").action(historyCommand);
|
|
1400
|
+
program.command("checkout <version>").description("Jump to a specific version or checkpoint").action(checkoutCommand);
|
|
1401
|
+
program.command("export").description("Export sprint plan as markdown").option("-o, --output <path>", "Output file path", "sprint-plan.md").action(exportCommand);
|
|
1402
|
+
program.command("clean").description("Reset sprint state (keeps config)").option("-f, --force", "Skip confirmation prompt").option("--snapshots", "Only clean snapshots").action(cleanCommand);
|
|
1403
|
+
program.command("doctor").description("Diagnose setup issues and check system requirements").action(doctorCommand);
|
|
1404
|
+
program.parse();
|
|
1405
|
+
//# sourceMappingURL=index.js.map
|