forgecraft 1.4.0 → 2.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/README.md +75 -7
- package/dist/chunk-U6CZUA5R.js +7048 -0
- package/dist/chunk-U6CZUA5R.js.map +1 -0
- package/dist/cli/index.js +544 -69
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +128 -6
- package/dist/index.js +17 -1
- package/package.json +9 -3
- package/dist/chunk-NYOV5D5J.js +0 -3129
- package/dist/chunk-NYOV5D5J.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,18 +6,26 @@ import {
|
|
|
6
6
|
Pipeline,
|
|
7
7
|
Worker,
|
|
8
8
|
buildAttachmentPrompt,
|
|
9
|
+
estimateCost,
|
|
9
10
|
formatAttachmentList,
|
|
11
|
+
formatCostEstimate,
|
|
12
|
+
generateGitHubActions,
|
|
13
|
+
generateGitLabCI,
|
|
14
|
+
generateVisualization,
|
|
10
15
|
getAdapter,
|
|
16
|
+
getTemplate,
|
|
11
17
|
listAdapters,
|
|
18
|
+
listTemplates,
|
|
12
19
|
loadAndValidateConfig,
|
|
13
20
|
parseAttachments,
|
|
21
|
+
refreshAdapters,
|
|
14
22
|
stageAttachments,
|
|
15
23
|
stateManager
|
|
16
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-U6CZUA5R.js";
|
|
17
25
|
|
|
18
26
|
// src/cli/index.ts
|
|
19
27
|
import { Command } from "commander";
|
|
20
|
-
import
|
|
28
|
+
import chalk27 from "chalk";
|
|
21
29
|
|
|
22
30
|
// src/cli/commands/init.ts
|
|
23
31
|
import fs from "fs/promises";
|
|
@@ -67,8 +75,11 @@ async function initCommand(options) {
|
|
|
67
75
|
choices: [
|
|
68
76
|
{ name: "Next.js (TypeScript + Tailwind + App Router)", value: "nextjs" },
|
|
69
77
|
{ name: "React + Vite (TypeScript SPA)", value: "react" },
|
|
78
|
+
{ name: "Vue 3 + Nuxt 3 (Composition API + Tailwind)", value: "vue" },
|
|
79
|
+
{ name: "SvelteKit (Svelte 5 + Tailwind)", value: "svelte" },
|
|
70
80
|
{ name: "Django (Python + DRF)", value: "django" },
|
|
71
|
-
{ name: "
|
|
81
|
+
{ name: "Flutter (Dart + Material Design 3)", value: "flutter" },
|
|
82
|
+
{ name: "Other (Express, Go, Rust, etc.)", value: "generic" }
|
|
72
83
|
],
|
|
73
84
|
default: options.framework || "nextjs"
|
|
74
85
|
},
|
|
@@ -114,6 +125,7 @@ async function initCommand(options) {
|
|
|
114
125
|
await fs.mkdir(forgeDir, { recursive: true });
|
|
115
126
|
await fs.mkdir(path.join(forgeDir, "snapshots"), { recursive: true });
|
|
116
127
|
await fs.mkdir(path.join(forgeDir, "designs"), { recursive: true });
|
|
128
|
+
await fs.mkdir(path.join(forgeDir, "adapters"), { recursive: true });
|
|
117
129
|
await fs.writeFile(
|
|
118
130
|
path.join(process.cwd(), "forge.config.json"),
|
|
119
131
|
JSON.stringify(config, null, 2)
|
|
@@ -148,6 +160,7 @@ async function initCommand(options) {
|
|
|
148
160
|
console.log(chalk.dim(" .forge/state.json \u2014 Sprint state"));
|
|
149
161
|
console.log(chalk.dim(" .forge/snapshots/ \u2014 Action snapshots"));
|
|
150
162
|
console.log(chalk.dim(" .forge/designs/ \u2014 Design metadata"));
|
|
163
|
+
console.log(chalk.dim(" .forge/adapters/ \u2014 Custom framework adapters"));
|
|
151
164
|
console.log(chalk.dim(" forge.config.json \u2014 Project config"));
|
|
152
165
|
if (answers.githubSync) {
|
|
153
166
|
console.log(
|
|
@@ -433,16 +446,25 @@ async function fixCommand(description, options) {
|
|
|
433
446
|
console.log(chalk8.red('\n Please describe the fix: forge fix "description"\n'));
|
|
434
447
|
return;
|
|
435
448
|
}
|
|
436
|
-
if (options?.image
|
|
437
|
-
|
|
449
|
+
if (options?.image) {
|
|
450
|
+
const { resolve } = await import("path");
|
|
451
|
+
const imgPath = resolve(options.image);
|
|
452
|
+
if (!existsSync(imgPath)) {
|
|
453
|
+
console.log(chalk8.red(`
|
|
438
454
|
Image not found: ${options.image}
|
|
439
455
|
`));
|
|
440
|
-
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
options.image = imgPath;
|
|
441
459
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
460
|
+
let parsed = { description, attachments: [] };
|
|
461
|
+
try {
|
|
462
|
+
parsed = parseAttachments(description);
|
|
463
|
+
if (parsed.attachments.length > 0) {
|
|
464
|
+
description = parsed.description;
|
|
465
|
+
stageAttachments(parsed.attachments);
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
446
468
|
}
|
|
447
469
|
console.log(chalk8.bold("\n forge fix\n"));
|
|
448
470
|
console.log(chalk8.dim(` "${description}"`));
|
|
@@ -457,15 +479,26 @@ async function fixCommand(description, options) {
|
|
|
457
479
|
const orchestrator = new Orchestrator(config);
|
|
458
480
|
const worker = new Worker(config, {});
|
|
459
481
|
const git = new GitManager();
|
|
460
|
-
|
|
482
|
+
let state;
|
|
483
|
+
try {
|
|
484
|
+
state = await stateManager.getState();
|
|
485
|
+
} catch {
|
|
486
|
+
state = { currentPhase: "init", currentStory: null, workerMode: null, queue: [], history: [] };
|
|
487
|
+
}
|
|
461
488
|
const spinner = ora3({ text: "Analyzing request...", indent: 2 }).start();
|
|
462
489
|
let decision;
|
|
463
490
|
try {
|
|
464
491
|
decision = await orchestrator.routeUserInput(description, state);
|
|
465
492
|
} catch (err) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
493
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
494
|
+
if (errMsg.includes("JSON") || errMsg.includes("parse") || errMsg.includes("Unexpected token")) {
|
|
495
|
+
spinner.warn("AI response parsing failed \u2014 falling back to direct fix mode");
|
|
496
|
+
decision = { action: "route-to-worker", workerMode: "fix", prompt: description };
|
|
497
|
+
} else {
|
|
498
|
+
spinner.fail("Failed to analyze request");
|
|
499
|
+
console.log(chalk8.red(` ${errMsg}`));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
469
502
|
}
|
|
470
503
|
switch (decision.action) {
|
|
471
504
|
case "route-to-worker": {
|
|
@@ -519,28 +552,33 @@ Read this image file to see the visual issue the user is referring to.`;
|
|
|
519
552
|
}
|
|
520
553
|
case "add-story": {
|
|
521
554
|
spinner.succeed("New feature detected");
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
try {
|
|
556
|
+
const plan = await stateManager.getPlan();
|
|
557
|
+
if (plan && plan.epics.length > 0) {
|
|
558
|
+
const newStory = {
|
|
559
|
+
id: `story-${Date.now()}`,
|
|
560
|
+
title: decision.story?.title || description,
|
|
561
|
+
description: decision.story?.description || description,
|
|
562
|
+
type: decision.story?.type || "fullstack",
|
|
563
|
+
status: "planned",
|
|
564
|
+
branch: null,
|
|
565
|
+
designApproved: false,
|
|
566
|
+
tags: [],
|
|
567
|
+
priority: 99,
|
|
568
|
+
dependencies: []
|
|
569
|
+
};
|
|
570
|
+
plan.epics[plan.epics.length - 1].stories.push(newStory);
|
|
571
|
+
await stateManager.savePlan(plan);
|
|
572
|
+
await git.commitAll(`forge: add story "${newStory.title}"`);
|
|
573
|
+
console.log(chalk8.green(` Added: ${newStory.title}`));
|
|
574
|
+
console.log(chalk8.dim(` Run ${chalk8.white("forge build")} or ${chalk8.white("forge auto")} to build it.
|
|
575
|
+
`));
|
|
576
|
+
} else {
|
|
577
|
+
console.log(chalk8.yellow(" No plan found. Run forge plan first to create a sprint plan.\n"));
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
console.log(chalk8.red(` Failed to add story: ${err instanceof Error ? err.message : err}
|
|
541
581
|
`));
|
|
542
|
-
} else {
|
|
543
|
-
console.log(chalk8.yellow(" No plan found. Run forge plan first to create a sprint plan.\n"));
|
|
544
582
|
}
|
|
545
583
|
break;
|
|
546
584
|
}
|
|
@@ -689,13 +727,52 @@ async function autoCommand(description, options) {
|
|
|
689
727
|
console.log(chalk10.dim(" Diagnose: forge doctor\n"));
|
|
690
728
|
return;
|
|
691
729
|
}
|
|
692
|
-
|
|
730
|
+
let rawConfig = await stateManager.getConfig();
|
|
693
731
|
if (!rawConfig) {
|
|
694
|
-
|
|
695
|
-
|
|
732
|
+
const hasPackageJson = existsSync2("package.json");
|
|
733
|
+
const hasManagePy = existsSync2("manage.py");
|
|
734
|
+
const hasPubspec = existsSync2("pubspec.yaml");
|
|
735
|
+
const hasCargo = existsSync2("Cargo.toml");
|
|
736
|
+
if (hasPackageJson || hasManagePy || hasPubspec || hasCargo) {
|
|
737
|
+
console.log(chalk10.dim("\n Existing project detected \u2014 auto-initializing Forge...\n"));
|
|
738
|
+
let framework = "other";
|
|
739
|
+
if (hasPackageJson) {
|
|
740
|
+
try {
|
|
741
|
+
const { readFileSync: readFileSync2 } = await import("fs");
|
|
742
|
+
const pkg = JSON.parse(readFileSync2("package.json", "utf-8"));
|
|
743
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
744
|
+
if (deps["next"]) framework = "nextjs";
|
|
745
|
+
else if (deps["nuxt"]) framework = "nuxt";
|
|
746
|
+
else if (deps["@sveltejs/kit"]) framework = "sveltekit";
|
|
747
|
+
else if (deps["vue"]) framework = "vue";
|
|
748
|
+
else if (deps["react"]) framework = "react";
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
} else if (hasManagePy) {
|
|
752
|
+
framework = "django";
|
|
753
|
+
} else if (hasPubspec) {
|
|
754
|
+
framework = "flutter";
|
|
755
|
+
}
|
|
756
|
+
rawConfig = {
|
|
757
|
+
framework,
|
|
758
|
+
model: "sonnet",
|
|
759
|
+
designPreview: "storybook",
|
|
760
|
+
githubSync: false,
|
|
761
|
+
githubRepo: null,
|
|
762
|
+
autoCommit: true,
|
|
763
|
+
storybook: { port: 6006 }
|
|
764
|
+
};
|
|
765
|
+
await stateManager.saveConfig(rawConfig);
|
|
766
|
+
console.log(chalk10.dim(` Framework: ${framework}
|
|
767
|
+
`));
|
|
768
|
+
} else {
|
|
769
|
+
console.log(chalk10.red("\n Forge not initialized. Run: forge init\n"));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
696
772
|
}
|
|
697
773
|
const config = loadAndValidateConfig(rawConfig);
|
|
698
774
|
if (!config) return;
|
|
775
|
+
await refreshAdapters();
|
|
699
776
|
const adapter = getAdapter(config.framework);
|
|
700
777
|
const skipDesign = options.skipDesign || !adapter.designSupport;
|
|
701
778
|
if (!description) {
|
|
@@ -728,6 +805,7 @@ async function autoCommand(description, options) {
|
|
|
728
805
|
mute: options.mute ?? false,
|
|
729
806
|
deploy: options.deploy ?? false,
|
|
730
807
|
skipDesign,
|
|
808
|
+
skipTests: options.skipTests ?? false,
|
|
731
809
|
allowedDomains,
|
|
732
810
|
attachments
|
|
733
811
|
});
|
|
@@ -1342,43 +1420,73 @@ async function checkoutCommand(version) {
|
|
|
1342
1420
|
// src/cli/commands/start.ts
|
|
1343
1421
|
import chalk18 from "chalk";
|
|
1344
1422
|
import { spawn } from "child_process";
|
|
1423
|
+
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
1424
|
+
import path6 from "path";
|
|
1345
1425
|
async function startCommand() {
|
|
1426
|
+
console.log(chalk18.bold("\n forge") + chalk18.dim(" start\n"));
|
|
1427
|
+
let devCommand = null;
|
|
1428
|
+
let label = "Project";
|
|
1346
1429
|
const config = await stateManager.getConfig();
|
|
1347
|
-
if (
|
|
1348
|
-
console.log(chalk18.red("\n Forge not initialized. Run: forge init\n"));
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
const adapter = getAdapter(config.framework);
|
|
1352
|
-
let devCommand = adapter.devCommand;
|
|
1353
|
-
if (adapter.id === "generic") {
|
|
1430
|
+
if (config) {
|
|
1354
1431
|
try {
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
} else if (pkg.scripts?.start) {
|
|
1360
|
-
devCommand = "npm start";
|
|
1361
|
-
} else if (pkg.scripts?.serve) {
|
|
1362
|
-
devCommand = "npm run serve";
|
|
1363
|
-
}
|
|
1432
|
+
await refreshAdapters();
|
|
1433
|
+
const adapter = getAdapter(config.framework);
|
|
1434
|
+
devCommand = adapter.devCommand;
|
|
1435
|
+
label = adapter.id === "generic" ? "Custom stack" : adapter.name;
|
|
1364
1436
|
} catch {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const detected = detectDevCommand();
|
|
1440
|
+
if (detected) {
|
|
1441
|
+
devCommand = detected;
|
|
1442
|
+
} else if (devCommand) {
|
|
1443
|
+
const npmMatch = devCommand.match(/^npm run (\S+)$/);
|
|
1444
|
+
if (npmMatch) {
|
|
1445
|
+
const scriptName = npmMatch[1];
|
|
1446
|
+
const pkgPath = path6.join(process.cwd(), "package.json");
|
|
1447
|
+
try {
|
|
1448
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1449
|
+
if (!pkg.scripts?.[scriptName]) {
|
|
1450
|
+
devCommand = null;
|
|
1451
|
+
}
|
|
1452
|
+
} catch {
|
|
1453
|
+
devCommand = null;
|
|
1372
1454
|
}
|
|
1373
1455
|
}
|
|
1374
1456
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1457
|
+
if (!config && detected) {
|
|
1458
|
+
label = detectProjectType();
|
|
1459
|
+
}
|
|
1460
|
+
if (!devCommand) {
|
|
1461
|
+
console.log(chalk18.red(" Could not detect how to start this project.\n"));
|
|
1462
|
+
const pkgPath = path6.join(process.cwd(), "package.json");
|
|
1463
|
+
if (existsSync4(pkgPath)) {
|
|
1464
|
+
try {
|
|
1465
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1466
|
+
const scripts = Object.keys(pkg.scripts || {});
|
|
1467
|
+
if (scripts.length > 0) {
|
|
1468
|
+
console.log(chalk18.dim(" Available scripts in package.json:"));
|
|
1469
|
+
for (const s of scripts) {
|
|
1470
|
+
console.log(chalk18.white(` npm run ${s}`));
|
|
1471
|
+
}
|
|
1472
|
+
console.log("");
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
console.log(chalk18.dim(" Expected one of:"));
|
|
1479
|
+
console.log(chalk18.dim(" - package.json with dev/start/serve script"));
|
|
1480
|
+
console.log(chalk18.dim(" - manage.py (Django)"));
|
|
1481
|
+
console.log(chalk18.dim(" - Cargo.toml (Rust)"));
|
|
1482
|
+
console.log(chalk18.dim(" - go.mod (Go)"));
|
|
1483
|
+
console.log(chalk18.dim(" - pubspec.yaml (Flutter)\n"));
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
console.log(chalk18.dim(` ${label} dev server`));
|
|
1379
1487
|
console.log(chalk18.dim(` Running: ${devCommand}
|
|
1380
1488
|
`));
|
|
1381
|
-
const child = spawn(
|
|
1489
|
+
const child = spawn(devCommand, {
|
|
1382
1490
|
cwd: process.cwd(),
|
|
1383
1491
|
stdio: "inherit",
|
|
1384
1492
|
shell: true
|
|
@@ -1397,6 +1505,48 @@ async function startCommand() {
|
|
|
1397
1505
|
}
|
|
1398
1506
|
});
|
|
1399
1507
|
}
|
|
1508
|
+
function detectDevCommand() {
|
|
1509
|
+
const pkgPath = path6.join(process.cwd(), "package.json");
|
|
1510
|
+
if (existsSync4(pkgPath)) {
|
|
1511
|
+
try {
|
|
1512
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1513
|
+
const scripts = pkg.scripts || {};
|
|
1514
|
+
if (scripts.dev) return "npm run dev";
|
|
1515
|
+
if (scripts.start) return "npm start";
|
|
1516
|
+
if (scripts.serve) return "npm run serve";
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (existsSync4(path6.join(process.cwd(), "manage.py"))) {
|
|
1521
|
+
return "python manage.py runserver";
|
|
1522
|
+
}
|
|
1523
|
+
if (existsSync4(path6.join(process.cwd(), "Cargo.toml"))) {
|
|
1524
|
+
return "cargo run";
|
|
1525
|
+
}
|
|
1526
|
+
if (existsSync4(path6.join(process.cwd(), "go.mod"))) {
|
|
1527
|
+
return "go run .";
|
|
1528
|
+
}
|
|
1529
|
+
if (existsSync4(path6.join(process.cwd(), "pubspec.yaml"))) {
|
|
1530
|
+
return "flutter run";
|
|
1531
|
+
}
|
|
1532
|
+
if (existsSync4(path6.join(process.cwd(), "Gemfile"))) {
|
|
1533
|
+
if (existsSync4(path6.join(process.cwd(), "bin", "rails"))) {
|
|
1534
|
+
return "bundle exec rails server";
|
|
1535
|
+
}
|
|
1536
|
+
return "bundle exec ruby app.rb";
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
function detectProjectType() {
|
|
1541
|
+
if (existsSync4(path6.join(process.cwd(), "next.config.ts")) || existsSync4(path6.join(process.cwd(), "next.config.js")) || existsSync4(path6.join(process.cwd(), "next.config.mjs"))) return "Next.js";
|
|
1542
|
+
if (existsSync4(path6.join(process.cwd(), "nuxt.config.ts"))) return "Nuxt";
|
|
1543
|
+
if (existsSync4(path6.join(process.cwd(), "svelte.config.js"))) return "SvelteKit";
|
|
1544
|
+
if (existsSync4(path6.join(process.cwd(), "manage.py"))) return "Django";
|
|
1545
|
+
if (existsSync4(path6.join(process.cwd(), "Cargo.toml"))) return "Rust";
|
|
1546
|
+
if (existsSync4(path6.join(process.cwd(), "go.mod"))) return "Go";
|
|
1547
|
+
if (existsSync4(path6.join(process.cwd(), "pubspec.yaml"))) return "Flutter";
|
|
1548
|
+
return "Project";
|
|
1549
|
+
}
|
|
1400
1550
|
|
|
1401
1551
|
// src/cli/commands/push.ts
|
|
1402
1552
|
import chalk19 from "chalk";
|
|
@@ -1497,12 +1647,331 @@ function getCurrentVersion() {
|
|
|
1497
1647
|
}
|
|
1498
1648
|
}
|
|
1499
1649
|
|
|
1650
|
+
// src/cli/commands/test.ts
|
|
1651
|
+
import chalk21 from "chalk";
|
|
1652
|
+
import ora6 from "ora";
|
|
1653
|
+
import fs5 from "fs/promises";
|
|
1654
|
+
import path7 from "path";
|
|
1655
|
+
async function testCommand(options) {
|
|
1656
|
+
console.log(chalk21.bold("\n forge") + chalk21.dim(" test\n"));
|
|
1657
|
+
const configPath = path7.join(process.cwd(), "forge.config.json");
|
|
1658
|
+
let config;
|
|
1659
|
+
try {
|
|
1660
|
+
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1661
|
+
config = JSON.parse(raw);
|
|
1662
|
+
} catch {
|
|
1663
|
+
console.log(chalk21.red(" No forge.config.json found. Run: forge init\n"));
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const plan = await stateManager.getPlan();
|
|
1667
|
+
if (!plan) {
|
|
1668
|
+
console.log(chalk21.red(" No sprint plan found. Run: forge plan\n"));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await refreshAdapters();
|
|
1672
|
+
let adapter = null;
|
|
1673
|
+
try {
|
|
1674
|
+
adapter = getAdapter(config.framework);
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
const worker = new Worker(config, { sandbox: false, yes: true });
|
|
1678
|
+
const git = new GitManager();
|
|
1679
|
+
const allStories = plan.epics.flatMap((e) => e.stories);
|
|
1680
|
+
let testable = allStories.filter(
|
|
1681
|
+
(s) => s.status === "reviewing" || s.status === "testing" || s.status === "done"
|
|
1682
|
+
);
|
|
1683
|
+
if (options.story) {
|
|
1684
|
+
testable = testable.filter((s) => s.id === options.story);
|
|
1685
|
+
if (testable.length === 0) {
|
|
1686
|
+
console.log(chalk21.red(` Story "${options.story}" not found or not built yet.
|
|
1687
|
+
`));
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (testable.length === 0) {
|
|
1692
|
+
console.log(chalk21.yellow(" No built stories to test. Run: forge build\n"));
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
console.log(chalk21.dim(` ${testable.length} stories to test
|
|
1696
|
+
`));
|
|
1697
|
+
for (let i = 0; i < testable.length; i++) {
|
|
1698
|
+
const story = testable[i];
|
|
1699
|
+
const label = `[${i + 1}/${testable.length}] ${story.title}`;
|
|
1700
|
+
const spinner = ora6({ text: label, indent: 2 }).start();
|
|
1701
|
+
const isGeneric = !adapter || adapter.id === "generic";
|
|
1702
|
+
const testInfo = isGeneric ? "Detect the test framework from config files. Install test dependencies if needed." : `Test framework: ${adapter.testFramework || "Vitest"}
|
|
1703
|
+
Test command: ${adapter.testCommand || "npm test"}`;
|
|
1704
|
+
const prompt = `
|
|
1705
|
+
Generate tests for: "${story.title}"
|
|
1706
|
+
Description: ${story.description}
|
|
1707
|
+
App: ${plan.project} (${plan.framework})
|
|
1708
|
+
|
|
1709
|
+
${testInfo}
|
|
1710
|
+
|
|
1711
|
+
1. Read the source files to understand what was built
|
|
1712
|
+
2. Create test files co-located with the source
|
|
1713
|
+
3. Write meaningful tests (happy path, errors, edge cases)
|
|
1714
|
+
4. Run the test suite \u2014 all tests must pass
|
|
1715
|
+
`;
|
|
1716
|
+
const result = await worker.run("test", prompt);
|
|
1717
|
+
if (result.success) {
|
|
1718
|
+
await git.commitAll(`test: ${story.title}`);
|
|
1719
|
+
spinner.succeed(`${label}` + chalk21.dim(` \xB7 ${result.filesCreated.length} test files`));
|
|
1720
|
+
} else {
|
|
1721
|
+
spinner.warn(`${label}` + chalk21.dim(" tests incomplete"));
|
|
1722
|
+
for (const err of result.errors) {
|
|
1723
|
+
console.log(chalk21.red(` ${err}`));
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
console.log(chalk21.green("\n Test generation complete.\n"));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/cli/commands/template.ts
|
|
1731
|
+
import chalk22 from "chalk";
|
|
1732
|
+
import inquirer8 from "inquirer";
|
|
1733
|
+
async function templateCommand(options) {
|
|
1734
|
+
const templates = listTemplates();
|
|
1735
|
+
if (options.list) {
|
|
1736
|
+
console.log(chalk22.bold("\n Forge Templates\n"));
|
|
1737
|
+
for (const t of templates) {
|
|
1738
|
+
console.log(` ${chalk22.cyan(t.id.padEnd(16))} ${t.name}`);
|
|
1739
|
+
console.log(chalk22.dim(` ${"".padEnd(16)} ${t.description}`));
|
|
1740
|
+
console.log(chalk22.dim(` ${"".padEnd(16)} Frameworks: ${t.suggestedFrameworks.join(", ")}
|
|
1741
|
+
`));
|
|
1742
|
+
}
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
console.log(chalk22.bold("\n forge") + chalk22.dim(" template\n"));
|
|
1746
|
+
let templateId;
|
|
1747
|
+
try {
|
|
1748
|
+
const answer = await inquirer8.prompt([
|
|
1749
|
+
{
|
|
1750
|
+
type: "list",
|
|
1751
|
+
name: "templateId",
|
|
1752
|
+
message: "Choose a starter template:",
|
|
1753
|
+
choices: templates.map((t) => ({
|
|
1754
|
+
name: `${t.name} \u2014 ${t.description}`,
|
|
1755
|
+
value: t.id
|
|
1756
|
+
}))
|
|
1757
|
+
}
|
|
1758
|
+
]);
|
|
1759
|
+
templateId = answer.templateId;
|
|
1760
|
+
} catch {
|
|
1761
|
+
console.log(chalk22.dim("\n Cancelled.\n"));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const template = getTemplate(templateId);
|
|
1765
|
+
if (!template) {
|
|
1766
|
+
console.log(chalk22.red(" Template not found.\n"));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
console.log(chalk22.green(`
|
|
1770
|
+
Selected: ${template.name}`));
|
|
1771
|
+
console.log(chalk22.dim(` ${template.description}
|
|
1772
|
+
`));
|
|
1773
|
+
console.log(chalk22.dim(" Suggested frameworks: " + template.suggestedFrameworks.join(", ")));
|
|
1774
|
+
console.log(chalk22.bold("\n To use this template, run:"));
|
|
1775
|
+
console.log(chalk22.cyan(` forge auto "${template.planDescription.split("\n")[0].trim()}"
|
|
1776
|
+
`));
|
|
1777
|
+
console.log(chalk22.dim(" Or copy the full description:\n"));
|
|
1778
|
+
const lines = template.planDescription.split("\n");
|
|
1779
|
+
for (const line of lines) {
|
|
1780
|
+
console.log(chalk22.dim(` ${line}`));
|
|
1781
|
+
}
|
|
1782
|
+
console.log("");
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/cli/commands/estimate.ts
|
|
1786
|
+
import chalk23 from "chalk";
|
|
1787
|
+
import fs6 from "fs/promises";
|
|
1788
|
+
import path8 from "path";
|
|
1789
|
+
async function estimateCommand() {
|
|
1790
|
+
console.log(chalk23.bold("\n forge") + chalk23.dim(" estimate\n"));
|
|
1791
|
+
const configPath = path8.join(process.cwd(), "forge.config.json");
|
|
1792
|
+
let config;
|
|
1793
|
+
try {
|
|
1794
|
+
const raw = await fs6.readFile(configPath, "utf-8");
|
|
1795
|
+
config = JSON.parse(raw);
|
|
1796
|
+
} catch {
|
|
1797
|
+
console.log(chalk23.red(" No forge.config.json found. Run: forge init\n"));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const plan = await stateManager.getPlan();
|
|
1801
|
+
if (!plan) {
|
|
1802
|
+
console.log(chalk23.red(" No sprint plan found. Run: forge plan\n"));
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const estimate = estimateCost(plan, config.model);
|
|
1806
|
+
console.log(formatCostEstimate(estimate));
|
|
1807
|
+
console.log("");
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/cli/commands/cicd.ts
|
|
1811
|
+
import chalk24 from "chalk";
|
|
1812
|
+
import inquirer9 from "inquirer";
|
|
1813
|
+
import fs7 from "fs/promises";
|
|
1814
|
+
import path9 from "path";
|
|
1815
|
+
async function cicdCommand(options) {
|
|
1816
|
+
console.log(chalk24.bold("\n forge") + chalk24.dim(" cicd\n"));
|
|
1817
|
+
const configPath = path9.join(process.cwd(), "forge.config.json");
|
|
1818
|
+
let config;
|
|
1819
|
+
try {
|
|
1820
|
+
const raw = await fs7.readFile(configPath, "utf-8");
|
|
1821
|
+
config = JSON.parse(raw);
|
|
1822
|
+
} catch {
|
|
1823
|
+
console.log(chalk24.red(" No forge.config.json found. Run: forge init\n"));
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
let provider = options.provider || "github";
|
|
1827
|
+
if (!options.provider) {
|
|
1828
|
+
try {
|
|
1829
|
+
const { selectedProvider } = await inquirer9.prompt([
|
|
1830
|
+
{
|
|
1831
|
+
type: "list",
|
|
1832
|
+
name: "selectedProvider",
|
|
1833
|
+
message: "CI/CD provider:",
|
|
1834
|
+
choices: [
|
|
1835
|
+
{ name: "GitHub Actions", value: "github" },
|
|
1836
|
+
{ name: "GitLab CI", value: "gitlab" }
|
|
1837
|
+
]
|
|
1838
|
+
}
|
|
1839
|
+
]);
|
|
1840
|
+
provider = selectedProvider;
|
|
1841
|
+
} catch {
|
|
1842
|
+
console.log(chalk24.dim("\n Cancelled.\n"));
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
await refreshAdapters();
|
|
1847
|
+
const adapter = getAdapter(config.framework);
|
|
1848
|
+
let content;
|
|
1849
|
+
let filePath;
|
|
1850
|
+
if (provider === "github") {
|
|
1851
|
+
content = generateGitHubActions(adapter);
|
|
1852
|
+
filePath = path9.join(process.cwd(), ".github", "workflows", "ci.yml");
|
|
1853
|
+
await fs7.mkdir(path9.dirname(filePath), { recursive: true });
|
|
1854
|
+
} else {
|
|
1855
|
+
content = generateGitLabCI(adapter);
|
|
1856
|
+
filePath = path9.join(process.cwd(), ".gitlab-ci.yml");
|
|
1857
|
+
}
|
|
1858
|
+
await fs7.writeFile(filePath, content);
|
|
1859
|
+
const relative = path9.relative(process.cwd(), filePath);
|
|
1860
|
+
console.log(chalk24.green(` Created: ${relative}`));
|
|
1861
|
+
console.log(chalk24.dim(` Framework: ${adapter.name}`));
|
|
1862
|
+
console.log(chalk24.dim(` Provider: ${provider === "github" ? "GitHub Actions" : "GitLab CI"}
|
|
1863
|
+
`));
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/cli/commands/viz.ts
|
|
1867
|
+
import chalk25 from "chalk";
|
|
1868
|
+
import ora7 from "ora";
|
|
1869
|
+
async function vizCommand(targetPath, options) {
|
|
1870
|
+
console.log(chalk25.bold("\n forge") + chalk25.dim(" viz\n"));
|
|
1871
|
+
const spinner = ora7({ text: "Scanning project...", indent: 2 }).start();
|
|
1872
|
+
try {
|
|
1873
|
+
const shouldOpen = options.open !== false;
|
|
1874
|
+
const { outputPath, graph } = await generateVisualization({
|
|
1875
|
+
workingDir: targetPath || void 0,
|
|
1876
|
+
outputPath: options.output,
|
|
1877
|
+
open: shouldOpen
|
|
1878
|
+
});
|
|
1879
|
+
spinner.succeed(
|
|
1880
|
+
`Scanned ${graph.totalFiles} files (${formatLines(graph.totalLines)} lines)`
|
|
1881
|
+
);
|
|
1882
|
+
const categories = {};
|
|
1883
|
+
for (const f of graph.files) {
|
|
1884
|
+
categories[f.category] = (categories[f.category] || 0) + 1;
|
|
1885
|
+
}
|
|
1886
|
+
console.log(chalk25.dim(`
|
|
1887
|
+
Framework: ${graph.framework}`));
|
|
1888
|
+
console.log(chalk25.dim(` Languages: ${Object.keys(graph.languages).join(", ")}`));
|
|
1889
|
+
if (graph.apiRoutes.length > 0) {
|
|
1890
|
+
console.log(chalk25.dim(` API routes: ${graph.apiRoutes.length}`));
|
|
1891
|
+
}
|
|
1892
|
+
console.log(chalk25.dim(`
|
|
1893
|
+
Dashboard: ${outputPath}`));
|
|
1894
|
+
if (shouldOpen) {
|
|
1895
|
+
console.log(chalk25.green(" Opened in browser\n"));
|
|
1896
|
+
} else {
|
|
1897
|
+
console.log(chalk25.dim(" Open the file in your browser to view.\n"));
|
|
1898
|
+
}
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
spinner.fail("Failed to generate visualization");
|
|
1901
|
+
console.log(chalk25.red(` ${err instanceof Error ? err.message : err}
|
|
1902
|
+
`));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function formatLines(n) {
|
|
1906
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
1907
|
+
return String(n);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/cli/commands/claude.ts
|
|
1911
|
+
import chalk26 from "chalk";
|
|
1912
|
+
import { spawn as spawn2 } from "child_process";
|
|
1913
|
+
async function claudeCommand() {
|
|
1914
|
+
console.log(chalk26.bold("\n forge") + chalk26.dim(" claude\n"));
|
|
1915
|
+
const cmd = await findClaudeCLI();
|
|
1916
|
+
if (!cmd) {
|
|
1917
|
+
console.log(chalk26.red(" Claude Code CLI not found.\n"));
|
|
1918
|
+
console.log(chalk26.dim(" Install it with:"));
|
|
1919
|
+
console.log(chalk26.white(" npm install -g @anthropic-ai/claude-code"));
|
|
1920
|
+
console.log(chalk26.dim(" Then log in:"));
|
|
1921
|
+
console.log(chalk26.white(" claude login\n"));
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
console.log(chalk26.dim(` Starting Claude Code...
|
|
1925
|
+
`));
|
|
1926
|
+
const isWindows = process.platform === "win32";
|
|
1927
|
+
const child = spawn2(cmd.cmd, cmd.args, {
|
|
1928
|
+
cwd: process.cwd(),
|
|
1929
|
+
stdio: "inherit",
|
|
1930
|
+
shell: isWindows
|
|
1931
|
+
// On Windows, use shell mode to resolve commands from PATH
|
|
1932
|
+
});
|
|
1933
|
+
child.on("error", (err) => {
|
|
1934
|
+
if (err.code === "ENOENT") {
|
|
1935
|
+
console.log(chalk26.red("\n Claude Code CLI not found in PATH."));
|
|
1936
|
+
console.log(chalk26.dim(" Install: npm install -g @anthropic-ai/claude-code\n"));
|
|
1937
|
+
} else {
|
|
1938
|
+
console.log(chalk26.red(`
|
|
1939
|
+
Failed to start Claude: ${err.message}
|
|
1940
|
+
`));
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
child.on("exit", (code) => {
|
|
1944
|
+
if (code && code !== 0) {
|
|
1945
|
+
console.log(chalk26.dim(`
|
|
1946
|
+
Claude exited with code ${code}
|
|
1947
|
+
`));
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async function findClaudeCLI() {
|
|
1952
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
1953
|
+
const isWindows = process.platform === "win32";
|
|
1954
|
+
try {
|
|
1955
|
+
const whichCmd = isWindows ? "where claude" : "which claude";
|
|
1956
|
+
execSync4(whichCmd, { stdio: "ignore" });
|
|
1957
|
+
return { cmd: "claude", args: [] };
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
const whichCmd = isWindows ? "where npx" : "which npx";
|
|
1962
|
+
execSync4(whichCmd, { stdio: "ignore" });
|
|
1963
|
+
return { cmd: "npx", args: ["@anthropic-ai/claude-code"] };
|
|
1964
|
+
} catch {
|
|
1965
|
+
}
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1500
1969
|
// src/cli/index.ts
|
|
1501
1970
|
var program = new Command();
|
|
1502
1971
|
program.name("forge").description(
|
|
1503
|
-
|
|
1504
|
-
).version("
|
|
1505
|
-
program.command("auto [description]").description("Fully autonomous mode \u2014 plan, build, and review in one command").option("--no-sandbox", "Disable sandbox (not recommended)").option("-y, --yes", "Skip all confirmation prompts (auto-approve everything)").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);
|
|
1972
|
+
chalk27.bold("Forge") + " \u2014 AI Development Orchestration Framework\n Structured multi-agent pipeline: plan \u2192 design \u2192 build \u2192 review"
|
|
1973
|
+
).version("2.0.0");
|
|
1974
|
+
program.command("auto [description]").description("Fully autonomous mode \u2014 plan, build, and review in one command").option("--no-sandbox", "Disable sandbox (not recommended)").option("-y, --yes", "Skip all confirmation prompts (auto-approve everything)").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)").option("--skip-tests", "Skip test generation phase").action(autoCommand);
|
|
1506
1975
|
program.command("resume").description("Resume an interrupted sprint from where it left off").option("--no-sandbox", "Disable sandbox").option("-y, --yes", "Skip all confirmation prompts (auto-approve everything)").option("-q, --quiet", "Spinners only").option("--mute", "Suppress sounds").option("--skip-design", "Skip design phase").action(resumeCommand);
|
|
1507
1976
|
program.command("sprint [description]").description("Run full pipeline with human gates between phases").action(sprintCommand);
|
|
1508
1977
|
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);
|
|
@@ -1510,8 +1979,10 @@ program.command("plan [description]").description("Generate a sprint plan from a
|
|
|
1510
1979
|
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);
|
|
1511
1980
|
program.command("build").description("Build stories sequentially with the Worker agent").option("-s, --story <storyId>", "Build a specific story only").action(buildCommand);
|
|
1512
1981
|
program.command("review").description("Run QA review on completed stories").option("-s, --story <storyId>", "Review a specific story only").action(reviewCommand);
|
|
1982
|
+
program.command("test").description("Generate and run tests for built stories").option("-s, --story <storyId>", "Test a specific story only").action(testCommand);
|
|
1513
1983
|
program.command("start").description("Start the dev server for the current project").action(startCommand);
|
|
1514
1984
|
program.command("push").description("Push project to GitHub (commits + tags)").action(pushCommand);
|
|
1985
|
+
program.command("claude").description("Launch Claude Code CLI in the current project").action(claudeCommand);
|
|
1515
1986
|
program.command("status").description("Show current sprint state and progress").action(statusCommand);
|
|
1516
1987
|
program.command("map").description("Visual sprint map with story status and dependencies").action(mapCommand);
|
|
1517
1988
|
program.command("diff <v1> [v2]").description("Show changes between two versions or tags").action(diffCommand);
|
|
@@ -1521,7 +1992,11 @@ program.command("history").description("Show version timeline, checkpoints, and
|
|
|
1521
1992
|
program.command("checkout <version>").description("Jump to a specific version or checkpoint").action(checkoutCommand);
|
|
1522
1993
|
program.command("export").description("Export sprint plan as markdown").option("-o, --output <path>", "Output file path", "sprint-plan.md").action(exportCommand);
|
|
1523
1994
|
program.command("clean").description("Reset sprint state (keeps config)").option("-f, --force", "Skip confirmation prompt").option("--snapshots", "Only clean snapshots").action(cleanCommand);
|
|
1995
|
+
program.command("viz [path]").description("Interactive project map \u2014 visualize files, dependencies, and data flow").option("-o, --output <path>", "Output HTML file path").option("--no-open", "Don't auto-open in browser").action(vizCommand);
|
|
1524
1996
|
program.command("doctor").description("Diagnose setup issues and check system requirements").action(doctorCommand);
|
|
1997
|
+
program.command("estimate").description("Estimate token usage and cost for the current sprint plan").action(estimateCommand);
|
|
1998
|
+
program.command("cicd").description("Generate CI/CD pipeline configuration").option("-p, --provider <provider>", "CI provider (github, gitlab)").action(cicdCommand);
|
|
1999
|
+
program.command("template").description("Browse and use starter app templates").option("-l, --list", "List all available templates").action(templateCommand);
|
|
1525
2000
|
program.command("upgrade").description("Upgrade Forge to the latest version").action(upgradeCommand);
|
|
1526
2001
|
program.parse();
|
|
1527
2002
|
//# sourceMappingURL=index.js.map
|