forge-workflow 0.0.6 → 0.0.7

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