forge-workflow 0.0.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.
Files changed (105) hide show
  1. package/.claude/commands/dev.md +314 -0
  2. package/.claude/commands/plan.md +389 -0
  3. package/.claude/commands/premerge.md +179 -0
  4. package/.claude/commands/research.md +42 -0
  5. package/.claude/commands/review.md +442 -0
  6. package/.claude/commands/rollback.md +721 -0
  7. package/.claude/commands/ship.md +134 -0
  8. package/.claude/commands/sonarcloud.md +152 -0
  9. package/.claude/commands/status.md +77 -0
  10. package/.claude/commands/validate.md +237 -0
  11. package/.claude/commands/verify.md +221 -0
  12. package/.claude/rules/greptile-review-process.md +285 -0
  13. package/.claude/rules/workflow.md +105 -0
  14. package/.claude/scripts/greptile-resolve.sh +526 -0
  15. package/.claude/scripts/load-env.sh +32 -0
  16. package/.forge/hooks/check-tdd.js +240 -0
  17. package/.github/PLUGIN_TEMPLATE.json +32 -0
  18. package/.mcp.json.example +12 -0
  19. package/AGENTS.md +169 -0
  20. package/CLAUDE.md +99 -0
  21. package/LICENSE +21 -0
  22. package/README.md +414 -0
  23. package/bin/forge-cmd.js +313 -0
  24. package/bin/forge-validate.js +303 -0
  25. package/bin/forge.js +4228 -0
  26. package/docs/AGENT_INSTALL_PROMPT.md +342 -0
  27. package/docs/ENHANCED_ONBOARDING.md +602 -0
  28. package/docs/EXAMPLES.md +482 -0
  29. package/docs/GREPTILE_SETUP.md +400 -0
  30. package/docs/MANUAL_REVIEW_GUIDE.md +106 -0
  31. package/docs/ROADMAP.md +359 -0
  32. package/docs/SETUP.md +632 -0
  33. package/docs/TOOLCHAIN.md +849 -0
  34. package/docs/VALIDATION.md +363 -0
  35. package/docs/WORKFLOW.md +400 -0
  36. package/docs/planning/PROGRESS.md +396 -0
  37. package/docs/plans/.gitkeep +0 -0
  38. package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +21 -0
  39. package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +362 -0
  40. package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +343 -0
  41. package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +26 -0
  42. package/docs/plans/2026-03-02-superpowers-gaps-design.md +239 -0
  43. package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +260 -0
  44. package/docs/plans/2026-03-04-agent-command-parity-design.md +163 -0
  45. package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +7 -0
  46. package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +165 -0
  47. package/docs/plans/2026-03-05-forge-uto-decisions.md +6 -0
  48. package/docs/plans/2026-03-05-forge-uto-design.md +116 -0
  49. package/docs/plans/2026-03-05-forge-uto-tasks.md +244 -0
  50. package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +52 -0
  51. package/docs/plans/2026-03-10-command-creator-and-eval-design.md +350 -0
  52. package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +426 -0
  53. package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +8 -0
  54. package/docs/plans/2026-03-10-stale-workflow-refs-design.md +80 -0
  55. package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +90 -0
  56. package/docs/plans/2026-03-14-beads-plan-context-decisions.md +9 -0
  57. package/docs/plans/2026-03-14-beads-plan-context-design.md +171 -0
  58. package/docs/plans/2026-03-14-beads-plan-context-tasks.md +160 -0
  59. package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +33 -0
  60. package/docs/plans/2026-03-14-skill-eval-loop-design.md +118 -0
  61. package/docs/plans/2026-03-14-skill-eval-loop-results.md +78 -0
  62. package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +160 -0
  63. package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +11 -0
  64. package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +145 -0
  65. package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +211 -0
  66. package/docs/research/TEMPLATE.md +292 -0
  67. package/docs/research/advanced-testing.md +297 -0
  68. package/docs/research/agent-permissions.md +167 -0
  69. package/docs/research/dependency-chain.md +328 -0
  70. package/docs/research/forge-workflow-v2.md +550 -0
  71. package/docs/research/plugin-architecture.md +772 -0
  72. package/docs/research/pr4-cli-automation.md +326 -0
  73. package/docs/research/premerge-verify-restructure.md +205 -0
  74. package/docs/research/skills-restructure.md +508 -0
  75. package/docs/research/sonarcloud-perfection-plan.md +166 -0
  76. package/docs/research/sonarcloud-quality-gate.md +184 -0
  77. package/docs/research/superpowers-integration.md +403 -0
  78. package/docs/research/superpowers.md +319 -0
  79. package/docs/research/test-environment.md +519 -0
  80. package/install.sh +1062 -0
  81. package/lefthook.yml +39 -0
  82. package/lib/agents/README.md +198 -0
  83. package/lib/agents/claude.plugin.json +28 -0
  84. package/lib/agents/cline.plugin.json +22 -0
  85. package/lib/agents/codex.plugin.json +19 -0
  86. package/lib/agents/copilot.plugin.json +24 -0
  87. package/lib/agents/cursor.plugin.json +25 -0
  88. package/lib/agents/kilocode.plugin.json +22 -0
  89. package/lib/agents/opencode.plugin.json +20 -0
  90. package/lib/agents/roo.plugin.json +23 -0
  91. package/lib/agents-config.js +2112 -0
  92. package/lib/commands/dev.js +513 -0
  93. package/lib/commands/plan.js +696 -0
  94. package/lib/commands/recommend.js +119 -0
  95. package/lib/commands/ship.js +377 -0
  96. package/lib/commands/status.js +378 -0
  97. package/lib/commands/validate.js +602 -0
  98. package/lib/context-merge.js +359 -0
  99. package/lib/plugin-catalog.js +360 -0
  100. package/lib/plugin-manager.js +166 -0
  101. package/lib/plugin-recommender.js +141 -0
  102. package/lib/project-discovery.js +491 -0
  103. package/lib/setup.js +118 -0
  104. package/lib/workflow-profiles.js +203 -0
  105. package/package.json +115 -0
package/bin/forge.js ADDED
@@ -0,0 +1,4228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Forge - Universal AI Agent Workflow
5
+ * https://github.com/harshanandak/forge
6
+ *
7
+ * Version is automatically read from package.json
8
+ *
9
+ * Usage:
10
+ * bun install forge-workflow -> Minimal install (AGENTS.md + docs)
11
+ * bunx forge setup -> Interactive agent configuration
12
+ * bunx forge setup --all -> Install for all agents
13
+ * bunx forge setup --agents claude,cursor
14
+ *
15
+ * CLI Flags:
16
+ * --path, -p <dir> Target project directory (creates if needed)
17
+ * --quick, -q Use all defaults, minimal prompts
18
+ * --skip-external Skip external services configuration
19
+ * --agents <list> Specify agents (--agents claude cursor OR --agents=claude,cursor)
20
+ * --all Install for all available agents
21
+ * --merge <mode> Merge strategy for existing files (smart|preserve|replace)
22
+ * --type <type> Workflow profile (critical|standard|simple|hotfix|docs|refactor)
23
+ * --interview Force context interview (gather project info)
24
+ * --help, -h Show help message
25
+ *
26
+ * Examples:
27
+ * npx forge setup --quick # All defaults, no prompts
28
+ * npx forge setup -p ./my-project # Setup in specific directory
29
+ * npx forge setup --agents claude cursor # Just these agents
30
+ * npx forge setup --skip-external # No service prompts
31
+ * npx forge setup --agents claude --quick # Quick + specific agent
32
+ *
33
+ * Also works with bun:
34
+ * bun add forge-workflow
35
+ * bunx forge setup --quick
36
+ */
37
+
38
+ const fs = require('node:fs');
39
+ const path = require('node:path');
40
+ const readline = require('node:readline');
41
+ const { execSync, execFileSync, spawnSync } = require('node:child_process');
42
+
43
+ // Get version from package.json (single source of truth)
44
+ const packageDir = path.dirname(__dirname);
45
+ const packageJson = require(path.join(packageDir, 'package.json'));
46
+ const VERSION = packageJson.version;
47
+
48
+ // Load PluginManager for discoverable agent architecture
49
+ const PluginManager = require('../lib/plugin-manager');
50
+
51
+ // Load enhanced onboarding modules
52
+ const contextMerge = require(path.join(packageDir, 'lib', 'context-merge'));
53
+ const projectDiscovery = require(path.join(packageDir, 'lib', 'project-discovery'));
54
+ // workflowProfiles is loaded but not currently used in the setup flow
55
+ // const _workflowProfiles = require(path.join(packageDir, 'lib', 'workflow-profiles'));
56
+
57
+ // Get the project root (let allows reassignment after --path flag handling)
58
+ let projectRoot = process.env.INIT_CWD || process.cwd();
59
+ const args = process.argv.slice(2);
60
+
61
+ // Detected package manager
62
+ let PKG_MANAGER = 'npm';
63
+
64
+ /**
65
+ * Securely execute a command with PATH validation
66
+ * Mitigates SonarCloud S4036: Ensures executables are from trusted locations
67
+ * @param {string} command - The command to execute
68
+ * @param {string[]} args - Command arguments
69
+ * @param {object} options - execFileSync options
70
+ */
71
+ function secureExecFileSync(command, args = [], options = {}) {
72
+ try {
73
+ // Resolve command's full path to validate it's in a trusted location
74
+ const isWindows = process.platform === 'win32';
75
+ const pathResolver = isWindows ? 'where.exe' : 'which';
76
+
77
+ const result = spawnSync(pathResolver, [command], {
78
+ encoding: 'utf8',
79
+ stdio: ['ignore', 'pipe', 'ignore']
80
+ });
81
+
82
+ if (result.status === 0 && result.stdout) {
83
+ // Command found - use resolved path for execution
84
+ // Handle both CRLF (Windows) and LF (Unix) line endings
85
+ const resolvedPath = result.stdout.trim().split(/\r?\n/)[0].trim();
86
+ return execFileSync(resolvedPath, args, options);
87
+ }
88
+ } catch (_err) { // NOSONAR - S2486: Intentionally ignored; falls back to direct command execution below
89
+ }
90
+
91
+ // Fallback: execute with command name (maintains compatibility)
92
+ // This is safe for our use case as we only execute known, hardcoded commands
93
+ return execFileSync(command, args, options);
94
+ }
95
+
96
+ /**
97
+ * Load agent definitions from plugin architecture
98
+ * Maintains backwards compatibility with original AGENTS object structure
99
+ */
100
+ function loadAgentsFromPlugins() {
101
+ const pluginManager = new PluginManager();
102
+ const agents = {};
103
+
104
+ pluginManager.getAllPlugins().forEach((plugin, id) => {
105
+ // Convert plugin structure to AGENTS structure for backwards compatibility
106
+ agents[id] = {
107
+ name: plugin.name,
108
+ description: plugin.description || '',
109
+ dirs: Object.values(plugin.directories || {}),
110
+ hasCommands: plugin.capabilities?.commands || plugin.setup?.copyCommands || false,
111
+ hasSkill: plugin.capabilities?.skills || plugin.setup?.createSkill || false,
112
+ linkFile: plugin.files?.rootConfig || '',
113
+ customSetup: plugin.setup?.customSetup || '',
114
+ needsConversion: plugin.setup?.needsConversion || false,
115
+ copyCommands: plugin.setup?.copyCommands || false,
116
+ promptFormat: plugin.setup?.promptFormat || false
117
+ };
118
+ });
119
+
120
+ return agents;
121
+ }
122
+
123
+ // Agent definitions - loaded from plugin system
124
+ const AGENTS = loadAgentsFromPlugins();
125
+
126
+ // SECURITY: Freeze AGENTS to prevent runtime manipulation
127
+ Object.freeze(AGENTS);
128
+ Object.values(AGENTS).forEach(agent => Object.freeze(agent));
129
+
130
+ /**
131
+ * Validate user input against security patterns
132
+ * Prevents shell injection, path traversal, and unicode attacks
133
+ * @param {string} input - User input to validate
134
+ * @param {string} type - Input type: 'path', 'agent', 'hash'
135
+ * @returns {{valid: boolean, error?: string}}
136
+ */
137
+ // Helper: Run common security checks on input - extracted to reduce cognitive complexity
138
+ function validateCommonSecurity(input) {
139
+ // Shell injection check - common shell metacharacters
140
+ if (/[;|&$`()<>\r\n]/.test(input)) {
141
+ return { valid: false, error: 'Invalid characters detected (shell metacharacters)' };
142
+ }
143
+
144
+ // URL encoding check - prevent encoded path traversal
145
+ if (/%2[eE]|%2[fF]|%5[cC]/.test(input)) {
146
+ return { valid: false, error: 'URL-encoded characters not allowed' };
147
+ }
148
+
149
+ // ASCII-only check - prevent unicode attacks
150
+ if (!/^[\x20-\x7E]+$/.test(input)) {
151
+ return { valid: false, error: 'Only ASCII printable characters allowed' };
152
+ }
153
+
154
+ return { valid: true }; // No security issues found
155
+ }
156
+
157
+ function validateUserInput(input, type) {
158
+ // Common security checks first
159
+ const securityResult = validateCommonSecurity(input);
160
+ if (!securityResult.valid) return securityResult;
161
+
162
+ // Type-specific validation - delegated to helpers
163
+ switch (type) {
164
+ case 'path':
165
+ return validatePathInput(input);
166
+ case 'directory_path':
167
+ return validateDirectoryPathInput(input);
168
+ case 'agent':
169
+ return validateAgentInput(input);
170
+ case 'hash':
171
+ return validateHashInput(input);
172
+ default:
173
+ return { valid: true };
174
+ }
175
+ }
176
+
177
+ // Helper: Validate 'path' type input - extracted to reduce cognitive complexity
178
+ function validatePathInput(input) {
179
+ const resolved = path.resolve(projectRoot, input);
180
+ if (!resolved.startsWith(path.resolve(projectRoot))) {
181
+ return { valid: false, error: 'Path outside project root' };
182
+ }
183
+ return { valid: true };
184
+ }
185
+
186
+ // Helper: Validate 'directory_path' type input - extracted to reduce cognitive complexity
187
+ function validateDirectoryPathInput(input) {
188
+ // Block null bytes
189
+ if (input.includes('\0')) {
190
+ return { valid: false, error: 'Null bytes not allowed in path' };
191
+ }
192
+
193
+ // Block absolute paths to sensitive system directories
194
+ const resolved = path.resolve(input);
195
+ const normalizedResolved = path.normalize(resolved).toLowerCase();
196
+
197
+ // Get platform-specific blocked paths
198
+ const blockedPaths = process.platform === 'win32'
199
+ ? [String.raw`c:\windows`, String.raw`c:\program files`, String.raw`c:\program files (x86)`]
200
+ : ['/etc', '/bin', '/sbin', '/boot', '/sys', '/proc', '/dev'];
201
+ const errorMsg = process.platform === 'win32'
202
+ ? 'Cannot target Windows system directories'
203
+ : 'Cannot target system directories';
204
+
205
+ if (blockedPaths.some(blocked => normalizedResolved.startsWith(blocked))) {
206
+ return { valid: false, error: errorMsg };
207
+ }
208
+
209
+ return { valid: true };
210
+ }
211
+
212
+ // Helper: Validate 'agent' type input - extracted to reduce cognitive complexity
213
+ function validateAgentInput(input) {
214
+ // Agent names: lowercase alphanumeric with hyphens only
215
+ if (!/^[a-z0-9-]+$/.test(input)) {
216
+ return { valid: false, error: 'Agent name must be lowercase alphanumeric with hyphens' };
217
+ }
218
+ return { valid: true };
219
+ }
220
+
221
+ // Helper: Validate 'hash' type input - extracted to reduce cognitive complexity
222
+ function validateHashInput(input) {
223
+ // Git commit hash: 4-40 hexadecimal characters
224
+ if (!/^[0-9a-f]{4,40}$/i.test(input)) {
225
+ return { valid: false, error: 'Invalid commit hash format (must be 4-40 hex chars)' };
226
+ }
227
+ return { valid: true };
228
+ }
229
+
230
+ /**
231
+ * Check write permission to a directory or file
232
+ * @param {string} filePath - Path to check
233
+ * @returns {{writable: boolean, error?: string}}
234
+ * @private - Currently unused but kept for future permission validation
235
+ */
236
+ function _checkWritePermission(filePath) {
237
+ try {
238
+ const dir = fs.statSync(filePath).isDirectory() ? filePath : path.dirname(filePath);
239
+ const testFile = path.join(dir, `.forge-write-test-${Date.now()}`);
240
+ fs.writeFileSync(testFile, 'test');
241
+ fs.unlinkSync(testFile);
242
+ return { writable: true };
243
+ } catch (err) {
244
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
245
+ const fix = process.platform === 'win32'
246
+ ? 'Run Command Prompt as Administrator'
247
+ : 'Try: sudo npx forge setup';
248
+ return { writable: false, error: `No write permission to ${filePath}. ${fix}` };
249
+ }
250
+ return { writable: false, error: err.message };
251
+ }
252
+ }
253
+
254
+ const COMMANDS = ['status', 'research', 'plan', 'dev', 'check', 'ship', 'review', 'merge', 'verify', 'rollback'];
255
+
256
+ // Code review tool options (reserved for future feature)
257
+ const _CODE_REVIEW_TOOLS = {
258
+ 'github-code-quality': {
259
+ name: 'GitHub Code Quality',
260
+ description: 'FREE, built-in - Zero setup required',
261
+ recommended: true
262
+ },
263
+ 'coderabbit': {
264
+ name: 'CodeRabbit',
265
+ description: 'FREE for open source - Install GitHub App at https://coderabbit.ai'
266
+ },
267
+ 'greptile': {
268
+ name: 'Greptile',
269
+ description: 'Paid ($99+/mo) - Enterprise code review',
270
+ requiresApiKey: true,
271
+ envVar: 'GREPTILE_API_KEY',
272
+ getKeyUrl: 'https://greptile.com'
273
+ }
274
+ };
275
+
276
+ // Code quality tool options (reserved for future feature)
277
+ const _CODE_QUALITY_TOOLS = {
278
+ 'eslint': {
279
+ name: 'ESLint only',
280
+ description: 'FREE, built-in - No external server required',
281
+ recommended: true
282
+ },
283
+ 'sonarcloud': {
284
+ name: 'SonarCloud',
285
+ description: '50k LoC free, cloud-hosted',
286
+ requiresApiKey: true,
287
+ envVars: ['SONAR_TOKEN', 'SONAR_ORGANIZATION', 'SONAR_PROJECT_KEY'],
288
+ getKeyUrl: 'https://sonarcloud.io/account/security'
289
+ },
290
+ 'sonarqube': {
291
+ name: 'SonarQube Community',
292
+ description: 'FREE, self-hosted, unlimited LoC',
293
+ envVars: ['SONARQUBE_URL', 'SONARQUBE_TOKEN'],
294
+ dockerCommand: 'docker run -d --name sonarqube -p 9000:9000 sonarqube:community'
295
+ }
296
+ };
297
+
298
+ // Helper function to safely execute commands (no user input)
299
+ function safeExec(cmd) {
300
+ try {
301
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
302
+ } catch (e) {
303
+ // Command execution failure is expected when tool is not installed or fails
304
+ // Returning null allows caller to handle missing tools gracefully
305
+ console.warn('Command execution failed:', e.message);
306
+ return null;
307
+ }
308
+ }
309
+
310
+ // Helper: Try to detect a package manager from lock file - returns true if found
311
+ function detectFromLockFile(name, lockFiles, versionPrefix) {
312
+ const found = lockFiles.some(f => fs.existsSync(path.join(projectRoot, f)));
313
+ if (!found) return false;
314
+
315
+ PKG_MANAGER = name;
316
+ const version = safeExec(`${name} --version`);
317
+ if (version) console.log(` ✓ ${versionPrefix}${version} (detected from lock file)`);
318
+ return true;
319
+ }
320
+
321
+ // Helper: Try to detect a package manager from command availability - returns true if found
322
+ function detectFromCommand(name, versionPrefix) {
323
+ const version = safeExec(`${name} --version`);
324
+ if (!version) return false;
325
+
326
+ PKG_MANAGER = name;
327
+ console.log(` ✓ ${versionPrefix}${version} (detected as package manager)`);
328
+ return true;
329
+ }
330
+
331
+ // Detect package manager from command availability and lock files
332
+ // Extracted to reduce cognitive complexity
333
+ function detectPackageManager(errors) {
334
+ // Check lock files first (most authoritative)
335
+ if (detectFromLockFile('bun', ['bun.lockb', 'bun.lock'], 'bun v')) return;
336
+ if (detectFromLockFile('pnpm', ['pnpm-lock.yaml'], 'pnpm ')) return;
337
+ if (detectFromLockFile('yarn', ['yarn.lock'], 'yarn ')) return;
338
+
339
+ // Fallback: detect from installed commands
340
+ if (detectFromCommand('bun', 'bun v')) return;
341
+ if (detectFromCommand('pnpm', 'pnpm ')) return;
342
+ if (detectFromCommand('yarn', 'yarn ')) return;
343
+ if (detectFromCommand('npm', 'npm ')) return;
344
+
345
+ // No package manager found
346
+ errors.push('npm, yarn, pnpm, or bun - Install a package manager');
347
+ }
348
+
349
+ // Prerequisite check function
350
+ function checkPrerequisites() {
351
+ const errors = [];
352
+ const warnings = [];
353
+
354
+ console.log('');
355
+ console.log('Checking prerequisites...');
356
+ console.log('');
357
+
358
+ // Check git
359
+ const gitVersion = safeExec('git --version');
360
+ if (gitVersion) {
361
+ console.log(` ✓ ${gitVersion}`);
362
+ } else {
363
+ errors.push('git - Install from https://git-scm.com');
364
+ }
365
+
366
+ // Check GitHub CLI
367
+ const ghVersion = safeExec('gh --version');
368
+ if (ghVersion) {
369
+ const firstLine = ghVersion.split('\n')[0];
370
+ console.log(` ✓ ${firstLine}`);
371
+ // Check if authenticated
372
+ const authStatus = safeExec('gh auth status');
373
+ if (!authStatus) {
374
+ warnings.push('GitHub CLI not authenticated. Run: gh auth login');
375
+ }
376
+ } else {
377
+ errors.push('gh (GitHub CLI) - Install from https://cli.github.com');
378
+ }
379
+
380
+ // Check Node.js version
381
+ const nodeVersion = Number.parseInt(process.version.slice(1).split('.')[0]);
382
+ if (nodeVersion >= 20) {
383
+ console.log(` ✓ node ${process.version}`);
384
+ } else {
385
+ errors.push(`Node.js 20+ required (current: ${process.version})`);
386
+ }
387
+
388
+ // Detect package manager
389
+ detectPackageManager(errors);
390
+
391
+ // Show errors
392
+ if (errors.length > 0) {
393
+ console.log('');
394
+ console.log('❌ Missing required tools:');
395
+ errors.forEach(err => console.log(` - ${err}`));
396
+ console.log('');
397
+ console.log('Please install missing tools and try again.');
398
+ process.exit(1);
399
+ }
400
+
401
+ // Show warnings
402
+ if (warnings.length > 0) {
403
+ console.log('');
404
+ console.log('⚠️ Warnings:');
405
+ warnings.forEach(warn => console.log(` - ${warn}`));
406
+ }
407
+
408
+ console.log('');
409
+ console.log(` Package manager: ${PKG_MANAGER}`);
410
+
411
+ return { errors, warnings };
412
+ }
413
+
414
+ // Universal SKILL.md content
415
+ const SKILL_CONTENT = `---
416
+ name: forge-workflow
417
+ description: 7-stage TDD-first workflow for feature development. Use when building features, fixing bugs, or shipping PRs.
418
+ category: Development Workflow
419
+ tags: [tdd, workflow, pr, git, testing]
420
+ tools: [Bash, Read, Write, Edit, Grep, Glob]
421
+ ---
422
+
423
+ # Forge Workflow Skill
424
+
425
+ A TDD-first workflow for AI coding agents. Ship features with confidence.
426
+
427
+ ## When to Use
428
+
429
+ Automatically invoke this skill when the user wants to:
430
+ - Build a new feature
431
+ - Fix a bug
432
+ - Create a pull request
433
+ - Run the development workflow
434
+
435
+ ## 7 Stages
436
+
437
+ | Stage | Command | Description |
438
+ |-------|---------|-------------|
439
+ | utility | \`/status\` | Check current context, active work, recent completions |
440
+ | 1 | \`/plan\` | Design intent -> research -> branch + worktree + task list |
441
+ | 2 | \`/dev\` | TDD development (implementer -> spec review -> quality review) |
442
+ | 3 | \`/validate\` | Type check, lint, security, tests - all fresh output |
443
+ | 4 | \`/ship\` | Push branch and create PR with full documentation |
444
+ | 5 | \`/review\` | Address ALL PR feedback (GitHub Actions, Greptile, SonarCloud) |
445
+ | 6 | \`/premerge\` | Update docs, hand off PR to user |
446
+ | 7 | \`/verify\` | Post-merge health check (CI on main, close Beads) |
447
+
448
+ ## Workflow Flow
449
+
450
+ \`\`\`
451
+ /status -> /plan -> /dev -> /validate -> /ship -> /review -> /premerge -> /verify
452
+ \`\`\`
453
+
454
+ ## Core Principles
455
+
456
+ - **TDD-First**: Write tests BEFORE implementation (RED-GREEN-REFACTOR)
457
+ - **Research-First**: Understand before building, document decisions
458
+ - **Security Built-In**: OWASP Top 10 analysis for every feature
459
+ - **Documentation Progressive**: Update at each stage, verify at end
460
+ `;
461
+
462
+ // Cursor MDC rule content
463
+ const CURSOR_RULE = `---
464
+ description: Forge 7-Stage TDD Workflow
465
+ alwaysApply: true
466
+ ---
467
+
468
+ # Forge Workflow Commands
469
+
470
+ Use these commands via \`/command-name\`:
471
+
472
+ 1. \`/status\` - Check current context, active work, recent completions
473
+ 2. \`/research\` - Deep research with web search, document to docs/research/
474
+ 3. \`/plan\` - Create implementation plan, branch, tracking
475
+ 4. \`/dev\` - TDD development (RED-GREEN-REFACTOR cycles)
476
+ 5. \`/check\` - Validation (type/lint/security/tests)
477
+ 6. \`/ship\` - Create PR with full documentation
478
+ 7. \`/review\` - Address ALL PR feedback
479
+ 8. \`/merge\` - Update docs, merge PR, cleanup
480
+ 9. \`/verify\` - Final documentation verification
481
+
482
+ See AGENTS.md for full workflow details.
483
+ `;
484
+
485
+ // Helper functions
486
+ const resolvedProjectRoot = path.resolve(projectRoot);
487
+
488
+ function ensureDir(dir) {
489
+ const fullPath = path.resolve(projectRoot, dir);
490
+
491
+ // SECURITY: Prevent path traversal
492
+ if (!fullPath.startsWith(resolvedProjectRoot)) {
493
+ console.error(` ✗ Security: Directory path escape blocked: ${dir}`);
494
+ return false;
495
+ }
496
+
497
+ if (!fs.existsSync(fullPath)) {
498
+ fs.mkdirSync(fullPath, { recursive: true });
499
+ }
500
+ return true;
501
+ }
502
+
503
+ function writeFile(filePath, content) {
504
+ try {
505
+ const fullPath = path.resolve(projectRoot, filePath);
506
+
507
+ // SECURITY: Prevent path traversal
508
+ if (!fullPath.startsWith(resolvedProjectRoot)) {
509
+ console.error(` ✗ Security: Write path escape blocked: ${filePath}`);
510
+ return false;
511
+ }
512
+
513
+ const dir = path.dirname(fullPath);
514
+ if (!fs.existsSync(dir)) {
515
+ fs.mkdirSync(dir, { recursive: true });
516
+ }
517
+ fs.writeFileSync(fullPath, content, { mode: 0o644 });
518
+ return true;
519
+ } catch (err) {
520
+ console.error(` ✗ Failed to write ${filePath}: ${err.message}`);
521
+ return false;
522
+ }
523
+ }
524
+
525
+ function readFile(filePath) {
526
+ try {
527
+ return fs.readFileSync(filePath, 'utf8');
528
+ } catch (err) {
529
+ if (process.env.DEBUG) {
530
+ console.warn(` ⚠ Could not read ${filePath}: ${err.message}`);
531
+ }
532
+ return null;
533
+ }
534
+ }
535
+
536
+ function copyFile(src, dest) {
537
+ try {
538
+ const destPath = path.resolve(projectRoot, dest);
539
+
540
+ // SECURITY: Prevent path traversal
541
+ if (!destPath.startsWith(resolvedProjectRoot)) {
542
+ console.error(` ✗ Security: Copy destination escape blocked: ${dest}`);
543
+ return false;
544
+ }
545
+
546
+ if (fs.existsSync(src)) {
547
+ const destDir = path.dirname(destPath);
548
+ if (!fs.existsSync(destDir)) {
549
+ fs.mkdirSync(destDir, { recursive: true });
550
+ }
551
+ fs.copyFileSync(src, destPath);
552
+ return true;
553
+ } else if (process.env.DEBUG) {
554
+ console.warn(` ⚠ Source file not found: ${src}`);
555
+ }
556
+ } catch (err) {
557
+ console.error(` ✗ Failed to copy ${src} -> ${dest}: ${err.message}`);
558
+ }
559
+ return false;
560
+ }
561
+
562
+ function createSymlinkOrCopy(source, target) {
563
+ const fullSource = path.resolve(projectRoot, source);
564
+ const fullTarget = path.resolve(projectRoot, target);
565
+ const resolvedProjectRoot = path.resolve(projectRoot);
566
+
567
+ // SECURITY: Prevent path traversal attacks
568
+ if (!fullSource.startsWith(resolvedProjectRoot)) {
569
+ console.error(` ✗ Security: Source path escape blocked: ${source}`);
570
+ return '';
571
+ }
572
+ if (!fullTarget.startsWith(resolvedProjectRoot)) {
573
+ console.error(` ✗ Security: Target path escape blocked: ${target}`);
574
+ return '';
575
+ }
576
+
577
+ try {
578
+ if (fs.existsSync(fullTarget)) {
579
+ fs.unlinkSync(fullTarget);
580
+ }
581
+ const targetDir = path.dirname(fullTarget);
582
+ if (!fs.existsSync(targetDir)) {
583
+ fs.mkdirSync(targetDir, { recursive: true });
584
+ }
585
+ try {
586
+ const relPath = path.relative(targetDir, fullSource);
587
+ fs.symlinkSync(relPath, fullTarget);
588
+ return 'linked';
589
+ } catch (error_) {
590
+ // Symlink creation may fail due to permissions or OS limitations (e.g., Windows without admin)
591
+ // Fall back to copying the file instead to ensure operation succeeds
592
+ console.warn('Symlink creation failed, falling back to copy:', error_.message);
593
+ fs.copyFileSync(fullSource, fullTarget);
594
+ return 'copied';
595
+ }
596
+ } catch (err) {
597
+ console.error(` ✗ Failed to link/copy ${source} -> ${target}: ${err.message}`);
598
+ return '';
599
+ }
600
+ }
601
+
602
+ function stripFrontmatter(content) {
603
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
604
+ return match ? match[1] : content;
605
+ }
606
+
607
+ // Read existing .env.local
608
+ function readEnvFile() {
609
+ const envPath = path.join(projectRoot, '.env.local');
610
+ try {
611
+ if (fs.existsSync(envPath)) {
612
+ return fs.readFileSync(envPath, 'utf8');
613
+ }
614
+ } catch (err) {
615
+ // File read failure is acceptable - file may not exist or have permission issues
616
+ // Return empty string to allow caller to proceed with defaults
617
+ console.warn('Failed to read .env.local:', err.message);
618
+ }
619
+ return '';
620
+ }
621
+
622
+ // Parse .env.local and return key-value pairs
623
+ function parseEnvFile() {
624
+ const content = readEnvFile();
625
+ const lines = content.split(/\r?\n/);
626
+ const vars = {};
627
+ lines.forEach(line => {
628
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
629
+ if (match) {
630
+ vars[match[1]] = match[2];
631
+ }
632
+ });
633
+ return vars;
634
+ }
635
+
636
+ // Write or update .env.local - PRESERVES existing values
637
+ function writeEnvTokens(tokens, preserveExisting = true) {
638
+ const envPath = path.join(projectRoot, '.env.local');
639
+ let content = readEnvFile();
640
+
641
+ // Parse existing content (handle both CRLF and LF line endings)
642
+ const lines = content.split(/\r?\n/);
643
+ const existingVars = {};
644
+ const existingKeys = new Set();
645
+ lines.forEach(line => {
646
+ const match = line.match(/^([A-Z_]+)=/);
647
+ if (match) {
648
+ existingVars[match[1]] = line;
649
+ existingKeys.add(match[1]);
650
+ }
651
+ });
652
+
653
+ // Track what was added vs preserved
654
+ let added = [];
655
+ let preserved = [];
656
+
657
+ // Add/update tokens - PRESERVE existing values if preserveExisting is true
658
+ Object.entries(tokens).forEach(([key, value]) => {
659
+ if (value?.trim()) {
660
+ if (preserveExisting && existingKeys.has(key)) {
661
+ // Keep existing value, don't overwrite
662
+ preserved.push(key);
663
+ } else {
664
+ // Add new token
665
+ existingVars[key] = `${key}=${value.trim()}`;
666
+ added.push(key);
667
+ }
668
+ }
669
+ });
670
+
671
+ // Rebuild file with comments
672
+ const outputLines = [];
673
+
674
+ // Add header if new file
675
+ if (!content.includes('# External Service API Keys')) {
676
+ outputLines.push(
677
+ '# External Service API Keys for Forge Workflow',
678
+ '# Get your keys from:',
679
+ '# Parallel AI: https://platform.parallel.ai',
680
+ '# Greptile: https://app.greptile.com/api',
681
+ '# SonarCloud: https://sonarcloud.io/account/security',
682
+ ''
683
+ );
684
+ }
685
+
686
+ // Add existing content (preserve order and comments)
687
+ lines.forEach(line => {
688
+ const match = line.match(/^([A-Z_]+)=/);
689
+ if (match && existingVars[match[1]]) {
690
+ outputLines.push(existingVars[match[1]]);
691
+ delete existingVars[match[1]]; // Mark as added
692
+ } else if (line.trim()) {
693
+ outputLines.push(line);
694
+ }
695
+ });
696
+
697
+ // Add any new tokens not in original file
698
+ Object.values(existingVars).forEach(line => {
699
+ outputLines.push(line);
700
+ });
701
+
702
+ // Ensure ends with newline
703
+ let finalContent = outputLines.join('\n').trim() + '\n';
704
+
705
+ fs.writeFileSync(envPath, finalContent);
706
+
707
+ // Add .env.local to .gitignore if not present
708
+ const gitignorePath = path.join(projectRoot, '.gitignore');
709
+ try {
710
+ let gitignore = '';
711
+ if (fs.existsSync(gitignorePath)) {
712
+ gitignore = fs.readFileSync(gitignorePath, 'utf8');
713
+ }
714
+ if (!gitignore.includes('.env.local')) {
715
+ fs.appendFileSync(gitignorePath, '\n# Local environment variables\n.env.local\n');
716
+ }
717
+ } catch (err) {
718
+ // Gitignore update is optional - failure doesn't prevent .env.local creation
719
+ // User can manually add .env.local to .gitignore if needed
720
+ console.warn('Failed to update .gitignore:', err.message);
721
+ }
722
+
723
+ return { added, preserved };
724
+ }
725
+
726
+ // Detect existing project installation status
727
+ // Smart merge for AGENTS.md - preserves USER sections, updates FORGE sections
728
+ function smartMergeAgentsMd(existingContent, newContent) {
729
+ // Check if existing content has markers
730
+ const hasUserMarkers = existingContent.includes('<!-- USER:START') && existingContent.includes('<!-- USER:END');
731
+ const hasForgeMarkers = existingContent.includes('<!-- FORGE:START') && existingContent.includes('<!-- FORGE:END');
732
+
733
+ if (!hasUserMarkers || !hasForgeMarkers) {
734
+ // Old format without markers - return empty to signal merge not possible
735
+ return '';
736
+ }
737
+
738
+ // Extract USER section from existing content
739
+ const userStartMatch = existingContent.match(/<!-- USER:START.*?-->([\s\S]*?)<!-- USER:END -->/);
740
+ const userSection = userStartMatch ? userStartMatch[0] : '';
741
+
742
+ // Extract FORGE section from new content
743
+ const forgeStartMatch = newContent.match(/(<!-- FORGE:START.*?-->[\s\S]*?<!-- FORGE:END -->)/);
744
+ const forgeSection = forgeStartMatch ? forgeStartMatch[0] : '';
745
+
746
+ // Build merged content
747
+ const setupInstructions = newContent.includes('<!-- FORGE:SETUP-INSTRUCTIONS')
748
+ ? newContent.match(/(<!-- FORGE:SETUP-INSTRUCTIONS[\s\S]*?-->)/)?.[0] || ''
749
+ : '';
750
+
751
+ let merged = '# AGENTS.md\n\n';
752
+
753
+ // Add setup instructions if this is first-time setup
754
+ if (setupInstructions && !existingContent.includes('FORGE:SETUP-INSTRUCTIONS')) {
755
+ merged += setupInstructions + '\n\n';
756
+ }
757
+
758
+ // Add preserved USER section
759
+ merged += userSection + '\n\n';
760
+
761
+ // Add updated FORGE section
762
+ merged += forgeSection + '\n\n';
763
+
764
+ // Add footer
765
+ merged += `---\n\n## 💡 Improving This Workflow\n\nEvery time you give the same instruction twice, add it to this file:\n1. User-specific rules → Add to USER:START section above\n2. Forge workflow improvements → Suggest to forge maintainers\n\n**Keep this file updated as you learn about the project.**\n\n---\n\nSee \`docs/WORKFLOW.md\` for complete workflow guide.\nSee \`docs/TOOLCHAIN.md\` for comprehensive tool reference.\n`;
766
+
767
+ return merged;
768
+ }
769
+
770
+ // Helper function for yes/no prompts with validation
771
+ async function askYesNo(question, prompt, defaultNo = true) {
772
+ const defaultText = defaultNo ? '[n]' : '[y]';
773
+ while (true) {
774
+ const answer = await question(`${prompt} (y/n) ${defaultText}: `);
775
+ const normalized = answer.trim().toLowerCase();
776
+
777
+ // Handle empty input (use default)
778
+ if (normalized === '') return !defaultNo;
779
+
780
+ // Accept yes variations
781
+ if (normalized === 'y' || normalized === 'yes') return true;
782
+
783
+ // Accept no variations
784
+ if (normalized === 'n' || normalized === 'no') return false;
785
+
786
+ // Invalid input - re-prompt
787
+ console.log(' Please enter y or n');
788
+ }
789
+ }
790
+
791
+ async function detectProjectStatus() {
792
+ const status = {
793
+ type: 'fresh', // 'fresh', 'upgrade', or 'partial'
794
+ hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
795
+ hasClaudeMd: fs.existsSync(path.join(projectRoot, 'CLAUDE.md')),
796
+ hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
797
+ hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
798
+ hasDocsWorkflow: fs.existsSync(path.join(projectRoot, 'docs/WORKFLOW.md')),
799
+ existingEnvVars: {},
800
+ agentsMdSize: 0,
801
+ claudeMdSize: 0,
802
+ agentsMdLines: 0,
803
+ claudeMdLines: 0,
804
+ // Project tools status
805
+ hasBeads: isBeadsInitialized(),
806
+ hasSkills: isSkillsInitialized(),
807
+ beadsInstallType: checkForBeads(),
808
+ skillsInstallType: checkForSkills(),
809
+ // Enhanced: Auto-detected project context
810
+ autoDetected: null
811
+ };
812
+
813
+ // Get file sizes and line counts for context warnings
814
+ if (status.hasAgentsMd) {
815
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
816
+ const stats = fs.statSync(agentsPath);
817
+ const content = fs.readFileSync(agentsPath, 'utf8');
818
+ status.agentsMdSize = stats.size;
819
+ status.agentsMdLines = content.split('\n').length;
820
+ }
821
+
822
+ if (status.hasClaudeMd) {
823
+ const claudePath = path.join(projectRoot, 'CLAUDE.md');
824
+ const stats = fs.statSync(claudePath);
825
+ const content = fs.readFileSync(claudePath, 'utf8');
826
+ status.claudeMdSize = stats.size;
827
+ status.claudeMdLines = content.split('\n').length;
828
+ }
829
+
830
+ // Determine installation type
831
+ if (status.hasAgentsMd && status.hasClaudeCommands && status.hasDocsWorkflow) {
832
+ status.type = 'upgrade'; // Full forge installation exists
833
+ } else if (status.hasClaudeCommands || status.hasEnvLocal) {
834
+ status.type = 'partial'; // Agent-specific files exist (not just base files from postinstall)
835
+ }
836
+ // else: 'fresh' - new installation (or just postinstall baseline with AGENTS.md)
837
+
838
+ // Parse existing env vars if .env.local exists
839
+ if (status.hasEnvLocal) {
840
+ status.existingEnvVars = parseEnvFile();
841
+ }
842
+
843
+ // Enhanced: Auto-detect project context (framework, language, stage, CI/CD)
844
+ try {
845
+ status.autoDetected = await projectDiscovery.autoDetect(projectRoot);
846
+ // Save context to .forge/context.json
847
+ await projectDiscovery.saveContext(status.autoDetected, projectRoot);
848
+ } catch (error) {
849
+ // Auto-detection is optional - don't fail setup if it errors
850
+ console.log(' Note: Auto-detection skipped (error:', error.message, ')');
851
+ status.autoDetected = null;
852
+ }
853
+
854
+ return status;
855
+ }
856
+
857
+ // Helper: Detect test framework from dependencies
858
+ function detectTestFramework(deps) {
859
+ if (deps.jest) return 'jest';
860
+ if (deps.vitest) return 'vitest';
861
+ if (deps.mocha) return 'mocha';
862
+ if (deps['@playwright/test']) return 'playwright';
863
+ if (deps.cypress) return 'cypress';
864
+ if (deps.karma) return 'karma';
865
+ return null;
866
+ }
867
+
868
+ // Helper: Detect language features (TypeScript, monorepo, Docker, CI/CD)
869
+ function detectLanguageFeatures(pkg) {
870
+ const features = {
871
+ typescript: false,
872
+ monorepo: false,
873
+ docker: false,
874
+ cicd: false
875
+ };
876
+
877
+ // Detect TypeScript
878
+ if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
879
+ features.typescript = true;
880
+ }
881
+
882
+ // Detect monorepo
883
+ if (pkg.workspaces ||
884
+ fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) ||
885
+ fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
886
+ features.monorepo = true;
887
+ }
888
+
889
+ // Detect Docker
890
+ if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
891
+ fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
892
+ features.docker = true;
893
+ }
894
+
895
+ // Detect CI/CD
896
+ if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
897
+ fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
898
+ fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
899
+ fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
900
+ features.cicd = true;
901
+ }
902
+
903
+ return features;
904
+ }
905
+
906
+ // Helper: Detect Next.js framework
907
+ function detectNextJs(deps) {
908
+ if (!deps.next) return null;
909
+
910
+ return {
911
+ framework: 'Next.js',
912
+ frameworkConfidence: 100,
913
+ projectType: 'fullstack',
914
+ buildTool: 'next',
915
+ testFramework: detectTestFramework(deps)
916
+ };
917
+ }
918
+
919
+ // Helper: Detect NestJS framework
920
+ function detectNestJs(deps) {
921
+ if (!deps['@nestjs/core'] && !deps['@nestjs/common']) return null;
922
+
923
+ return {
924
+ framework: 'NestJS',
925
+ frameworkConfidence: 100,
926
+ projectType: 'backend',
927
+ buildTool: 'nest',
928
+ testFramework: 'jest'
929
+ };
930
+ }
931
+
932
+ // Helper: Detect Angular framework
933
+ function detectAngular(deps) {
934
+ if (!deps['@angular/core'] && !deps['@angular/cli']) return null;
935
+
936
+ return {
937
+ framework: 'Angular',
938
+ frameworkConfidence: 100,
939
+ projectType: 'frontend',
940
+ buildTool: 'ng',
941
+ testFramework: 'karma'
942
+ };
943
+ }
944
+
945
+ // Helper: Detect Vue.js framework
946
+ function detectVue(deps) {
947
+ if (!deps.vue) return null;
948
+
949
+ if (deps.nuxt) {
950
+ return {
951
+ framework: 'Nuxt',
952
+ frameworkConfidence: 100,
953
+ projectType: 'fullstack',
954
+ buildTool: 'nuxt',
955
+ testFramework: detectTestFramework(deps)
956
+ };
957
+ }
958
+
959
+ const hasVite = deps.vite;
960
+ const hasWebpack = deps.webpack;
961
+
962
+ // Determine build tool without nested ternary
963
+ let buildTool = 'vue-cli';
964
+ if (hasVite) {
965
+ buildTool = 'vite';
966
+ } else if (hasWebpack) {
967
+ buildTool = 'webpack';
968
+ }
969
+
970
+ return {
971
+ framework: 'Vue.js',
972
+ frameworkConfidence: deps['@vue/cli'] ? 100 : 90,
973
+ projectType: 'frontend',
974
+ buildTool,
975
+ testFramework: detectTestFramework(deps)
976
+ };
977
+ }
978
+
979
+ // Helper: Detect React framework
980
+ function detectReact(deps) {
981
+ if (!deps.react) return null;
982
+
983
+ const hasVite = deps.vite;
984
+ const hasReactScripts = deps['react-scripts'];
985
+
986
+ // Determine build tool without nested ternary
987
+ let buildTool = 'webpack';
988
+ if (hasVite) {
989
+ buildTool = 'vite';
990
+ } else if (hasReactScripts) {
991
+ buildTool = 'create-react-app';
992
+ }
993
+
994
+ return {
995
+ framework: 'React',
996
+ frameworkConfidence: 95,
997
+ projectType: 'frontend',
998
+ buildTool,
999
+ testFramework: detectTestFramework(deps)
1000
+ };
1001
+ }
1002
+
1003
+ // Helper: Detect Express framework
1004
+ function detectExpress(deps, features) {
1005
+ if (!deps.express) return null;
1006
+
1007
+ return {
1008
+ framework: 'Express',
1009
+ frameworkConfidence: 90,
1010
+ projectType: 'backend',
1011
+ buildTool: features.typescript ? 'tsc' : 'node',
1012
+ testFramework: detectTestFramework(deps)
1013
+ };
1014
+ }
1015
+
1016
+ // Helper: Detect Fastify framework
1017
+ function detectFastify(deps, features) {
1018
+ if (!deps.fastify) return null;
1019
+
1020
+ return {
1021
+ framework: 'Fastify',
1022
+ frameworkConfidence: 95,
1023
+ projectType: 'backend',
1024
+ buildTool: features.typescript ? 'tsc' : 'node',
1025
+ testFramework: detectTestFramework(deps)
1026
+ };
1027
+ }
1028
+
1029
+ // Helper: Detect Svelte framework
1030
+ function detectSvelte(deps) {
1031
+ if (!deps.svelte) return null;
1032
+
1033
+ if (deps['@sveltejs/kit']) {
1034
+ return {
1035
+ framework: 'SvelteKit',
1036
+ frameworkConfidence: 100,
1037
+ projectType: 'fullstack',
1038
+ buildTool: 'vite',
1039
+ testFramework: detectTestFramework(deps)
1040
+ };
1041
+ }
1042
+
1043
+ return {
1044
+ framework: 'Svelte',
1045
+ frameworkConfidence: 95,
1046
+ projectType: 'frontend',
1047
+ buildTool: 'vite',
1048
+ testFramework: detectTestFramework(deps)
1049
+ };
1050
+ }
1051
+
1052
+ // Helper: Detect Remix framework
1053
+ function detectRemix(deps) {
1054
+ if (!deps['@remix-run/react']) return null;
1055
+
1056
+ return {
1057
+ framework: 'Remix',
1058
+ frameworkConfidence: 100,
1059
+ projectType: 'fullstack',
1060
+ buildTool: 'remix',
1061
+ testFramework: detectTestFramework(deps)
1062
+ };
1063
+ }
1064
+
1065
+ // Helper: Detect Astro framework
1066
+ function detectAstro(deps) {
1067
+ if (!deps.astro) return null;
1068
+
1069
+ return {
1070
+ framework: 'Astro',
1071
+ frameworkConfidence: 100,
1072
+ projectType: 'frontend',
1073
+ buildTool: 'astro',
1074
+ testFramework: detectTestFramework(deps)
1075
+ };
1076
+ }
1077
+
1078
+ // Helper: Detect generic Node.js project
1079
+ function detectGenericNodeJs(pkg, deps, features) {
1080
+ if (!pkg.main && !pkg.scripts?.start) return null;
1081
+
1082
+ return {
1083
+ framework: 'Node.js',
1084
+ frameworkConfidence: 70,
1085
+ projectType: 'backend',
1086
+ buildTool: features.typescript ? 'tsc' : 'node',
1087
+ testFramework: detectTestFramework(deps)
1088
+ };
1089
+ }
1090
+
1091
+ // Helper: Detect generic JavaScript/TypeScript project (fallback)
1092
+ function detectGenericProject(deps, features) {
1093
+ const hasVite = deps.vite;
1094
+ const hasWebpack = deps.webpack;
1095
+
1096
+ // Determine build tool without nested ternary
1097
+ let buildTool = 'npm';
1098
+ if (hasVite) {
1099
+ buildTool = 'vite';
1100
+ } else if (hasWebpack) {
1101
+ buildTool = 'webpack';
1102
+ }
1103
+
1104
+ return {
1105
+ framework: features.typescript ? 'TypeScript' : 'JavaScript',
1106
+ frameworkConfidence: 60,
1107
+ projectType: 'library',
1108
+ buildTool,
1109
+ testFramework: detectTestFramework(deps)
1110
+ };
1111
+ }
1112
+
1113
+ /**
1114
+ * Read package.json from project root
1115
+ * @returns {object|null} Parsed package.json or null if not found
1116
+ */
1117
+ function readPackageJson() {
1118
+ try {
1119
+ const pkgPath = path.join(projectRoot, 'package.json');
1120
+ if (!fs.existsSync(pkgPath)) {
1121
+ return null;
1122
+ }
1123
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
1124
+ } catch (_err) { // NOSONAR - S2486: Returns null on invalid/missing package.json
1125
+ return null;
1126
+ }
1127
+ }
1128
+
1129
+ // Detect project type from package.json
1130
+ function detectProjectType() {
1131
+ const detection = {
1132
+ hasPackageJson: false,
1133
+ framework: null,
1134
+ frameworkConfidence: 0,
1135
+ language: 'javascript',
1136
+ languageConfidence: 100,
1137
+ projectType: null,
1138
+ buildTool: null,
1139
+ testFramework: null,
1140
+ features: {
1141
+ typescript: false,
1142
+ monorepo: false,
1143
+ docker: false,
1144
+ cicd: false
1145
+ }
1146
+ };
1147
+
1148
+ const pkg = readPackageJson();
1149
+ if (!pkg) return detection;
1150
+
1151
+ detection.hasPackageJson = true;
1152
+
1153
+ // Detect language features
1154
+ detection.features = detectLanguageFeatures(pkg);
1155
+ if (detection.features.typescript) {
1156
+ detection.language = 'typescript';
1157
+ }
1158
+
1159
+ // Framework detection with confidence scoring
1160
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1161
+
1162
+ // Try framework detectors in priority order
1163
+ const frameworkResult =
1164
+ detectNextJs(deps) ||
1165
+ detectNestJs(deps) ||
1166
+ detectAngular(deps) ||
1167
+ detectVue(deps) ||
1168
+ detectReact(deps) ||
1169
+ detectExpress(deps, detection.features) ||
1170
+ detectFastify(deps, detection.features) ||
1171
+ detectSvelte(deps) ||
1172
+ detectRemix(deps) ||
1173
+ detectAstro(deps) ||
1174
+ detectGenericNodeJs(pkg, deps, detection.features) ||
1175
+ detectGenericProject(deps, detection.features);
1176
+
1177
+ // Merge framework detection results
1178
+ if (frameworkResult) {
1179
+ Object.assign(detection, frameworkResult);
1180
+ }
1181
+
1182
+ return detection;
1183
+ }
1184
+
1185
+ // Display project detection results
1186
+ function displayProjectType(detection) {
1187
+ if (!detection.hasPackageJson) return;
1188
+
1189
+ console.log('');
1190
+ console.log(' 📦 Project Detection:');
1191
+
1192
+ if (detection.framework) {
1193
+ const confidence = detection.frameworkConfidence >= 90 ? '✓' : '~';
1194
+ console.log(` Framework: ${detection.framework} ${confidence}`);
1195
+ }
1196
+
1197
+ if (detection.projectType) {
1198
+ console.log(` Type: ${detection.projectType}`);
1199
+ }
1200
+
1201
+ if (detection.buildTool) {
1202
+ console.log(` Build: ${detection.buildTool}`);
1203
+ }
1204
+
1205
+ if (detection.testFramework) {
1206
+ console.log(` Tests: ${detection.testFramework}`);
1207
+ }
1208
+
1209
+ const features = [];
1210
+ if (detection.features.typescript) features.push('TypeScript');
1211
+ if (detection.features.monorepo) features.push('Monorepo');
1212
+ if (detection.features.docker) features.push('Docker');
1213
+ if (detection.features.cicd) features.push('CI/CD');
1214
+
1215
+ if (features.length > 0) {
1216
+ console.log(` Features: ${features.join(', ')}`);
1217
+ }
1218
+ }
1219
+
1220
+ // Generate framework-specific tips
1221
+ function generateFrameworkTips(detection) {
1222
+ const tips = {
1223
+ 'Next.js': [
1224
+ '- Use `npm run dev` for development with hot reload',
1225
+ '- Server components are default in App Router',
1226
+ '- API routes live in `app/api/` or `pages/api/`'
1227
+ ],
1228
+ 'React': [
1229
+ '- Prefer functional components with hooks',
1230
+ '- Use `React.memo()` for expensive components',
1231
+ '- State management: Context API or external library'
1232
+ ],
1233
+ 'Vue.js': [
1234
+ '- Use Composition API for better TypeScript support',
1235
+ '- `<script setup>` is the recommended syntax',
1236
+ '- Pinia is the official state management'
1237
+ ],
1238
+ 'Angular': [
1239
+ '- Use standalone components (Angular 14+)',
1240
+ '- Signals for reactive state (Angular 16+)',
1241
+ '- RxJS for async operations'
1242
+ ],
1243
+ 'NestJS': [
1244
+ '- Dependency injection via decorators',
1245
+ '- Use `@nestjs/config` for environment variables',
1246
+ '- Guards for authentication, Interceptors for logging'
1247
+ ],
1248
+ 'Express': [
1249
+ '- Use middleware for cross-cutting concerns',
1250
+ '- Error handling with next(err)',
1251
+ '- Consider Helmet.js for security headers'
1252
+ ],
1253
+ 'Fastify': [
1254
+ '- Schema-based validation with JSON Schema',
1255
+ '- Plugins for reusable functionality',
1256
+ '- Async/await by default'
1257
+ ],
1258
+ 'SvelteKit': [
1259
+ '- File-based routing in `src/routes/`',
1260
+ '- Server-side rendering by default',
1261
+ '- Form actions for mutations'
1262
+ ],
1263
+ 'Nuxt': [
1264
+ '- Auto-imports for components and composables',
1265
+ '- `useAsyncData()` for data fetching',
1266
+ '- Nitro server engine for deployment'
1267
+ ],
1268
+ 'Remix': [
1269
+ '- Loaders for data fetching',
1270
+ '- Actions for mutations',
1271
+ '- Progressive enhancement by default'
1272
+ ],
1273
+ 'Astro': [
1274
+ '- Zero JS by default',
1275
+ '- Use client:* directives for interactivity',
1276
+ '- Content collections for type-safe content'
1277
+ ]
1278
+ };
1279
+
1280
+ return tips[detection.framework] || [];
1281
+ }
1282
+
1283
+ // Update AGENTS.md with project type metadata
1284
+ function updateAgentsMdWithProjectType(detection) {
1285
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
1286
+ if (!fs.existsSync(agentsPath)) return;
1287
+
1288
+ let content = fs.readFileSync(agentsPath, 'utf-8');
1289
+
1290
+ // Find the project description line (line 3)
1291
+ const lines = content.split('\n');
1292
+ let insertIndex = -1;
1293
+
1294
+ for (let i = 0; i < Math.min(lines.length, 10); i++) {
1295
+ if (lines[i].startsWith('This is a ')) {
1296
+ insertIndex = i + 1;
1297
+ break;
1298
+ }
1299
+ }
1300
+
1301
+ if (insertIndex === -1) return;
1302
+
1303
+ // Build metadata section
1304
+ const metadata = [];
1305
+ metadata.push('');
1306
+ if (detection.framework) {
1307
+ metadata.push(`**Framework**: ${detection.framework}`);
1308
+ }
1309
+ if (detection.language && detection.language !== 'javascript') {
1310
+ metadata.push(`**Language**: ${detection.language}`);
1311
+ }
1312
+ if (detection.projectType) {
1313
+ metadata.push(`**Type**: ${detection.projectType}`);
1314
+ }
1315
+ if (detection.buildTool) {
1316
+ metadata.push(`**Build**: \`${detection.buildTool}\``);
1317
+ }
1318
+ if (detection.testFramework) {
1319
+ metadata.push(`**Tests**: ${detection.testFramework}`);
1320
+ }
1321
+
1322
+ // Add framework-specific tips
1323
+ const tips = generateFrameworkTips(detection);
1324
+ if (tips.length > 0) {
1325
+ metadata.push('', '**Framework conventions**:', ...tips);
1326
+ }
1327
+
1328
+ // Insert metadata
1329
+ lines.splice(insertIndex, 0, ...metadata);
1330
+
1331
+ fs.writeFileSync(agentsPath, lines.join('\n'), 'utf-8');
1332
+ }
1333
+
1334
+ // Helper: Calculate estimated tokens (rough: ~4 chars per token)
1335
+ function estimateTokens(bytes) {
1336
+ return Math.ceil(bytes / 4);
1337
+ }
1338
+
1339
+ // Helper: Create instruction files result object
1340
+ function createInstructionFilesResult(createAgentsMd = false, createClaudeMd = false, skipAgentsMd = false, skipClaudeMd = false) {
1341
+ return {
1342
+ createAgentsMd,
1343
+ createClaudeMd,
1344
+ skipAgentsMd,
1345
+ skipClaudeMd
1346
+ };
1347
+ }
1348
+
1349
+ // Helper: Handle scenario where both AGENTS.md and CLAUDE.md exist
1350
+ async function handleBothFilesExist(question, projectStatus) {
1351
+ const totalLines = projectStatus.agentsMdLines + projectStatus.claudeMdLines;
1352
+ const totalTokens = estimateTokens(projectStatus.agentsMdSize + projectStatus.claudeMdSize);
1353
+
1354
+ console.log('');
1355
+ console.log('⚠️ WARNING: Multiple Instruction Files Detected');
1356
+ console.log('='.repeat(60));
1357
+ console.log(` AGENTS.md: ${projectStatus.agentsMdLines} lines (~${estimateTokens(projectStatus.agentsMdSize)} tokens)`);
1358
+ console.log(` CLAUDE.md: ${projectStatus.claudeMdLines} lines (~${estimateTokens(projectStatus.claudeMdSize)} tokens)`);
1359
+ console.log(` Total: ${totalLines} lines (~${totalTokens} tokens)`);
1360
+ console.log('');
1361
+ console.log(' ⚠️ Claude Code reads BOTH files on every request');
1362
+ console.log(' ⚠️ This increases context usage and costs');
1363
+ console.log('');
1364
+ console.log(' Options:');
1365
+ console.log(' 1) Keep CLAUDE.md only (recommended for Claude Code only)');
1366
+ console.log(' 2) Keep AGENTS.md only (recommended for multi-agent users)');
1367
+ console.log(' 3) Keep both (higher context usage)');
1368
+ console.log('');
1369
+
1370
+ while (true) {
1371
+ const choice = await question('Your choice (1/2/3) [2]: ');
1372
+ const normalized = choice.trim() || '2';
1373
+
1374
+ if (normalized === '1') {
1375
+ console.log(' ✓ Will keep CLAUDE.md, remove AGENTS.md');
1376
+ return createInstructionFilesResult(false, false, true, false);
1377
+ } else if (normalized === '2') {
1378
+ console.log(' ✓ Will keep AGENTS.md, remove CLAUDE.md');
1379
+ return createInstructionFilesResult(false, false, false, true);
1380
+ } else if (normalized === '3') {
1381
+ console.log(' ✓ Will keep both files (context: ~' + totalTokens + ' tokens)');
1382
+ return createInstructionFilesResult(false, false, false, false);
1383
+ } else {
1384
+ console.log(' Please enter 1, 2, or 3');
1385
+ }
1386
+ }
1387
+ }
1388
+
1389
+ // Helper: Handle scenario where only CLAUDE.md exists
1390
+ async function handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents) {
1391
+ if (hasOtherAgents) {
1392
+ console.log('');
1393
+ console.log('📋 Found existing CLAUDE.md (' + projectStatus.claudeMdLines + ' lines)');
1394
+ console.log(' You selected multiple agents. Recommendation:');
1395
+ console.log(' → Migrate to AGENTS.md (works with all agents)');
1396
+ console.log('');
1397
+
1398
+ const migrate = await askYesNo(question, 'Migrate CLAUDE.md to AGENTS.md?', false);
1399
+ if (migrate) {
1400
+ console.log(' ✓ Will migrate content to AGENTS.md');
1401
+ return createInstructionFilesResult(true, false, false, true);
1402
+ } else {
1403
+ console.log(' ✓ Will keep CLAUDE.md and create AGENTS.md');
1404
+ return createInstructionFilesResult(true, false, false, false);
1405
+ }
1406
+ } else {
1407
+ // Claude Code only - keep CLAUDE.md
1408
+ console.log(' ✓ Keeping existing CLAUDE.md');
1409
+ return createInstructionFilesResult(false, false, false, false);
1410
+ }
1411
+ }
1412
+
1413
+ // Helper: Handle scenario where only AGENTS.md exists
1414
+ async function handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents) {
1415
+ if (hasClaude && !hasOtherAgents) {
1416
+ console.log('');
1417
+ console.log('📋 Found existing AGENTS.md (' + projectStatus.agentsMdLines + ' lines)');
1418
+ console.log(' You selected Claude Code only. Options:');
1419
+ console.log(' 1) Keep AGENTS.md (works fine)');
1420
+ console.log(' 2) Rename to CLAUDE.md (Claude-specific naming)');
1421
+ console.log('');
1422
+
1423
+ const rename = await askYesNo(question, 'Rename to CLAUDE.md?', true);
1424
+ if (rename) {
1425
+ console.log(' ✓ Will rename to CLAUDE.md');
1426
+ return createInstructionFilesResult(false, true, true, false);
1427
+ } else {
1428
+ console.log(' ✓ Keeping AGENTS.md');
1429
+ return createInstructionFilesResult(false, false, false, false);
1430
+ }
1431
+ } else {
1432
+ // Multi-agent or other agents - keep AGENTS.md
1433
+ console.log(' ✓ Keeping existing AGENTS.md');
1434
+ return createInstructionFilesResult(false, false, false, false);
1435
+ }
1436
+ }
1437
+
1438
+ // Helper: Handle scenario where no instruction files exist (fresh install)
1439
+ function handleNoFilesExist(hasClaude, hasOtherAgents) {
1440
+ if (hasClaude && !hasOtherAgents) {
1441
+ // Claude Code only → create CLAUDE.md
1442
+ console.log(' ✓ Will create CLAUDE.md (Claude Code specific)');
1443
+ return createInstructionFilesResult(false, true, false, false);
1444
+ } else if (!hasClaude && hasOtherAgents) {
1445
+ // Other agents only → create AGENTS.md
1446
+ console.log(' ✓ Will create AGENTS.md (universal)');
1447
+ return createInstructionFilesResult(true, false, false, false);
1448
+ } else {
1449
+ // Multiple agents including Claude → create AGENTS.md + reference CLAUDE.md
1450
+ console.log(' ✓ Will create AGENTS.md (main) + CLAUDE.md (reference)');
1451
+ return createInstructionFilesResult(true, true, false, false);
1452
+ }
1453
+ }
1454
+
1455
+ // Smart file selection with context warnings
1456
+ // @private - Currently unused, reserved for future interactive setup flow
1457
+ async function _handleInstructionFiles(rl, question, selectedAgents, projectStatus) {
1458
+ const hasClaude = selectedAgents.some(a => a.key === 'claude');
1459
+ const hasOtherAgents = selectedAgents.some(a => a.key !== 'claude');
1460
+
1461
+ // Scenario 1: Both files exist (potential context bloat)
1462
+ if (projectStatus.hasAgentsMd && projectStatus.hasClaudeMd) {
1463
+ return await handleBothFilesExist(question, projectStatus);
1464
+ }
1465
+
1466
+ // Scenario 2: Only CLAUDE.md exists
1467
+ if (projectStatus.hasClaudeMd && !projectStatus.hasAgentsMd) {
1468
+ return await handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents);
1469
+ }
1470
+
1471
+ // Scenario 3: Only AGENTS.md exists
1472
+ if (projectStatus.hasAgentsMd && !projectStatus.hasClaudeMd) {
1473
+ return await handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents);
1474
+ }
1475
+
1476
+ // Scenario 4: Neither file exists (fresh install)
1477
+ return handleNoFilesExist(hasClaude, hasOtherAgents);
1478
+ }
1479
+
1480
+ // Prompt for code review tool selection - extracted to reduce cognitive complexity
1481
+ async function promptForCodeReviewTool(question) {
1482
+ console.log('');
1483
+ console.log('Code Review Tool');
1484
+ console.log('----------------');
1485
+ console.log('Select your code review integration:');
1486
+ console.log('');
1487
+ console.log(' 1) GitHub Code Quality (FREE, built-in) [RECOMMENDED]');
1488
+ console.log(' Zero setup - uses GitHub\'s built-in code quality features');
1489
+ console.log('');
1490
+ console.log(' 2) CodeRabbit (FREE for open source)');
1491
+ console.log(' AI-powered reviews - install GitHub App at https://coderabbit.ai');
1492
+ console.log('');
1493
+ console.log(' 3) Greptile (Paid - $99+/mo)');
1494
+ console.log(' Enterprise code review - https://greptile.com');
1495
+ console.log('');
1496
+ console.log(' 4) Skip code review integration');
1497
+ console.log('');
1498
+
1499
+ const choice = await question('Select [1]: ') || '1';
1500
+ const tokens = {};
1501
+
1502
+ switch (choice) {
1503
+ case '1': {
1504
+ tokens['CODE_REVIEW_TOOL'] = 'github-code-quality';
1505
+ console.log(' ✓ Using GitHub Code Quality (FREE)');
1506
+ break;
1507
+ }
1508
+ case '2': {
1509
+ tokens['CODE_REVIEW_TOOL'] = 'coderabbit';
1510
+ console.log(' ✓ Using CodeRabbit - Install the GitHub App to activate');
1511
+ console.log(' https://coderabbit.ai');
1512
+ break;
1513
+ }
1514
+ case '3': {
1515
+ const greptileKey = await question(' Enter Greptile API key: ');
1516
+ if (greptileKey?.trim()) {
1517
+ tokens['CODE_REVIEW_TOOL'] = 'greptile';
1518
+ tokens['GREPTILE_API_KEY'] = greptileKey.trim();
1519
+ console.log(' ✓ Greptile configured');
1520
+ } else {
1521
+ tokens['CODE_REVIEW_TOOL'] = 'none';
1522
+ console.log(' Skipped - No API key provided');
1523
+ }
1524
+ break;
1525
+ }
1526
+ default: {
1527
+ tokens['CODE_REVIEW_TOOL'] = 'none';
1528
+ console.log(' Skipped code review integration');
1529
+ }
1530
+ }
1531
+
1532
+ return tokens;
1533
+ }
1534
+
1535
+ // Prompt for code quality tool selection - extracted to reduce cognitive complexity
1536
+ async function promptForCodeQualityTool(question) {
1537
+ console.log('');
1538
+ console.log('Code Quality Tool');
1539
+ console.log('-----------------');
1540
+ console.log('Select your code quality/security scanner:');
1541
+ console.log('');
1542
+ console.log(' 1) ESLint only (FREE, built-in) [RECOMMENDED]');
1543
+ console.log(' No external server required - uses project\'s linting');
1544
+ console.log('');
1545
+ console.log(' 2) SonarCloud (50k LoC free, cloud-hosted)');
1546
+ console.log(' Get token: https://sonarcloud.io/account/security');
1547
+ console.log('');
1548
+ console.log(' 3) SonarQube Community (FREE, self-hosted, unlimited LoC)');
1549
+ console.log(' Run: docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
1550
+ console.log('');
1551
+ console.log(' 4) Skip code quality integration');
1552
+ console.log('');
1553
+
1554
+ const choice = await question('Select [1]: ') || '1';
1555
+ const tokens = {};
1556
+
1557
+ switch (choice) {
1558
+ case '1': {
1559
+ tokens['CODE_QUALITY_TOOL'] = 'eslint';
1560
+ console.log(' ✓ Using ESLint (built-in)');
1561
+ break;
1562
+ }
1563
+ case '2': {
1564
+ const sonarToken = await question(' Enter SonarCloud token: ');
1565
+ const sonarOrg = await question(' Enter SonarCloud organization: ');
1566
+ const sonarProject = await question(' Enter SonarCloud project key: ');
1567
+ if (sonarToken?.trim()) {
1568
+ tokens['CODE_QUALITY_TOOL'] = 'sonarcloud';
1569
+ tokens['SONAR_TOKEN'] = sonarToken.trim();
1570
+ if (sonarOrg) tokens['SONAR_ORGANIZATION'] = sonarOrg.trim();
1571
+ if (sonarProject) tokens['SONAR_PROJECT_KEY'] = sonarProject.trim();
1572
+ console.log(' ✓ SonarCloud configured');
1573
+ } else {
1574
+ tokens['CODE_QUALITY_TOOL'] = 'eslint';
1575
+ console.log(' Falling back to ESLint');
1576
+ }
1577
+ break;
1578
+ }
1579
+ case '3': {
1580
+ console.log('');
1581
+ console.log(' SonarQube Self-Hosted Setup:');
1582
+ console.log(' docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
1583
+ console.log(' Access: http://localhost:9000 (admin/admin)');
1584
+ console.log('');
1585
+ const sqUrl = await question(' Enter SonarQube URL [http://localhost:9000]: ') || 'http://localhost:9000';
1586
+ const sqToken = await question(' Enter SonarQube token (optional): ');
1587
+ tokens['CODE_QUALITY_TOOL'] = 'sonarqube';
1588
+ tokens['SONARQUBE_URL'] = sqUrl;
1589
+ if (sqToken?.trim()) {
1590
+ tokens['SONARQUBE_TOKEN'] = sqToken.trim();
1591
+ }
1592
+ console.log(' ✓ SonarQube self-hosted configured');
1593
+ break;
1594
+ }
1595
+ default: {
1596
+ tokens['CODE_QUALITY_TOOL'] = 'none';
1597
+ console.log(' Skipped code quality integration');
1598
+ }
1599
+ }
1600
+
1601
+ return tokens;
1602
+ }
1603
+
1604
+ // Prompt for research tool selection - extracted to reduce cognitive complexity
1605
+ async function promptForResearchTool(question) {
1606
+ console.log('');
1607
+ console.log('Research Tool');
1608
+ console.log('-------------');
1609
+ console.log('Select your research tool for /research stage:');
1610
+ console.log('');
1611
+ console.log(' 1) Manual research only [DEFAULT]');
1612
+ console.log(' Use web browser and codebase exploration');
1613
+ console.log('');
1614
+ console.log(' 2) Parallel AI (comprehensive web research)');
1615
+ console.log(' Get key: https://platform.parallel.ai');
1616
+ console.log('');
1617
+
1618
+ const choice = await question('Select [1]: ') || '1';
1619
+ const tokens = {};
1620
+
1621
+ if (choice === '2') {
1622
+ const parallelKey = await question(' Enter Parallel AI API key: ');
1623
+ if (parallelKey?.trim()) {
1624
+ tokens['PARALLEL_API_KEY'] = parallelKey.trim();
1625
+ console.log(' ✓ Parallel AI configured');
1626
+ } else {
1627
+ console.log(' Skipped - No API key provided');
1628
+ }
1629
+ } else {
1630
+ console.log(' ✓ Using manual research');
1631
+ }
1632
+
1633
+ return tokens;
1634
+ }
1635
+
1636
+ // Helper: Check existing service configuration - extracted to reduce cognitive complexity
1637
+ async function checkExistingServiceConfig(question, projectStatus) {
1638
+ const existingEnvVars = projectStatus?.existingEnvVars || parseEnvFile();
1639
+ const hasCodeReviewTool = existingEnvVars.CODE_REVIEW_TOOL;
1640
+ const hasCodeQualityTool = existingEnvVars.CODE_QUALITY_TOOL;
1641
+ const hasExistingConfig = hasCodeReviewTool || hasCodeQualityTool;
1642
+
1643
+ if (!hasExistingConfig) {
1644
+ return true; // No existing config, proceed with configuration
1645
+ }
1646
+
1647
+ console.log('External services already configured:');
1648
+ if (hasCodeReviewTool) {
1649
+ console.log(` - CODE_REVIEW_TOOL: ${hasCodeReviewTool}`);
1650
+ }
1651
+ if (hasCodeQualityTool) {
1652
+ console.log(` - CODE_QUALITY_TOOL: ${hasCodeQualityTool}`);
1653
+ }
1654
+ console.log('');
1655
+
1656
+ const reconfigure = await askYesNo(question, 'Reconfigure external services?', true);
1657
+ if (!reconfigure) {
1658
+ console.log('');
1659
+ console.log('Keeping existing configuration.');
1660
+ return false; // Skip configuration
1661
+ }
1662
+ console.log('');
1663
+ return true; // Proceed with configuration
1664
+ }
1665
+
1666
+ // Helper: Display Context7 MCP status for selected agents - extracted to reduce cognitive complexity
1667
+ function displayMcpStatus(selectedAgents) {
1668
+ console.log('');
1669
+ console.log('Context7 MCP - Library Documentation');
1670
+ console.log('-------------------------------------');
1671
+ console.log('Provides up-to-date library docs for AI coding agents.');
1672
+ console.log('');
1673
+
1674
+ // Show what was/will be auto-installed
1675
+ if (selectedAgents.includes('claude')) {
1676
+ console.log(' ✓ Auto-installed for Claude Code (.mcp.json)');
1677
+ }
1678
+ // Show manual setup instructions for GUI-based agents
1679
+ const manualMcpMap = {
1680
+ cursor: 'Cursor: Configure via Cursor Settings > MCP',
1681
+ cline: 'Cline: Install via MCP Marketplace',
1682
+ };
1683
+ const needsManualMcp = Object.entries(manualMcpMap)
1684
+ .filter(([key]) => selectedAgents.includes(key))
1685
+ .map(([, msg]) => msg);
1686
+
1687
+ if (needsManualMcp.length > 0) {
1688
+ needsManualMcp.forEach(msg => console.log(` ! ${msg}`));
1689
+ console.log('');
1690
+ console.log(' Package: @upstash/context7-mcp@latest');
1691
+ console.log(' Docs: https://github.com/upstash/context7-mcp');
1692
+ }
1693
+ }
1694
+
1695
+ // Helper: Display env token write results - extracted to reduce cognitive complexity
1696
+ function displayEnvTokenResults(added, preserved) {
1697
+ console.log('');
1698
+ if (preserved.length > 0) {
1699
+ console.log('Preserved existing values:');
1700
+ preserved.forEach(key => {
1701
+ console.log(` - ${key} already configured - keeping existing value`);
1702
+ });
1703
+ console.log('');
1704
+ }
1705
+ if (added.length > 0) {
1706
+ console.log('Added new configuration:');
1707
+ added.forEach(key => {
1708
+ console.log(` - ${key}`);
1709
+ });
1710
+ console.log('');
1711
+ }
1712
+ console.log('Configuration saved to .env.local');
1713
+ console.log('Note: .env.local has been added to .gitignore');
1714
+ }
1715
+
1716
+ // Configure external services interactively
1717
+ async function configureExternalServices(rl, question, selectedAgents = [], projectStatus = null) {
1718
+ console.log('');
1719
+ console.log('==============================================');
1720
+ console.log(' External Services Configuration');
1721
+ console.log('==============================================');
1722
+ console.log('');
1723
+
1724
+ // Check existing configuration
1725
+ const shouldContinue = await checkExistingServiceConfig(question, projectStatus);
1726
+ if (!shouldContinue) {
1727
+ return;
1728
+ }
1729
+
1730
+ console.log('Would you like to configure external services?');
1731
+ console.log('(You can also add them later to .env.local)');
1732
+ console.log('');
1733
+
1734
+ const configure = await askYesNo(question, 'Configure external services?', false);
1735
+
1736
+ if (!configure) {
1737
+ console.log('');
1738
+ console.log('Skipping external services. You can configure them later by editing .env.local');
1739
+ return;
1740
+ }
1741
+
1742
+ // Prompt for each service and collect tokens
1743
+ const tokens = {};
1744
+
1745
+ // CODE REVIEW TOOL
1746
+ Object.assign(tokens, await promptForCodeReviewTool(question));
1747
+
1748
+ // CODE QUALITY TOOL
1749
+ Object.assign(tokens, await promptForCodeQualityTool(question));
1750
+
1751
+ // RESEARCH TOOL
1752
+ Object.assign(tokens, await promptForResearchTool(question));
1753
+
1754
+ // Context7 MCP - Library Documentation
1755
+ displayMcpStatus(selectedAgents);
1756
+
1757
+ // Save package manager preference
1758
+ tokens['PKG_MANAGER'] = PKG_MANAGER;
1759
+
1760
+ // Write all tokens to .env.local (preserving existing values)
1761
+ const { added, preserved } = writeEnvTokens(tokens, true);
1762
+ displayEnvTokenResults(added, preserved);
1763
+ }
1764
+
1765
+ // Display the Forge banner
1766
+ function showBanner(subtitle = 'Universal AI Agent Workflow') {
1767
+ console.log('');
1768
+ console.log(' ███████╗ ██████╗ ██████╗ ██████╗ ███████╗');
1769
+ console.log(' ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝');
1770
+ console.log(' █████╗ ██║ ██║██████╔╝██║ ███╗█████╗ ');
1771
+ console.log(' ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ ');
1772
+ console.log(' ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗');
1773
+ console.log(' ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝');
1774
+ console.log(` v${VERSION}`);
1775
+ console.log('');
1776
+ if (subtitle) {
1777
+ console.log(` ${subtitle}`);
1778
+ }
1779
+ }
1780
+
1781
+ // Setup core documentation and directories
1782
+ function setupCoreDocs() {
1783
+ // Create core directories
1784
+ ensureDir('docs/planning');
1785
+ ensureDir('docs/research');
1786
+
1787
+ // Copy WORKFLOW.md
1788
+ const workflowSrc = path.join(packageDir, 'docs/WORKFLOW.md');
1789
+ if (copyFile(workflowSrc, 'docs/WORKFLOW.md')) {
1790
+ console.log(' Created: docs/WORKFLOW.md');
1791
+ }
1792
+
1793
+ // Copy research TEMPLATE.md
1794
+ const templateSrc = path.join(packageDir, 'docs/research/TEMPLATE.md');
1795
+ if (copyFile(templateSrc, 'docs/research/TEMPLATE.md')) {
1796
+ console.log(' Created: docs/research/TEMPLATE.md');
1797
+ }
1798
+
1799
+ // Create PROGRESS.md if not exists
1800
+ const progressPath = path.join(projectRoot, 'docs/planning/PROGRESS.md');
1801
+ if (!fs.existsSync(progressPath)) {
1802
+ writeFile('docs/planning/PROGRESS.md', `# Project Progress
1803
+
1804
+ ## Current Focus
1805
+ <!-- What you're working on -->
1806
+
1807
+ ## Completed
1808
+ <!-- Completed features -->
1809
+
1810
+ ## Upcoming
1811
+ <!-- Next priorities -->
1812
+ `);
1813
+ console.log(' Created: docs/planning/PROGRESS.md');
1814
+ }
1815
+ }
1816
+
1817
+ // Minimal installation (postinstall)
1818
+ function minimalInstall() {
1819
+ // Check if this looks like a project (has package.json)
1820
+ const hasPackageJson = fs.existsSync(path.join(projectRoot, 'package.json'));
1821
+
1822
+ if (!hasPackageJson) {
1823
+ console.log('');
1824
+ console.log(' ✅ Forge installed successfully!');
1825
+ console.log('');
1826
+ console.log(' To set up in a project:');
1827
+ console.log(' cd your-project');
1828
+ console.log(' npx forge setup');
1829
+ console.log('');
1830
+ console.log(' Or specify a project directory:');
1831
+ console.log(' npx forge setup --path ./my-project');
1832
+ console.log('');
1833
+ return;
1834
+ }
1835
+
1836
+ showBanner();
1837
+ console.log('');
1838
+
1839
+ // Setup core documentation
1840
+ setupCoreDocs();
1841
+
1842
+ // Copy AGENTS.md (only if not exists - preserve user customizations in minimal install)
1843
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
1844
+ if (fs.existsSync(agentsPath)) {
1845
+ console.log(' Skipped: AGENTS.md (already exists)');
1846
+ } else {
1847
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1848
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
1849
+ console.log(' Created: AGENTS.md (universal standard)');
1850
+
1851
+ // Detect project type and update AGENTS.md
1852
+ const detection = detectProjectType();
1853
+ if (detection.hasPackageJson) {
1854
+ updateAgentsMdWithProjectType(detection);
1855
+ displayProjectType(detection);
1856
+ }
1857
+ }
1858
+ }
1859
+
1860
+ console.log('');
1861
+ console.log('Minimal installation complete!');
1862
+ console.log('');
1863
+ console.log('To configure for your AI coding agents, run:');
1864
+ console.log('');
1865
+ console.log(' bun add -d lefthook # Install git hooks (one-time)');
1866
+ console.log(' bunx forge setup # Interactive setup (agents + API tokens)');
1867
+ console.log('');
1868
+ console.log('Or specify agents directly:');
1869
+ console.log(' bunx forge setup --agents claude,cursor');
1870
+ console.log(' bunx forge setup --all');
1871
+ console.log('');
1872
+ }
1873
+
1874
+ // Helper: Setup Claude agent
1875
+ function setupClaudeAgent(skipFiles = {}) {
1876
+ // Copy commands from package (unless skipped)
1877
+ if (skipFiles.claudeCommands) {
1878
+ console.log(' Skipped: .claude/commands/ (keeping existing)');
1879
+ } else {
1880
+ COMMANDS.forEach(cmd => {
1881
+ const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
1882
+ copyFile(src, `.claude/commands/${cmd}.md`);
1883
+ });
1884
+ console.log(' Copied: 9 workflow commands');
1885
+ }
1886
+
1887
+ // Copy rules
1888
+ const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
1889
+ copyFile(rulesSrc, '.claude/rules/workflow.md');
1890
+
1891
+ // Copy scripts
1892
+ const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
1893
+ copyFile(scriptSrc, '.claude/scripts/load-env.sh');
1894
+ }
1895
+
1896
+ // Helper: Setup Cursor agent
1897
+ function setupCursorAgent() {
1898
+ writeFile('.cursor/rules/forge-workflow.mdc', CURSOR_RULE);
1899
+ console.log(' Created: .cursor/rules/forge-workflow.mdc');
1900
+ }
1901
+
1902
+ // Helper: Convert command to agent-specific format
1903
+ function convertCommandToAgentFormat(cmd, content, agent) {
1904
+ let targetContent = content;
1905
+ let targetFile = cmd;
1906
+
1907
+ if (agent.needsConversion) {
1908
+ targetContent = stripFrontmatter(content);
1909
+ }
1910
+
1911
+ if (agent.promptFormat) {
1912
+ targetFile = cmd.replace('.md', '.prompt.md');
1913
+ targetContent = stripFrontmatter(content);
1914
+ }
1915
+
1916
+ return { targetFile, targetContent };
1917
+ }
1918
+
1919
+ // Helper: Copy commands for agent
1920
+ function copyAgentCommands(agent, claudeCommands) {
1921
+ if (!claudeCommands) return;
1922
+ if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat) return;
1923
+
1924
+ Object.entries(claudeCommands).forEach(([cmd, content]) => {
1925
+ const { targetFile, targetContent } = convertCommandToAgentFormat(cmd, content, agent);
1926
+ const targetDir = agent.dirs[0]; // First dir is commands/workflows
1927
+ writeFile(`${targetDir}/${targetFile}`, targetContent);
1928
+ });
1929
+ console.log(' Converted: 9 workflow commands');
1930
+ }
1931
+
1932
+ // Helper: Copy rules for agent
1933
+ function copyAgentRules(agent) {
1934
+ if (!agent.needsConversion) return;
1935
+
1936
+ const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
1937
+ if (!fs.existsSync(workflowMdPath)) return;
1938
+
1939
+ const rulesDir = agent.dirs.find(d => d.includes('/rules'));
1940
+ if (!rulesDir) return;
1941
+
1942
+ const ruleContent = readFile(workflowMdPath);
1943
+ if (ruleContent) {
1944
+ writeFile(`${rulesDir}/workflow.md`, ruleContent);
1945
+ }
1946
+ }
1947
+
1948
+ // Helper: Create skill file for agent
1949
+ function createAgentSkill(agent) {
1950
+ if (!agent.hasSkill) return;
1951
+
1952
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1953
+ if (skillDir) {
1954
+ writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
1955
+ console.log(' Created: forge-workflow skill');
1956
+ }
1957
+ }
1958
+
1959
+ // Helper: Setup MCP config for Claude
1960
+ function setupClaudeMcpConfig() {
1961
+ const mcpPath = path.join(projectRoot, '.mcp.json');
1962
+ if (fs.existsSync(mcpPath)) {
1963
+ console.log(' Skipped: .mcp.json already exists');
1964
+ return;
1965
+ }
1966
+
1967
+ const mcpConfig = {
1968
+ mcpServers: {
1969
+ context7: {
1970
+ command: 'npx',
1971
+ args: ['-y', '@upstash/context7-mcp@latest']
1972
+ }
1973
+ }
1974
+ };
1975
+ writeFile('.mcp.json', JSON.stringify(mcpConfig, null, 2));
1976
+ console.log(' Created: .mcp.json with Context7 MCP');
1977
+ }
1978
+
1979
+ // Helper: Create agent link file
1980
+ function createAgentLinkFile(agent) {
1981
+ if (!agent.linkFile) return;
1982
+
1983
+ const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile);
1984
+ if (result) {
1985
+ console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
1986
+ }
1987
+ }
1988
+
1989
+ // Setup specific agent
1990
+ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
1991
+ const agent = AGENTS[agentKey];
1992
+ if (!agent) return;
1993
+
1994
+ console.log(`\nSetting up ${agent.name}...`);
1995
+
1996
+ // Create directories
1997
+ agent.dirs.forEach(dir => ensureDir(dir));
1998
+
1999
+ // Handle agent-specific setup
2000
+ if (agentKey === 'claude') {
2001
+ setupClaudeAgent(skipFiles);
2002
+ }
2003
+
2004
+ if (agent.customSetup === 'cursor') {
2005
+ setupCursorAgent();
2006
+ }
2007
+
2008
+ // Convert/copy commands
2009
+ copyAgentCommands(agent, claudeCommands);
2010
+
2011
+ // Copy rules if needed
2012
+ copyAgentRules(agent);
2013
+
2014
+ // Create SKILL.md
2015
+ createAgentSkill(agent);
2016
+
2017
+ // Setup MCP configs
2018
+ if (agentKey === 'claude') {
2019
+ setupClaudeMcpConfig();
2020
+ }
2021
+
2022
+ // Create link file
2023
+ createAgentLinkFile(agent);
2024
+ }
2025
+
2026
+
2027
+ // =============================================
2028
+ // Helper Functions for Interactive Setup
2029
+ // =============================================
2030
+
2031
+ /**
2032
+ * Display existing installation status
2033
+ */
2034
+ function displayInstallationStatus(projectStatus) {
2035
+ if (projectStatus.type === 'fresh') return;
2036
+
2037
+ console.log('==============================================');
2038
+ console.log(' Existing Installation Detected');
2039
+ console.log('==============================================');
2040
+ console.log('');
2041
+
2042
+ if (projectStatus.type === 'upgrade') {
2043
+ console.log('Found existing Forge installation:');
2044
+ } else {
2045
+ console.log('Found partial installation:');
2046
+ }
2047
+
2048
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
2049
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
2050
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
2051
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
2052
+ console.log('');
2053
+ }
2054
+
2055
+ /**
2056
+ * Handle AGENTS.md file without markers - offers 3 options
2057
+ * Extracted to reduce cognitive complexity
2058
+ */
2059
+ async function promptForAgentsMdWithoutMarkers(question, skipFiles, agentsPath) {
2060
+ console.log('');
2061
+ console.log('Found existing AGENTS.md without Forge markers.');
2062
+ console.log('This file may contain your custom agent instructions.');
2063
+ console.log('');
2064
+ console.log('How would you like to proceed?');
2065
+ console.log(' 1. Intelligent merge (preserve your content + add Forge workflow)');
2066
+ console.log(' 2. Keep existing (skip Forge installation for this file)');
2067
+ console.log(' 3. Replace (backup created at AGENTS.md.backup)');
2068
+ console.log('');
2069
+
2070
+ let validChoice = false;
2071
+ while (!validChoice) {
2072
+ const answer = await question('Your choice (1-3) [1]: ');
2073
+ const choice = answer.trim() || '1';
2074
+
2075
+ if (choice === '1') {
2076
+ // Intelligent merge
2077
+ skipFiles.useSemanticMerge = true;
2078
+ skipFiles.agentsMd = false;
2079
+ console.log(' Will use intelligent merge (preserving your content)');
2080
+ validChoice = true;
2081
+ } else if (choice === '2') {
2082
+ // Keep existing
2083
+ skipFiles.agentsMd = true;
2084
+ console.log(' Keeping existing AGENTS.md');
2085
+ validChoice = true;
2086
+ } else if (choice === '3') {
2087
+ // Replace (backup first)
2088
+ try {
2089
+ fs.copyFileSync(agentsPath, agentsPath + '.backup');
2090
+ console.log(' Backup created: AGENTS.md.backup');
2091
+ } catch (err) {
2092
+ console.log(' Warning: Could not create backup');
2093
+ console.warn('Backup creation failed:', err.message);
2094
+ }
2095
+ skipFiles.agentsMd = false;
2096
+ skipFiles.useSemanticMerge = false;
2097
+ console.log(' Will replace AGENTS.md');
2098
+ validChoice = true;
2099
+ } else {
2100
+ console.log(' Please enter 1, 2, or 3');
2101
+ }
2102
+ }
2103
+ }
2104
+
2105
+ /**
2106
+ * Prompt for file overwrite and update skipFiles
2107
+ * Enhanced: For AGENTS.md without markers, offers intelligent merge option
2108
+ */
2109
+ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
2110
+ if (!exists) return;
2111
+
2112
+ const fileLabels = {
2113
+ agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
2114
+ claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
2115
+ };
2116
+
2117
+ const config = fileLabels[fileType];
2118
+ if (!config) return;
2119
+
2120
+ // Enhanced: For AGENTS.md, check if it has Forge markers
2121
+ if (fileType === 'agentsMd') {
2122
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
2123
+ const existingContent = fs.readFileSync(agentsPath, 'utf8');
2124
+ const hasUserMarkers = existingContent.includes('<!-- USER:START');
2125
+ const hasForgeMarkers = existingContent.includes('<!-- FORGE:START');
2126
+
2127
+ if (!hasUserMarkers && !hasForgeMarkers) {
2128
+ // No markers - offer 3 options via helper function
2129
+ await promptForAgentsMdWithoutMarkers(question, skipFiles, agentsPath);
2130
+ return;
2131
+ }
2132
+ }
2133
+
2134
+ // Default behavior: Binary y/n for files with markers or .claude/commands
2135
+ const overwrite = await askYesNo(question, config.prompt, true);
2136
+ if (overwrite) {
2137
+ console.log(` Will overwrite ${config.message}`);
2138
+ } else {
2139
+ skipFiles[config.key] = true;
2140
+ console.log(` Keeping existing ${config.message}`);
2141
+ }
2142
+ }
2143
+
2144
+ /**
2145
+ * Display agent selection options
2146
+ */
2147
+ function displayAgentOptions(agentKeys) {
2148
+ console.log('STEP 1: Select AI Coding Agents');
2149
+ console.log('================================');
2150
+ console.log('');
2151
+ console.log('Which AI coding agents do you use?');
2152
+ console.log('(Enter numbers separated by spaces, or "all")');
2153
+ console.log('');
2154
+
2155
+ agentKeys.forEach((key, index) => {
2156
+ const agent = AGENTS[key];
2157
+ console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
2158
+ });
2159
+ console.log('');
2160
+ console.log(' all) Install for all agents');
2161
+ console.log('');
2162
+ }
2163
+
2164
+ /**
2165
+ * Validate and parse agent selection input
2166
+ */
2167
+ function validateAgentSelection(input, agentKeys) {
2168
+ // Handle empty input
2169
+ if (!input?.trim()) {
2170
+ return { valid: false, agents: [], message: 'Please enter at least one agent number or "all".' };
2171
+ }
2172
+
2173
+ // Handle "all" selection
2174
+ if (input.toLowerCase() === 'all') {
2175
+ return { valid: true, agents: agentKeys, message: null };
2176
+ }
2177
+
2178
+ // Parse numbers
2179
+ const nums = input.split(/[\s,]+/).map(n => Number.parseInt(n.trim())).filter(n => !Number.isNaN(n));
2180
+
2181
+ // Validate numbers are in range
2182
+ const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
2183
+ const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
2184
+
2185
+ if (invalidNums.length > 0) {
2186
+ console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
2187
+ }
2188
+
2189
+ // Deduplicate selected agents using Set
2190
+ const selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
2191
+
2192
+ if (selectedAgents.length === 0) {
2193
+ return { valid: false, agents: [], message: 'No valid agents selected. Please try again.' };
2194
+ }
2195
+
2196
+ return { valid: true, agents: selectedAgents, message: null };
2197
+ }
2198
+
2199
+ /**
2200
+ * Prompt for agent selection with validation loop
2201
+ */
2202
+ async function promptForAgentSelection(question, agentKeys) {
2203
+ displayAgentOptions(agentKeys);
2204
+
2205
+ let selectedAgents = [];
2206
+
2207
+ // Loop until valid input is provided
2208
+ while (selectedAgents.length === 0) {
2209
+ const answer = await question('Your selection: ');
2210
+ const result = validateAgentSelection(answer, agentKeys);
2211
+
2212
+ if (result.valid) {
2213
+ selectedAgents = result.agents;
2214
+ } else if (result.message) {
2215
+ console.log(` ${result.message}`);
2216
+ }
2217
+ }
2218
+
2219
+ return selectedAgents;
2220
+ }
2221
+
2222
+ /**
2223
+ * Attempt semantic merge with fallback to replace
2224
+ * Reduces cognitive complexity by extracting merge logic (S3776)
2225
+ * @param {string} destPath - Destination file path
2226
+ * @param {string} existingContent - Existing file content
2227
+ * @param {string} newContent - New template content
2228
+ * @param {string} srcPath - Source template path
2229
+ */
2230
+ function trySemanticMerge(destPath, existingContent, newContent, srcPath) {
2231
+ try {
2232
+ // Add markers to enable future marker-based updates
2233
+ const semanticMerged = contextMerge.semanticMerge(existingContent, newContent, {
2234
+ addMarkers: true
2235
+ });
2236
+ fs.writeFileSync(destPath, semanticMerged, 'utf8');
2237
+ console.log(' Updated: AGENTS.md (intelligent merge - preserved your content)');
2238
+ console.log(' Note: Added USER/FORGE markers for future updates');
2239
+ } catch (error) {
2240
+ console.log(` Warning: Semantic merge failed (${error.message}), using replace strategy`);
2241
+ if (copyFile(srcPath, 'AGENTS.md')) {
2242
+ console.log(' Updated: AGENTS.md (universal standard)');
2243
+ }
2244
+ }
2245
+ }
2246
+
2247
+ /**
2248
+ * Handle AGENTS.md installation
2249
+ */
2250
+ async function installAgentsMd(skipFiles) {
2251
+ if (skipFiles.agentsMd) {
2252
+ console.log(' Skipped: AGENTS.md (keeping existing)');
2253
+ return;
2254
+ }
2255
+
2256
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
2257
+ const agentsDest = path.join(projectRoot, 'AGENTS.md');
2258
+
2259
+ // Try smart merge if file exists
2260
+ if (fs.existsSync(agentsDest)) {
2261
+ const existingContent = fs.readFileSync(agentsDest, 'utf8');
2262
+ const newContent = fs.readFileSync(agentsSrc, 'utf8');
2263
+ const merged = smartMergeAgentsMd(existingContent, newContent);
2264
+
2265
+ if (merged) {
2266
+ // Has markers - use existing smart merge
2267
+ fs.writeFileSync(agentsDest, merged, 'utf8');
2268
+ console.log(' Updated: AGENTS.md (preserved USER sections)');
2269
+ } else if (skipFiles.useSemanticMerge) {
2270
+ // Enhanced: No markers but user chose intelligent merge
2271
+ trySemanticMerge(agentsDest, existingContent, newContent, agentsSrc);
2272
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2273
+ // No markers, do normal copy (user already approved overwrite)
2274
+ console.log(' Updated: AGENTS.md (universal standard)');
2275
+ }
2276
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2277
+ // New file
2278
+ console.log(' Created: AGENTS.md (universal standard)');
2279
+
2280
+ // Detect project type and update AGENTS.md
2281
+ const detection = detectProjectType();
2282
+ if (detection.hasPackageJson) {
2283
+ updateAgentsMdWithProjectType(detection);
2284
+ displayProjectType(detection);
2285
+ }
2286
+ }
2287
+ }
2288
+
2289
+ /**
2290
+ * Load Claude commands for conversion
2291
+ */
2292
+ function loadClaudeCommands(selectedAgents) {
2293
+ const claudeCommands = {};
2294
+ const needsClaudeCommands = selectedAgents.includes('claude') ||
2295
+ selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
2296
+
2297
+ if (!needsClaudeCommands) {
2298
+ return claudeCommands;
2299
+ }
2300
+
2301
+ COMMANDS.forEach(cmd => {
2302
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
2303
+ const content = readFile(cmdPath);
2304
+ if (content) {
2305
+ claudeCommands[`${cmd}.md`] = content;
2306
+ }
2307
+ });
2308
+
2309
+ return claudeCommands;
2310
+ }
2311
+
2312
+ /**
2313
+ * Setup agents with progress indication
2314
+ * Delegates to setupSelectedAgents to avoid duplicate implementations (S4144)
2315
+ */
2316
+ function setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles) {
2317
+ setupSelectedAgents(selectedAgents, claudeCommands, skipFiles);
2318
+ }
2319
+
2320
+ /**
2321
+ * Display final setup summary
2322
+ */
2323
+ function displaySetupSummary(selectedAgents) {
2324
+ console.log('');
2325
+ console.log('==============================================');
2326
+ console.log(` Forge v${VERSION} Setup Complete!`);
2327
+ console.log('==============================================');
2328
+ console.log('');
2329
+ console.log('What\'s installed:');
2330
+ console.log(' - AGENTS.md (universal instructions)');
2331
+ console.log(' - docs/WORKFLOW.md (full workflow guide)');
2332
+ console.log(' - docs/research/TEMPLATE.md (research template)');
2333
+ console.log(' - docs/planning/PROGRESS.md (progress tracking)');
2334
+
2335
+ selectedAgents.forEach(key => {
2336
+ const agent = AGENTS[key];
2337
+ if (agent.linkFile) {
2338
+ console.log(` - ${agent.linkFile} (${agent.name})`);
2339
+ }
2340
+ if (agent.hasCommands) {
2341
+ console.log(` - .claude/commands/ (9 workflow commands)`);
2342
+ }
2343
+ if (agent.hasSkill) {
2344
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
2345
+ if (skillDir) {
2346
+ console.log(` - ${skillDir}/SKILL.md`);
2347
+ }
2348
+ }
2349
+ });
2350
+
2351
+ console.log('');
2352
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2353
+ console.log('📋 NEXT STEP - Complete AGENTS.md');
2354
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2355
+ console.log('');
2356
+ console.log('Ask your AI agent:');
2357
+ console.log(' "Fill in the project description in AGENTS.md"');
2358
+ console.log('');
2359
+ console.log('The agent will:');
2360
+ console.log(' ✓ Add one-sentence project description');
2361
+ console.log(' ✓ Confirm package manager');
2362
+ console.log(' ✓ Verify build commands');
2363
+ console.log('');
2364
+ console.log('Takes ~30 seconds. Done!');
2365
+ console.log('');
2366
+ console.log('💡 As you work: Add project patterns to AGENTS.md');
2367
+ console.log(' USER:START section. Keep it minimal - budget is');
2368
+ console.log(' ~150-200 instructions max.');
2369
+ console.log('');
2370
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2371
+ console.log('');
2372
+ console.log('Project Tools Status:');
2373
+ console.log('');
2374
+
2375
+ // Beads status
2376
+ if (isBeadsInitialized()) {
2377
+ console.log(' ✓ Beads initialized - Track work: bd ready');
2378
+ } else if (checkForBeads()) {
2379
+ console.log(' ! Beads available - Run: bd init');
2380
+ } else {
2381
+ console.log(` - Beads not installed - Run: ${PKG_MANAGER} install -g @beads/bd && bd init`);
2382
+ }
2383
+
2384
+ // Skills status
2385
+ if (isSkillsInitialized()) {
2386
+ console.log(' ✓ Skills initialized - Manage skills: skills list');
2387
+ } else if (checkForSkills()) {
2388
+ console.log(' ! Skills available - Run: skills init');
2389
+ } else {
2390
+ console.log(` - Skills not installed - Run: ${PKG_MANAGER} install -g @forge/skills`);
2391
+ }
2392
+
2393
+ console.log('');
2394
+ console.log('Start with: /status');
2395
+ console.log('');
2396
+ console.log(`Package manager: ${PKG_MANAGER}`);
2397
+ console.log('');
2398
+ }
2399
+
2400
+
2401
+ // Interactive setup
2402
+ // @private - Currently unused, reserved for future interactive flow
2403
+ async function _interactiveSetup() {
2404
+ const rl = readline.createInterface({
2405
+ input: process.stdin,
2406
+ output: process.stdout
2407
+ });
2408
+
2409
+ let setupCompleted = false;
2410
+
2411
+ // Handle Ctrl+C gracefully
2412
+ rl.on('close', () => {
2413
+ if (!setupCompleted) {
2414
+ console.log('\n\nSetup cancelled.');
2415
+ process.exit(0);
2416
+ }
2417
+ });
2418
+
2419
+ // Handle input errors
2420
+ rl.on('error', (err) => {
2421
+ console.error('Input error:', err.message);
2422
+ process.exit(1);
2423
+ });
2424
+
2425
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
2426
+
2427
+ showBanner('Agent Configuration');
2428
+
2429
+ // Show target directory
2430
+ console.log(` Target directory: ${process.cwd()}`);
2431
+ console.log(' (Use --path <dir> to change target directory)');
2432
+ console.log('');
2433
+
2434
+ // Check prerequisites first
2435
+ checkPrerequisites();
2436
+ console.log('');
2437
+
2438
+ // =============================================
2439
+ // PROJECT DETECTION
2440
+ // =============================================
2441
+ const projectStatus = await detectProjectStatus();
2442
+ displayInstallationStatus(projectStatus);
2443
+
2444
+ // Track which files to skip based on user choices
2445
+ const skipFiles = {
2446
+ agentsMd: false,
2447
+ claudeCommands: false
2448
+ };
2449
+
2450
+ // Ask about overwriting existing files
2451
+ await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
2452
+ await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
2453
+
2454
+ if (projectStatus.type !== 'fresh') {
2455
+ console.log('');
2456
+ }
2457
+
2458
+ // =============================================
2459
+ // STEP 1: Agent Selection
2460
+ // =============================================
2461
+ const agentKeys = Object.keys(AGENTS);
2462
+ const selectedAgents = await promptForAgentSelection(question, agentKeys);
2463
+
2464
+ console.log('');
2465
+ console.log('Installing Forge workflow...');
2466
+
2467
+ // Install AGENTS.md
2468
+ await installAgentsMd(skipFiles);
2469
+ console.log('');
2470
+
2471
+ // Setup core documentation
2472
+ setupCoreDocs();
2473
+ console.log('');
2474
+
2475
+ // Load Claude commands if needed
2476
+ let claudeCommands = {};
2477
+ if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
2478
+ // First ensure Claude is set up
2479
+ if (selectedAgents.includes('claude')) {
2480
+ setupAgent('claude', null, skipFiles);
2481
+ }
2482
+ // Then load the commands
2483
+ claudeCommands = loadClaudeCommands(selectedAgents);
2484
+ }
2485
+
2486
+ // Setup each selected agent with progress indication
2487
+ setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles);
2488
+
2489
+ // =============================================
2490
+ // STEP 2: Project Tools Setup
2491
+ // =============================================
2492
+ await setupProjectTools(rl, question);
2493
+
2494
+ // =============================================
2495
+ // STEP 3: External Services Configuration
2496
+ // =============================================
2497
+ console.log('');
2498
+ console.log('STEP 3: External Services (Optional)');
2499
+ console.log('=====================================');
2500
+
2501
+ await configureExternalServices(rl, question, selectedAgents, projectStatus);
2502
+
2503
+ setupCompleted = true;
2504
+ rl.close();
2505
+
2506
+ // =============================================
2507
+ // Final Summary
2508
+ // =============================================
2509
+ displaySetupSummary(selectedAgents);
2510
+ }
2511
+
2512
+ // Parse CLI flags
2513
+ function parseFlags() {
2514
+ const flags = {
2515
+ quick: false,
2516
+ skipExternal: false,
2517
+ agents: null,
2518
+ all: false,
2519
+ help: false,
2520
+ path: null,
2521
+ merge: null, // 'smart'|'preserve'|'replace'
2522
+ type: null, // 'critical'|'standard'|'simple'|'hotfix'|'docs'|'refactor'
2523
+ interview: false, // Force context interview
2524
+ budget: null, // Budget mode for recommend command
2525
+ };
2526
+
2527
+ for (let i = 0; i < args.length;) {
2528
+ const arg = args[i];
2529
+
2530
+ if (arg === '--quick' || arg === '-q') {
2531
+ flags.quick = true;
2532
+ i++;
2533
+ } else if (arg === '--skip-external' || arg === '--skip-services') {
2534
+ flags.skipExternal = true;
2535
+ i++;
2536
+ } else if (arg === '--all') {
2537
+ flags.all = true;
2538
+ i++;
2539
+ } else if (arg === '--help' || arg === '-h') {
2540
+ flags.help = true;
2541
+ i++;
2542
+ } else if (arg === '--path' || arg === '-p' || arg.startsWith('--path=')) {
2543
+ const result = parsePathFlag(args, i);
2544
+ flags.path = result.value;
2545
+ i = result.nextIndex;
2546
+ } else if (arg === '--agents' || arg.startsWith('--agents=')) {
2547
+ const result = parseAgentsFlag(args, i);
2548
+ flags.agents = result.value;
2549
+ i = result.nextIndex;
2550
+ } else if (arg === '--merge' || arg.startsWith('--merge=')) {
2551
+ const result = parseMergeFlag(args, i);
2552
+ flags.merge = result.value;
2553
+ i = result.nextIndex;
2554
+ } else if (arg === '--type' || arg.startsWith('--type=')) {
2555
+ const result = parseTypeFlag(args, i);
2556
+ flags.type = result.value;
2557
+ i = result.nextIndex;
2558
+ } else if (arg === '--interview') {
2559
+ flags.interview = true;
2560
+ i++;
2561
+ } else if (arg === '--budget' || arg.startsWith('--budget=')) {
2562
+ if (arg.startsWith('--budget=')) {
2563
+ flags.budget = arg.split('=')[1];
2564
+ } else if (i + 1 < args.length) {
2565
+ flags.budget = args[i + 1];
2566
+ i++;
2567
+ }
2568
+ i++;
2569
+ } else {
2570
+ i++;
2571
+ }
2572
+ }
2573
+
2574
+ return flags;
2575
+ }
2576
+
2577
+ // Parse --path flag with validation - extracted to reduce complexity
2578
+ function parsePathFlag(args, i) {
2579
+ let inputPath = null;
2580
+ let nextIndex = i + 1;
2581
+
2582
+ if (args[i].startsWith('--path=')) {
2583
+ // --path=/some/dir format
2584
+ inputPath = args[i].replace('--path=', '');
2585
+ } else if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
2586
+ // --path <directory> format
2587
+ inputPath = args[i + 1];
2588
+ nextIndex = i + 2;
2589
+ }
2590
+
2591
+ if (inputPath) {
2592
+ const validation = validateUserInput(inputPath, 'directory_path');
2593
+ if (!validation.valid) {
2594
+ console.error(`Error: Invalid --path value: ${validation.error}`);
2595
+ process.exit(1);
2596
+ }
2597
+ }
2598
+
2599
+ return { value: inputPath, nextIndex };
2600
+ }
2601
+
2602
+ // Parse --agents flag with list - extracted to reduce complexity
2603
+ function parseAgentsFlag(args, i) {
2604
+ if (args[i].startsWith('--agents=')) {
2605
+ // --agents=claude,cursor format
2606
+ return { value: args[i].replace('--agents=', ''), nextIndex: i + 1 };
2607
+ }
2608
+
2609
+ // --agents claude cursor format
2610
+ const agentList = [];
2611
+ let j = i + 1;
2612
+ while (j < args.length && !args[j].startsWith('-')) {
2613
+ agentList.push(args[j]);
2614
+ j++;
2615
+ }
2616
+
2617
+ return { value: agentList.length > 0 ? agentList.join(',') : null, nextIndex: j };
2618
+ }
2619
+
2620
+ // Parse --merge flag with enum validation - extracted to reduce complexity
2621
+ function parseMergeFlag(args, i) {
2622
+ const validModes = ['smart', 'preserve', 'replace'];
2623
+ let mergeMode = null;
2624
+ let nextIndex = i + 1;
2625
+
2626
+ if (args[i].startsWith('--merge=')) {
2627
+ // --merge=smart format
2628
+ mergeMode = args[i].replace('--merge=', '');
2629
+ } else if (i + 1 < args.length) {
2630
+ // --merge smart format
2631
+ mergeMode = args[i + 1];
2632
+ nextIndex = i + 2;
2633
+ } else {
2634
+ console.error('--merge requires a value: smart, preserve, or replace');
2635
+ process.exit(1);
2636
+ }
2637
+
2638
+ if (!validModes.includes(mergeMode)) {
2639
+ console.error(`Invalid --merge value: ${mergeMode}`);
2640
+ console.error('Valid options: smart, preserve, replace');
2641
+ process.exit(1);
2642
+ }
2643
+
2644
+ return { value: mergeMode, nextIndex };
2645
+ }
2646
+
2647
+ // Parse --type flag with enum validation - extracted to reduce complexity
2648
+ function parseTypeFlag(args, i) {
2649
+ const validTypes = ['critical', 'standard', 'simple', 'hotfix', 'docs', 'refactor'];
2650
+ let workType = null;
2651
+ let nextIndex = i + 1;
2652
+
2653
+ if (args[i].startsWith('--type=')) {
2654
+ // --type=critical format
2655
+ workType = args[i].replace('--type=', '');
2656
+ } else if (i + 1 < args.length) {
2657
+ // --type critical format
2658
+ workType = args[i + 1];
2659
+ nextIndex = i + 2;
2660
+ } else {
2661
+ console.error('--type requires a value');
2662
+ console.error(`Valid options: ${validTypes.join(', ')}`);
2663
+ process.exit(1);
2664
+ }
2665
+
2666
+ if (!validTypes.includes(workType)) {
2667
+ console.error(`Invalid --type value: ${workType}`);
2668
+ console.error(`Valid options: ${validTypes.join(', ')}`);
2669
+ process.exit(1);
2670
+ }
2671
+
2672
+ return { value: workType, nextIndex };
2673
+ }
2674
+
2675
+ // Validate agent names
2676
+ function validateAgents(agentList) {
2677
+ const requested = agentList.split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
2678
+ const valid = requested.filter(a => AGENTS[a]);
2679
+ const invalid = requested.filter(a => !AGENTS[a]);
2680
+
2681
+ if (invalid.length > 0) {
2682
+ console.log(` Warning: Unknown agents ignored: ${invalid.join(', ')}`);
2683
+ console.log(` Available agents: ${Object.keys(AGENTS).join(', ')}`);
2684
+ }
2685
+
2686
+ return valid;
2687
+ }
2688
+
2689
+ // Show help text
2690
+ function showHelp() {
2691
+ showBanner();
2692
+ console.log('');
2693
+ console.log('Usage:');
2694
+ console.log(' npx forge setup [options] Interactive agent configuration');
2695
+ console.log(' npx forge recommend Show recommended tools for your project');
2696
+ console.log(' npx forge Minimal install (AGENTS.md + docs)');
2697
+ console.log('');
2698
+ console.log('Options:');
2699
+ console.log(' --path, -p <dir> Target project directory (default: current directory)');
2700
+ console.log(' Creates the directory if it doesn\'t exist');
2701
+ console.log(' --quick, -q Use all defaults, minimal prompts');
2702
+ console.log(' Auto-selects: all agents, GitHub Code Quality, ESLint');
2703
+ console.log(' --skip-external Skip external services configuration');
2704
+ console.log(' --agents <list> Specify agents directly (skip selection prompt)');
2705
+ console.log(' Accepts: --agents claude cursor');
2706
+ console.log(' --agents=claude,cursor');
2707
+ console.log(' --all Install for all available agents');
2708
+ console.log(' --merge <mode> Merge strategy for existing AGENTS.md files');
2709
+ console.log(' Options: smart (intelligent merge), preserve (keep existing),');
2710
+ console.log(' replace (overwrite with new)');
2711
+ console.log(' --type <type> Set workflow profile type manually');
2712
+ console.log(' Options: critical, standard, simple, hotfix, docs, refactor');
2713
+ console.log(' --interview Force context interview (gather project information)');
2714
+ console.log(' --budget <mode> Budget mode for recommend (free, open-source, startup, professional, custom)');
2715
+ console.log(' --help, -h Show this help message');
2716
+ console.log('');
2717
+ console.log('Available agents:');
2718
+ Object.keys(AGENTS).forEach(key => {
2719
+ const agent = AGENTS[key];
2720
+ console.log(` ${key.padEnd(14)} ${agent.name.padEnd(20)} ${agent.description}`);
2721
+ });
2722
+ console.log('');
2723
+ console.log('Examples:');
2724
+ console.log(' npx forge setup # Interactive setup');
2725
+ console.log(' npx forge setup --quick # All defaults, no prompts');
2726
+ console.log(' npx forge setup -p ./my-project # Setup in specific directory');
2727
+ console.log(' npx forge setup --path=/home/user/app # Same, different syntax');
2728
+ console.log(' npx forge setup --agents claude cursor # Just these agents');
2729
+ console.log(' npx forge setup --agents=claude,cursor # Same, different syntax');
2730
+ console.log(' npx forge setup --skip-external # No service configuration');
2731
+ console.log(' npx forge setup --agents claude --quick # Quick + specific agent');
2732
+ console.log(' npx forge setup --all --skip-external # All agents, no services');
2733
+ console.log(' npx forge setup --merge=smart # Use intelligent merge for existing files');
2734
+ console.log(' npx forge setup --type=critical # Set workflow profile manually');
2735
+ console.log(' npx forge setup --interview # Force context interview');
2736
+ console.log('');
2737
+ console.log('Also works with bun:');
2738
+ console.log(' bunx forge setup --quick');
2739
+ console.log('');
2740
+ }
2741
+
2742
+ // Install git hooks via lefthook
2743
+ // SECURITY: Uses execSync with HARDCODED strings only (no user input)
2744
+ function installGitHooks() {
2745
+ console.log('Installing git hooks (TDD enforcement)...');
2746
+
2747
+ // Check if lefthook.yml exists (it should, as it's in the package)
2748
+ const lefthookConfig = path.join(packageDir, 'lefthook.yml');
2749
+ const targetHooks = path.join(projectRoot, '.forge/hooks');
2750
+
2751
+ try {
2752
+ // Copy lefthook.yml to project root
2753
+ const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
2754
+ if (!fs.existsSync(lefthookTarget)) {
2755
+ if (copyFile(lefthookConfig, 'lefthook.yml')) {
2756
+ console.log(' ✓ Created lefthook.yml');
2757
+ }
2758
+ }
2759
+
2760
+ // Copy check-tdd.js hook script
2761
+ const hookSource = path.join(packageDir, '.forge/hooks/check-tdd.js');
2762
+ if (fs.existsSync(hookSource)) {
2763
+ // Ensure .forge/hooks directory exists
2764
+ if (!fs.existsSync(targetHooks)) {
2765
+ fs.mkdirSync(targetHooks, { recursive: true });
2766
+ }
2767
+
2768
+ const hookTarget = path.join(targetHooks, 'check-tdd.js');
2769
+ if (copyFile(hookSource, hookTarget)) {
2770
+ console.log(' ✓ Created .forge/hooks/check-tdd.js');
2771
+
2772
+ // Make hook executable (Unix systems)
2773
+ try {
2774
+ fs.chmodSync(hookTarget, 0o755);
2775
+ } catch (err) {
2776
+ // Windows doesn't need chmod
2777
+ console.warn('chmod not available (Windows):', err.message);
2778
+ }
2779
+ }
2780
+ }
2781
+
2782
+ // Try to install lefthook hooks
2783
+ // SECURITY: Using execFileSync with hardcoded commands (no user input)
2784
+ try {
2785
+ // Try npx first (local install), fallback to global
2786
+ try {
2787
+ secureExecFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
2788
+ console.log(' ✓ Lefthook hooks installed (local)');
2789
+ } catch (error_) {
2790
+ // Fallback to global lefthook
2791
+ console.warn('npx lefthook failed, trying global:', error_.message);
2792
+ execFileSync('lefthook', ['version'], { stdio: 'ignore' });
2793
+ execFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
2794
+ console.log(' ✓ Lefthook hooks installed (global)');
2795
+ }
2796
+ } catch (err) {
2797
+ console.warn('Lefthook installation failed:', err.message);
2798
+ console.log(' ℹ Lefthook not found. Install it:');
2799
+ console.log(' bun add -d lefthook (recommended)');
2800
+ console.log(' OR: bun add -g lefthook (global)');
2801
+ console.log(' Then run: bunx lefthook install');
2802
+ }
2803
+
2804
+ console.log('');
2805
+
2806
+ } catch (error) {
2807
+ console.log(' ⚠ Failed to install hooks:', error.message);
2808
+ console.log(' You can install manually later with: lefthook install');
2809
+ console.log('');
2810
+ }
2811
+ }
2812
+
2813
+ // Check if lefthook is already installed in project
2814
+ function checkForLefthook() {
2815
+ const pkgPath = path.join(projectRoot, 'package.json');
2816
+ if (!fs.existsSync(pkgPath)) return false;
2817
+
2818
+ try {
2819
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2820
+ return Boolean(pkg.devDependencies?.lefthook || pkg.dependencies?.lefthook);
2821
+ } catch (err) {
2822
+ console.warn('Failed to check lefthook in package.json:', err.message);
2823
+ return false;
2824
+ }
2825
+ }
2826
+
2827
+ // Check if Beads is installed (global, local, or bunx-capable)
2828
+ function checkForBeads() {
2829
+ // Try global install first
2830
+ try {
2831
+ secureExecFileSync('bd', ['version'], { stdio: 'ignore' });
2832
+ return 'global';
2833
+ } catch (err) {
2834
+ // Not global
2835
+ console.warn('Beads not found globally:', err.message);
2836
+ }
2837
+
2838
+ // Check if bunx can run it
2839
+ try {
2840
+ secureExecFileSync('bunx', ['@beads/bd', 'version'], { stdio: 'ignore' });
2841
+ return 'bunx';
2842
+ } catch (err) {
2843
+ // Not bunx-capable
2844
+ console.warn('Beads not available via bunx:', err.message);
2845
+ }
2846
+
2847
+ // Check local project installation
2848
+ const pkgPath = path.join(projectRoot, 'package.json');
2849
+ if (!fs.existsSync(pkgPath)) return null;
2850
+
2851
+ try {
2852
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2853
+ const isInstalled = pkg.devDependencies?.['@beads/bd'] || pkg.dependencies?.['@beads/bd'];
2854
+ return isInstalled ? 'local' : null;
2855
+ } catch (err) {
2856
+ console.warn('Failed to check Beads in package.json:', err.message);
2857
+ return null;
2858
+ }
2859
+ }
2860
+ // Check if Beads is initialized in project
2861
+ function isBeadsInitialized() {
2862
+ return fs.existsSync(path.join(projectRoot, '.beads'));
2863
+ }
2864
+
2865
+ // Initialize Beads in the project
2866
+ function initializeBeads(installType) {
2867
+ console.log('Initializing Beads in project...');
2868
+
2869
+ try {
2870
+ // SECURITY: execFileSync with hardcoded commands
2871
+ if (installType === 'global') {
2872
+ secureExecFileSync('bd', ['init'], { stdio: 'inherit', cwd: projectRoot });
2873
+ } else if (installType === 'bunx') {
2874
+ secureExecFileSync('bunx', ['@beads/bd', 'init'], { stdio: 'inherit', cwd: projectRoot });
2875
+ } else if (installType === 'local') {
2876
+ secureExecFileSync('npx', ['bd', 'init'], { stdio: 'inherit', cwd: projectRoot });
2877
+ }
2878
+ console.log(' ✓ Beads initialized');
2879
+ return true;
2880
+ } catch (err) {
2881
+ console.log(' ⚠ Failed to initialize Beads:', err.message);
2882
+ console.log(' Run manually: bd init');
2883
+ return false;
2884
+ }
2885
+ }
2886
+
2887
+ // Check if Skills CLI is installed
2888
+ function checkForSkills() {
2889
+ // Try global install first
2890
+ try {
2891
+ secureExecFileSync('skills', ['--version'], { stdio: 'ignore' });
2892
+ return 'global';
2893
+ } catch (_err) { // NOSONAR - S2486: Expected when Skills is not installed globally
2894
+ }
2895
+
2896
+ // Check if bunx can run it
2897
+ try {
2898
+ secureExecFileSync('bunx', ['@forge/skills', '--version'], { stdio: 'ignore' });
2899
+ return 'bunx';
2900
+ } catch (_err) { // NOSONAR - S2486: Expected when Skills is not available via bunx
2901
+ }
2902
+
2903
+ // Check local project installation
2904
+ const pkgPath = path.join(projectRoot, 'package.json');
2905
+ if (!fs.existsSync(pkgPath)) return null;
2906
+
2907
+ try {
2908
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2909
+ const isInstalled = pkg.devDependencies?.['@forge/skills'] || pkg.dependencies?.['@forge/skills'];
2910
+ return isInstalled ? 'local' : null;
2911
+ } catch (_err) { // NOSONAR - S2486: Returns null on malformed package.json
2912
+ return null;
2913
+ }
2914
+ }
2915
+
2916
+ // Check if Skills is initialized in project
2917
+ function isSkillsInitialized() {
2918
+ return fs.existsSync(path.join(projectRoot, '.skills'));
2919
+ }
2920
+
2921
+ // Initialize Skills in the project
2922
+ function initializeSkills(installType) {
2923
+ console.log('Initializing Skills in project...');
2924
+
2925
+ try {
2926
+ // Using secureExecFileSync to validate PATH and mitigate S4036
2927
+ if (installType === 'global') {
2928
+ secureExecFileSync('skills', ['init'], { stdio: 'inherit', cwd: projectRoot });
2929
+ } else if (installType === 'bunx') {
2930
+ secureExecFileSync('bunx', ['@forge/skills', 'init'], { stdio: 'inherit', cwd: projectRoot });
2931
+ } else if (installType === 'local') {
2932
+ secureExecFileSync('npx', ['skills', 'init'], { stdio: 'inherit', cwd: projectRoot });
2933
+ }
2934
+ console.log(' ✓ Skills initialized');
2935
+ return true;
2936
+ } catch (err) {
2937
+ console.log(' ⚠ Failed to initialize Skills:', err.message);
2938
+ console.log(' Run manually: skills init');
2939
+ return false;
2940
+ }
2941
+ }
2942
+
2943
+ // Prompt for Beads setup - extracted to reduce cognitive complexity
2944
+ async function promptBeadsSetup(question) {
2945
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2946
+ console.log('Beads Setup (Recommended)');
2947
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2948
+ console.log('');
2949
+
2950
+ const beadsInitialized = isBeadsInitialized();
2951
+ const beadsStatus = checkForBeads();
2952
+
2953
+ if (beadsInitialized) {
2954
+ console.log('✓ Beads is already initialized in this project');
2955
+ console.log('');
2956
+ return;
2957
+ }
2958
+
2959
+ if (beadsStatus) {
2960
+ // Already installed, just need to initialize
2961
+ console.log(`ℹ Beads is installed (${beadsStatus}), but not initialized`);
2962
+ const initBeads = await question('Initialize Beads in this project? (y/n): ');
2963
+
2964
+ if (initBeads.toLowerCase() === 'y') {
2965
+ initializeBeads(beadsStatus);
2966
+ } else {
2967
+ console.log('Skipped Beads initialization. Run manually: bd init');
2968
+ }
2969
+ console.log('');
2970
+ return;
2971
+ }
2972
+
2973
+ // Not installed
2974
+ console.log('ℹ Beads is not installed');
2975
+ const installBeads = await question('Install Beads? (y/n): ');
2976
+
2977
+ if (installBeads.toLowerCase() !== 'y') {
2978
+ console.log('Skipped Beads installation');
2979
+ console.log('');
2980
+ return;
2981
+ }
2982
+
2983
+ console.log('');
2984
+ console.log('Choose installation method:');
2985
+ console.log(' 1. Global (recommended) - Available system-wide');
2986
+ console.log(' 2. Local - Project-specific devDependency');
2987
+ console.log(' 3. Bunx - Use via bunx (requires bun)');
2988
+ console.log('');
2989
+ const method = await question('Choose method (1-3): ');
2990
+
2991
+ console.log('');
2992
+ installBeadsWithMethod(method);
2993
+ console.log('');
2994
+ }
2995
+
2996
+ // Helper: Install tool via bunx - extracted to reduce cognitive complexity
2997
+ function installViaBunx(packageName, versionArgs, initFn, toolName) {
2998
+ console.log('Testing bunx capability...');
2999
+ try {
3000
+ secureExecFileSync('bunx', [packageName, ...versionArgs], { stdio: 'ignore' });
3001
+ console.log(' ✓ Bunx is available');
3002
+ initFn('bunx');
3003
+ } catch (err) {
3004
+ console.warn(`${toolName} bunx test failed:`, err.message);
3005
+ console.log(' ⚠ Bunx not available. Install bun first: curl -fsSL https://bun.sh/install | bash');
3006
+ }
3007
+ }
3008
+
3009
+ // Helper: Install Beads with chosen method - extracted to reduce cognitive complexity
3010
+ // SECURITY NOTE: Downloads and executes a remote PowerShell script.
3011
+ // The npm @beads/bd package is broken on Windows (GitHub Issue #1031, closed "not planned"),
3012
+ // so the official PowerShell installer is the only supported path.
3013
+ // Mitigations: HTTPS transport (prevents MITM), official beads repo, user-visible URL.
3014
+ // TODO: Pin to a versioned release tag once beads publishes tagged releases (e.g. v0.49.1).
3015
+ const BEADS_INSTALL_PS1_URL = 'https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1';
3016
+
3017
+ function installBeadsOnWindows() {
3018
+ console.log(' (Windows detected: using PowerShell installer)');
3019
+ console.log(` Downloading: ${BEADS_INSTALL_PS1_URL}`);
3020
+ secureExecFileSync('powershell.exe', [
3021
+ '-NoProfile', '-NonInteractive', '-Command',
3022
+ `irm ${BEADS_INSTALL_PS1_URL} | iex`
3023
+ ], { stdio: 'inherit' });
3024
+ }
3025
+
3026
+ function installBeadsWithMethod(method) {
3027
+ try {
3028
+ // SECURITY: secureExecFileSync with hardcoded commands
3029
+ if (method === '1') {
3030
+ console.log('Installing Beads globally...');
3031
+ if (process.platform === 'win32') {
3032
+ installBeadsOnWindows();
3033
+ } else {
3034
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
3035
+ secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
3036
+ }
3037
+ console.log(' ✓ Beads installed globally');
3038
+ initializeBeads('global');
3039
+ } else if (method === '2') {
3040
+ console.log('Installing Beads locally...');
3041
+ // On Windows, npm postinstall for @beads/bd runs Expand-Archive which has EPERM file-locking
3042
+ // (GitHub Issue #1031, closed "not planned") — same root cause as global install.
3043
+ // Redirect Windows users to the global PowerShell installer instead.
3044
+ if (process.platform === 'win32') {
3045
+ console.log(' ⚠ Local install not supported on Windows (npm @beads/bd EPERM issue).');
3046
+ console.log(' Falling back to global PowerShell installer...');
3047
+ installBeadsOnWindows();
3048
+ } else {
3049
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
3050
+ secureExecFileSync(pkgManager, ['install', '-D', '@beads/bd'], { stdio: 'inherit', cwd: projectRoot });
3051
+ }
3052
+ console.log(' ✓ Beads installed');
3053
+ // On Windows the fallback was global (PowerShell installer), so init as 'global'
3054
+ initializeBeads(process.platform === 'win32' ? 'global' : 'local');
3055
+ } else if (method === '3') {
3056
+ installViaBunx('@beads/bd', ['version'], initializeBeads, 'Beads');
3057
+ } else {
3058
+ console.log('Invalid choice. Skipping Beads installation.');
3059
+ }
3060
+ } catch (err) {
3061
+ console.warn('Beads installation failed:', err.message);
3062
+ console.log(' ⚠ Failed to install Beads:', err.message);
3063
+ if (process.platform === 'win32') {
3064
+ console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
3065
+ } else {
3066
+ console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
3067
+ }
3068
+ }
3069
+ }
3070
+
3071
+ // Helper: Get package-manager-specific install args for Skills
3072
+ function getSkillsInstallArgs(scope) {
3073
+ const globalFlag = scope === 'global' ? '-g' : '-D';
3074
+ if (PKG_MANAGER === 'yarn' && scope === 'global') {
3075
+ return ['global', 'add', '@forge/skills'];
3076
+ }
3077
+ const cmd = (PKG_MANAGER === 'bun' || PKG_MANAGER === 'pnpm') ? 'add' : 'install';
3078
+ return [cmd, globalFlag, '@forge/skills'];
3079
+ }
3080
+
3081
+ // Helper: Install Skills with chosen method - extracted to reduce cognitive complexity
3082
+ function installSkillsWithMethod(method) {
3083
+ try {
3084
+ if (method === '1') {
3085
+ console.log('Installing Skills globally...');
3086
+ secureExecFileSync(PKG_MANAGER, getSkillsInstallArgs('global'), { stdio: 'inherit' });
3087
+ console.log(' ✓ Skills installed globally');
3088
+ initializeSkills('global');
3089
+ } else if (method === '2') {
3090
+ console.log('Installing Skills locally...');
3091
+ secureExecFileSync(PKG_MANAGER, getSkillsInstallArgs('local'), { stdio: 'inherit', cwd: projectRoot });
3092
+ console.log(' ✓ Skills installed locally');
3093
+ initializeSkills('local');
3094
+ } else if (method === '3') {
3095
+ installViaBunx('@forge/skills', ['--version'], initializeSkills, 'Skills');
3096
+ } else {
3097
+ console.log('Invalid choice. Skipping Skills installation.');
3098
+ }
3099
+ } catch (err) {
3100
+ console.warn('Skills installation failed:', err.message);
3101
+ console.log(' ⚠ Failed to install Skills:', err.message);
3102
+ console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @forge/skills && skills init`);
3103
+ }
3104
+ }
3105
+
3106
+ // Prompt for Skills setup - extracted to reduce cognitive complexity
3107
+ async function promptSkillsSetup(question) {
3108
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
3109
+ console.log('Skills CLI Setup (Recommended)');
3110
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
3111
+ console.log('');
3112
+
3113
+ const skillsInitialized = isSkillsInitialized();
3114
+ const skillsStatus = checkForSkills();
3115
+
3116
+ if (skillsInitialized) {
3117
+ console.log('✓ Skills is already initialized in this project');
3118
+ console.log('');
3119
+ return;
3120
+ }
3121
+
3122
+ if (skillsStatus) {
3123
+ // Already installed, just need to initialize
3124
+ console.log(`ℹ Skills is installed (${skillsStatus}), but not initialized`);
3125
+ const initSkills = await question('Initialize Skills in this project? (y/n): ');
3126
+
3127
+ if (initSkills.toLowerCase() === 'y') {
3128
+ initializeSkills(skillsStatus);
3129
+ } else {
3130
+ console.log('Skipped Skills initialization. Run manually: skills init');
3131
+ }
3132
+ console.log('');
3133
+ return;
3134
+ }
3135
+
3136
+ // Not installed
3137
+ console.log('ℹ Skills is not installed');
3138
+ const installSkills = await question('Install Skills CLI? (y/n): ');
3139
+
3140
+ if (installSkills.toLowerCase() !== 'y') {
3141
+ console.log('Skipped Skills installation');
3142
+ console.log('');
3143
+ return;
3144
+ }
3145
+
3146
+ console.log('');
3147
+ console.log('Choose installation method:');
3148
+ console.log(' 1. Global (recommended) - Available system-wide');
3149
+ console.log(' 2. Local - Project-specific devDependency');
3150
+ console.log(' 3. Bunx - Use via bunx (requires bun)');
3151
+ console.log('');
3152
+ const installMethod = await question('Choose installation method (1-3): ');
3153
+
3154
+ console.log('');
3155
+ installSkillsWithMethod(installMethod);
3156
+ console.log('');
3157
+ }
3158
+
3159
+ // Interactive setup for Beads and Skills
3160
+ async function setupProjectTools(rl, question) {
3161
+ console.log('');
3162
+ console.log('═══════════════════════════════════════════════════════════');
3163
+ console.log(' STEP 2: Project Tools (Recommended)');
3164
+ console.log('═══════════════════════════════════════════════════════════');
3165
+ console.log('');
3166
+ console.log('Forge recommends three tools for enhanced workflows:');
3167
+ console.log('');
3168
+ console.log('• Beads - Git-backed issue tracking');
3169
+ console.log(' Persists tasks across sessions, tracks dependencies.');
3170
+ console.log(' Command: bd ready, bd create, bd close');
3171
+ console.log('');
3172
+ console.log('• Skills - Universal SKILL.md management');
3173
+ console.log(' Manage AI agent skills across all agents.');
3174
+ console.log(' Command: skills create, skills list, skills sync');
3175
+ console.log('');
3176
+
3177
+ // Use helper functions to reduce complexity
3178
+ await promptBeadsSetup(question);
3179
+ await promptSkillsSetup(question);
3180
+ }
3181
+
3182
+ // Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
3183
+ function autoSetupBeadsInQuickMode() {
3184
+ const beadsStatus = checkForBeads();
3185
+ const beadsInitialized = isBeadsInitialized();
3186
+
3187
+ if (!beadsInitialized && beadsStatus) {
3188
+ console.log('📦 Initializing Beads...');
3189
+ initializeBeads(beadsStatus);
3190
+ console.log('');
3191
+ } else if (!beadsInitialized && !beadsStatus) {
3192
+ console.log('📦 Installing Beads globally...');
3193
+ try {
3194
+ // SECURITY: use PowerShell on Windows (npm @beads/bd is broken on Windows - Issue #1031)
3195
+ if (process.platform === 'win32') {
3196
+ installBeadsOnWindows();
3197
+ } else {
3198
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
3199
+ secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
3200
+ }
3201
+ console.log(' ✓ Beads installed globally');
3202
+ initializeBeads('global');
3203
+ } catch (err) {
3204
+ // Installation failed - provide manual instructions
3205
+ console.log(' ⚠ Could not install Beads automatically');
3206
+ console.log(` Error: ${err.message}`);
3207
+ if (process.platform === 'win32') {
3208
+ console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
3209
+ } else {
3210
+ console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
3211
+ }
3212
+ }
3213
+ console.log('');
3214
+ }
3215
+ }
3216
+
3217
+ // Helper: Auto-install lefthook if not present - extracted to reduce cognitive complexity
3218
+ function autoInstallLefthook() {
3219
+ const hasLefthook = checkForLefthook();
3220
+ if (hasLefthook) return;
3221
+
3222
+ console.log('📦 Installing lefthook for git hooks...');
3223
+ try {
3224
+ // SECURITY: secureExecFileSync with PKG_MANAGER — cross-platform support
3225
+ const installArgs = PKG_MANAGER === 'yarn'
3226
+ ? ['add', '--dev', 'lefthook']
3227
+ : PKG_MANAGER === 'npm'
3228
+ ? ['install', '--save-dev', 'lefthook']
3229
+ : PKG_MANAGER === 'pnpm'
3230
+ ? ['add', '-D', 'lefthook'] // pnpm requires uppercase -D for devDependencies
3231
+ : ['add', '-d', 'lefthook']; // bun uses 'add -d'
3232
+ secureExecFileSync(PKG_MANAGER, installArgs, { stdio: 'inherit', cwd: projectRoot });
3233
+ console.log(' ✓ Lefthook installed');
3234
+ } catch (err) {
3235
+ console.warn('Lefthook auto-install failed:', err.message);
3236
+ console.log(' ⚠ Could not install lefthook automatically');
3237
+ console.log(` Run manually: ${PKG_MANAGER === 'yarn' ? 'yarn add --dev' : PKG_MANAGER === 'npm' ? 'npm install --save-dev' : PKG_MANAGER === 'pnpm' ? 'pnpm add -D' : 'bun add -d'} lefthook`);
3238
+ }
3239
+ console.log('');
3240
+ }
3241
+
3242
+ // Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
3243
+ function verifyToolInstall(command, args, toolName) {
3244
+ try {
3245
+ secureExecFileSync(command, args, { stdio: 'ignore' });
3246
+ return true;
3247
+ } catch (_err) { // NOSONAR - S2486: Intentionally ignored; verification failure is handled by caller
3248
+ console.log(` ⚠ ${toolName} installed but not callable. Check your PATH.`);
3249
+ return false;
3250
+ }
3251
+ }
3252
+
3253
+ // Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
3254
+ function autoSetupToolsInQuickMode() {
3255
+ // Beads: auto-install or initialize
3256
+ autoSetupBeadsInQuickMode();
3257
+
3258
+ // Post-install verification for Beads
3259
+ if (isBeadsInitialized()) {
3260
+ verifyToolInstall('bd', ['version'], 'Beads');
3261
+ }
3262
+
3263
+ // Skills: only initialize if already installed (recommended tool)
3264
+ const skillsStatus = checkForSkills();
3265
+ if (skillsStatus && !isSkillsInitialized()) {
3266
+ console.log('📦 Initializing Skills...');
3267
+ initializeSkills(skillsStatus);
3268
+ console.log('');
3269
+ } else if (!skillsStatus) {
3270
+ const installCmd = PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g';
3271
+ console.log(` ℹ Skills not found — install with: ${installCmd} @forge/skills`);
3272
+ console.log('');
3273
+ }
3274
+ }
3275
+
3276
+ // Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
3277
+ function configureDefaultExternalServices(skipExternal) {
3278
+ if (skipExternal) {
3279
+ console.log('');
3280
+ console.log('Skipping external services configuration...');
3281
+ return;
3282
+ }
3283
+
3284
+ console.log('');
3285
+ console.log('Configuring default services...');
3286
+ console.log('');
3287
+
3288
+ const tokens = {
3289
+ CODE_REVIEW_TOOL: 'github-code-quality',
3290
+ CODE_QUALITY_TOOL: 'eslint',
3291
+ PKG_MANAGER: PKG_MANAGER
3292
+ };
3293
+
3294
+ writeEnvTokens(tokens);
3295
+
3296
+ console.log(' * Code Review: GitHub Code Quality (FREE)');
3297
+ console.log(' * Code Quality: ESLint (built-in)');
3298
+ console.log('');
3299
+ console.log('Configuration saved to .env.local');
3300
+ }
3301
+
3302
+ // Quick setup with defaults
3303
+ async function quickSetup(selectedAgents, skipExternal) {
3304
+ showBanner('Quick Setup');
3305
+ console.log('');
3306
+ console.log('Quick mode: Using defaults...');
3307
+ console.log('');
3308
+
3309
+ // Check prerequisites
3310
+ checkPrerequisites();
3311
+ console.log('');
3312
+
3313
+ // Copy AGENTS.md
3314
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
3315
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
3316
+ console.log(' Created: AGENTS.md (universal standard)');
3317
+ }
3318
+ console.log('');
3319
+
3320
+ // Setup core documentation
3321
+ setupCoreDocs();
3322
+ console.log('');
3323
+
3324
+ // Auto-install lefthook if missing
3325
+ autoInstallLefthook();
3326
+
3327
+ // Auto-setup project tools (Beads, Skills)
3328
+ autoSetupToolsInQuickMode();
3329
+
3330
+ // Load Claude commands and setup agents (reuse existing helpers)
3331
+ const claudeCommands = loadAndSetupClaudeCommands(selectedAgents);
3332
+ setupSelectedAgents(selectedAgents, claudeCommands);
3333
+
3334
+ // Install git hooks for TDD enforcement
3335
+ console.log('');
3336
+ installGitHooks();
3337
+
3338
+ // Configure external services with defaults (unless skipped)
3339
+ configureDefaultExternalServices(skipExternal);
3340
+
3341
+ // Final summary
3342
+ console.log('');
3343
+ console.log('==============================================');
3344
+ console.log(` Forge v${VERSION} Quick Setup Complete!`);
3345
+ console.log('==============================================');
3346
+ console.log('');
3347
+ console.log('Next steps:');
3348
+ console.log(' 1. Start with: /status');
3349
+ console.log(' 2. Read the guide: docs/WORKFLOW.md');
3350
+ console.log('');
3351
+ console.log('Happy shipping!');
3352
+ console.log('');
3353
+ }
3354
+
3355
+ // Helper: Apply merge strategy to existing AGENTS.md - extracted to reduce cognitive complexity
3356
+ function applyAgentsMdMergeStrategy(mergeStrategy, agentsSrc, agentsDest, existingContent, newContent) {
3357
+ if (mergeStrategy === 'preserve') {
3358
+ console.log(' Preserved: AGENTS.md (--merge=preserve)');
3359
+ return;
3360
+ }
3361
+
3362
+ if (mergeStrategy === 'replace') {
3363
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
3364
+ console.log(' Replaced: AGENTS.md (--merge=replace)');
3365
+ }
3366
+ return;
3367
+ }
3368
+
3369
+ // Default: smart merge
3370
+ const merged = smartMergeAgentsMd(existingContent, newContent);
3371
+ if (merged) {
3372
+ fs.writeFileSync(agentsDest, merged, 'utf8');
3373
+ console.log(' Updated: AGENTS.md (smart merge, preserved USER sections)');
3374
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
3375
+ console.log(' Updated: AGENTS.md (universal standard)');
3376
+ }
3377
+ }
3378
+
3379
+ // Setup AGENTS.md file with merge strategy - extracted to reduce cognitive complexity
3380
+ function setupAgentsMdFile(flags, skipFiles) {
3381
+ if (skipFiles.agentsMd) {
3382
+ console.log(' Skipped: AGENTS.md (keeping existing)');
3383
+ return;
3384
+ }
3385
+
3386
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
3387
+ const agentsDest = path.join(projectRoot, 'AGENTS.md');
3388
+ const mergeStrategy = flags.merge || 'smart';
3389
+
3390
+ if (fs.existsSync(agentsDest)) {
3391
+ const existingContent = fs.readFileSync(agentsDest, 'utf8');
3392
+ const newContent = fs.readFileSync(agentsSrc, 'utf8');
3393
+ applyAgentsMdMergeStrategy(mergeStrategy, agentsSrc, agentsDest, existingContent, newContent);
3394
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
3395
+ console.log(' Created: AGENTS.md (universal standard)');
3396
+ const detection = detectProjectType();
3397
+ if (detection.hasPackageJson) {
3398
+ updateAgentsMdWithProjectType(detection);
3399
+ displayProjectType(detection);
3400
+ }
3401
+ }
3402
+ }
3403
+
3404
+ // Helper: Handle user-provided flags override - extracted to reduce cognitive complexity
3405
+ function handleFlagsOverride(flags, projectStatus) {
3406
+ if (!flags.type && !flags.interview) {
3407
+ return;
3408
+ }
3409
+
3410
+ console.log('User-provided flags:');
3411
+ if (flags.type) {
3412
+ console.log(` --type=${flags.type} (workflow profile override)`);
3413
+ saveWorkflowTypeOverride(flags.type, projectStatus.autoDetected);
3414
+ }
3415
+ if (flags.interview) {
3416
+ console.log(' --interview (context interview mode)');
3417
+ console.log(' Note: Enhanced context gathering is a future feature');
3418
+ }
3419
+ console.log('');
3420
+ }
3421
+
3422
+ // Helper: Save workflow type override to context - extracted to reduce cognitive complexity
3423
+ function saveWorkflowTypeOverride(type, autoDetected) {
3424
+ if (!autoDetected) {
3425
+ return;
3426
+ }
3427
+ try {
3428
+ const contextPath = path.join(projectRoot, '.forge', 'context.json');
3429
+ if (fs.existsSync(contextPath)) {
3430
+ const contextData = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
3431
+ contextData.user_provided = contextData.user_provided || {};
3432
+ contextData.user_provided.workflowType = type;
3433
+ contextData.last_updated = new Date().toISOString();
3434
+ fs.writeFileSync(contextPath, JSON.stringify(contextData, null, 2), 'utf8');
3435
+ }
3436
+ } catch (error) {
3437
+ console.warn(' Warning: Could not save workflow type override:', error.message);
3438
+ }
3439
+ }
3440
+
3441
+ // Helper: Display existing installation status - extracted to reduce cognitive complexity
3442
+ function displayExistingInstallation(projectStatus) {
3443
+ if (projectStatus.type === 'fresh') {
3444
+ return;
3445
+ }
3446
+
3447
+ console.log('==============================================');
3448
+ console.log(' Existing Installation Detected');
3449
+ console.log('==============================================');
3450
+ console.log('');
3451
+
3452
+ console.log(projectStatus.type === 'upgrade'
3453
+ ? 'Found existing Forge installation:'
3454
+ : 'Found partial installation:');
3455
+
3456
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
3457
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
3458
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
3459
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
3460
+ console.log('');
3461
+ }
3462
+
3463
+ // Helper: Prompt for overwrite decisions - extracted to reduce cognitive complexity
3464
+ async function promptForOverwriteDecisions(question, projectStatus) {
3465
+ const skipFiles = {
3466
+ agentsMd: false,
3467
+ claudeCommands: false
3468
+ };
3469
+
3470
+ if (projectStatus.hasAgentsMd) {
3471
+ const overwriteAgents = await askYesNo(question, 'Found existing AGENTS.md. Overwrite?', true);
3472
+ skipFiles.agentsMd = !overwriteAgents;
3473
+ console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
3474
+ }
3475
+
3476
+ if (projectStatus.hasClaudeCommands) {
3477
+ const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
3478
+ skipFiles.claudeCommands = !overwriteCommands;
3479
+ console.log(overwriteCommands ? ' Will overwrite .claude/commands/' : ' Keeping existing .claude/commands/');
3480
+ }
3481
+
3482
+ if (projectStatus.type !== 'fresh') {
3483
+ console.log('');
3484
+ }
3485
+
3486
+ return skipFiles;
3487
+ }
3488
+
3489
+ // Helper: Load and setup Claude commands - extracted to reduce cognitive complexity
3490
+ function loadAndSetupClaudeCommands(selectedAgents, skipFiles) {
3491
+ const claudeCommands = {};
3492
+ const needsClaudeCommands = selectedAgents.includes('claude') ||
3493
+ selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
3494
+
3495
+ if (!needsClaudeCommands) {
3496
+ return claudeCommands;
3497
+ }
3498
+
3499
+ // First ensure Claude is set up
3500
+ if (selectedAgents.includes('claude')) {
3501
+ setupAgent('claude', null, skipFiles);
3502
+ }
3503
+
3504
+ // Then load the commands (from existing or newly created)
3505
+ COMMANDS.forEach(cmd => {
3506
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
3507
+ const content = readFile(cmdPath);
3508
+ if (content) {
3509
+ claudeCommands[`${cmd}.md`] = content;
3510
+ }
3511
+ });
3512
+
3513
+ return claudeCommands;
3514
+ }
3515
+
3516
+ // Helper: Setup all selected agents - extracted to reduce cognitive complexity
3517
+ function setupSelectedAgents(selectedAgents, claudeCommands, skipFiles) {
3518
+ const totalAgents = selectedAgents.length;
3519
+ selectedAgents.forEach((agentKey, index) => {
3520
+ const agent = AGENTS[agentKey];
3521
+ console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
3522
+ if (agentKey !== 'claude') { // Claude already done above
3523
+ setupAgent(agentKey, claudeCommands, skipFiles);
3524
+ }
3525
+ });
3526
+
3527
+ console.log('');
3528
+ console.log('Agent configuration complete!');
3529
+ console.log('');
3530
+ console.log('Installed for:');
3531
+ selectedAgents.forEach(key => {
3532
+ const agent = AGENTS[key];
3533
+ console.log(` * ${agent.name}`);
3534
+ });
3535
+ }
3536
+
3537
+ // Helper: Configure external services step - extracted to reduce cognitive complexity
3538
+ async function handleExternalServicesStep(flags, rl, question, selectedAgents, projectStatus) {
3539
+ if (flags.skipExternal) {
3540
+ console.log('');
3541
+ console.log('Skipping external services configuration...');
3542
+ return;
3543
+ }
3544
+
3545
+ console.log('');
3546
+ console.log('STEP 2: External Services (Optional)');
3547
+ console.log('=====================================');
3548
+ await configureExternalServices(rl, question, selectedAgents, projectStatus);
3549
+ }
3550
+
3551
+ // Interactive setup with flag support
3552
+ async function interactiveSetupWithFlags(flags) {
3553
+ const rl = readline.createInterface({
3554
+ input: process.stdin,
3555
+ output: process.stdout
3556
+ });
3557
+
3558
+ let setupCompleted = false;
3559
+
3560
+ // Handle Ctrl+C gracefully
3561
+ rl.on('close', () => {
3562
+ if (!setupCompleted) {
3563
+ console.log('\n\nSetup cancelled.');
3564
+ process.exit(0);
3565
+ }
3566
+ });
3567
+
3568
+ // Handle input errors
3569
+ rl.on('error', (err) => {
3570
+ console.error('Input error:', err.message);
3571
+ process.exit(1);
3572
+ });
3573
+
3574
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
3575
+
3576
+ showBanner('Agent Configuration');
3577
+
3578
+ // Show target directory
3579
+ console.log(` Target directory: ${process.cwd()}`);
3580
+ console.log(' (Use --path <dir> to change target directory)');
3581
+ console.log('');
3582
+
3583
+ // Check prerequisites first
3584
+ checkPrerequisites();
3585
+ console.log('');
3586
+
3587
+ // PROJECT DETECTION
3588
+ const projectStatus = await detectProjectStatus();
3589
+
3590
+ // Handle user-provided flags to override auto-detection
3591
+ handleFlagsOverride(flags, projectStatus);
3592
+
3593
+ // Display existing installation status
3594
+ displayExistingInstallation(projectStatus);
3595
+
3596
+ // Prompt for overwrite decisions
3597
+ const skipFiles = await promptForOverwriteDecisions(question, projectStatus);
3598
+
3599
+ // STEP 1: Agent Selection (delegated to helper)
3600
+ const agentKeys = Object.keys(AGENTS);
3601
+ const selectedAgents = await promptForAgentSelection(question, agentKeys);
3602
+
3603
+ console.log('');
3604
+ console.log('Installing Forge workflow...');
3605
+
3606
+ // Setup AGENTS.md (delegated to helper)
3607
+ setupAgentsMdFile(flags, skipFiles);
3608
+ console.log('');
3609
+
3610
+ // Setup core documentation
3611
+ setupCoreDocs();
3612
+ console.log('');
3613
+
3614
+ // Load Claude commands if needed (delegated to helper)
3615
+ const claudeCommands = loadAndSetupClaudeCommands(selectedAgents, skipFiles);
3616
+
3617
+ // Setup each selected agent with progress indication (delegated to helper)
3618
+ setupSelectedAgents(selectedAgents, claudeCommands, skipFiles);
3619
+
3620
+ // Handle external services step (delegated to helper)
3621
+ await handleExternalServicesStep(flags, rl, question, selectedAgents, projectStatus);
3622
+
3623
+ setupCompleted = true;
3624
+ rl.close();
3625
+
3626
+ // Display final summary (delegated to helper)
3627
+ displaySetupSummary(selectedAgents);
3628
+ }
3629
+
3630
+ // Main
3631
+ // Helper: Handle --path setup
3632
+ function handlePathSetup(targetPath) {
3633
+ const resolvedPath = path.resolve(targetPath);
3634
+
3635
+ // Create directory if it doesn't exist
3636
+ if (!fs.existsSync(resolvedPath)) {
3637
+ try {
3638
+ fs.mkdirSync(resolvedPath, { recursive: true });
3639
+ console.log(`Created directory: ${resolvedPath}`);
3640
+ } catch (err) {
3641
+ console.error(`Error creating directory: ${err.message}`);
3642
+ process.exit(1);
3643
+ }
3644
+ }
3645
+
3646
+ // Verify it's a directory
3647
+ if (!fs.statSync(resolvedPath).isDirectory()) {
3648
+ console.error(`Error: ${resolvedPath} is not a directory`);
3649
+ process.exit(1);
3650
+ }
3651
+
3652
+ // Change to target directory
3653
+ try {
3654
+ process.chdir(resolvedPath);
3655
+ console.log(`Working directory: ${resolvedPath}`);
3656
+ console.log('');
3657
+ } catch (err) {
3658
+ console.error(`Error changing to directory: ${err.message}`);
3659
+ process.exit(1);
3660
+ }
3661
+
3662
+ // Return the resolved path so caller can update projectRoot
3663
+ return resolvedPath;
3664
+ }
3665
+
3666
+ // Helper: Determine selected agents from flags
3667
+ function determineSelectedAgents(flags) {
3668
+ if (flags.all) {
3669
+ return Object.keys(AGENTS);
3670
+ }
3671
+
3672
+ if (flags.agents) {
3673
+ const selectedAgents = validateAgents(flags.agents);
3674
+ if (selectedAgents.length === 0) {
3675
+ console.log('No valid agents specified.');
3676
+ console.log('Available agents:', Object.keys(AGENTS).join(', '));
3677
+ process.exit(1);
3678
+ }
3679
+ return selectedAgents;
3680
+ }
3681
+
3682
+ return [];
3683
+ }
3684
+
3685
+ // Helper: Handle setup command in non-quick mode
3686
+ async function handleSetupCommand(selectedAgents, flags) {
3687
+ showBanner('Installing for specified agents...');
3688
+ console.log('');
3689
+
3690
+ // Check prerequisites
3691
+ checkPrerequisites();
3692
+ console.log('');
3693
+
3694
+ // Copy AGENTS.md
3695
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
3696
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
3697
+ console.log(' Created: AGENTS.md (universal standard)');
3698
+ }
3699
+ console.log('');
3700
+
3701
+ // Setup core documentation
3702
+ setupCoreDocs();
3703
+ console.log('');
3704
+
3705
+ // Load Claude commands if needed
3706
+ const claudeCommands = loadClaudeCommands(selectedAgents);
3707
+
3708
+ // Setup agents
3709
+ selectedAgents.forEach(agentKey => {
3710
+ if (agentKey !== 'claude') {
3711
+ setupAgent(agentKey, claudeCommands);
3712
+ }
3713
+ });
3714
+
3715
+ console.log('');
3716
+ console.log('Agent configuration complete!');
3717
+
3718
+ // Install git hooks for TDD enforcement
3719
+ console.log('');
3720
+ installGitHooks();
3721
+
3722
+ // External services (unless skipped)
3723
+ await handleExternalServices(flags.skipExternal, selectedAgents);
3724
+
3725
+ console.log('');
3726
+ console.log('Done! Get started with: /status');
3727
+ }
3728
+
3729
+ // Helper: Handle external services configuration
3730
+ async function handleExternalServices(skipExternal, selectedAgents) {
3731
+ if (skipExternal) {
3732
+ console.log('');
3733
+ console.log('Skipping external services configuration...');
3734
+ return;
3735
+ }
3736
+
3737
+ const rl = readline.createInterface({
3738
+ input: process.stdin,
3739
+ output: process.stdout
3740
+ });
3741
+
3742
+ let setupCompleted = false;
3743
+ rl.on('close', () => {
3744
+ if (!setupCompleted) {
3745
+ console.log('\n\nSetup cancelled.');
3746
+ process.exit(0);
3747
+ }
3748
+ });
3749
+
3750
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
3751
+ await configureExternalServices(rl, question, selectedAgents);
3752
+ setupCompleted = true;
3753
+ rl.close();
3754
+ }
3755
+
3756
+ async function main() {
3757
+ const command = args[0];
3758
+ const flags = parseFlags();
3759
+
3760
+ // Show help
3761
+ if (flags.help) {
3762
+ showHelp();
3763
+ return;
3764
+ }
3765
+
3766
+ // Handle --path option: change to target directory
3767
+ if (flags.path) {
3768
+ // Update projectRoot after changing directory to maintain state consistency
3769
+ projectRoot = handlePathSetup(flags.path);
3770
+ }
3771
+
3772
+ if (command === 'setup') {
3773
+ // Determine agents to install
3774
+ let selectedAgents = determineSelectedAgents(flags);
3775
+
3776
+ // Quick mode
3777
+ if (flags.quick) {
3778
+ // If no agents specified in quick mode, use all
3779
+ if (selectedAgents.length === 0) {
3780
+ selectedAgents = Object.keys(AGENTS);
3781
+ }
3782
+ await quickSetup(selectedAgents, flags.skipExternal);
3783
+ return;
3784
+ }
3785
+
3786
+ // Agents specified via flag (non-quick mode)
3787
+ if (selectedAgents.length > 0) {
3788
+ await handleSetupCommand(selectedAgents, flags);
3789
+ return;
3790
+ }
3791
+
3792
+ // Interactive setup (skip-external still applies)
3793
+ await interactiveSetupWithFlags(flags);
3794
+ } else if (command === 'recommend') {
3795
+ const { handleRecommend, formatRecommendations } = require('../lib/commands/recommend');
3796
+ const result = handleRecommend(flags, projectRoot);
3797
+ if (result.error) {
3798
+ console.error(`Error: ${result.error}`);
3799
+ process.exitCode = 1;
3800
+ return;
3801
+ }
3802
+ console.log(formatRecommendations(result.recommendations));
3803
+ } else if (command === 'rollback') {
3804
+ // Execute rollback menu
3805
+ await showRollbackMenu();
3806
+ } else {
3807
+ // Default: minimal install (postinstall behavior)
3808
+ minimalInstall();
3809
+ }
3810
+ }
3811
+
3812
+ // ============================================================================
3813
+ // ROLLBACK SYSTEM - TDD Validated
3814
+ // ============================================================================
3815
+ // Security: All inputs validated before use in git commands
3816
+ // See test/rollback-validation.test.js for validation test coverage
3817
+
3818
+ // Helper: Validate commit hash for rollback - extracted to reduce cognitive complexity
3819
+ function validateCommitHash(target) {
3820
+ if (target !== 'HEAD' && !/^[0-9a-f]{4,40}$/i.test(target)) {
3821
+ return { valid: false, error: 'Invalid commit hash format' };
3822
+ }
3823
+ return { valid: true };
3824
+ }
3825
+
3826
+ // Helper: Validate file paths for partial rollback - extracted to reduce cognitive complexity
3827
+ function validatePartialRollbackPaths(target) {
3828
+ const files = target.split(',').map(f => f.trim());
3829
+ for (const file of files) {
3830
+ // Reject shell metacharacters
3831
+ if (/[;|&$`()<>\r\n]/.test(file)) {
3832
+ return { valid: false, error: `Invalid characters in path: ${file}` };
3833
+ }
3834
+ // Reject URL-encoded path traversal attempts
3835
+ if (/%2[eE]|%2[fF]|%5[cC]/.test(file)) {
3836
+ return { valid: false, error: `URL-encoded characters not allowed: ${file}` };
3837
+ }
3838
+ // Reject non-ASCII/unicode characters
3839
+ if (!/^[\x20-\x7E]+$/.test(file)) {
3840
+ return { valid: false, error: `Only ASCII characters allowed in path: ${file}` };
3841
+ }
3842
+ // Prevent path traversal
3843
+ const resolved = path.resolve(projectRoot, file);
3844
+ if (!resolved.startsWith(projectRoot)) {
3845
+ return { valid: false, error: `Path outside project: ${file}` };
3846
+ }
3847
+ }
3848
+ return { valid: true };
3849
+ }
3850
+
3851
+ // Helper: Validate branch range for rollback - extracted to reduce cognitive complexity
3852
+ function validateBranchRange(target) {
3853
+ if (!target.includes('..')) {
3854
+ return { valid: false, error: 'Branch range must use format: start..end' };
3855
+ }
3856
+ const [start, end] = target.split('..');
3857
+ if (!/^[0-9a-f]{4,40}$/i.test(start) || !/^[0-9a-f]{4,40}$/i.test(end)) {
3858
+ return { valid: false, error: 'Invalid commit hashes in range' };
3859
+ }
3860
+ return { valid: true };
3861
+ }
3862
+
3863
+ // Validate rollback inputs (security-critical)
3864
+ function validateRollbackInput(method, target) {
3865
+ const validMethods = ['commit', 'pr', 'partial', 'branch'];
3866
+ if (!validMethods.includes(method)) {
3867
+ return { valid: false, error: 'Invalid method' };
3868
+ }
3869
+
3870
+ // Delegate to method-specific validators
3871
+ if (method === 'commit' || method === 'pr') {
3872
+ return validateCommitHash(target);
3873
+ }
3874
+
3875
+ if (method === 'partial') {
3876
+ return validatePartialRollbackPaths(target);
3877
+ }
3878
+
3879
+ if (method === 'branch') {
3880
+ return validateBranchRange(target);
3881
+ }
3882
+
3883
+ return { valid: true };
3884
+ }
3885
+
3886
+ // Extract USER sections before rollback
3887
+ // Helper: Extract USER:START/END marker sections from content
3888
+ function extractUserMarkerSections(content) {
3889
+ const sections = {};
3890
+ const userRegex = /<!-- USER:START -->([\s\S]*?)<!-- USER:END -->/g;
3891
+ let match;
3892
+ let index = 0;
3893
+
3894
+ while ((match = userRegex.exec(content)) !== null) {
3895
+ sections[`user_${index}`] = match[1];
3896
+ index++;
3897
+ }
3898
+
3899
+ return sections;
3900
+ }
3901
+
3902
+ // Helper: Extract custom commands from directory
3903
+ function extractCustomCommands(filePath) {
3904
+ const customCommandsDir = path.join(path.dirname(filePath), '.claude', 'commands', 'custom');
3905
+
3906
+ if (!fs.existsSync(customCommandsDir)) {
3907
+ return null;
3908
+ }
3909
+
3910
+ return fs.readdirSync(customCommandsDir)
3911
+ .filter(f => f.endsWith('.md'))
3912
+ .map(f => ({
3913
+ name: f,
3914
+ content: fs.readFileSync(path.join(customCommandsDir, f), 'utf-8')
3915
+ }));
3916
+ }
3917
+
3918
+ function extractUserSections(filePath) {
3919
+ if (!fs.existsSync(filePath)) return {};
3920
+
3921
+ const content = fs.readFileSync(filePath, 'utf-8');
3922
+ const sections = extractUserMarkerSections(content);
3923
+
3924
+ // Extract custom commands
3925
+ const customCommands = extractCustomCommands(filePath);
3926
+ if (customCommands) {
3927
+ sections.customCommands = customCommands;
3928
+ }
3929
+
3930
+ return sections;
3931
+ }
3932
+
3933
+ // Restore USER sections after rollback
3934
+ function preserveUserSections(filePath, savedSections) {
3935
+ if (!fs.existsSync(filePath) || Object.keys(savedSections).length === 0) {
3936
+ return;
3937
+ }
3938
+
3939
+ let content = fs.readFileSync(filePath, 'utf-8');
3940
+
3941
+ // Restore USER sections
3942
+ let index = 0;
3943
+ content = content.replaceAll(
3944
+ /<!-- USER:START -->[\s\S]*?<!-- USER:END -->/g,
3945
+ () => {
3946
+ const section = savedSections[`user_${index}`];
3947
+ index++;
3948
+ return section ? `<!-- USER:START -->${section}<!-- USER:END -->` : '';
3949
+ }
3950
+ );
3951
+
3952
+ fs.writeFileSync(filePath, content, 'utf-8');
3953
+
3954
+ // Restore custom commands
3955
+ if (savedSections.customCommands) {
3956
+ const customCommandsDir = path.join(path.dirname(filePath), '.claude', 'commands', 'custom');
3957
+ if (!fs.existsSync(customCommandsDir)) {
3958
+ fs.mkdirSync(customCommandsDir, { recursive: true });
3959
+ }
3960
+
3961
+ savedSections.customCommands.forEach(cmd => {
3962
+ fs.writeFileSync(
3963
+ path.join(customCommandsDir, cmd.name),
3964
+ cmd.content,
3965
+ 'utf-8'
3966
+ );
3967
+ });
3968
+ }
3969
+ }
3970
+
3971
+ // Perform rollback operation
3972
+ // Helper: Check git working directory is clean
3973
+ function checkGitWorkingDirectory() {
3974
+ try {
3975
+ const { execSync } = require('node:child_process');
3976
+ const status = execSync('git status --porcelain', { encoding: 'utf-8' });
3977
+ if (status.trim() !== '') {
3978
+ console.log(' ❌ Working directory has uncommitted changes');
3979
+ console.log(' Commit or stash changes before rollback');
3980
+ return false;
3981
+ }
3982
+ return true;
3983
+ } catch (err) {
3984
+ console.log(' ❌ Git error:', err.message);
3985
+ return false;
3986
+ }
3987
+ }
3988
+
3989
+ // Helper: Update Beads issue after PR rollback
3990
+ function updateBeadsIssue(commitMessage) {
3991
+ const issueMatch = commitMessage.match(/#(\d+)/);
3992
+ if (!issueMatch) return;
3993
+
3994
+ try {
3995
+ const { execFileSync } = require('node:child_process');
3996
+ execFileSync('bd', ['update', issueMatch[1], '--status', 'reverted', '--comment', 'PR reverted'], { stdio: 'inherit' });
3997
+ console.log(` Updated Beads issue #${issueMatch[1]} to 'reverted'`);
3998
+ } catch {
3999
+ // Beads not installed - silently continue
4000
+ }
4001
+ }
4002
+
4003
+ // Helper: Handle commit rollback
4004
+ function handleCommitRollback(target, dryRun, execSync) {
4005
+ if (dryRun) {
4006
+ console.log(` Would revert: ${target}`);
4007
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
4008
+ console.log(' Affected files:');
4009
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
4010
+ } else {
4011
+ execSync(`git revert --no-edit ${target}`, { stdio: 'inherit' });
4012
+ }
4013
+ }
4014
+
4015
+ // Helper: Handle PR rollback
4016
+ function handlePrRollback(target, dryRun, execSync) {
4017
+ if (dryRun) {
4018
+ console.log(` Would revert merge: ${target}`);
4019
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
4020
+ console.log(' Affected files:');
4021
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
4022
+ } else {
4023
+ execSync(`git revert -m 1 --no-edit ${target}`, { stdio: 'inherit' });
4024
+
4025
+ // Update Beads issue if linked
4026
+ const commitMsg = execSync(`git log -1 --format=%B ${target}`, { encoding: 'utf-8' });
4027
+ updateBeadsIssue(commitMsg);
4028
+ }
4029
+ }
4030
+
4031
+ // Helper: Handle partial file rollback
4032
+ function handlePartialRollback(target, dryRun, _execSync) {
4033
+ const { execFileSync } = require('node:child_process');
4034
+ const files = target.split(',').map(f => f.trim());
4035
+ if (dryRun) {
4036
+ console.log(' Would restore files:');
4037
+ files.forEach(f => console.log(` - ${f}`));
4038
+ } else {
4039
+ files.forEach(f => {
4040
+ execFileSync('git', ['checkout', 'HEAD~1', '--', f], { stdio: 'inherit' });
4041
+ });
4042
+ execFileSync('git', ['commit', '-m', `chore: rollback ${files.join(', ')}`], { stdio: 'inherit' });
4043
+ }
4044
+ }
4045
+
4046
+ // Helper: Handle branch range rollback
4047
+ function handleBranchRollback(target, dryRun, _execSync) {
4048
+ const [startCommit, endCommit] = target.split('..');
4049
+ if (dryRun) {
4050
+ console.log(` Would revert range: ${startCommit}..${endCommit}`);
4051
+ const commits = execSync(`git log --oneline ${startCommit}..${endCommit}`, { encoding: 'utf-8' });
4052
+ console.log(' Commits to revert:');
4053
+ commits.trim().split('\n').forEach(c => console.log(` ${c}`));
4054
+ } else {
4055
+ execSync(`git revert --no-edit ${startCommit}..${endCommit}`, { stdio: 'inherit' });
4056
+ }
4057
+ }
4058
+
4059
+ // Helper: Finalize rollback by restoring user sections
4060
+ function finalizeRollback(agentsPath, savedSections) {
4061
+ const { execSync } = require('node:child_process');
4062
+
4063
+ console.log(' 📦 Restoring user content...');
4064
+ preserveUserSections(agentsPath, savedSections);
4065
+
4066
+ // Amend commit to include restored USER sections
4067
+ if (fs.existsSync(agentsPath)) {
4068
+ execSync('git add AGENTS.md', { stdio: 'inherit' });
4069
+ execSync('git commit --amend --no-edit', { stdio: 'inherit' });
4070
+ }
4071
+
4072
+ console.log('');
4073
+ console.log(' ✅ Rollback complete');
4074
+ console.log(' User content preserved');
4075
+ }
4076
+
4077
+ async function performRollback(method, target, dryRun = false) {
4078
+ console.log('');
4079
+ console.log(` 🔄 Rollback: ${method}`);
4080
+ console.log(` Target: ${target}`);
4081
+ if (dryRun) {
4082
+ console.log(' Mode: DRY RUN (preview only)');
4083
+ }
4084
+ console.log('');
4085
+
4086
+ // Validate inputs BEFORE any git operations
4087
+ const validation = validateRollbackInput(method, target);
4088
+ if (!validation.valid) {
4089
+ console.log(` ❌ ${validation.error}`);
4090
+ return false;
4091
+ }
4092
+
4093
+ // Check for clean working directory
4094
+ if (!checkGitWorkingDirectory()) {
4095
+ return false;
4096
+ }
4097
+
4098
+ // Extract USER sections before rollback
4099
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
4100
+ const savedSections = extractUserSections(agentsPath);
4101
+
4102
+ if (!dryRun) {
4103
+ console.log(' 📦 Backing up user content...');
4104
+ }
4105
+
4106
+ try {
4107
+ const { execSync } = require('node:child_process');
4108
+
4109
+ if (method === 'commit') {
4110
+ handleCommitRollback(target, dryRun, execSync);
4111
+ } else if (method === 'pr') {
4112
+ handlePrRollback(target, dryRun, execSync);
4113
+ } else if (method === 'partial') {
4114
+ handlePartialRollback(target, dryRun, execSync);
4115
+ } else if (method === 'branch') {
4116
+ handleBranchRollback(target, dryRun, execSync);
4117
+ }
4118
+
4119
+ if (!dryRun) {
4120
+ finalizeRollback(agentsPath, savedSections);
4121
+ }
4122
+
4123
+ return true;
4124
+ } catch (err) {
4125
+ console.log('');
4126
+ console.log(' ❌ Rollback failed:', err.message);
4127
+ console.log(' Try manual rollback with: git revert <commit>');
4128
+ return false;
4129
+ }
4130
+ }
4131
+
4132
+ // Interactive rollback menu
4133
+ async function showRollbackMenu() {
4134
+ console.log('');
4135
+ console.log(' 🔄 Forge Rollback');
4136
+ console.log('');
4137
+ console.log(' Choose rollback method:');
4138
+ console.log('');
4139
+ console.log(' 1. Rollback last commit');
4140
+ console.log(' 2. Rollback specific commit');
4141
+ console.log(' 3. Rollback merged PR');
4142
+ console.log(' 4. Rollback specific files only');
4143
+ console.log(' 5. Rollback entire branch');
4144
+ console.log(' 6. Preview rollback (dry run)');
4145
+ console.log('');
4146
+
4147
+ const rl = readline.createInterface({
4148
+ input: process.stdin,
4149
+ output: process.stdout
4150
+ });
4151
+
4152
+ const choice = await new Promise(resolve => {
4153
+ rl.question(' Enter choice (1-6): ', resolve);
4154
+ });
4155
+
4156
+ let method, target, dryRun = false;
4157
+
4158
+ switch (choice.trim()) {
4159
+ case '1': {
4160
+ method = 'commit';
4161
+ target = 'HEAD';
4162
+ break;
4163
+ }
4164
+ case '2': {
4165
+ target = await new Promise(resolve => {
4166
+ rl.question(' Enter commit hash: ', resolve);
4167
+ });
4168
+ method = 'commit';
4169
+ break;
4170
+ }
4171
+ case '3': {
4172
+ target = await new Promise(resolve => {
4173
+ rl.question(' Enter merge commit hash: ', resolve);
4174
+ });
4175
+ method = 'pr';
4176
+ break;
4177
+ }
4178
+ case '4': {
4179
+ target = await new Promise(resolve => {
4180
+ rl.question(' Enter file paths (comma-separated): ', resolve);
4181
+ });
4182
+ method = 'partial';
4183
+ break;
4184
+ }
4185
+ case '5': {
4186
+ const start = await new Promise(resolve => {
4187
+ rl.question(' Enter start commit: ', resolve);
4188
+ });
4189
+ const end = await new Promise(resolve => {
4190
+ rl.question(' Enter end commit: ', resolve);
4191
+ });
4192
+ target = `${start.trim()}..${end.trim()}`;
4193
+ method = 'branch';
4194
+ break;
4195
+ }
4196
+ case '6': {
4197
+ dryRun = true;
4198
+ const dryMethod = await new Promise(resolve => {
4199
+ rl.question(' Preview method (commit/pr/partial/branch): ', resolve);
4200
+ });
4201
+ method = dryMethod.trim();
4202
+ target = await new Promise(resolve => {
4203
+ rl.question(' Enter target (commit/files/range): ', resolve);
4204
+ });
4205
+ break;
4206
+ }
4207
+ default: {
4208
+ console.log(' Invalid choice');
4209
+ rl.close();
4210
+ return;
4211
+ }
4212
+ }
4213
+
4214
+ rl.close();
4215
+
4216
+ await performRollback(method, target, dryRun);
4217
+ }
4218
+
4219
+ // Only execute main() when run directly, not when imported
4220
+ if (require.main === module) {
4221
+ (async () => { // NOSONAR - S7785: Top-level await requires ESM; this file uses CommonJS
4222
+ try {
4223
+ await main();
4224
+ } catch (error) {
4225
+ console.error(error);
4226
+ }
4227
+ })();
4228
+ }