@devobsessed/code-captain 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/install.js CHANGED
@@ -454,6 +454,156 @@ class CodeCaptainInstaller {
454
454
  }
455
455
  }
456
456
 
457
+ // Generate a random GUID (uppercase)
458
+ generateGuid() {
459
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
460
+ .replace(/[xy]/g, (c) => {
461
+ const r = (Math.random() * 16) | 0;
462
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
463
+ })
464
+ .toUpperCase();
465
+ }
466
+
467
+ // Add code-captain.csproj to the first .sln file found in the current directory
468
+ async addProjectToSln(slnPath, csprojFileName) {
469
+ const content = await fs.readFile(slnPath, "utf8");
470
+ const projectName = path.basename(csprojFileName, ".csproj");
471
+ const idMarker = `"${csprojFileName}"`;
472
+
473
+ if (content.includes(idMarker)) return "up-to-date";
474
+
475
+ const projectGuid = this.generateGuid();
476
+ // SDK-style C# project type GUID
477
+ const typeGuid = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}";
478
+ const projectBlock =
479
+ `Project("${typeGuid}") = "${projectName}", "${csprojFileName}", "{${projectGuid}}"\n` +
480
+ `EndProject\n`;
481
+
482
+ // Insert before the Global section
483
+ let updated = content.replace(/^(Global\r?\n)/m, `${projectBlock}$1`);
484
+
485
+ // Parse solution configs and add ActiveCfg entries (but not Build.0, so it's excluded from builds)
486
+ const configSectionMatch = content.match(
487
+ /GlobalSection\(SolutionConfigurationPlatforms\)[^\n]*\n([\s\S]*?)EndGlobalSection/
488
+ );
489
+ if (configSectionMatch) {
490
+ const configLines = configSectionMatch[1]
491
+ .split(/\r?\n/)
492
+ .map((l) => l.trim())
493
+ .filter((l) => l.includes(" = "));
494
+ const configNames = configLines.map((l) => l.split(" = ")[0]);
495
+
496
+ if (configNames.length > 0) {
497
+ const activeCfgEntries = configNames
498
+ .map((c) => `\t\t{${projectGuid}}.${c}.ActiveCfg = ${c}`)
499
+ .join("\n");
500
+
501
+ updated = updated.replace(
502
+ /(GlobalSection\(ProjectConfigurationPlatforms\)[^\n]*\n)([\s\S]*?)(EndGlobalSection)/,
503
+ `$1$2${activeCfgEntries}\n\t$3`
504
+ );
505
+ }
506
+ }
507
+
508
+ await fs.copy(slnPath, `${slnPath}.backup`);
509
+ await fs.writeFile(slnPath, updated);
510
+ return "updated";
511
+ }
512
+
513
+ // Remove Code Captain ItemGroup from Directory.Build.props (migration from old approach)
514
+ async migrateDirectoryBuildProps() {
515
+ const targetPath = "Directory.Build.props";
516
+ if (!(await fs.pathExists(targetPath))) return null;
517
+
518
+ const content = await fs.readFile(targetPath, "utf8");
519
+ if (!content.includes('Label="Code Captain"')) return null;
520
+
521
+ // Remove the comment + ItemGroup block
522
+ let updated = content
523
+ .replace(/[ \t]*<!--[^\n]*Code Captain[^\n]*-->\r?\n/g, "")
524
+ .replace(
525
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>\r?\n?/g,
526
+ ""
527
+ );
528
+
529
+ await fs.copy(targetPath, `${targetPath}.backup`);
530
+ await fs.writeFile(targetPath, updated);
531
+ return "migrated";
532
+ }
533
+
534
+ // Install code-captain.csproj and register it in the .sln
535
+ async installCodeCaptainProject() {
536
+ const targetPath = "code-captain.csproj";
537
+ const templateSource = "copilot/code-captain.csproj";
538
+ const marker = 'Label="Code Captain"';
539
+
540
+ // Read template
541
+ let templateContent;
542
+ if (this.config.localSource) {
543
+ const localPath = path.join(this.config.localSource, templateSource);
544
+ if (!(await fs.pathExists(localPath))) {
545
+ throw new Error(`Template not found: ${localPath}`);
546
+ }
547
+ templateContent = await fs.readFile(localPath, "utf8");
548
+ } else {
549
+ const url = `${this.config.baseUrl}/${templateSource}`;
550
+ const response = await this.fetchWithTimeout(url, {}, 20000);
551
+ if (!response.ok) {
552
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
553
+ }
554
+ templateContent = await response.text();
555
+ }
556
+
557
+ // Extract the ItemGroup block from the template (for update comparison)
558
+ const templateBlockMatch = templateContent.match(
559
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
560
+ );
561
+ if (!templateBlockMatch) {
562
+ throw new Error("Template missing Code Captain ItemGroup");
563
+ }
564
+
565
+ const exists = await fs.pathExists(targetPath);
566
+ let csprojAction;
567
+
568
+ if (!exists) {
569
+ await fs.writeFile(targetPath, templateContent);
570
+ csprojAction = "created";
571
+ } else {
572
+ const existingContent = await fs.readFile(targetPath, "utf8");
573
+ if (existingContent.includes(marker)) {
574
+ const existingBlock = existingContent.match(
575
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
576
+ );
577
+ if (existingBlock && existingBlock[0] === templateBlockMatch[0]) {
578
+ csprojAction = "up-to-date";
579
+ } else {
580
+ await fs.copy(targetPath, `${targetPath}.backup`);
581
+ const updatedContent = existingContent.replace(
582
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/,
583
+ templateBlockMatch[0]
584
+ );
585
+ await fs.writeFile(targetPath, updatedContent);
586
+ csprojAction = "updated";
587
+ }
588
+ } else {
589
+ // Not a Code Captain file — don't touch it
590
+ csprojAction = "skipped";
591
+ }
592
+ }
593
+
594
+ // Register in .sln
595
+ const slnFiles = await this.detectSlnFiles();
596
+ let slnAction = "no-sln";
597
+ if (slnFiles.length > 0) {
598
+ slnAction = await this.addProjectToSln(slnFiles[0], targetPath);
599
+ }
600
+
601
+ // Migrate: remove Code Captain section from Directory.Build.props if present
602
+ const migrationAction = await this.migrateDirectoryBuildProps();
603
+
604
+ return { csprojAction, slnAction, migrationAction };
605
+ }
606
+
457
607
  // Auto-detect IDE preference
458
608
  detectIDE() {
459
609
  const detections = [];
@@ -553,7 +703,7 @@ class CodeCaptainInstaller {
553
703
  type: "confirm",
554
704
  name: "enableVsSolution",
555
705
  message:
556
- "Install VS Solution View (Directory.Build.props) to make Code Captain files visible in Visual Studio Solution Explorer?",
706
+ "Install VS Solution View (code-captain.csproj) to make Code Captain files visible in Visual Studio Solution Explorer?",
557
707
  default: true,
558
708
  },
559
709
  ]);
@@ -726,7 +876,7 @@ class CodeCaptainInstaller {
726
876
  { name: "Copilot Agents", value: "agents", checked: true },
727
877
  { name: "Copilot Prompts", value: "prompts", checked: true },
728
878
  {
729
- name: "VS Solution View (Directory.Build.props)",
879
+ name: "VS Solution View (code-captain.csproj)",
730
880
  value: "vs-solution",
731
881
  checked: false,
732
882
  },
@@ -916,7 +1066,7 @@ class CodeCaptainInstaller {
916
1066
 
917
1067
  // Extract the ItemGroup block from template
918
1068
  const itemGroupMatch = templateContent.match(
919
- /[ \t]*<ItemGroup Label="Code Captain">[\s\S]*?<\/ItemGroup>/
1069
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
920
1070
  );
921
1071
  if (!itemGroupMatch) {
922
1072
  throw new Error("Template missing Code Captain ItemGroup");
@@ -937,7 +1087,7 @@ class CodeCaptainInstaller {
937
1087
  if (existingContent.includes(marker)) {
938
1088
  // Case 3: Already has Code Captain content — replace if changed
939
1089
  const existingBlock = existingContent.match(
940
- /[ \t]*<ItemGroup Label="Code Captain">[\s\S]*?<\/ItemGroup>/
1090
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
941
1091
  );
942
1092
  if (existingBlock && existingBlock[0] === itemGroupBlock) {
943
1093
  return { action: "up-to-date" };
@@ -1145,7 +1295,7 @@ class CodeCaptainInstaller {
1145
1295
  spinner.text = `Installing files... (${completed}/${files.length})`;
1146
1296
  }
1147
1297
 
1148
- // Handle Directory.Build.props for vs-solution component
1298
+ // Handle vs-solution component (code-captain.csproj + .sln registration)
1149
1299
  let vsSolutionResult = null;
1150
1300
  const shouldInstallVsSolution =
1151
1301
  (installOptions.installAll && installOptions.installVsSolution) ||
@@ -1154,8 +1304,8 @@ class CodeCaptainInstaller {
1154
1304
  selectedComponents.includes("vs-solution"));
1155
1305
 
1156
1306
  if (shouldInstallVsSolution) {
1157
- spinner.text = "Installing Directory.Build.props...";
1158
- vsSolutionResult = await this.installDirectoryBuildProps();
1307
+ spinner.text = "Installing code-captain.csproj...";
1308
+ vsSolutionResult = await this.installCodeCaptainProject();
1159
1309
  }
1160
1310
 
1161
1311
  spinner.succeed(
@@ -1303,16 +1453,27 @@ class CodeCaptainInstaller {
1303
1453
  break;
1304
1454
  }
1305
1455
 
1306
- // Show Directory.Build.props result if applicable
1456
+ // Show VS Solution View result if applicable
1307
1457
  if (installResult.vsSolutionResult) {
1308
- const action = installResult.vsSolutionResult.action;
1309
- const messages = {
1310
- created: "Created Directory.Build.props — .github/ files now visible in VS Solution Explorer",
1311
- merged: "Merged Code Captain items into existing Directory.Build.props (backup saved as .backup)",
1312
- updated: "Updated Code Captain section in Directory.Build.props (backup saved as .backup)",
1313
- "up-to-date": "Directory.Build.props is already up to date",
1458
+ const { csprojAction, slnAction, migrationAction } =
1459
+ installResult.vsSolutionResult;
1460
+ const csprojMessages = {
1461
+ created: "Created code-captain.csproj",
1462
+ updated: "Updated code-captain.csproj (backup saved as .backup)",
1463
+ "up-to-date": "code-captain.csproj is already up to date",
1464
+ skipped: "code-captain.csproj skipped (file exists but was not created by Code Captain)",
1465
+ };
1466
+ const slnMessages = {
1467
+ updated: "registered in solution (.sln backup saved as .backup)",
1468
+ "up-to-date": "already registered in solution",
1469
+ "no-sln": "no .sln file found — add code-captain.csproj to your solution manually",
1314
1470
  };
1315
- console.log(chalk.cyan("\n📁 VS Solution View:"), messages[action]);
1471
+ let message = csprojMessages[csprojAction] || csprojAction;
1472
+ if (slnMessages[slnAction]) message += ` — ${slnMessages[slnAction]}`;
1473
+ if (migrationAction === "migrated") {
1474
+ message += " — removed Code Captain section from Directory.Build.props (backup saved)";
1475
+ }
1476
+ console.log(chalk.cyan("\n📁 VS Solution View:"), message);
1316
1477
  }
1317
1478
 
1318
1479
  console.log(
@@ -0,0 +1,14 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <!-- Code Captain: Make Copilot and Code Captain files visible in VS Solution Explorer -->
3
+ <PropertyGroup>
4
+ <TargetFramework>netstandard2.0</TargetFramework>
5
+ <IsPackable>false</IsPackable>
6
+ </PropertyGroup>
7
+ <ItemGroup Label="Code Captain">
8
+ <None Include=".github\copilot-instructions.md" Link="Code Captain\copilot-instructions.md" />
9
+ <None Include=".github\agents\**\*" Link="Code Captain\agents\%(RecursiveDir)%(Filename)%(Extension)" />
10
+ <None Include=".github\prompts\**\*" Link="Code Captain\prompts\%(RecursiveDir)%(Filename)%(Extension)" />
11
+ <None Include=".code-captain\docs\**\*" Link="Code Captain\docs\%(RecursiveDir)%(Filename)%(Extension)" />
12
+ <None Include=".code-captain\specs\**\*" Link="Code Captain\specs\%(RecursiveDir)%(Filename)%(Extension)" />
13
+ </ItemGroup>
14
+ </Project>
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.3.0",
3
- "timestamp": "2026-02-12T13:09:10.578Z",
2
+ "version": "0.4.0",
3
+ "timestamp": "2026-02-19T00:00:00.000Z",
4
4
  "commit": "a18e9dc145c3990823850611b321d0713e63d657",
5
5
  "description": "Code Captain file manifest for change detection",
6
6
  "files": {
@@ -225,9 +225,17 @@
225
225
  "size": 475,
226
226
  "lastModified": "2026-02-11T23:21:47.573Z",
227
227
  "version": "0.3.0",
228
- "component": "vs-solution",
228
+ "component": "vs-solution-legacy",
229
229
  "description": "<!-- Code Captain: Make .github/ Copilot files visible in VS Solution Explorer -->"
230
230
  },
231
+ "copilot/code-captain.csproj": {
232
+ "hash": "sha256:035c6482ba0c7e80c40669e01c9b557719450fa4be1bd6b07f1ff757f926619c",
233
+ "size": 857,
234
+ "lastModified": "2026-02-19T00:00:00.000Z",
235
+ "version": "0.4.0",
236
+ "component": "vs-solution",
237
+ "description": "<!-- Code Captain: Make Copilot and Code Captain files visible in VS Solution Explorer -->"
238
+ },
231
239
  "claude-code/agents/code-captain.md": {
232
240
  "hash": "sha256:6d87de22934ab5eecc48a899907dba6c38d2ecc30e96aa871eaf4fe4d500a08f",
233
241
  "size": 5832,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devobsessed/code-captain",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Unified AI Development Agent System with intelligent change detection",
5
5
  "type": "module",
6
6
  "bin": {