forge-workflow 1.0.0 → 1.1.1

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/bin/forge.js CHANGED
@@ -1,140 +1,1871 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * Forge v1.1.0 - Universal AI Agent Workflow
5
+ * https://github.com/harshanandak/forge
6
+ *
7
+ * Usage:
8
+ * npm install forge-workflow -> Minimal install (AGENTS.md + docs)
9
+ * npx forge setup -> Interactive agent configuration
10
+ * npx forge setup --all -> Install for all agents
11
+ * npx forge setup --agents claude,cursor,windsurf
12
+ *
13
+ * CLI Flags:
14
+ * --quick, -q Use all defaults, minimal prompts
15
+ * --skip-external Skip external services configuration
16
+ * --agents <list> Specify agents (--agents claude cursor OR --agents=claude,cursor)
17
+ * --all Install for all available agents
18
+ * --help, -h Show help message
19
+ *
20
+ * Examples:
21
+ * npx forge setup --quick # All defaults, no prompts
22
+ * npx forge setup --agents claude cursor # Just these agents
23
+ * npx forge setup --skip-external # No service prompts
24
+ * npx forge setup --agents claude --quick # Quick + specific agent
25
+ *
26
+ * Also works with bun:
27
+ * bun add forge-workflow
28
+ * bunx forge setup --quick
29
+ */
30
+
3
31
  const fs = require('fs');
4
32
  const path = require('path');
33
+ const readline = require('readline');
34
+ const { execSync } = require('child_process');
5
35
 
6
- const COMMANDS = [
7
- 'status.md', 'research.md', 'plan.md', 'dev.md', 'check.md',
8
- 'ship.md', 'review.md', 'merge.md', 'verify.md'
9
- ];
36
+ // Get the project root and package directory
37
+ const projectRoot = process.env.INIT_CWD || process.cwd();
38
+ const packageDir = path.dirname(__dirname);
39
+ const args = process.argv.slice(2);
10
40
 
11
- const SKILLS_PARALLEL_AI = [
12
- 'SKILL.md', 'README.md', 'api-reference.md', 'quick-reference.md', 'research-workflows.md'
13
- ];
41
+ // Detected package manager
42
+ let PKG_MANAGER = 'npm';
14
43
 
15
- const SKILLS_SONARCLOUD = ['SKILL.md', 'reference.md'];
44
+ // Agent definitions
45
+ const AGENTS = {
46
+ claude: {
47
+ name: 'Claude Code',
48
+ description: "Anthropic's CLI agent",
49
+ dirs: ['.claude/commands', '.claude/rules', '.claude/skills/forge-workflow', '.claude/scripts'],
50
+ hasCommands: true,
51
+ hasSkill: true,
52
+ linkFile: 'CLAUDE.md'
53
+ },
54
+ cursor: {
55
+ name: 'Cursor',
56
+ description: 'AI-first code editor',
57
+ dirs: ['.cursor/rules', '.cursor/skills/forge-workflow'],
58
+ hasSkill: true,
59
+ linkFile: '.cursorrules',
60
+ customSetup: 'cursor'
61
+ },
62
+ windsurf: {
63
+ name: 'Windsurf',
64
+ description: "Codeium's agentic IDE",
65
+ dirs: ['.windsurf/workflows', '.windsurf/rules', '.windsurf/skills/forge-workflow'],
66
+ hasSkill: true,
67
+ linkFile: '.windsurfrules',
68
+ needsConversion: true
69
+ },
70
+ kilocode: {
71
+ name: 'Kilo Code',
72
+ description: 'VS Code extension',
73
+ dirs: ['.kilocode/workflows', '.kilocode/rules', '.kilocode/skills/forge-workflow'],
74
+ hasSkill: true,
75
+ needsConversion: true
76
+ },
77
+ antigravity: {
78
+ name: 'Google Antigravity',
79
+ description: "Google's agent IDE",
80
+ dirs: ['.agent/workflows', '.agent/rules', '.agent/skills/forge-workflow'],
81
+ hasSkill: true,
82
+ linkFile: 'GEMINI.md',
83
+ needsConversion: true
84
+ },
85
+ copilot: {
86
+ name: 'GitHub Copilot',
87
+ description: "GitHub's AI assistant",
88
+ dirs: ['.github/prompts', '.github/instructions'],
89
+ linkFile: '.github/copilot-instructions.md',
90
+ needsConversion: true,
91
+ promptFormat: true
92
+ },
93
+ continue: {
94
+ name: 'Continue',
95
+ description: 'Open-source AI assistant',
96
+ dirs: ['.continue/prompts', '.continue/skills/forge-workflow'],
97
+ hasSkill: true,
98
+ needsConversion: true,
99
+ continueFormat: true
100
+ },
101
+ opencode: {
102
+ name: 'OpenCode',
103
+ description: 'Open-source agent',
104
+ dirs: ['.opencode/commands', '.opencode/skills/forge-workflow'],
105
+ hasSkill: true,
106
+ copyCommands: true
107
+ },
108
+ cline: {
109
+ name: 'Cline',
110
+ description: 'VS Code agent extension',
111
+ dirs: ['.cline/skills/forge-workflow'],
112
+ hasSkill: true,
113
+ linkFile: '.clinerules'
114
+ },
115
+ roo: {
116
+ name: 'Roo Code',
117
+ description: 'Cline fork with modes',
118
+ dirs: ['.roo/commands'],
119
+ linkFile: '.clinerules',
120
+ needsConversion: true
121
+ },
122
+ aider: {
123
+ name: 'Aider',
124
+ description: 'Terminal-based agent',
125
+ dirs: [],
126
+ customSetup: 'aider'
127
+ }
128
+ };
16
129
 
17
- console.log('');
18
- console.log(' ___ ');
19
- console.log(' | _|___ _ _ ___ ___ ');
20
- console.log(' | _| . || \'_|| . || -_|');
21
- console.log(' |_| |___||_| |_ ||___|');
22
- console.log(' |___| ');
23
- console.log('');
24
- console.log('Installing Forge - 9-Stage TDD-First Workflow...');
25
- console.log('');
130
+ // SECURITY: Freeze AGENTS to prevent runtime manipulation
131
+ Object.freeze(AGENTS);
132
+ Object.values(AGENTS).forEach(agent => Object.freeze(agent));
26
133
 
27
- // Get the project root (where npm install was run)
28
- const projectRoot = process.env.INIT_CWD || process.cwd();
134
+ const COMMANDS = ['status', 'research', 'plan', 'dev', 'check', 'ship', 'review', 'merge', 'verify'];
29
135
 
30
- // Get the package directory (where forge is installed)
31
- const packageDir = path.dirname(__dirname);
136
+ // Code review tool options
137
+ const CODE_REVIEW_TOOLS = {
138
+ 'github-code-quality': {
139
+ name: 'GitHub Code Quality',
140
+ description: 'FREE, built-in - Zero setup required',
141
+ recommended: true
142
+ },
143
+ 'coderabbit': {
144
+ name: 'CodeRabbit',
145
+ description: 'FREE for open source - Install GitHub App at https://coderabbit.ai'
146
+ },
147
+ 'greptile': {
148
+ name: 'Greptile',
149
+ description: 'Paid ($99+/mo) - Enterprise code review',
150
+ requiresApiKey: true,
151
+ envVar: 'GREPTILE_API_KEY',
152
+ getKeyUrl: 'https://greptile.com'
153
+ }
154
+ };
155
+
156
+ // Code quality tool options
157
+ const CODE_QUALITY_TOOLS = {
158
+ 'eslint': {
159
+ name: 'ESLint only',
160
+ description: 'FREE, built-in - No external server required',
161
+ recommended: true
162
+ },
163
+ 'sonarcloud': {
164
+ name: 'SonarCloud',
165
+ description: '50k LoC free, cloud-hosted',
166
+ requiresApiKey: true,
167
+ envVars: ['SONAR_TOKEN', 'SONAR_ORGANIZATION', 'SONAR_PROJECT_KEY'],
168
+ getKeyUrl: 'https://sonarcloud.io/account/security'
169
+ },
170
+ 'sonarqube': {
171
+ name: 'SonarQube Community',
172
+ description: 'FREE, self-hosted, unlimited LoC',
173
+ envVars: ['SONARQUBE_URL', 'SONARQUBE_TOKEN'],
174
+ dockerCommand: 'docker run -d --name sonarqube -p 9000:9000 sonarqube:community'
175
+ }
176
+ };
177
+
178
+ // Helper function to safely execute commands (no user input)
179
+ function safeExec(cmd) {
180
+ try {
181
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
182
+ } catch (e) {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ // Prerequisite check function
188
+ function checkPrerequisites() {
189
+ const errors = [];
190
+ const warnings = [];
191
+
192
+ console.log('');
193
+ console.log('Checking prerequisites...');
194
+ console.log('');
195
+
196
+ // Check git
197
+ const gitVersion = safeExec('git --version');
198
+ if (gitVersion) {
199
+ console.log(` ✓ ${gitVersion}`);
200
+ } else {
201
+ errors.push('git - Install from https://git-scm.com');
202
+ }
203
+
204
+ // Check GitHub CLI
205
+ const ghVersion = safeExec('gh --version');
206
+ if (ghVersion) {
207
+ console.log(` ✓ ${ghVersion.split('\\n')[0]}`);
208
+ // Check if authenticated
209
+ const authStatus = safeExec('gh auth status');
210
+ if (!authStatus) {
211
+ warnings.push('GitHub CLI not authenticated. Run: gh auth login');
212
+ }
213
+ } else {
214
+ errors.push('gh (GitHub CLI) - Install from https://cli.github.com');
215
+ }
216
+
217
+ // Check Node.js version
218
+ const nodeVersion = parseInt(process.version.slice(1).split('.')[0]);
219
+ if (nodeVersion >= 20) {
220
+ console.log(` ✓ node ${process.version}`);
221
+ } else {
222
+ errors.push(`Node.js 20+ required (current: ${process.version})`);
223
+ }
224
+
225
+ // Detect package manager
226
+ const bunVersion = safeExec('bun --version');
227
+ if (bunVersion) {
228
+ PKG_MANAGER = 'bun';
229
+ console.log(` ✓ bun v${bunVersion} (detected as package manager)`);
230
+ } else {
231
+ const pnpmVersion = safeExec('pnpm --version');
232
+ if (pnpmVersion) {
233
+ PKG_MANAGER = 'pnpm';
234
+ console.log(` ✓ pnpm ${pnpmVersion} (detected as package manager)`);
235
+ } else {
236
+ const yarnVersion = safeExec('yarn --version');
237
+ if (yarnVersion) {
238
+ PKG_MANAGER = 'yarn';
239
+ console.log(` ✓ yarn ${yarnVersion} (detected as package manager)`);
240
+ } else {
241
+ const npmVersion = safeExec('npm --version');
242
+ if (npmVersion) {
243
+ PKG_MANAGER = 'npm';
244
+ console.log(` ✓ npm ${npmVersion} (detected as package manager)`);
245
+ } else {
246
+ errors.push('npm, yarn, pnpm, or bun - Install a package manager');
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ // Also detect from lock files if present
253
+ const bunLock = path.join(projectRoot, 'bun.lockb');
254
+ const bunLock2 = path.join(projectRoot, 'bun.lock');
255
+ const pnpmLock = path.join(projectRoot, 'pnpm-lock.yaml');
256
+ const yarnLock = path.join(projectRoot, 'yarn.lock');
257
+
258
+ if (fs.existsSync(bunLock) || fs.existsSync(bunLock2)) {
259
+ PKG_MANAGER = 'bun';
260
+ } else if (fs.existsSync(pnpmLock)) {
261
+ PKG_MANAGER = 'pnpm';
262
+ } else if (fs.existsSync(yarnLock)) {
263
+ PKG_MANAGER = 'yarn';
264
+ }
265
+
266
+ // Show errors
267
+ if (errors.length > 0) {
268
+ console.log('');
269
+ console.log('❌ Missing required tools:');
270
+ errors.forEach(err => console.log(` - ${err}`));
271
+ console.log('');
272
+ console.log('Please install missing tools and try again.');
273
+ process.exit(1);
274
+ }
275
+
276
+ // Show warnings
277
+ if (warnings.length > 0) {
278
+ console.log('');
279
+ console.log('⚠️ Warnings:');
280
+ warnings.forEach(warn => console.log(` - ${warn}`));
281
+ }
282
+
283
+ console.log('');
284
+ console.log(` Package manager: ${PKG_MANAGER}`);
285
+
286
+ return { errors, warnings };
287
+ }
288
+
289
+ // Universal SKILL.md content
290
+ const SKILL_CONTENT = `---
291
+ name: forge-workflow
292
+ description: 9-stage TDD-first workflow for feature development. Use when building features, fixing bugs, or shipping PRs.
293
+ category: Development Workflow
294
+ tags: [tdd, workflow, pr, git, testing]
295
+ tools: [Bash, Read, Write, Edit, Grep, Glob]
296
+ ---
297
+
298
+ # Forge Workflow Skill
299
+
300
+ A TDD-first workflow for AI coding agents. Ship features with confidence.
301
+
302
+ ## When to Use
303
+
304
+ Automatically invoke this skill when the user wants to:
305
+ - Build a new feature
306
+ - Fix a bug
307
+ - Create a pull request
308
+ - Run the development workflow
309
+
310
+ ## 9 Stages
311
+
312
+ | Stage | Command | Description |
313
+ |-------|---------|-------------|
314
+ | 1 | \`/status\` | Check current context, active work, recent completions |
315
+ | 2 | \`/research\` | Deep research with web search, document to docs/research/ |
316
+ | 3 | \`/plan\` | Create implementation plan, branch, OpenSpec if strategic |
317
+ | 4 | \`/dev\` | TDD development (RED-GREEN-REFACTOR cycles) |
318
+ | 5 | \`/check\` | Validation (type/lint/security/tests) |
319
+ | 6 | \`/ship\` | Create PR with full documentation |
320
+ | 7 | \`/review\` | Address ALL PR feedback |
321
+ | 8 | \`/merge\` | Update docs, merge PR, cleanup |
322
+ | 9 | \`/verify\` | Final documentation verification |
323
+
324
+ ## Workflow Flow
325
+
326
+ \`\`\`
327
+ /status -> /research -> /plan -> /dev -> /check -> /ship -> /review -> /merge -> /verify
328
+ \`\`\`
329
+
330
+ ## Core Principles
331
+
332
+ - **TDD-First**: Write tests BEFORE implementation (RED-GREEN-REFACTOR)
333
+ - **Research-First**: Understand before building, document decisions
334
+ - **Security Built-In**: OWASP Top 10 analysis for every feature
335
+ - **Documentation Progressive**: Update at each stage, verify at end
336
+ `;
337
+
338
+ // Cursor MDC rule content
339
+ const CURSOR_RULE = `---
340
+ description: Forge 9-Stage TDD Workflow
341
+ alwaysApply: true
342
+ ---
343
+
344
+ # Forge Workflow Commands
345
+
346
+ Use these commands via \`/command-name\`:
347
+
348
+ 1. \`/status\` - Check current context, active work, recent completions
349
+ 2. \`/research\` - Deep research with web search, document to docs/research/
350
+ 3. \`/plan\` - Create implementation plan, branch, tracking
351
+ 4. \`/dev\` - TDD development (RED-GREEN-REFACTOR cycles)
352
+ 5. \`/check\` - Validation (type/lint/security/tests)
353
+ 6. \`/ship\` - Create PR with full documentation
354
+ 7. \`/review\` - Address ALL PR feedback
355
+ 8. \`/merge\` - Update docs, merge PR, cleanup
356
+ 9. \`/verify\` - Final documentation verification
357
+
358
+ See AGENTS.md for full workflow details.
359
+ `;
360
+
361
+ // Helper functions
362
+ const resolvedProjectRoot = path.resolve(projectRoot);
363
+
364
+ function ensureDir(dir) {
365
+ const fullPath = path.resolve(projectRoot, dir);
366
+
367
+ // SECURITY: Prevent path traversal
368
+ if (!fullPath.startsWith(resolvedProjectRoot)) {
369
+ console.error(` ✗ Security: Directory path escape blocked: ${dir}`);
370
+ return false;
371
+ }
32
372
 
33
- // Directories to create
34
- const dirs = [
35
- '.claude/commands',
36
- '.claude/rules',
37
- '.claude/skills/parallel-ai',
38
- '.claude/skills/sonarcloud',
39
- '.claude/scripts',
40
- 'docs/research'
41
- ];
42
-
43
- // Create directories
44
- console.log('Creating directories...');
45
- dirs.forEach(dir => {
46
- const fullPath = path.join(projectRoot, dir);
47
373
  if (!fs.existsSync(fullPath)) {
48
374
  fs.mkdirSync(fullPath, { recursive: true });
49
375
  }
50
- });
376
+ return true;
377
+ }
378
+
379
+ function writeFile(filePath, content) {
380
+ try {
381
+ const fullPath = path.resolve(projectRoot, filePath);
382
+
383
+ // SECURITY: Prevent path traversal
384
+ if (!fullPath.startsWith(resolvedProjectRoot)) {
385
+ console.error(` ✗ Security: Write path escape blocked: ${filePath}`);
386
+ return false;
387
+ }
388
+
389
+ const dir = path.dirname(fullPath);
390
+ if (!fs.existsSync(dir)) {
391
+ fs.mkdirSync(dir, { recursive: true });
392
+ }
393
+ fs.writeFileSync(fullPath, content, { mode: 0o644 });
394
+ return true;
395
+ } catch (err) {
396
+ console.error(` ✗ Failed to write ${filePath}: ${err.message}`);
397
+ return false;
398
+ }
399
+ }
400
+
401
+ function readFile(filePath) {
402
+ try {
403
+ return fs.readFileSync(filePath, 'utf8');
404
+ } catch (err) {
405
+ if (process.env.DEBUG) {
406
+ console.warn(` ⚠ Could not read ${filePath}: ${err.message}`);
407
+ }
408
+ return null;
409
+ }
410
+ }
51
411
 
52
- // Copy function
53
412
  function copyFile(src, dest) {
54
413
  try {
414
+ const destPath = path.resolve(projectRoot, dest);
415
+
416
+ // SECURITY: Prevent path traversal
417
+ if (!destPath.startsWith(resolvedProjectRoot)) {
418
+ console.error(` ✗ Security: Copy destination escape blocked: ${dest}`);
419
+ return false;
420
+ }
421
+
55
422
  if (fs.existsSync(src)) {
56
- fs.copyFileSync(src, dest);
423
+ const destDir = path.dirname(destPath);
424
+ if (!fs.existsSync(destDir)) {
425
+ fs.mkdirSync(destDir, { recursive: true });
426
+ }
427
+ fs.copyFileSync(src, destPath);
57
428
  return true;
429
+ } else {
430
+ if (process.env.DEBUG) {
431
+ console.warn(` ⚠ Source file not found: ${src}`);
432
+ }
58
433
  }
59
434
  } catch (err) {
60
- // Silently continue if file doesn't exist
435
+ console.error(` ✗ Failed to copy ${src} -> ${dest}: ${err.message}`);
61
436
  }
62
437
  return false;
63
438
  }
64
439
 
65
- // Copy commands
66
- console.log('Copying workflow commands...');
67
- COMMANDS.forEach(cmd => {
68
- const src = path.join(packageDir, '.claude/commands', cmd);
69
- const dest = path.join(projectRoot, '.claude/commands', cmd);
70
- if (copyFile(src, dest)) {
71
- console.log(` ✓ ${cmd}`);
72
- }
73
- });
74
-
75
- // Copy rules
76
- console.log('Copying workflow rules...');
77
- const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
78
- const rulesDest = path.join(projectRoot, '.claude/rules/workflow.md');
79
- if (copyFile(rulesSrc, rulesDest)) {
80
- console.log(' ✓ workflow.md');
81
- }
82
-
83
- // Copy skills
84
- console.log('Copying skills...');
85
- SKILLS_PARALLEL_AI.forEach(file => {
86
- const src = path.join(packageDir, '.claude/skills/parallel-ai', file);
87
- const dest = path.join(projectRoot, '.claude/skills/parallel-ai', file);
88
- copyFile(src, dest);
89
- });
90
- console.log(' ✓ parallel-ai');
91
-
92
- SKILLS_SONARCLOUD.forEach(file => {
93
- const src = path.join(packageDir, '.claude/skills/sonarcloud', file);
94
- const dest = path.join(projectRoot, '.claude/skills/sonarcloud', file);
95
- copyFile(src, dest);
96
- });
97
- console.log(' sonarcloud');
98
-
99
- // Copy scripts
100
- console.log('Copying scripts...');
101
- const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
102
- const scriptDest = path.join(projectRoot, '.claude/scripts/load-env.sh');
103
- if (copyFile(scriptSrc, scriptDest)) {
104
- console.log(' ✓ load-env.sh');
105
- }
106
-
107
- // Copy documentation
108
- console.log('Copying documentation...');
109
- const workflowSrc = path.join(packageDir, 'docs/WORKFLOW.md');
110
- const workflowDest = path.join(projectRoot, 'docs/WORKFLOW.md');
111
- if (copyFile(workflowSrc, workflowDest)) {
112
- console.log(' ✓ WORKFLOW.md');
113
- }
114
-
115
- const templateSrc = path.join(packageDir, 'docs/research/TEMPLATE.md');
116
- const templateDest = path.join(projectRoot, 'docs/research/TEMPLATE.md');
117
- if (copyFile(templateSrc, templateDest)) {
118
- console.log(' ✓ research/TEMPLATE.md');
119
- }
120
-
121
- console.log('');
122
- console.log('✅ Forge installed successfully!');
123
- console.log('');
124
- console.log('┌─────────────────────────────────────────────────────────┐');
125
- console.log('│ GET STARTED │');
126
- console.log('├─────────────────────────────────────────────────────────┤');
127
- console.log('│ /status - Check current context │');
128
- console.log('│ /research - Start researching a feature │');
129
- console.log('│ /plan - Create implementation plan │');
130
- console.log('│ /dev - Start TDD development │');
131
- console.log('│ /check - Run validation │');
132
- console.log('│ /ship - Create pull request │');
133
- console.log('│ /review - Address PR feedback │');
134
- console.log('│ /merge - Merge and cleanup │');
135
- console.log('│ /verify - Final documentation check │');
136
- console.log('├─────────────────────────────────────────────────────────┤');
137
- console.log('│ Full guide: docs/WORKFLOW.md │');
138
- console.log('│ Research template: docs/research/TEMPLATE.md │');
139
- console.log('└─────────────────────────────────────────────────────────┘');
140
- console.log('');
440
+ function createSymlinkOrCopy(source, target) {
441
+ const fullSource = path.resolve(projectRoot, source);
442
+ const fullTarget = path.resolve(projectRoot, target);
443
+ const resolvedProjectRoot = path.resolve(projectRoot);
444
+
445
+ // SECURITY: Prevent path traversal attacks
446
+ if (!fullSource.startsWith(resolvedProjectRoot)) {
447
+ console.error(` ✗ Security: Source path escape blocked: ${source}`);
448
+ return false;
449
+ }
450
+ if (!fullTarget.startsWith(resolvedProjectRoot)) {
451
+ console.error(` ✗ Security: Target path escape blocked: ${target}`);
452
+ return false;
453
+ }
454
+
455
+ try {
456
+ if (fs.existsSync(fullTarget)) {
457
+ fs.unlinkSync(fullTarget);
458
+ }
459
+ const targetDir = path.dirname(fullTarget);
460
+ if (!fs.existsSync(targetDir)) {
461
+ fs.mkdirSync(targetDir, { recursive: true });
462
+ }
463
+ try {
464
+ const relPath = path.relative(targetDir, fullSource);
465
+ fs.symlinkSync(relPath, fullTarget);
466
+ return 'linked';
467
+ } catch (symlinkErr) {
468
+ fs.copyFileSync(fullSource, fullTarget);
469
+ return 'copied';
470
+ }
471
+ } catch (err) {
472
+ console.error(` Failed to link/copy ${source} -> ${target}: ${err.message}`);
473
+ return false;
474
+ }
475
+ }
476
+
477
+ function stripFrontmatter(content) {
478
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
479
+ return match ? match[1] : content;
480
+ }
481
+
482
+ // Read existing .env.local
483
+ function readEnvFile() {
484
+ const envPath = path.join(projectRoot, '.env.local');
485
+ try {
486
+ if (fs.existsSync(envPath)) {
487
+ return fs.readFileSync(envPath, 'utf8');
488
+ }
489
+ } catch (err) {}
490
+ return '';
491
+ }
492
+
493
+ // Parse .env.local and return key-value pairs
494
+ function parseEnvFile() {
495
+ const content = readEnvFile();
496
+ const lines = content.split(/\r?\n/);
497
+ const vars = {};
498
+ lines.forEach(line => {
499
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
500
+ if (match) {
501
+ vars[match[1]] = match[2];
502
+ }
503
+ });
504
+ return vars;
505
+ }
506
+
507
+ // Write or update .env.local - PRESERVES existing values
508
+ function writeEnvTokens(tokens, preserveExisting = true) {
509
+ const envPath = path.join(projectRoot, '.env.local');
510
+ let content = readEnvFile();
511
+
512
+ // Parse existing content (handle both CRLF and LF line endings)
513
+ const lines = content.split(/\r?\n/);
514
+ const existingVars = {};
515
+ const existingKeys = new Set();
516
+ lines.forEach(line => {
517
+ const match = line.match(/^([A-Z_]+)=/);
518
+ if (match) {
519
+ existingVars[match[1]] = line;
520
+ existingKeys.add(match[1]);
521
+ }
522
+ });
523
+
524
+ // Track what was added vs preserved
525
+ let added = [];
526
+ let preserved = [];
527
+
528
+ // Add/update tokens - PRESERVE existing values if preserveExisting is true
529
+ Object.entries(tokens).forEach(([key, value]) => {
530
+ if (value && value.trim()) {
531
+ if (preserveExisting && existingKeys.has(key)) {
532
+ // Keep existing value, don't overwrite
533
+ preserved.push(key);
534
+ } else {
535
+ // Add new token
536
+ existingVars[key] = `${key}=${value.trim()}`;
537
+ added.push(key);
538
+ }
539
+ }
540
+ });
541
+
542
+ // Rebuild file with comments
543
+ const outputLines = [];
544
+
545
+ // Add header if new file
546
+ if (!content.includes('# External Service API Keys')) {
547
+ outputLines.push('# External Service API Keys for Forge Workflow');
548
+ outputLines.push('# Get your keys from:');
549
+ outputLines.push('# Parallel AI: https://platform.parallel.ai');
550
+ outputLines.push('# Greptile: https://app.greptile.com/api');
551
+ outputLines.push('# SonarCloud: https://sonarcloud.io/account/security');
552
+ outputLines.push('');
553
+ }
554
+
555
+ // Add existing content (preserve order and comments)
556
+ lines.forEach(line => {
557
+ const match = line.match(/^([A-Z_]+)=/);
558
+ if (match && existingVars[match[1]]) {
559
+ outputLines.push(existingVars[match[1]]);
560
+ delete existingVars[match[1]]; // Mark as added
561
+ } else if (line.trim()) {
562
+ outputLines.push(line);
563
+ }
564
+ });
565
+
566
+ // Add any new tokens not in original file
567
+ Object.values(existingVars).forEach(line => {
568
+ outputLines.push(line);
569
+ });
570
+
571
+ // Ensure ends with newline
572
+ let finalContent = outputLines.join('\n').trim() + '\n';
573
+
574
+ fs.writeFileSync(envPath, finalContent);
575
+
576
+ // Add .env.local to .gitignore if not present
577
+ const gitignorePath = path.join(projectRoot, '.gitignore');
578
+ try {
579
+ let gitignore = '';
580
+ if (fs.existsSync(gitignorePath)) {
581
+ gitignore = fs.readFileSync(gitignorePath, 'utf8');
582
+ }
583
+ if (!gitignore.includes('.env.local')) {
584
+ fs.appendFileSync(gitignorePath, '\n# Local environment variables\n.env.local\n');
585
+ }
586
+ } catch (err) {}
587
+
588
+ return { added, preserved };
589
+ }
590
+
591
+ // Detect existing project installation status
592
+ function detectProjectStatus() {
593
+ const status = {
594
+ type: 'fresh', // 'fresh', 'upgrade', or 'partial'
595
+ hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
596
+ hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
597
+ hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
598
+ hasDocsWorkflow: fs.existsSync(path.join(projectRoot, 'docs/WORKFLOW.md')),
599
+ existingEnvVars: {}
600
+ };
601
+
602
+ // Determine installation type
603
+ if (status.hasAgentsMd && status.hasClaudeCommands && status.hasDocsWorkflow) {
604
+ status.type = 'upgrade'; // Full forge installation exists
605
+ } else if (status.hasAgentsMd || status.hasClaudeCommands || status.hasEnvLocal) {
606
+ status.type = 'partial'; // Some files exist
607
+ }
608
+ // else: 'fresh' - new installation
609
+
610
+ // Parse existing env vars if .env.local exists
611
+ if (status.hasEnvLocal) {
612
+ status.existingEnvVars = parseEnvFile();
613
+ }
614
+
615
+ return status;
616
+ }
617
+
618
+ // Configure external services interactively
619
+ async function configureExternalServices(rl, question, selectedAgents = [], projectStatus = null) {
620
+ console.log('');
621
+ console.log('==============================================');
622
+ console.log(' External Services Configuration');
623
+ console.log('==============================================');
624
+ console.log('');
625
+
626
+ // Check if external services are already configured
627
+ const existingEnvVars = projectStatus?.existingEnvVars || parseEnvFile();
628
+ const hasCodeReviewTool = existingEnvVars.CODE_REVIEW_TOOL;
629
+ const hasCodeQualityTool = existingEnvVars.CODE_QUALITY_TOOL;
630
+ const hasExistingConfig = hasCodeReviewTool || hasCodeQualityTool;
631
+
632
+ if (hasExistingConfig) {
633
+ console.log('External services already configured:');
634
+ if (hasCodeReviewTool) {
635
+ console.log(` - CODE_REVIEW_TOOL: ${hasCodeReviewTool}`);
636
+ }
637
+ if (hasCodeQualityTool) {
638
+ console.log(` - CODE_QUALITY_TOOL: ${hasCodeQualityTool}`);
639
+ }
640
+ console.log('');
641
+
642
+ const reconfigure = await question('Reconfigure external services? (y/n) [n]: ');
643
+ if (reconfigure.toLowerCase() !== 'y' && reconfigure.toLowerCase() !== 'yes') {
644
+ console.log('');
645
+ console.log('Keeping existing configuration.');
646
+ return;
647
+ }
648
+ console.log('');
649
+ }
650
+
651
+ console.log('Would you like to configure external services?');
652
+ console.log('(You can also add them later to .env.local)');
653
+ console.log('');
654
+
655
+ const configure = await question('Configure external services? (y/n): ');
656
+
657
+ if (configure.toLowerCase() !== 'y' && configure.toLowerCase() !== 'yes') {
658
+ console.log('');
659
+ console.log('Skipping external services. You can configure them later by editing .env.local');
660
+ return;
661
+ }
662
+
663
+ const tokens = {};
664
+
665
+ // ============================================
666
+ // CODE REVIEW TOOL SELECTION
667
+ // ============================================
668
+ console.log('');
669
+ console.log('Code Review Tool');
670
+ console.log('----------------');
671
+ console.log('Select your code review integration:');
672
+ console.log('');
673
+ console.log(' 1) GitHub Code Quality (FREE, built-in) [RECOMMENDED]');
674
+ console.log(' Zero setup - uses GitHub\'s built-in code quality features');
675
+ console.log('');
676
+ console.log(' 2) CodeRabbit (FREE for open source)');
677
+ console.log(' AI-powered reviews - install GitHub App at https://coderabbit.ai');
678
+ console.log('');
679
+ console.log(' 3) Greptile (Paid - $99+/mo)');
680
+ console.log(' Enterprise code review - https://greptile.com');
681
+ console.log('');
682
+ console.log(' 4) Skip code review integration');
683
+ console.log('');
684
+
685
+ const codeReviewChoice = await question('Select [1]: ') || '1';
686
+
687
+ switch (codeReviewChoice) {
688
+ case '1':
689
+ tokens['CODE_REVIEW_TOOL'] = 'github-code-quality';
690
+ console.log(' ✓ Using GitHub Code Quality (FREE)');
691
+ break;
692
+ case '2':
693
+ tokens['CODE_REVIEW_TOOL'] = 'coderabbit';
694
+ console.log(' ✓ Using CodeRabbit - Install the GitHub App to activate');
695
+ console.log(' https://coderabbit.ai');
696
+ break;
697
+ case '3':
698
+ const greptileKey = await question(' Enter Greptile API key: ');
699
+ if (greptileKey && greptileKey.trim()) {
700
+ tokens['CODE_REVIEW_TOOL'] = 'greptile';
701
+ tokens['GREPTILE_API_KEY'] = greptileKey.trim();
702
+ console.log(' ✓ Greptile configured');
703
+ } else {
704
+ tokens['CODE_REVIEW_TOOL'] = 'none';
705
+ console.log(' Skipped - No API key provided');
706
+ }
707
+ break;
708
+ default:
709
+ tokens['CODE_REVIEW_TOOL'] = 'none';
710
+ console.log(' Skipped code review integration');
711
+ }
712
+
713
+ // ============================================
714
+ // CODE QUALITY TOOL SELECTION
715
+ // ============================================
716
+ console.log('');
717
+ console.log('Code Quality Tool');
718
+ console.log('-----------------');
719
+ console.log('Select your code quality/security scanner:');
720
+ console.log('');
721
+ console.log(' 1) ESLint only (FREE, built-in) [RECOMMENDED]');
722
+ console.log(' No external server required - uses project\'s linting');
723
+ console.log('');
724
+ console.log(' 2) SonarCloud (50k LoC free, cloud-hosted)');
725
+ console.log(' Get token: https://sonarcloud.io/account/security');
726
+ console.log('');
727
+ console.log(' 3) SonarQube Community (FREE, self-hosted, unlimited LoC)');
728
+ console.log(' Run: docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
729
+ console.log('');
730
+ console.log(' 4) Skip code quality integration');
731
+ console.log('');
732
+
733
+ const codeQualityChoice = await question('Select [1]: ') || '1';
734
+
735
+ switch (codeQualityChoice) {
736
+ case '1':
737
+ tokens['CODE_QUALITY_TOOL'] = 'eslint';
738
+ console.log(' ✓ Using ESLint (built-in)');
739
+ break;
740
+ case '2':
741
+ const sonarToken = await question(' Enter SonarCloud token: ');
742
+ const sonarOrg = await question(' Enter SonarCloud organization: ');
743
+ const sonarProject = await question(' Enter SonarCloud project key: ');
744
+ if (sonarToken && sonarToken.trim()) {
745
+ tokens['CODE_QUALITY_TOOL'] = 'sonarcloud';
746
+ tokens['SONAR_TOKEN'] = sonarToken.trim();
747
+ if (sonarOrg) tokens['SONAR_ORGANIZATION'] = sonarOrg.trim();
748
+ if (sonarProject) tokens['SONAR_PROJECT_KEY'] = sonarProject.trim();
749
+ console.log(' ✓ SonarCloud configured');
750
+ } else {
751
+ tokens['CODE_QUALITY_TOOL'] = 'eslint';
752
+ console.log(' Falling back to ESLint');
753
+ }
754
+ break;
755
+ case '3':
756
+ console.log('');
757
+ console.log(' SonarQube Self-Hosted Setup:');
758
+ console.log(' docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
759
+ console.log(' Access: http://localhost:9000 (admin/admin)');
760
+ console.log('');
761
+ const sqUrl = await question(' Enter SonarQube URL [http://localhost:9000]: ') || 'http://localhost:9000';
762
+ const sqToken = await question(' Enter SonarQube token (optional): ');
763
+ tokens['CODE_QUALITY_TOOL'] = 'sonarqube';
764
+ tokens['SONARQUBE_URL'] = sqUrl;
765
+ if (sqToken && sqToken.trim()) {
766
+ tokens['SONARQUBE_TOKEN'] = sqToken.trim();
767
+ }
768
+ console.log(' ✓ SonarQube self-hosted configured');
769
+ break;
770
+ default:
771
+ tokens['CODE_QUALITY_TOOL'] = 'none';
772
+ console.log(' Skipped code quality integration');
773
+ }
774
+
775
+ // ============================================
776
+ // RESEARCH TOOL SELECTION
777
+ // ============================================
778
+ console.log('');
779
+ console.log('Research Tool');
780
+ console.log('-------------');
781
+ console.log('Select your research tool for /research stage:');
782
+ console.log('');
783
+ console.log(' 1) Manual research only [DEFAULT]');
784
+ console.log(' Use web browser and codebase exploration');
785
+ console.log('');
786
+ console.log(' 2) Parallel AI (comprehensive web research)');
787
+ console.log(' Get key: https://platform.parallel.ai');
788
+ console.log('');
789
+
790
+ const researchChoice = await question('Select [1]: ') || '1';
791
+
792
+ if (researchChoice === '2') {
793
+ const parallelKey = await question(' Enter Parallel AI API key: ');
794
+ if (parallelKey && parallelKey.trim()) {
795
+ tokens['PARALLEL_API_KEY'] = parallelKey.trim();
796
+ console.log(' ✓ Parallel AI configured');
797
+ } else {
798
+ console.log(' Skipped - No API key provided');
799
+ }
800
+ } else {
801
+ console.log(' ✓ Using manual research');
802
+ }
803
+
804
+ // ============================================
805
+ // CONTEXT7 MCP - Library Documentation
806
+ // ============================================
807
+ console.log('');
808
+ console.log('Context7 MCP - Library Documentation');
809
+ console.log('-------------------------------------');
810
+ console.log('Provides up-to-date library docs for AI coding agents.');
811
+ console.log('');
812
+
813
+ // Show what was/will be auto-installed
814
+ if (selectedAgents.includes('claude')) {
815
+ console.log(' ✓ Auto-installed for Claude Code (.mcp.json)');
816
+ }
817
+ if (selectedAgents.includes('continue')) {
818
+ console.log(' ✓ Auto-installed for Continue (.continue/config.yaml)');
819
+ }
820
+
821
+ // Show manual setup instructions for GUI-based agents
822
+ const needsManualMcp = [];
823
+ if (selectedAgents.includes('cursor')) needsManualMcp.push('Cursor: Configure via Cursor Settings > MCP');
824
+ if (selectedAgents.includes('windsurf')) needsManualMcp.push('Windsurf: Install via Plugin Store');
825
+ if (selectedAgents.includes('cline')) needsManualMcp.push('Cline: Install via MCP Marketplace');
826
+
827
+ if (needsManualMcp.length > 0) {
828
+ needsManualMcp.forEach(msg => console.log(` ! ${msg}`));
829
+ console.log('');
830
+ console.log(' Package: @upstash/context7-mcp@latest');
831
+ console.log(' Docs: https://github.com/upstash/context7-mcp');
832
+ }
833
+
834
+ // Save package manager preference
835
+ tokens['PKG_MANAGER'] = PKG_MANAGER;
836
+
837
+ // Write all tokens to .env.local (preserving existing values)
838
+ const { added, preserved } = writeEnvTokens(tokens, true);
839
+
840
+ console.log('');
841
+ if (preserved.length > 0) {
842
+ console.log('Preserved existing values:');
843
+ preserved.forEach(key => {
844
+ console.log(` - ${key} already configured - keeping existing value`);
845
+ });
846
+ console.log('');
847
+ }
848
+ if (added.length > 0) {
849
+ console.log('Added new configuration:');
850
+ added.forEach(key => {
851
+ console.log(` - ${key}`);
852
+ });
853
+ console.log('');
854
+ }
855
+ console.log('Configuration saved to .env.local');
856
+ console.log('Note: .env.local has been added to .gitignore');
857
+ }
858
+
859
+ // Display the Forge banner
860
+ function showBanner(subtitle = 'Universal AI Agent Workflow') {
861
+ console.log('');
862
+ console.log(' ███████╗ ██████╗ ██████╗ ██████╗ ███████╗');
863
+ console.log(' ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝');
864
+ console.log(' █████╗ ██║ ██║██████╔╝██║ ███╗█████╗ ');
865
+ console.log(' ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ ');
866
+ console.log(' ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗');
867
+ console.log(' ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝');
868
+ console.log(' v1.1.0');
869
+ console.log('');
870
+ if (subtitle) {
871
+ console.log(` ${subtitle}`);
872
+ }
873
+ }
874
+
875
+ // Minimal installation (postinstall)
876
+ function minimalInstall() {
877
+ showBanner();
878
+ console.log('');
879
+
880
+ // Create core directories
881
+ ensureDir('docs/planning');
882
+ ensureDir('docs/research');
883
+
884
+ // Copy AGENTS.md (only if not exists - preserve user customizations in minimal install)
885
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
886
+ if (fs.existsSync(agentsPath)) {
887
+ console.log(' Skipped: AGENTS.md (already exists)');
888
+ } else {
889
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
890
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
891
+ console.log(' Created: AGENTS.md (universal standard)');
892
+ }
893
+ }
894
+
895
+ // Copy documentation
896
+ const workflowSrc = path.join(packageDir, 'docs/WORKFLOW.md');
897
+ if (copyFile(workflowSrc, 'docs/WORKFLOW.md')) {
898
+ console.log(' Created: docs/WORKFLOW.md');
899
+ }
900
+
901
+ const templateSrc = path.join(packageDir, 'docs/research/TEMPLATE.md');
902
+ if (copyFile(templateSrc, 'docs/research/TEMPLATE.md')) {
903
+ console.log(' Created: docs/research/TEMPLATE.md');
904
+ }
905
+
906
+ // Create PROGRESS.md if not exists
907
+ const progressPath = path.join(projectRoot, 'docs/planning/PROGRESS.md');
908
+ if (!fs.existsSync(progressPath)) {
909
+ writeFile('docs/planning/PROGRESS.md', `# Project Progress
910
+
911
+ ## Current Focus
912
+ <!-- What you're working on -->
913
+
914
+ ## Completed
915
+ <!-- Completed features -->
916
+
917
+ ## Upcoming
918
+ <!-- Next priorities -->
919
+ `);
920
+ console.log(' Created: docs/planning/PROGRESS.md');
921
+ }
922
+
923
+ console.log('');
924
+ console.log('Minimal installation complete!');
925
+ console.log('');
926
+ console.log('To configure for your AI coding agents, run:');
927
+ console.log('');
928
+ console.log(' npx forge setup # Interactive setup (agents + API tokens)');
929
+ console.log(' bunx forge setup # Same with bun');
930
+ console.log('');
931
+ console.log('Or specify agents directly:');
932
+ console.log(' npx forge setup --agents claude,cursor,windsurf');
933
+ console.log(' npx forge setup --all');
934
+ console.log('');
935
+ }
936
+
937
+ // Setup specific agent
938
+ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
939
+ const agent = AGENTS[agentKey];
940
+ if (!agent) return;
941
+
942
+ console.log(`\nSetting up ${agent.name}...`);
943
+
944
+ // Create directories
945
+ agent.dirs.forEach(dir => ensureDir(dir));
946
+
947
+ // Handle Claude Code specifically (downloads commands)
948
+ if (agentKey === 'claude') {
949
+ // Copy commands from package (unless skipped)
950
+ if (skipFiles.claudeCommands) {
951
+ console.log(' Skipped: .claude/commands/ (keeping existing)');
952
+ } else {
953
+ COMMANDS.forEach(cmd => {
954
+ const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
955
+ copyFile(src, `.claude/commands/${cmd}.md`);
956
+ });
957
+ console.log(' Copied: 9 workflow commands');
958
+ }
959
+
960
+ // Copy rules
961
+ const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
962
+ copyFile(rulesSrc, '.claude/rules/workflow.md');
963
+
964
+ // Copy scripts
965
+ const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
966
+ copyFile(scriptSrc, '.claude/scripts/load-env.sh');
967
+ }
968
+
969
+ // Custom setups
970
+ if (agent.customSetup === 'cursor') {
971
+ writeFile('.cursor/rules/forge-workflow.mdc', CURSOR_RULE);
972
+ console.log(' Created: .cursor/rules/forge-workflow.mdc');
973
+ }
974
+
975
+ if (agent.customSetup === 'aider') {
976
+ const aiderPath = path.join(projectRoot, '.aider.conf.yml');
977
+ if (!fs.existsSync(aiderPath)) {
978
+ writeFile('.aider.conf.yml', `# Aider configuration
979
+ # Read AGENTS.md for workflow instructions
980
+ read:
981
+ - AGENTS.md
982
+ - docs/WORKFLOW.md
983
+ `);
984
+ console.log(' Created: .aider.conf.yml');
985
+ } else {
986
+ console.log(' Skipped: .aider.conf.yml already exists');
987
+ }
988
+ return;
989
+ }
990
+
991
+ // Convert/copy commands
992
+ if (claudeCommands && (agent.needsConversion || agent.copyCommands || agent.promptFormat || agent.continueFormat)) {
993
+ Object.entries(claudeCommands).forEach(([cmd, content]) => {
994
+ let targetContent = content;
995
+ let targetFile = cmd;
996
+
997
+ if (agent.needsConversion) {
998
+ targetContent = stripFrontmatter(content);
999
+ }
1000
+
1001
+ if (agent.promptFormat) {
1002
+ targetFile = cmd.replace('.md', '.prompt.md');
1003
+ targetContent = stripFrontmatter(content);
1004
+ }
1005
+
1006
+ if (agent.continueFormat) {
1007
+ const baseName = cmd.replace('.md', '');
1008
+ targetFile = `${baseName}.prompt`;
1009
+ targetContent = `---
1010
+ name: ${baseName}
1011
+ description: Forge workflow command - ${baseName}
1012
+ invokable: true
1013
+ ---
1014
+
1015
+ ${stripFrontmatter(content)}`;
1016
+ }
1017
+
1018
+ const targetDir = agent.dirs[0]; // First dir is commands/workflows
1019
+ writeFile(`${targetDir}/${targetFile}`, targetContent);
1020
+ });
1021
+ console.log(' Converted: 9 workflow commands');
1022
+ }
1023
+
1024
+ // Copy rules if needed
1025
+ if (agent.needsConversion && fs.existsSync(path.join(projectRoot, '.claude/rules/workflow.md'))) {
1026
+ const rulesDir = agent.dirs.find(d => d.includes('/rules'));
1027
+ if (rulesDir) {
1028
+ const ruleContent = readFile(path.join(projectRoot, '.claude/rules/workflow.md'));
1029
+ if (ruleContent) {
1030
+ writeFile(`${rulesDir}/workflow.md`, ruleContent);
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ // Create SKILL.md
1036
+ if (agent.hasSkill) {
1037
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1038
+ if (skillDir) {
1039
+ writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
1040
+ console.log(' Created: forge-workflow skill');
1041
+ }
1042
+ }
1043
+
1044
+ // Create .mcp.json with Context7 MCP (Claude Code only)
1045
+ if (agentKey === 'claude') {
1046
+ const mcpPath = path.join(projectRoot, '.mcp.json');
1047
+ if (!fs.existsSync(mcpPath)) {
1048
+ const mcpConfig = {
1049
+ mcpServers: {
1050
+ context7: {
1051
+ command: 'npx',
1052
+ args: ['-y', '@upstash/context7-mcp@latest']
1053
+ }
1054
+ }
1055
+ };
1056
+ writeFile('.mcp.json', JSON.stringify(mcpConfig, null, 2));
1057
+ console.log(' Created: .mcp.json with Context7 MCP');
1058
+ } else {
1059
+ console.log(' Skipped: .mcp.json already exists');
1060
+ }
1061
+ }
1062
+
1063
+ // Create config.yaml with Context7 MCP (Continue only)
1064
+ if (agentKey === 'continue') {
1065
+ const configPath = path.join(projectRoot, '.continue/config.yaml');
1066
+ if (!fs.existsSync(configPath)) {
1067
+ const continueConfig = `# Continue Configuration
1068
+ # https://docs.continue.dev/customize/deep-dives/configuration
1069
+
1070
+ name: Forge Workflow
1071
+ version: "1.0"
1072
+
1073
+ # MCP Servers for enhanced capabilities
1074
+ mcpServers:
1075
+ - name: context7
1076
+ command: npx
1077
+ args:
1078
+ - "-y"
1079
+ - "@upstash/context7-mcp@latest"
1080
+
1081
+ # Rules loaded from .continuerules
1082
+ `;
1083
+ writeFile('.continue/config.yaml', continueConfig);
1084
+ console.log(' Created: config.yaml with Context7 MCP');
1085
+ } else {
1086
+ console.log(' Skipped: config.yaml already exists');
1087
+ }
1088
+ }
1089
+
1090
+ // Create link file
1091
+ if (agent.linkFile) {
1092
+ const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile);
1093
+ if (result) {
1094
+ console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ // Interactive setup
1100
+ async function interactiveSetup() {
1101
+ const rl = readline.createInterface({
1102
+ input: process.stdin,
1103
+ output: process.stdout
1104
+ });
1105
+
1106
+ // Handle Ctrl+C gracefully
1107
+ rl.on('close', () => {
1108
+ console.log('\n\nSetup cancelled.');
1109
+ process.exit(0);
1110
+ });
1111
+
1112
+ // Handle input errors
1113
+ rl.on('error', (err) => {
1114
+ console.error('Input error:', err.message);
1115
+ process.exit(1);
1116
+ });
1117
+
1118
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
1119
+
1120
+ showBanner('Agent Configuration');
1121
+
1122
+ // Check prerequisites first
1123
+ checkPrerequisites();
1124
+ console.log('');
1125
+
1126
+ // =============================================
1127
+ // PROJECT DETECTION
1128
+ // =============================================
1129
+ const projectStatus = detectProjectStatus();
1130
+
1131
+ if (projectStatus.type !== 'fresh') {
1132
+ console.log('==============================================');
1133
+ console.log(' Existing Installation Detected');
1134
+ console.log('==============================================');
1135
+ console.log('');
1136
+
1137
+ if (projectStatus.type === 'upgrade') {
1138
+ console.log('Found existing Forge installation:');
1139
+ } else {
1140
+ console.log('Found partial installation:');
1141
+ }
1142
+
1143
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
1144
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
1145
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
1146
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
1147
+ console.log('');
1148
+ }
1149
+
1150
+ // Track which files to skip based on user choices
1151
+ const skipFiles = {
1152
+ agentsMd: false,
1153
+ claudeCommands: false
1154
+ };
1155
+
1156
+ // Ask about overwriting AGENTS.md if it exists
1157
+ if (projectStatus.hasAgentsMd) {
1158
+ const overwriteAgents = await question('Found existing AGENTS.md. Overwrite? (y/n) [n]: ');
1159
+ if (overwriteAgents.toLowerCase() !== 'y' && overwriteAgents.toLowerCase() !== 'yes') {
1160
+ skipFiles.agentsMd = true;
1161
+ console.log(' Keeping existing AGENTS.md');
1162
+ } else {
1163
+ console.log(' Will overwrite AGENTS.md');
1164
+ }
1165
+ }
1166
+
1167
+ // Ask about overwriting .claude/commands/ if it exists
1168
+ if (projectStatus.hasClaudeCommands) {
1169
+ const overwriteCommands = await question('Found existing .claude/commands/. Overwrite? (y/n) [n]: ');
1170
+ if (overwriteCommands.toLowerCase() !== 'y' && overwriteCommands.toLowerCase() !== 'yes') {
1171
+ skipFiles.claudeCommands = true;
1172
+ console.log(' Keeping existing .claude/commands/');
1173
+ } else {
1174
+ console.log(' Will overwrite .claude/commands/');
1175
+ }
1176
+ }
1177
+
1178
+ if (projectStatus.type !== 'fresh') {
1179
+ console.log('');
1180
+ }
1181
+
1182
+ // =============================================
1183
+ // STEP 1: Agent Selection
1184
+ // =============================================
1185
+ console.log('STEP 1: Select AI Coding Agents');
1186
+ console.log('================================');
1187
+ console.log('');
1188
+ console.log('Which AI coding agents do you use?');
1189
+ console.log('(Enter numbers separated by spaces, or "all")');
1190
+ console.log('');
1191
+
1192
+ const agentKeys = Object.keys(AGENTS);
1193
+ agentKeys.forEach((key, index) => {
1194
+ const agent = AGENTS[key];
1195
+ console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
1196
+ });
1197
+ console.log('');
1198
+ console.log(' all) Install for all agents');
1199
+ console.log('');
1200
+
1201
+ let selectedAgents = [];
1202
+
1203
+ // Loop until valid input is provided
1204
+ while (selectedAgents.length === 0) {
1205
+ const answer = await question('Your selection: ');
1206
+
1207
+ // Handle empty input - reprompt
1208
+ if (!answer || !answer.trim()) {
1209
+ console.log(' Please enter at least one agent number or "all".');
1210
+ continue;
1211
+ }
1212
+
1213
+ if (answer.toLowerCase() === 'all') {
1214
+ selectedAgents = agentKeys;
1215
+ } else {
1216
+ const nums = answer.split(/[\s,]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
1217
+
1218
+ // Validate numbers are in range
1219
+ const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
1220
+ const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
1221
+
1222
+ if (invalidNums.length > 0) {
1223
+ console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1224
+ }
1225
+
1226
+ // Deduplicate selected agents using Set
1227
+ selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1228
+ }
1229
+
1230
+ if (selectedAgents.length === 0) {
1231
+ console.log(' No valid agents selected. Please try again.');
1232
+ }
1233
+ }
1234
+
1235
+ console.log('');
1236
+ console.log('Installing Forge workflow...');
1237
+
1238
+ // Copy AGENTS.md unless skipped
1239
+ if (skipFiles.agentsMd) {
1240
+ console.log(' Skipped: AGENTS.md (keeping existing)');
1241
+ } else {
1242
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1243
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
1244
+ console.log(' Created: AGENTS.md (universal standard)');
1245
+ }
1246
+ }
1247
+
1248
+ // Load Claude commands if needed
1249
+ let claudeCommands = {};
1250
+ if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
1251
+ // First ensure Claude is set up
1252
+ if (selectedAgents.includes('claude')) {
1253
+ setupAgent('claude', null, skipFiles);
1254
+ }
1255
+ // Then load the commands (from existing or newly created)
1256
+ COMMANDS.forEach(cmd => {
1257
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
1258
+ const content = readFile(cmdPath);
1259
+ if (content) {
1260
+ claudeCommands[`${cmd}.md`] = content;
1261
+ }
1262
+ });
1263
+ }
1264
+
1265
+ // Setup each selected agent with progress indication
1266
+ const totalAgents = selectedAgents.length;
1267
+ selectedAgents.forEach((agentKey, index) => {
1268
+ const agent = AGENTS[agentKey];
1269
+ console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
1270
+ if (agentKey !== 'claude') { // Claude already done above
1271
+ setupAgent(agentKey, claudeCommands, skipFiles);
1272
+ }
1273
+ });
1274
+
1275
+ // Agent installation success
1276
+ console.log('');
1277
+ console.log('Agent configuration complete!');
1278
+ console.log('');
1279
+ console.log('Installed for:');
1280
+ selectedAgents.forEach(key => {
1281
+ const agent = AGENTS[key];
1282
+ console.log(` * ${agent.name}`);
1283
+ });
1284
+
1285
+ // =============================================
1286
+ // STEP 2: External Services Configuration
1287
+ // =============================================
1288
+ console.log('');
1289
+ console.log('STEP 2: External Services (Optional)');
1290
+ console.log('=====================================');
1291
+
1292
+ await configureExternalServices(rl, question, selectedAgents, projectStatus);
1293
+
1294
+ rl.close();
1295
+
1296
+ // =============================================
1297
+ // Final Summary
1298
+ // =============================================
1299
+ console.log('');
1300
+ console.log('==============================================');
1301
+ console.log(' Forge v1.1.0 Setup Complete!');
1302
+ console.log('==============================================');
1303
+ console.log('');
1304
+ console.log('What\'s installed:');
1305
+ console.log(' - AGENTS.md (universal instructions)');
1306
+ console.log(' - docs/WORKFLOW.md (full workflow guide)');
1307
+ console.log(' - docs/research/TEMPLATE.md (research template)');
1308
+ console.log(' - docs/planning/PROGRESS.md (progress tracking)');
1309
+ selectedAgents.forEach(key => {
1310
+ const agent = AGENTS[key];
1311
+ if (agent.linkFile) {
1312
+ console.log(` - ${agent.linkFile} (${agent.name})`);
1313
+ }
1314
+ if (agent.hasCommands) {
1315
+ console.log(` - .claude/commands/ (9 workflow commands)`);
1316
+ }
1317
+ if (agent.hasSkill) {
1318
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1319
+ if (skillDir) {
1320
+ console.log(` - ${skillDir}/SKILL.md`);
1321
+ }
1322
+ }
1323
+ });
1324
+ console.log('');
1325
+ console.log('Next steps:');
1326
+ console.log(` 1. Install optional tools:`);
1327
+ console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
1328
+ console.log(` ${PKG_MANAGER} install -g @fission-ai/openspec`);
1329
+ console.log(' 2. Start with: /status');
1330
+ console.log(' 3. Read the guide: docs/WORKFLOW.md');
1331
+ console.log('');
1332
+ console.log(`Package manager detected: ${PKG_MANAGER}`);
1333
+ console.log('');
1334
+ console.log('Happy shipping!');
1335
+ console.log('');
1336
+ }
1337
+
1338
+ // Parse CLI flags
1339
+ function parseFlags() {
1340
+ const flags = {
1341
+ quick: false,
1342
+ skipExternal: false,
1343
+ agents: null,
1344
+ all: false,
1345
+ help: false
1346
+ };
1347
+
1348
+ for (let i = 0; i < args.length; i++) {
1349
+ const arg = args[i];
1350
+
1351
+ if (arg === '--quick' || arg === '-q') {
1352
+ flags.quick = true;
1353
+ } else if (arg === '--skip-external' || arg === '--skip-services') {
1354
+ flags.skipExternal = true;
1355
+ } else if (arg === '--all') {
1356
+ flags.all = true;
1357
+ } else if (arg === '--help' || arg === '-h') {
1358
+ flags.help = true;
1359
+ } else if (arg === '--agents') {
1360
+ // --agents claude cursor format
1361
+ const agentList = [];
1362
+ for (let j = i + 1; j < args.length; j++) {
1363
+ if (args[j].startsWith('-')) break;
1364
+ agentList.push(args[j]);
1365
+ }
1366
+ if (agentList.length > 0) {
1367
+ flags.agents = agentList.join(',');
1368
+ }
1369
+ } else if (arg.startsWith('--agents=')) {
1370
+ // --agents=claude,cursor format
1371
+ flags.agents = arg.replace('--agents=', '');
1372
+ }
1373
+ }
1374
+
1375
+ return flags;
1376
+ }
1377
+
1378
+ // Validate agent names
1379
+ function validateAgents(agentList) {
1380
+ const requested = agentList.split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
1381
+ const valid = requested.filter(a => AGENTS[a]);
1382
+ const invalid = requested.filter(a => !AGENTS[a]);
1383
+
1384
+ if (invalid.length > 0) {
1385
+ console.log(` Warning: Unknown agents ignored: ${invalid.join(', ')}`);
1386
+ console.log(` Available agents: ${Object.keys(AGENTS).join(', ')}`);
1387
+ }
1388
+
1389
+ return valid;
1390
+ }
1391
+
1392
+ // Show help text
1393
+ function showHelp() {
1394
+ showBanner();
1395
+ console.log('');
1396
+ console.log('Usage:');
1397
+ console.log(' npx forge setup [options] Interactive agent configuration');
1398
+ console.log(' npx forge Minimal install (AGENTS.md + docs)');
1399
+ console.log('');
1400
+ console.log('Options:');
1401
+ console.log(' --quick, -q Use all defaults, minimal prompts');
1402
+ console.log(' Auto-selects: all agents, GitHub Code Quality, ESLint');
1403
+ console.log(' --skip-external Skip external services configuration');
1404
+ console.log(' --agents <list> Specify agents directly (skip selection prompt)');
1405
+ console.log(' Accepts: --agents claude cursor');
1406
+ console.log(' --agents=claude,cursor');
1407
+ console.log(' --all Install for all available agents');
1408
+ console.log(' --help, -h Show this help message');
1409
+ console.log('');
1410
+ console.log('Available agents:');
1411
+ Object.keys(AGENTS).forEach(key => {
1412
+ const agent = AGENTS[key];
1413
+ console.log(` ${key.padEnd(14)} ${agent.name.padEnd(20)} ${agent.description}`);
1414
+ });
1415
+ console.log('');
1416
+ console.log('Examples:');
1417
+ console.log(' npx forge setup # Interactive setup');
1418
+ console.log(' npx forge setup --quick # All defaults, no prompts');
1419
+ console.log(' npx forge setup --agents claude cursor # Just these agents');
1420
+ console.log(' npx forge setup --agents=claude,cursor # Same, different syntax');
1421
+ console.log(' npx forge setup --skip-external # No service configuration');
1422
+ console.log(' npx forge setup --agents claude --quick # Quick + specific agent');
1423
+ console.log(' npx forge setup --all --skip-external # All agents, no services');
1424
+ console.log('');
1425
+ console.log('Also works with bun:');
1426
+ console.log(' bunx forge setup --quick');
1427
+ console.log('');
1428
+ }
1429
+
1430
+ // Quick setup with defaults
1431
+ async function quickSetup(selectedAgents, skipExternal) {
1432
+ showBanner('Quick Setup');
1433
+ console.log('');
1434
+ console.log('Quick mode: Using defaults...');
1435
+ console.log('');
1436
+
1437
+ // Check prerequisites
1438
+ checkPrerequisites();
1439
+ console.log('');
1440
+
1441
+ // Copy AGENTS.md
1442
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1443
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
1444
+ console.log(' Created: AGENTS.md (universal standard)');
1445
+ }
1446
+
1447
+ // Load Claude commands if needed
1448
+ let claudeCommands = {};
1449
+ if (selectedAgents.includes('claude')) {
1450
+ setupAgent('claude', null);
1451
+ }
1452
+
1453
+ if (selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
1454
+ COMMANDS.forEach(cmd => {
1455
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
1456
+ const content = readFile(cmdPath);
1457
+ if (content) {
1458
+ claudeCommands[`${cmd}.md`] = content;
1459
+ }
1460
+ });
1461
+ }
1462
+
1463
+ // Setup each selected agent
1464
+ const totalAgents = selectedAgents.length;
1465
+ selectedAgents.forEach((agentKey, index) => {
1466
+ const agent = AGENTS[agentKey];
1467
+ console.log(`[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
1468
+ if (agentKey !== 'claude') {
1469
+ setupAgent(agentKey, claudeCommands);
1470
+ }
1471
+ });
1472
+
1473
+ console.log('');
1474
+ console.log('Agent configuration complete!');
1475
+ console.log('');
1476
+ console.log('Installed for:');
1477
+ selectedAgents.forEach(key => {
1478
+ const agent = AGENTS[key];
1479
+ console.log(` * ${agent.name}`);
1480
+ });
1481
+
1482
+ // Configure external services with defaults (unless skipped)
1483
+ if (!skipExternal) {
1484
+ console.log('');
1485
+ console.log('Configuring default services...');
1486
+ console.log('');
1487
+
1488
+ const tokens = {
1489
+ CODE_REVIEW_TOOL: 'github-code-quality',
1490
+ CODE_QUALITY_TOOL: 'eslint',
1491
+ PKG_MANAGER: PKG_MANAGER
1492
+ };
1493
+
1494
+ writeEnvTokens(tokens);
1495
+
1496
+ console.log(' * Code Review: GitHub Code Quality (FREE)');
1497
+ console.log(' * Code Quality: ESLint (built-in)');
1498
+ console.log('');
1499
+ console.log('Configuration saved to .env.local');
1500
+ } else {
1501
+ console.log('');
1502
+ console.log('Skipping external services configuration...');
1503
+ }
1504
+
1505
+ // Final summary
1506
+ console.log('');
1507
+ console.log('==============================================');
1508
+ console.log(' Forge v1.1.0 Quick Setup Complete!');
1509
+ console.log('==============================================');
1510
+ console.log('');
1511
+ console.log('Next steps:');
1512
+ console.log(' 1. Start with: /status');
1513
+ console.log(' 2. Read the guide: docs/WORKFLOW.md');
1514
+ console.log('');
1515
+ console.log('Happy shipping!');
1516
+ console.log('');
1517
+ }
1518
+
1519
+ // Interactive setup with flag support
1520
+ async function interactiveSetupWithFlags(flags) {
1521
+ const rl = readline.createInterface({
1522
+ input: process.stdin,
1523
+ output: process.stdout
1524
+ });
1525
+
1526
+ // Handle Ctrl+C gracefully
1527
+ rl.on('close', () => {
1528
+ console.log('\n\nSetup cancelled.');
1529
+ process.exit(0);
1530
+ });
1531
+
1532
+ // Handle input errors
1533
+ rl.on('error', (err) => {
1534
+ console.error('Input error:', err.message);
1535
+ process.exit(1);
1536
+ });
1537
+
1538
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
1539
+
1540
+ showBanner('Agent Configuration');
1541
+
1542
+ // Check prerequisites first
1543
+ checkPrerequisites();
1544
+ console.log('');
1545
+
1546
+ // =============================================
1547
+ // PROJECT DETECTION
1548
+ // =============================================
1549
+ const projectStatus = detectProjectStatus();
1550
+
1551
+ if (projectStatus.type !== 'fresh') {
1552
+ console.log('==============================================');
1553
+ console.log(' Existing Installation Detected');
1554
+ console.log('==============================================');
1555
+ console.log('');
1556
+
1557
+ if (projectStatus.type === 'upgrade') {
1558
+ console.log('Found existing Forge installation:');
1559
+ } else {
1560
+ console.log('Found partial installation:');
1561
+ }
1562
+
1563
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
1564
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
1565
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
1566
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
1567
+ console.log('');
1568
+ }
1569
+
1570
+ // Track which files to skip based on user choices
1571
+ const skipFiles = {
1572
+ agentsMd: false,
1573
+ claudeCommands: false
1574
+ };
1575
+
1576
+ // Ask about overwriting AGENTS.md if it exists
1577
+ if (projectStatus.hasAgentsMd) {
1578
+ const overwriteAgents = await question('Found existing AGENTS.md. Overwrite? (y/n) [n]: ');
1579
+ if (overwriteAgents.toLowerCase() !== 'y' && overwriteAgents.toLowerCase() !== 'yes') {
1580
+ skipFiles.agentsMd = true;
1581
+ console.log(' Keeping existing AGENTS.md');
1582
+ } else {
1583
+ console.log(' Will overwrite AGENTS.md');
1584
+ }
1585
+ }
1586
+
1587
+ // Ask about overwriting .claude/commands/ if it exists
1588
+ if (projectStatus.hasClaudeCommands) {
1589
+ const overwriteCommands = await question('Found existing .claude/commands/. Overwrite? (y/n) [n]: ');
1590
+ if (overwriteCommands.toLowerCase() !== 'y' && overwriteCommands.toLowerCase() !== 'yes') {
1591
+ skipFiles.claudeCommands = true;
1592
+ console.log(' Keeping existing .claude/commands/');
1593
+ } else {
1594
+ console.log(' Will overwrite .claude/commands/');
1595
+ }
1596
+ }
1597
+
1598
+ if (projectStatus.type !== 'fresh') {
1599
+ console.log('');
1600
+ }
1601
+
1602
+ // =============================================
1603
+ // STEP 1: Agent Selection
1604
+ // =============================================
1605
+ console.log('STEP 1: Select AI Coding Agents');
1606
+ console.log('================================');
1607
+ console.log('');
1608
+ console.log('Which AI coding agents do you use?');
1609
+ console.log('(Enter numbers separated by spaces, or "all")');
1610
+ console.log('');
1611
+
1612
+ const agentKeys = Object.keys(AGENTS);
1613
+ agentKeys.forEach((key, index) => {
1614
+ const agent = AGENTS[key];
1615
+ console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
1616
+ });
1617
+ console.log('');
1618
+ console.log(' all) Install for all agents');
1619
+ console.log('');
1620
+
1621
+ let selectedAgents = [];
1622
+
1623
+ // Loop until valid input is provided
1624
+ while (selectedAgents.length === 0) {
1625
+ const answer = await question('Your selection: ');
1626
+
1627
+ // Handle empty input - reprompt
1628
+ if (!answer || !answer.trim()) {
1629
+ console.log(' Please enter at least one agent number or "all".');
1630
+ continue;
1631
+ }
1632
+
1633
+ if (answer.toLowerCase() === 'all') {
1634
+ selectedAgents = agentKeys;
1635
+ } else {
1636
+ const nums = answer.split(/[\s,]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
1637
+
1638
+ // Validate numbers are in range
1639
+ const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
1640
+ const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
1641
+
1642
+ if (invalidNums.length > 0) {
1643
+ console.log(` Warning: Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1644
+ }
1645
+
1646
+ // Deduplicate selected agents using Set
1647
+ selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1648
+ }
1649
+
1650
+ if (selectedAgents.length === 0) {
1651
+ console.log(' No valid agents selected. Please try again.');
1652
+ }
1653
+ }
1654
+
1655
+ console.log('');
1656
+ console.log('Installing Forge workflow...');
1657
+
1658
+ // Copy AGENTS.md unless skipped
1659
+ if (skipFiles.agentsMd) {
1660
+ console.log(' Skipped: AGENTS.md (keeping existing)');
1661
+ } else {
1662
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1663
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
1664
+ console.log(' Created: AGENTS.md (universal standard)');
1665
+ }
1666
+ }
1667
+
1668
+ // Load Claude commands if needed
1669
+ let claudeCommands = {};
1670
+ if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
1671
+ // First ensure Claude is set up
1672
+ if (selectedAgents.includes('claude')) {
1673
+ setupAgent('claude', null, skipFiles);
1674
+ }
1675
+ // Then load the commands (from existing or newly created)
1676
+ COMMANDS.forEach(cmd => {
1677
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
1678
+ const content = readFile(cmdPath);
1679
+ if (content) {
1680
+ claudeCommands[`${cmd}.md`] = content;
1681
+ }
1682
+ });
1683
+ }
1684
+
1685
+ // Setup each selected agent with progress indication
1686
+ const totalAgents = selectedAgents.length;
1687
+ selectedAgents.forEach((agentKey, index) => {
1688
+ const agent = AGENTS[agentKey];
1689
+ console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
1690
+ if (agentKey !== 'claude') { // Claude already done above
1691
+ setupAgent(agentKey, claudeCommands, skipFiles);
1692
+ }
1693
+ });
1694
+
1695
+ // Agent installation success
1696
+ console.log('');
1697
+ console.log('Agent configuration complete!');
1698
+ console.log('');
1699
+ console.log('Installed for:');
1700
+ selectedAgents.forEach(key => {
1701
+ const agent = AGENTS[key];
1702
+ console.log(` * ${agent.name}`);
1703
+ });
1704
+
1705
+ // =============================================
1706
+ // STEP 2: External Services Configuration
1707
+ // =============================================
1708
+ if (!flags.skipExternal) {
1709
+ console.log('');
1710
+ console.log('STEP 2: External Services (Optional)');
1711
+ console.log('=====================================');
1712
+
1713
+ await configureExternalServices(rl, question, selectedAgents, projectStatus);
1714
+ } else {
1715
+ console.log('');
1716
+ console.log('Skipping external services configuration...');
1717
+ }
1718
+
1719
+ rl.close();
1720
+
1721
+ // =============================================
1722
+ // Final Summary
1723
+ // =============================================
1724
+ console.log('');
1725
+ console.log('==============================================');
1726
+ console.log(' Forge v1.1.0 Setup Complete!');
1727
+ console.log('==============================================');
1728
+ console.log('');
1729
+ console.log('What\'s installed:');
1730
+ console.log(' - AGENTS.md (universal instructions)');
1731
+ console.log(' - docs/WORKFLOW.md (full workflow guide)');
1732
+ console.log(' - docs/research/TEMPLATE.md (research template)');
1733
+ console.log(' - docs/planning/PROGRESS.md (progress tracking)');
1734
+ selectedAgents.forEach(key => {
1735
+ const agent = AGENTS[key];
1736
+ if (agent.linkFile) {
1737
+ console.log(` - ${agent.linkFile} (${agent.name})`);
1738
+ }
1739
+ if (agent.hasCommands) {
1740
+ console.log(` - .claude/commands/ (9 workflow commands)`);
1741
+ }
1742
+ if (agent.hasSkill) {
1743
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1744
+ if (skillDir) {
1745
+ console.log(` - ${skillDir}/SKILL.md`);
1746
+ }
1747
+ }
1748
+ });
1749
+ console.log('');
1750
+ console.log('Next steps:');
1751
+ console.log(` 1. Install optional tools:`);
1752
+ console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
1753
+ console.log(` ${PKG_MANAGER} install -g @fission-ai/openspec`);
1754
+ console.log(' 2. Start with: /status');
1755
+ console.log(' 3. Read the guide: docs/WORKFLOW.md');
1756
+ console.log('');
1757
+ console.log(`Package manager detected: ${PKG_MANAGER}`);
1758
+ console.log('');
1759
+ console.log('Happy shipping!');
1760
+ console.log('');
1761
+ }
1762
+
1763
+ // Main
1764
+ async function main() {
1765
+ const command = args[0];
1766
+ const flags = parseFlags();
1767
+
1768
+ // Show help
1769
+ if (flags.help) {
1770
+ showHelp();
1771
+ return;
1772
+ }
1773
+
1774
+ if (command === 'setup') {
1775
+ // Determine agents to install
1776
+ let selectedAgents = [];
1777
+
1778
+ if (flags.all) {
1779
+ selectedAgents = Object.keys(AGENTS);
1780
+ } else if (flags.agents) {
1781
+ selectedAgents = validateAgents(flags.agents);
1782
+ if (selectedAgents.length === 0) {
1783
+ console.log('No valid agents specified.');
1784
+ console.log('Available agents:', Object.keys(AGENTS).join(', '));
1785
+ process.exit(1);
1786
+ }
1787
+ }
1788
+
1789
+ // Quick mode
1790
+ if (flags.quick) {
1791
+ // If no agents specified in quick mode, use all
1792
+ if (selectedAgents.length === 0) {
1793
+ selectedAgents = Object.keys(AGENTS);
1794
+ }
1795
+ await quickSetup(selectedAgents, flags.skipExternal);
1796
+ return;
1797
+ }
1798
+
1799
+ // Agents specified via flag (non-quick mode)
1800
+ if (selectedAgents.length > 0) {
1801
+ showBanner('Installing for specified agents...');
1802
+ console.log('');
1803
+
1804
+ // Check prerequisites
1805
+ checkPrerequisites();
1806
+ console.log('');
1807
+
1808
+ // Copy AGENTS.md
1809
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1810
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
1811
+ console.log(' Created: AGENTS.md (universal standard)');
1812
+ }
1813
+
1814
+ // Load Claude commands if needed
1815
+ let claudeCommands = {};
1816
+ if (selectedAgents.includes('claude')) {
1817
+ setupAgent('claude', null);
1818
+ }
1819
+
1820
+ if (selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
1821
+ COMMANDS.forEach(cmd => {
1822
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
1823
+ const content = readFile(cmdPath);
1824
+ if (content) {
1825
+ claudeCommands[`${cmd}.md`] = content;
1826
+ }
1827
+ });
1828
+ }
1829
+
1830
+ // Setup agents
1831
+ selectedAgents.forEach(agentKey => {
1832
+ if (agentKey !== 'claude') {
1833
+ setupAgent(agentKey, claudeCommands);
1834
+ }
1835
+ });
1836
+
1837
+ console.log('');
1838
+ console.log('Agent configuration complete!');
1839
+
1840
+ // External services (unless skipped)
1841
+ if (!flags.skipExternal) {
1842
+ const rl = readline.createInterface({
1843
+ input: process.stdin,
1844
+ output: process.stdout
1845
+ });
1846
+ rl.on('close', () => {
1847
+ console.log('\n\nSetup cancelled.');
1848
+ process.exit(0);
1849
+ });
1850
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
1851
+ await configureExternalServices(rl, question, selectedAgents);
1852
+ rl.close();
1853
+ } else {
1854
+ console.log('');
1855
+ console.log('Skipping external services configuration...');
1856
+ }
1857
+
1858
+ console.log('');
1859
+ console.log('Done! Get started with: /status');
1860
+ return;
1861
+ }
1862
+
1863
+ // Interactive setup (skip-external still applies)
1864
+ await interactiveSetupWithFlags(flags);
1865
+ } else {
1866
+ // Default: minimal install (postinstall behavior)
1867
+ minimalInstall();
1868
+ }
1869
+ }
1870
+
1871
+ main().catch(console.error);