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