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