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