@devobsessed/code-captain 0.0.8 → 0.0.9

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