@devobsessed/code-captain 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +214 -0
  2. package/bin/install.js +1048 -0
  3. package/claude-code/README.md +276 -0
  4. package/claude-code/agents/code-captain.md +121 -0
  5. package/claude-code/agents/spec-generator.md +271 -0
  6. package/claude-code/agents/story-creator.md +309 -0
  7. package/claude-code/agents/tech-spec.md +440 -0
  8. package/claude-code/commands/cc-initialize.md +520 -0
  9. package/copilot/README.md +210 -0
  10. package/copilot/chatmodes/Code Captain.chatmode.md +60 -0
  11. package/copilot/docs/best-practices.md +74 -0
  12. package/copilot/prompts/create-adr.prompt.md +468 -0
  13. package/copilot/prompts/create-spec.prompt.md +430 -0
  14. package/copilot/prompts/edit-spec.prompt.md +396 -0
  15. package/copilot/prompts/execute-task.prompt.md +144 -0
  16. package/copilot/prompts/explain-code.prompt.md +292 -0
  17. package/copilot/prompts/initialize.prompt.md +65 -0
  18. package/copilot/prompts/new-command.prompt.md +310 -0
  19. package/copilot/prompts/plan-product.prompt.md +450 -0
  20. package/copilot/prompts/research.prompt.md +329 -0
  21. package/copilot/prompts/status.prompt.md +424 -0
  22. package/copilot/prompts/swab.prompt.md +217 -0
  23. package/cursor/README.md +224 -0
  24. package/cursor/cc.md +183 -0
  25. package/cursor/cc.mdc +69 -0
  26. package/cursor/commands/create-adr.md +504 -0
  27. package/cursor/commands/create-spec.md +430 -0
  28. package/cursor/commands/edit-spec.md +405 -0
  29. package/cursor/commands/execute-task.md +514 -0
  30. package/cursor/commands/explain-code.md +289 -0
  31. package/cursor/commands/initialize.md +397 -0
  32. package/cursor/commands/new-command.md +312 -0
  33. package/cursor/commands/plan-product.md +466 -0
  34. package/cursor/commands/research.md +317 -0
  35. package/cursor/commands/status.md +413 -0
  36. package/cursor/commands/swab.md +209 -0
  37. package/cursor/docs/best-practices.md +74 -0
  38. package/cursor/integrations/azure-devops/create-azure-work-items.md +403 -0
  39. package/cursor/integrations/azure-devops/sync-azure-work-items.md +486 -0
  40. package/cursor/integrations/github/create-github-issues.md +765 -0
  41. package/cursor/integrations/github/scripts/create-issues-batch.sh +272 -0
  42. package/cursor/integrations/github/sync-github-issues.md +237 -0
  43. package/cursor/integrations/github/sync.md +305 -0
  44. package/manifest.json +381 -0
  45. package/package.json +58 -0
  46. package/windsurf/README.md +254 -0
  47. package/windsurf/rules/cc.md +5 -0
  48. package/windsurf/workflows/create-adr.md +331 -0
  49. package/windsurf/workflows/create-spec.md +280 -0
  50. package/windsurf/workflows/edit-spec.md +273 -0
  51. package/windsurf/workflows/execute-task.md +276 -0
  52. package/windsurf/workflows/explain-code.md +292 -0
  53. package/windsurf/workflows/initialize.md +298 -0
  54. package/windsurf/workflows/new-command.md +321 -0
  55. package/windsurf/workflows/status.md +213 -0
package/bin/install.js ADDED
@@ -0,0 +1,1048 @@
1
+ #!/usr/bin/env node
2
+
3
+ import boxen from 'boxen';
4
+ import chalk from 'chalk';
5
+ import { spawn } from 'cross-spawn';
6
+ import fs from 'fs-extra';
7
+ import inquirer from 'inquirer';
8
+ import fetch from 'node-fetch';
9
+ import ora from 'ora';
10
+ import path, { dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ class CodeCaptainInstaller {
17
+ constructor() {
18
+ this.config = {
19
+ repoUrl: 'https://github.com/devobsessed/code-captain',
20
+ baseUrl: 'https://raw.githubusercontent.com/devobsessed/code-captain/main',
21
+ version: 'main',
22
+ localSource: process.env.CC_LOCAL_SOURCE || null,
23
+ versionFile: '.code-captain/.version',
24
+ manifestFile: '.code-captain/.manifest.json'
25
+ };
26
+
27
+ this.ides = {
28
+ cursor: {
29
+ name: 'Cursor',
30
+ description: 'AI-first code editor with built-in AI agent capabilities',
31
+ details: 'Uses .code-captain/ structure + .cursor/rules/cc.mdc'
32
+ },
33
+ copilot: {
34
+ name: 'VS Code with GitHub Copilot',
35
+ description: 'Visual Studio Code with GitHub Copilot extension',
36
+ details: 'Uses .github/chatmodes/ + .github/prompts/ + .code-captain/docs/'
37
+ },
38
+ windsurf: {
39
+ name: 'Windsurf',
40
+ description: 'Codeium\'s AI-powered development environment',
41
+ details: 'Uses windsurf/rules/ for custom workflows'
42
+ },
43
+ claude: {
44
+ name: 'Claude Code',
45
+ description: 'Direct integration with Claude for development workflows',
46
+ details: 'Uses .code-captain/claude/ structure with agents and commands'
47
+ }
48
+ };
49
+ }
50
+
51
+ // Display welcome banner
52
+ showWelcome() {
53
+ const banner = boxen(
54
+ chalk.bold.green('Code Captain 2.0') + '\n' +
55
+ chalk.gray('Unified AI Development Agent System') + '\n\n' +
56
+ chalk.blue('šŸš€ Interactive Installation Wizard'),
57
+ {
58
+ padding: 1,
59
+ margin: 1,
60
+ borderStyle: 'round',
61
+ borderColor: 'green',
62
+ textAlignment: 'center'
63
+ }
64
+ );
65
+
66
+ console.log(banner);
67
+ }
68
+
69
+ // Check system compatibility
70
+ async checkCompatibility() {
71
+ const spinner = ora('Checking system compatibility...').start();
72
+
73
+ try {
74
+ // Check Node.js version
75
+ const nodeVersion = process.version;
76
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
77
+
78
+ if (majorVersion < 16) {
79
+ spinner.fail(`Node.js ${nodeVersion} detected. Requires Node.js 16 or higher.`);
80
+ console.log(chalk.yellow('\nšŸ“¦ Please update Node.js:'));
81
+ console.log(chalk.blue(' Visit: https://nodejs.org/'));
82
+ process.exit(1);
83
+ }
84
+
85
+ // Check if we're in a Git repository
86
+ const isGitRepo = await fs.pathExists('.git');
87
+
88
+ // Check for existing Code Captain installations
89
+ const existingInstallations = await this.detectExistingInstallations();
90
+
91
+ spinner.succeed('System compatibility check passed');
92
+
93
+ return {
94
+ nodeVersion,
95
+ isGitRepo,
96
+ existingInstallations
97
+ };
98
+ } catch (error) {
99
+ spinner.fail('Compatibility check failed');
100
+ console.error(chalk.red('Error:'), error.message);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ // Detect existing Code Captain installations
106
+ async detectExistingInstallations() {
107
+ const installations = [];
108
+
109
+ // Define all possible Code Captain paths
110
+ const pathsToCheck = {
111
+ 'Code Captain Core': ['.code-captain/'],
112
+ 'Cursor Integration': ['.cursor/rules/cc.mdc', '.cursor/rules/'],
113
+ 'Copilot Integration': ['.github/chatmodes/', '.github/prompts/'],
114
+ 'Windsurf Integration': ['windsurf/rules/', 'windsurf/workflows/'],
115
+ 'Claude Integration': ['.code-captain/claude/', 'claude-code/', '.claude/'],
116
+ 'Legacy Structure': ['cursor/', 'copilot/', 'windsurf/']
117
+ };
118
+
119
+ for (const [name, paths] of Object.entries(pathsToCheck)) {
120
+ for (const path of paths) {
121
+ if (await fs.pathExists(path)) {
122
+ installations.push(name);
123
+ break; // Only add each installation type once
124
+ }
125
+ }
126
+ }
127
+
128
+ return [...new Set(installations)]; // Remove duplicates
129
+ }
130
+
131
+ // Calculate SHA256 hash of a local file
132
+ async calculateFileHash(filePath) {
133
+ try {
134
+ const crypto = await import('crypto');
135
+ const content = await fs.readFile(filePath, 'utf8');
136
+ return crypto.default.createHash('sha256').update(content).digest('hex');
137
+ } catch (error) {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // Get remote manifest with file versions/hashes
143
+ async getRemoteManifest() {
144
+ try {
145
+ const manifestUrl = `${this.config.baseUrl}/manifest.json`;
146
+
147
+ if (this.config.localSource) {
148
+ const localManifestPath = path.join(this.config.localSource, 'manifest.json');
149
+ if (await fs.pathExists(localManifestPath)) {
150
+ const content = await fs.readFile(localManifestPath, 'utf8');
151
+ return JSON.parse(content);
152
+ }
153
+ } else {
154
+ const response = await fetch(manifestUrl);
155
+ if (response.ok) {
156
+ return await response.json();
157
+ }
158
+ }
159
+
160
+ // Fallback: generate manifest from current commit
161
+ return await this.generateFallbackManifest();
162
+ } catch (error) {
163
+ console.warn(chalk.yellow('Warning: Could not fetch remote manifest, using fallback'));
164
+ return await this.generateFallbackManifest();
165
+ }
166
+ }
167
+
168
+ // Generate fallback manifest if remote manifest doesn't exist
169
+ async generateFallbackManifest() {
170
+ const timestamp = new Date().toISOString();
171
+ return {
172
+ version: this.config.version,
173
+ timestamp,
174
+ commit: 'unknown',
175
+ files: {} // Will be populated as files are downloaded
176
+ };
177
+ }
178
+
179
+ // Get local manifest if it exists
180
+ async getLocalManifest() {
181
+ try {
182
+ if (await fs.pathExists(this.config.manifestFile)) {
183
+ const content = await fs.readFile(this.config.manifestFile, 'utf8');
184
+ return JSON.parse(content);
185
+ }
186
+ } catch (error) {
187
+ console.warn(chalk.yellow('Warning: Could not read local manifest'));
188
+ }
189
+ return null;
190
+ }
191
+
192
+ // Compare manifests and detect changes
193
+ async detectChanges(selectedIDE) {
194
+ const spinner = ora('Analyzing file changes...').start();
195
+
196
+ try {
197
+ const [remoteManifest, localManifest] = await Promise.all([
198
+ this.getRemoteManifest(),
199
+ this.getLocalManifest()
200
+ ]);
201
+
202
+ if (!localManifest) {
203
+ spinner.succeed('No previous manifest found - treating as fresh installation');
204
+ return {
205
+ isFirstInstall: true,
206
+ changes: [],
207
+ newFiles: [],
208
+ recommendations: ['Full installation recommended (no change tracking available)']
209
+ };
210
+ }
211
+
212
+ const files = this.getIDEFiles(selectedIDE);
213
+ const changes = [];
214
+ const newFiles = [];
215
+ let filesAnalyzed = 0;
216
+
217
+ for (const file of files) {
218
+ const remotePath = file.source;
219
+ const localPath = file.target;
220
+
221
+ filesAnalyzed++;
222
+ spinner.text = `Analyzing changes... (${filesAnalyzed}/${files.length})`;
223
+
224
+ // Check if file exists locally
225
+ const localExists = await fs.pathExists(localPath);
226
+ const remoteFileInfo = remoteManifest.files?.[remotePath];
227
+
228
+ if (!localExists) {
229
+ newFiles.push({
230
+ file: remotePath,
231
+ component: file.component,
232
+ reason: 'File does not exist locally'
233
+ });
234
+ continue;
235
+ }
236
+
237
+ // Calculate actual hash of local file
238
+ const localFileHash = await this.calculateFileHash(localPath);
239
+
240
+ if (!localFileHash) {
241
+ // Can't read local file - treat as needs update
242
+ changes.push({
243
+ file: remotePath,
244
+ component: file.component,
245
+ reason: 'Unable to read local file'
246
+ });
247
+ continue;
248
+ }
249
+
250
+ // Compare with remote hash
251
+ if (remoteFileInfo && remoteFileInfo.hash) {
252
+ const remoteHash = remoteFileInfo.hash.replace('sha256:', '');
253
+
254
+ if (localFileHash !== remoteHash) {
255
+ changes.push({
256
+ file: remotePath,
257
+ component: file.component,
258
+ localVersion: localManifest.files?.[remotePath]?.version || 'unknown',
259
+ remoteVersion: remoteFileInfo.version || 'latest',
260
+ reason: 'File content has changed',
261
+ localHash: localFileHash.substring(0, 8),
262
+ remoteHash: remoteHash.substring(0, 8)
263
+ });
264
+ }
265
+ } else {
266
+ // No remote file info - treat as new in remote
267
+ newFiles.push({
268
+ file: remotePath,
269
+ component: file.component,
270
+ reason: 'New file in remote repository'
271
+ });
272
+ }
273
+ }
274
+
275
+ const recommendations = this.generateUpdateRecommendations(changes, newFiles);
276
+
277
+ spinner.succeed(`Found ${changes.length} updated files, ${newFiles.length} new files`);
278
+
279
+ return {
280
+ isFirstInstall: false,
281
+ localVersion: localManifest.version,
282
+ remoteVersion: remoteManifest.version,
283
+ changes,
284
+ newFiles,
285
+ recommendations
286
+ };
287
+
288
+ } catch (error) {
289
+ spinner.fail('Could not analyze changes');
290
+ return {
291
+ isFirstInstall: false,
292
+ changes: [],
293
+ newFiles: [],
294
+ recommendations: ['Unable to detect changes - consider full update'],
295
+ error: error.message
296
+ };
297
+ }
298
+ }
299
+
300
+ // Generate smart update recommendations
301
+ generateUpdateRecommendations(changes, newFiles) {
302
+ const recommendations = [];
303
+ const changedComponents = new Set();
304
+
305
+ // Collect components with changes
306
+ [...changes, ...newFiles].forEach(item => {
307
+ if (item.component) {
308
+ changedComponents.add(item.component);
309
+ }
310
+ });
311
+
312
+ if (changedComponents.size === 0) {
313
+ recommendations.push('āœ… All files are up to date!');
314
+ } else {
315
+ recommendations.push(`šŸ“¦ Recommended updates: ${Array.from(changedComponents).join(', ')}`);
316
+
317
+ // Specific recommendations
318
+ if (changedComponents.has('commands')) {
319
+ recommendations.push('šŸš€ Commands updated - new features or bug fixes available');
320
+ }
321
+ if (changedComponents.has('rules')) {
322
+ recommendations.push('āš™ļø Rules updated - improved AI agent behavior');
323
+ }
324
+ if (changedComponents.has('docs')) {
325
+ recommendations.push('šŸ“š Documentation updated - check for new best practices');
326
+ }
327
+ }
328
+
329
+ return recommendations;
330
+ }
331
+
332
+ // Save manifest after successful installation
333
+ async saveManifest(remoteManifest, installedFiles) {
334
+ try {
335
+ const manifest = {
336
+ ...remoteManifest,
337
+ installedAt: new Date().toISOString(),
338
+ files: {}
339
+ };
340
+
341
+ // Calculate actual hashes of installed files
342
+ for (const file of installedFiles) {
343
+ const localHash = await this.calculateFileHash(file.target);
344
+
345
+ if (localHash) {
346
+ // Use remote file info as base, but update with actual installed hash
347
+ const remoteFileInfo = remoteManifest.files?.[file.source] || {};
348
+
349
+ manifest.files[file.source] = {
350
+ ...remoteFileInfo,
351
+ hash: `sha256:${localHash}`,
352
+ installedAt: new Date().toISOString(),
353
+ actualSize: (await fs.stat(file.target).catch(() => ({ size: 0 }))).size
354
+ };
355
+ }
356
+ }
357
+
358
+ await fs.ensureDir(path.dirname(this.config.manifestFile));
359
+ await fs.writeFile(this.config.manifestFile, JSON.stringify(manifest, null, 2));
360
+
361
+ return true;
362
+ } catch (error) {
363
+ console.warn(chalk.yellow(`Warning: Could not save manifest: ${error.message}`));
364
+ return false;
365
+ }
366
+ }
367
+
368
+ // Auto-detect IDE preference
369
+ detectIDE() {
370
+ const detections = [];
371
+
372
+ // Check for Cursor
373
+ if (fs.pathExistsSync('.cursor') || this.commandExists('cursor')) {
374
+ detections.push('cursor');
375
+ }
376
+
377
+ // Check for VS Code
378
+ if (this.commandExists('code')) {
379
+ detections.push('copilot');
380
+ }
381
+
382
+ // Check for Windsurf
383
+ if (fs.pathExistsSync('windsurf') || this.commandExists('windsurf')) {
384
+ detections.push('windsurf');
385
+ }
386
+
387
+ // Check for Claude Code
388
+ if (fs.pathExistsSync('claude-code') || fs.pathExistsSync('.code-captain/claude') || fs.pathExistsSync('.claude')) {
389
+ detections.push('claude');
390
+ }
391
+
392
+ return detections;
393
+ }
394
+
395
+ // Check if command exists
396
+ commandExists(command) {
397
+ try {
398
+ const result = spawn.sync(command, ['--version'], {
399
+ stdio: ['pipe', 'pipe', 'pipe'],
400
+ timeout: 5000,
401
+ windowsHide: true
402
+ });
403
+ return result.status === 0;
404
+ } catch {
405
+ return false;
406
+ }
407
+ }
408
+
409
+ // IDE selection prompt
410
+ async selectIDE() {
411
+ const detected = this.detectIDE();
412
+
413
+ console.log('\n' + chalk.bold.blue('šŸŽÆ IDE Selection'));
414
+ console.log(chalk.gray('═'.repeat(50)));
415
+
416
+ if (detected.length > 0) {
417
+ console.log(chalk.green(`\n✨ Auto-detected: ${detected.map(id => this.ides[id].name).join(', ')}`));
418
+ }
419
+
420
+ const choices = Object.entries(this.ides).map(([key, ide]) => ({
421
+ name: ide.name,
422
+ value: key,
423
+ short: ide.name
424
+ }));
425
+
426
+ const { selectedIDE } = await inquirer.prompt([
427
+ {
428
+ type: 'list',
429
+ name: 'selectedIDE',
430
+ message: 'Which IDE/environment are you using?',
431
+ choices: choices,
432
+ pageSize: 6,
433
+ default: detected.length > 0 ? detected[0] : 'cursor'
434
+ }
435
+ ]);
436
+
437
+ return selectedIDE;
438
+ }
439
+
440
+ // Select installation components
441
+ async selectInstallationComponents(selectedIDE, existingInstallations) {
442
+ // First, detect what's changed
443
+ const changeInfo = await this.detectChanges(selectedIDE);
444
+
445
+ if (changeInfo.isFirstInstall) {
446
+ // Fresh installation - install everything
447
+ return {
448
+ installAll: true,
449
+ changeInfo
450
+ };
451
+ }
452
+
453
+ console.log('\n' + chalk.bold.blue('šŸ” Change Analysis'));
454
+ console.log(chalk.gray('═'.repeat(50)));
455
+
456
+ // Show version information
457
+ if (changeInfo.localVersion && changeInfo.remoteVersion) {
458
+ console.log(chalk.blue('Current version:'), changeInfo.localVersion);
459
+ console.log(chalk.blue('Available version:'), changeInfo.remoteVersion);
460
+ }
461
+
462
+ // Show what's changed
463
+ if (changeInfo.changes.length > 0) {
464
+ console.log(chalk.yellow('\nšŸ“ Updated Files:'));
465
+ changeInfo.changes.forEach(change => {
466
+ console.log(chalk.gray(` • ${change.file} (${change.component})`));
467
+ console.log(chalk.gray(` ${change.reason}`));
468
+ if (change.localHash && change.remoteHash) {
469
+ console.log(chalk.gray(` Local: ${change.localHash}... → Remote: ${change.remoteHash}...`));
470
+ }
471
+ });
472
+ }
473
+
474
+ if (changeInfo.newFiles.length > 0) {
475
+ console.log(chalk.green('\nšŸ†• New Files:'));
476
+ changeInfo.newFiles.forEach(file => {
477
+ console.log(chalk.gray(` • ${file.file} (${file.component})`));
478
+ console.log(chalk.gray(` ${file.reason}`));
479
+ });
480
+ }
481
+
482
+ // Show recommendations
483
+ console.log(chalk.bold.cyan('\nšŸ’” Recommendations:'));
484
+ changeInfo.recommendations.forEach(rec => {
485
+ console.log(chalk.gray(` ${rec}`));
486
+ });
487
+
488
+ if (changeInfo.changes.length === 0 && changeInfo.newFiles.length === 0) {
489
+ console.log(chalk.green('\n✨ All files are up to date!'));
490
+
491
+ const { forceUpdate } = await inquirer.prompt([
492
+ {
493
+ type: 'confirm',
494
+ name: 'forceUpdate',
495
+ message: 'All files are current. Force reinstall anyway?',
496
+ default: false
497
+ }
498
+ ]);
499
+
500
+ if (!forceUpdate) {
501
+ return {
502
+ skipInstall: true,
503
+ changeInfo
504
+ };
505
+ }
506
+ }
507
+
508
+ console.log('\n' + chalk.bold.blue('šŸ”§ Component Selection'));
509
+ console.log(chalk.gray('═'.repeat(50)));
510
+
511
+ const componentChoices = this.getComponentChoices(selectedIDE);
512
+
513
+ // Pre-select components that have changes
514
+ const changedComponents = new Set();
515
+ [...changeInfo.changes, ...changeInfo.newFiles].forEach(item => {
516
+ if (item.component) {
517
+ changedComponents.add(item.component);
518
+ }
519
+ });
520
+
521
+ // Update choices to pre-select changed components
522
+ componentChoices.forEach(choice => {
523
+ if (changedComponents.has(choice.value)) {
524
+ choice.checked = true;
525
+ choice.name += chalk.yellow(' (has updates)');
526
+ }
527
+ });
528
+
529
+ const { components } = await inquirer.prompt([
530
+ {
531
+ type: 'checkbox',
532
+ name: 'components',
533
+ message: 'Select components to install/update:',
534
+ choices: componentChoices,
535
+ pageSize: 10,
536
+ validate: (answer) => {
537
+ if (answer.length === 0) {
538
+ return 'Please select at least one component to install.';
539
+ }
540
+ return true;
541
+ }
542
+ }
543
+ ]);
544
+
545
+ const { createBackups } = await inquirer.prompt([
546
+ {
547
+ type: 'confirm',
548
+ name: 'createBackups',
549
+ message: 'Create backups of existing files before overwriting?',
550
+ default: true
551
+ }
552
+ ]);
553
+
554
+ return {
555
+ selectedComponents: components,
556
+ createBackups,
557
+ installAll: false,
558
+ changeInfo
559
+ };
560
+ }
561
+
562
+ // Get component choices based on IDE
563
+ getComponentChoices(selectedIDE) {
564
+ const baseChoices = [
565
+ { name: 'Core Commands', value: 'commands', checked: true },
566
+ { name: 'Documentation & Best Practices', value: 'docs', checked: true }
567
+ ];
568
+
569
+ switch (selectedIDE) {
570
+ case 'cursor':
571
+ return [
572
+ ...baseChoices,
573
+ { name: 'Cursor Rules (.cursor/rules/cc.mdc)', value: 'rules', checked: true },
574
+ { name: 'GitHub Integration', value: 'github', checked: true },
575
+ { name: 'Azure DevOps Integration', value: 'azure', checked: true }
576
+ ];
577
+
578
+ case 'copilot':
579
+ return [
580
+ ...baseChoices,
581
+ { name: 'GitHub Copilot Chatmodes', value: 'chatmodes', checked: true },
582
+ { name: 'GitHub Copilot Prompts', value: 'prompts', checked: true }
583
+ ];
584
+
585
+ case 'windsurf':
586
+ return [
587
+ ...baseChoices,
588
+ { name: 'Windsurf Rules', value: 'rules', checked: true },
589
+ { name: 'Windsurf Workflows', value: 'workflows', checked: true }
590
+ ];
591
+
592
+ case 'claude':
593
+ return [
594
+ ...baseChoices,
595
+ { name: 'Claude Agents', value: 'agents', checked: true },
596
+ { name: 'Claude Commands', value: 'claude-commands', checked: true }
597
+ ];
598
+
599
+ default:
600
+ return baseChoices;
601
+ }
602
+ }
603
+
604
+ // Confirmation prompt
605
+ async confirmInstallation(selectedIDE, systemInfo, installOptions) {
606
+ console.log('\n' + chalk.bold.yellow('šŸ“‹ Installation Summary'));
607
+ console.log(chalk.gray('═'.repeat(50)));
608
+
609
+ const ide = this.ides[selectedIDE];
610
+ console.log(chalk.blue('Selected IDE:'), chalk.bold(ide.name));
611
+ console.log(chalk.blue('Description:'), ide.description);
612
+ console.log(chalk.blue('Installation:'), ide.details);
613
+ console.log(chalk.blue('Node.js:'), systemInfo.nodeVersion);
614
+ console.log(chalk.blue('Git Repository:'), systemInfo.isGitRepo ? 'Yes' : 'No');
615
+
616
+ if (systemInfo.existingInstallations.length > 0) {
617
+ console.log(chalk.yellow('Existing installations:'), systemInfo.existingInstallations.join(', '));
618
+ }
619
+
620
+ if (!installOptions.installAll) {
621
+ console.log(chalk.blue('Components to install:'), installOptions.selectedComponents.join(', '));
622
+ console.log(chalk.blue('Create backups:'), installOptions.createBackups ? 'Yes' : 'No');
623
+
624
+ // Show change summary
625
+ const { changeInfo } = installOptions;
626
+ if (changeInfo && (changeInfo.changes.length > 0 || changeInfo.newFiles.length > 0)) {
627
+ console.log(chalk.blue('Files to update:'), `${changeInfo.changes.length} changed, ${changeInfo.newFiles.length} new`);
628
+ }
629
+ } else {
630
+ console.log(chalk.blue('Installation type:'), 'Full installation (new setup)');
631
+ }
632
+
633
+ const { confirmed } = await inquirer.prompt([
634
+ {
635
+ type: 'confirm',
636
+ name: 'confirmed',
637
+ message: 'Proceed with installation?',
638
+ default: true
639
+ }
640
+ ]);
641
+
642
+ return confirmed;
643
+ }
644
+
645
+ // Create backup of existing file
646
+ async createBackup(targetPath, shouldBackup = true) {
647
+ if (!shouldBackup) {
648
+ return null;
649
+ }
650
+
651
+ if (await fs.pathExists(targetPath)) {
652
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
653
+ const backupPath = `${targetPath}.backup.${timestamp}`;
654
+
655
+ try {
656
+ await fs.copy(targetPath, backupPath);
657
+ return backupPath;
658
+ } catch (error) {
659
+ console.warn(chalk.yellow(`Warning: Could not backup ${targetPath}: ${error.message}`));
660
+ return null;
661
+ }
662
+ }
663
+ return null;
664
+ }
665
+
666
+ // Download file from URL or local source
667
+ async downloadFile(relativePath, targetPath, shouldBackup = true) {
668
+ try {
669
+ let content;
670
+
671
+ if (this.config.localSource) {
672
+ // Local source mode
673
+ const localPath = path.join(this.config.localSource, relativePath);
674
+ content = await fs.readFile(localPath, 'utf8');
675
+ } else {
676
+ // Remote download mode
677
+ const url = `${this.config.baseUrl}/${relativePath}`;
678
+ const response = await fetch(url);
679
+
680
+ if (!response.ok) {
681
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
682
+ }
683
+
684
+ content = await response.text();
685
+ }
686
+
687
+ // Ensure target directory exists
688
+ await fs.ensureDir(path.dirname(targetPath));
689
+
690
+ // Create backup if file exists
691
+ await this.createBackup(targetPath, shouldBackup);
692
+
693
+ // Write file
694
+ await fs.writeFile(targetPath, content);
695
+
696
+ return true;
697
+ } catch (error) {
698
+ throw new Error(`Failed to download ${relativePath}: ${error.message}`);
699
+ }
700
+ }
701
+
702
+ // Get IDE-specific file list
703
+ getIDEFiles(selectedIDE, selectedComponents = null) {
704
+ const files = [];
705
+
706
+ // If no components specified, include all files (fresh installation)
707
+ const includeAll = !selectedComponents;
708
+
709
+ switch (selectedIDE) {
710
+ case 'cursor':
711
+ // Cursor rules
712
+ if (includeAll || selectedComponents.includes('rules')) {
713
+ files.push(
714
+ { source: 'cursor/cc.mdc', target: '.cursor/rules/cc.mdc', component: 'rules' }
715
+ );
716
+ }
717
+
718
+ // Core commands and docs
719
+ if (includeAll || selectedComponents.includes('commands')) {
720
+ files.push(
721
+ { source: 'cursor/cc.md', target: '.code-captain/cc.md', component: 'commands' }
722
+ );
723
+
724
+ const cursorCommands = [
725
+ 'create-adr.md', 'create-spec.md', 'edit-spec.md', 'execute-task.md',
726
+ 'explain-code.md', 'initialize.md', 'new-command.md', 'plan-product.md',
727
+ 'research.md', 'status.md', 'swab.md'
728
+ ];
729
+
730
+ cursorCommands.forEach(cmd => {
731
+ files.push({
732
+ source: `cursor/commands/${cmd}`,
733
+ target: `.code-captain/commands/${cmd}`,
734
+ component: 'commands'
735
+ });
736
+ });
737
+ }
738
+
739
+ // GitHub integration
740
+ if (includeAll || selectedComponents.includes('github')) {
741
+ const githubFiles = [
742
+ 'integrations/github/create-github-issues.md',
743
+ 'integrations/github/sync-github-issues.md',
744
+ 'integrations/github/sync.md'
745
+ ];
746
+
747
+ githubFiles.forEach(file => {
748
+ files.push({
749
+ source: `cursor/${file}`,
750
+ target: `.code-captain/${file}`,
751
+ component: 'github'
752
+ });
753
+ });
754
+ }
755
+
756
+ // Azure DevOps integration
757
+ if (includeAll || selectedComponents.includes('azure')) {
758
+ const azureFiles = [
759
+ 'integrations/azure-devops/create-azure-work-items.md',
760
+ 'integrations/azure-devops/sync-azure-work-items.md'
761
+ ];
762
+
763
+ azureFiles.forEach(file => {
764
+ files.push({
765
+ source: `cursor/${file}`,
766
+ target: `.code-captain/${file}`,
767
+ component: 'azure'
768
+ });
769
+ });
770
+ }
771
+
772
+ // Documentation
773
+ if (includeAll || selectedComponents.includes('docs')) {
774
+ files.push({
775
+ source: 'cursor/docs/best-practices.md',
776
+ target: '.code-captain/docs/best-practices.md',
777
+ component: 'docs'
778
+ });
779
+ }
780
+
781
+ break;
782
+
783
+ case 'copilot':
784
+ // Chatmodes
785
+ if (includeAll || selectedComponents.includes('chatmodes')) {
786
+ files.push(
787
+ { source: 'copilot/chatmodes/Code Captain.chatmode.md', target: '.github/chatmodes/Code Captain.chatmode.md', component: 'chatmodes' }
788
+ );
789
+ }
790
+
791
+ // Prompts
792
+ if (includeAll || selectedComponents.includes('prompts')) {
793
+ const copilotPrompts = [
794
+ 'create-adr.prompt.md', 'create-spec.prompt.md', 'edit-spec.prompt.md',
795
+ 'execute-task.prompt.md', 'explain-code.prompt.md', 'initialize.prompt.md',
796
+ 'new-command.prompt.md', 'plan-product.prompt.md', 'research.prompt.md',
797
+ 'status.prompt.md', 'swab.prompt.md'
798
+ ];
799
+
800
+ copilotPrompts.forEach(prompt => {
801
+ files.push({
802
+ source: `copilot/prompts/${prompt}`,
803
+ target: `.github/prompts/${prompt}`,
804
+ component: 'prompts'
805
+ });
806
+ });
807
+ }
808
+
809
+ // Documentation
810
+ if (includeAll || selectedComponents.includes('docs')) {
811
+ files.push({
812
+ source: 'copilot/docs/best-practices.md',
813
+ target: '.code-captain/docs/best-practices.md',
814
+ component: 'docs'
815
+ });
816
+ }
817
+
818
+ break;
819
+
820
+ case 'windsurf':
821
+ // Rules
822
+ if (includeAll || selectedComponents.includes('rules')) {
823
+ files.push(
824
+ { source: 'windsurf/rules/cc.md', target: 'windsurf/rules/cc.md', component: 'rules' }
825
+ );
826
+ }
827
+
828
+ // Workflows
829
+ if (includeAll || selectedComponents.includes('workflows')) {
830
+ const windsurfWorkflows = [
831
+ 'create-adr.md', 'create-spec.md', 'edit-spec.md', 'execute-task.md',
832
+ 'explain-code.md', 'initialize.md', 'new-command.md', 'status.md'
833
+ ];
834
+
835
+ windsurfWorkflows.forEach(workflow => {
836
+ files.push({
837
+ source: `windsurf/workflows/${workflow}`,
838
+ target: `windsurf/workflows/${workflow}`,
839
+ component: 'workflows'
840
+ });
841
+ });
842
+ }
843
+
844
+ break;
845
+
846
+ case 'claude':
847
+ // Claude agents
848
+ if (includeAll || selectedComponents.includes('agents')) {
849
+ const claudeAgents = [
850
+ 'code-captain.md', 'spec-generator.md', 'spec-orchestrator.md',
851
+ 'story-creator.md', 'tech-spec.md'
852
+ ];
853
+
854
+ claudeAgents.forEach(agent => {
855
+ files.push({
856
+ source: `claude-code/agents/${agent}`,
857
+ target: `.code-captain/claude/agents/${agent}`,
858
+ component: 'agents'
859
+ });
860
+ });
861
+ }
862
+
863
+ // Claude commands
864
+ if (includeAll || selectedComponents.includes('claude-commands')) {
865
+ const claudeCommands = [
866
+ 'cc-create-spec.md', 'cc-initialize.md'
867
+ ];
868
+
869
+ claudeCommands.forEach(command => {
870
+ files.push({
871
+ source: `claude-code/commands/${command}`,
872
+ target: `.code-captain/claude/commands/${command}`,
873
+ component: 'claude-commands'
874
+ });
875
+ });
876
+ }
877
+
878
+ break;
879
+ }
880
+
881
+ return files;
882
+ }
883
+
884
+ // Install files for selected IDE
885
+ async installFiles(selectedIDE, installOptions) {
886
+ const selectedComponents = installOptions.installAll ? null : installOptions.selectedComponents;
887
+ const files = this.getIDEFiles(selectedIDE, selectedComponents);
888
+ const spinner = ora(`Installing ${this.ides[selectedIDE].name} integration...`).start();
889
+
890
+ try {
891
+ let completed = 0;
892
+ const backupPaths = [];
893
+
894
+ for (const file of files) {
895
+ const shouldBackup = installOptions.createBackups !== false; // Default to true if not specified
896
+ await this.downloadFile(file.source, file.target, shouldBackup);
897
+ completed++;
898
+ spinner.text = `Installing files... (${completed}/${files.length})`;
899
+ }
900
+
901
+ // Save manifest for future change detection
902
+ if (installOptions.changeInfo) {
903
+ const remoteManifest = await this.getRemoteManifest();
904
+ await this.saveManifest(remoteManifest, files);
905
+ }
906
+
907
+ spinner.succeed(`${this.ides[selectedIDE].name} integration installed successfully!`);
908
+
909
+ return {
910
+ totalFiles: files.length,
911
+ targetDir: selectedIDE === 'copilot' ? '.github + .code-captain/docs' :
912
+ selectedIDE === 'windsurf' ? 'windsurf' :
913
+ selectedIDE === 'claude' ? '.code-captain/claude' : '.code-captain (+ .cursor/rules)',
914
+ components: installOptions.installAll ? 'All components' : installOptions.selectedComponents.join(', '),
915
+ changesDetected: installOptions.changeInfo && (installOptions.changeInfo.changes.length > 0 || installOptions.changeInfo.newFiles.length > 0)
916
+ };
917
+ } catch (error) {
918
+ spinner.fail('Installation failed');
919
+ throw error;
920
+ }
921
+ }
922
+
923
+ // Show post-installation instructions
924
+ showInstructions(selectedIDE, installResult) {
925
+ const ide = this.ides[selectedIDE];
926
+
927
+ console.log('\n' + boxen(
928
+ chalk.bold.green('šŸŽ‰ Installation Complete!') + '\n\n' +
929
+ chalk.blue('IDE:') + ` ${ide.name}\n` +
930
+ chalk.blue('Files installed:') + ` ${installResult.totalFiles}\n` +
931
+ chalk.blue('Location:') + ` ${installResult.targetDir}/\n` +
932
+ chalk.blue('Components:') + ` ${installResult.components}`,
933
+ {
934
+ padding: 1,
935
+ margin: 1,
936
+ borderStyle: 'round',
937
+ borderColor: 'green'
938
+ }
939
+ ));
940
+
941
+ console.log(chalk.bold.yellow('\nšŸ“š Next Steps:'));
942
+ console.log(chalk.gray('═'.repeat(50)));
943
+
944
+ switch (selectedIDE) {
945
+ case 'cursor':
946
+ console.log(chalk.blue('1.') + ' Restart Cursor to load the new rule from ' + chalk.cyan('.cursor/rules/cc.mdc'));
947
+ console.log(chalk.blue('2.') + ' Use ' + chalk.cyan('cc: initialize') + ' to set up your project');
948
+ console.log(chalk.blue('3.') + ' Try ' + chalk.cyan('cc: plan-product') + ' for product planning');
949
+ console.log(chalk.blue('4.') + ' Use ' + chalk.cyan('cc: create-spec') + ' for feature specifications');
950
+ break;
951
+
952
+ case 'copilot':
953
+ console.log(chalk.blue('1.') + ' Restart VS Code to load chatmodes from ' + chalk.cyan('.github/chatmodes/'));
954
+ console.log(chalk.blue('2.') + ' Open GitHub Copilot Chat in VS Code');
955
+ console.log(chalk.blue('3.') + ' Type ' + chalk.cyan('@Code Captain') + ' to access the chatmode');
956
+ console.log(chalk.blue('4.') + ' Use prompts from ' + chalk.cyan('.github/prompts/') + ' for workflows');
957
+ break;
958
+
959
+ case 'windsurf':
960
+ console.log(chalk.blue('1.') + ' Restart Windsurf to load the new workflows');
961
+ console.log(chalk.blue('2.') + ' Use the AI agent with Code Captain commands');
962
+ console.log(chalk.blue('3.') + ' Try ' + chalk.cyan('cc: initialize') + ' to set up your project');
963
+ break;
964
+
965
+ case 'claude':
966
+ console.log(chalk.blue('1.') + ' Claude agents and commands are installed in ' + chalk.cyan('.code-captain/claude/'));
967
+ console.log(chalk.blue('2.') + ' Reference the agents in ' + chalk.cyan('.code-captain/claude/agents/') + ' for specialized workflows');
968
+ console.log(chalk.blue('3.') + ' Use command templates from ' + chalk.cyan('.code-captain/claude/commands/'));
969
+ console.log(chalk.blue('4.') + ' Import agent contexts directly into Claude conversations');
970
+ break;
971
+ }
972
+
973
+ console.log('\n' + chalk.green('šŸš€ Ready to start building with Code Captain!'));
974
+ console.log(chalk.gray('Documentation: https://github.com/devobsessed/code-captain'));
975
+
976
+ // Show backup information if backups were created
977
+ if (installResult.totalFiles > 0) {
978
+ console.log('\n' + chalk.yellow('šŸ’¾ Backup Information:'));
979
+ console.log(chalk.gray('Existing files were backed up with timestamps (e.g., filename.backup.2024-01-01T12-00-00-000Z)'));
980
+ console.log(chalk.gray('You can safely delete backup files once you\'re satisfied with the installation.'));
981
+ }
982
+ }
983
+
984
+ // Handle installation errors
985
+ handleError(error, selectedIDE) {
986
+ console.error('\n' + chalk.red('āŒ Installation failed'));
987
+ console.error(chalk.red('Error:'), error.message);
988
+
989
+ console.log('\n' + chalk.yellow('šŸ”§ Troubleshooting:'));
990
+ console.log(chalk.blue('1.') + ' Check your internet connection');
991
+ console.log(chalk.blue('2.') + ' Ensure you have write permissions in this directory');
992
+ console.log(chalk.blue('3.') + ' Try running with ' + chalk.cyan('CC_LOCAL_SOURCE=path npx @devobsessed/code-captain'));
993
+
994
+ if (selectedIDE) {
995
+ console.log(chalk.blue('4.') + ` Try a different IDE option`);
996
+ }
997
+
998
+ console.log('\n' + chalk.gray('For help: https://github.com/devobsessed/code-captain/issues'));
999
+ }
1000
+
1001
+ // Main installation flow
1002
+ async run() {
1003
+ try {
1004
+ // Show welcome
1005
+ this.showWelcome();
1006
+
1007
+ // Check compatibility
1008
+ const systemInfo = await this.checkCompatibility();
1009
+
1010
+ // Select IDE
1011
+ const selectedIDE = await this.selectIDE();
1012
+
1013
+ // Select installation components
1014
+ const installOptions = await this.selectInstallationComponents(selectedIDE, systemInfo.existingInstallations);
1015
+
1016
+ if (installOptions.skipInstall) {
1017
+ console.log(chalk.yellow('\nšŸ‘‹ Installation cancelled due to no changes detected.'));
1018
+ process.exit(0);
1019
+ }
1020
+
1021
+ // Confirm installation
1022
+ const confirmed = await this.confirmInstallation(selectedIDE, systemInfo, installOptions);
1023
+
1024
+ if (!confirmed) {
1025
+ console.log(chalk.yellow('\nšŸ‘‹ Installation cancelled'));
1026
+ process.exit(0);
1027
+ }
1028
+
1029
+ // Install files
1030
+ const installResult = await this.installFiles(selectedIDE, installOptions);
1031
+
1032
+ // Show instructions
1033
+ this.showInstructions(selectedIDE, installResult);
1034
+
1035
+ } catch (error) {
1036
+ this.handleError(error);
1037
+ process.exit(1);
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ // Run installer if called directly
1043
+ if (import.meta.url === `file://${process.argv[1]}`) {
1044
+ const installer = new CodeCaptainInstaller();
1045
+ installer.run();
1046
+ }
1047
+
1048
+ export default CodeCaptainInstaller;