forge-workflow 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorrules +149 -0
- package/bin/forge.js +43 -3
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_issue.js +172 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/claim.js +5 -0
- package/lib/commands/close.js +5 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/create.js +5 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/issue.js +5 -0
- package/lib/commands/list.js +5 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/ready.js +5 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/show.js +5 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +19 -1
- package/lib/commands/update.js +5 -0
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/lib/eval-runner.js +50 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- 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
|
+
};
|