@devobsessed/code-captain 0.3.3 → 0.4.1

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
@@ -444,16 +444,217 @@ class CodeCaptainInstaller {
444
444
  return recommendations;
445
445
  }
446
446
 
447
- // Detect .sln files in the current directory
447
+ // Detect .sln / .slnx files in the current directory
448
448
  async detectSlnFiles() {
449
449
  try {
450
450
  const entries = await fs.readdir(".");
451
- return entries.filter((entry) => entry.endsWith(".sln"));
451
+ return entries.filter(
452
+ (entry) => entry.endsWith(".sln") || entry.endsWith(".slnx")
453
+ );
452
454
  } catch {
453
455
  return [];
454
456
  }
455
457
  }
456
458
 
459
+ // Add code-captain.csproj to an XML-format .slnx file
460
+ async addProjectToSlnx(slnxPath, csprojFileName) {
461
+ const content = await fs.readFile(slnxPath, "utf8");
462
+ const idMarker = `Path="${csprojFileName}"`;
463
+
464
+ if (content.includes(idMarker)) return "up-to-date";
465
+
466
+ // Detect line ending style
467
+ const nl = content.includes("\r\n") ? "\r\n" : "\n";
468
+
469
+ const updated = content.replace(
470
+ /<\/Solution>/,
471
+ ` <Project Path="${csprojFileName}" />${nl}</Solution>`
472
+ );
473
+
474
+ if (updated === content) {
475
+ throw new Error(
476
+ `Could not find </Solution> in ${slnxPath}. Please add code-captain.csproj to your solution manually in Visual Studio.`
477
+ );
478
+ }
479
+
480
+ await fs.copy(slnxPath, `${slnxPath}.backup`);
481
+ await fs.writeFile(slnxPath, updated);
482
+ return "updated";
483
+ }
484
+
485
+ // Generate a random GUID (uppercase)
486
+ generateGuid() {
487
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
488
+ .replace(/[xy]/g, (c) => {
489
+ const r = (Math.random() * 16) | 0;
490
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
491
+ })
492
+ .toUpperCase();
493
+ }
494
+
495
+ // Add code-captain.csproj to the first .sln file found in the current directory
496
+ async addProjectToSln(slnPath, csprojFileName) {
497
+ const content = await fs.readFile(slnPath, "utf8");
498
+ const projectName = path.basename(csprojFileName, ".csproj");
499
+ const idMarker = `"${csprojFileName}"`;
500
+
501
+ if (content.includes(idMarker)) return "up-to-date";
502
+
503
+ // Detect line ending style used by the .sln file
504
+ const nl = content.includes("\r\n") ? "\r\n" : "\n";
505
+
506
+ const projectGuid = this.generateGuid();
507
+ // SDK-style C# project type GUID
508
+ const typeGuid = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}";
509
+ const projectBlock =
510
+ `Project("${typeGuid}") = "${projectName}", "${csprojFileName}", "{${projectGuid}}"${nl}` +
511
+ `EndProject${nl}`;
512
+
513
+ // Find the last EndProject and insert after it (more reliable than searching for Global)
514
+ const lastEndProjectIdx = content.lastIndexOf(`EndProject${nl}`);
515
+ let updated;
516
+ if (lastEndProjectIdx !== -1) {
517
+ const insertAt = lastEndProjectIdx + `EndProject${nl}`.length;
518
+ updated = content.slice(0, insertAt) + projectBlock + content.slice(insertAt);
519
+ } else {
520
+ // Fallback: insert before Global section
521
+ updated = content.replace(/^(Global\r?\n)/m, `${projectBlock}$1`);
522
+ }
523
+
524
+ if (updated === content) {
525
+ // Regex fallback also failed — .sln format not recognized
526
+ throw new Error(
527
+ `Could not find insertion point in ${slnPath}. Please add code-captain.csproj to your solution manually in Visual Studio.`
528
+ );
529
+ }
530
+
531
+ // Parse solution configs and add ActiveCfg entries (but not Build.0, so it's excluded from builds)
532
+ const configSectionMatch = content.match(
533
+ /GlobalSection\(SolutionConfigurationPlatforms\)[^\n]*\n([\s\S]*?)EndGlobalSection/
534
+ );
535
+ if (configSectionMatch) {
536
+ const configLines = configSectionMatch[1]
537
+ .split(/\r?\n/)
538
+ .map((l) => l.trim())
539
+ .filter((l) => l.includes(" = "));
540
+ const configNames = configLines.map((l) => l.split(" = ")[0]);
541
+
542
+ if (configNames.length > 0) {
543
+ const activeCfgEntries = configNames
544
+ .map((c) => `\t\t{${projectGuid}}.${c}.ActiveCfg = ${c}`)
545
+ .join(nl);
546
+
547
+ updated = updated.replace(
548
+ /(GlobalSection\(ProjectConfigurationPlatforms\)[^\n]*\n)([\s\S]*?)(EndGlobalSection)/,
549
+ `$1$2${activeCfgEntries}${nl}\t$3`
550
+ );
551
+ }
552
+ }
553
+
554
+ await fs.copy(slnPath, `${slnPath}.backup`);
555
+ await fs.writeFile(slnPath, updated);
556
+ return "updated";
557
+ }
558
+
559
+ // Remove Code Captain ItemGroup from Directory.Build.props (migration from old approach)
560
+ async migrateDirectoryBuildProps() {
561
+ const targetPath = "Directory.Build.props";
562
+ if (!(await fs.pathExists(targetPath))) return null;
563
+
564
+ const content = await fs.readFile(targetPath, "utf8");
565
+ if (!content.includes('Label="Code Captain"')) return null;
566
+
567
+ // Remove the comment + ItemGroup block
568
+ let updated = content
569
+ .replace(/[ \t]*<!--[^\n]*Code Captain[^\n]*-->\r?\n/g, "")
570
+ .replace(
571
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>\r?\n?/g,
572
+ ""
573
+ );
574
+
575
+ await fs.copy(targetPath, `${targetPath}.backup`);
576
+ await fs.writeFile(targetPath, updated);
577
+ return "migrated";
578
+ }
579
+
580
+ // Install code-captain.csproj and register it in the .sln
581
+ async installCodeCaptainProject() {
582
+ const targetPath = "code-captain.csproj";
583
+ const templateSource = "copilot/code-captain.csproj";
584
+ const marker = 'Label="Code Captain"';
585
+
586
+ // Read template
587
+ let templateContent;
588
+ if (this.config.localSource) {
589
+ const localPath = path.join(this.config.localSource, templateSource);
590
+ if (!(await fs.pathExists(localPath))) {
591
+ throw new Error(`Template not found: ${localPath}`);
592
+ }
593
+ templateContent = await fs.readFile(localPath, "utf8");
594
+ } else {
595
+ const url = `${this.config.baseUrl}/${templateSource}`;
596
+ const response = await this.fetchWithTimeout(url, {}, 20000);
597
+ if (!response.ok) {
598
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
599
+ }
600
+ templateContent = await response.text();
601
+ }
602
+
603
+ // Extract the ItemGroup block from the template (for update comparison)
604
+ const templateBlockMatch = templateContent.match(
605
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
606
+ );
607
+ if (!templateBlockMatch) {
608
+ throw new Error("Template missing Code Captain ItemGroup");
609
+ }
610
+
611
+ const exists = await fs.pathExists(targetPath);
612
+ let csprojAction;
613
+
614
+ if (!exists) {
615
+ await fs.writeFile(targetPath, templateContent);
616
+ csprojAction = "created";
617
+ } else {
618
+ const existingContent = await fs.readFile(targetPath, "utf8");
619
+ if (existingContent.includes(marker)) {
620
+ const existingBlock = existingContent.match(
621
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
622
+ );
623
+ if (existingBlock && existingBlock[0] === templateBlockMatch[0]) {
624
+ csprojAction = "up-to-date";
625
+ } else {
626
+ await fs.copy(targetPath, `${targetPath}.backup`);
627
+ const updatedContent = existingContent.replace(
628
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/,
629
+ templateBlockMatch[0]
630
+ );
631
+ await fs.writeFile(targetPath, updatedContent);
632
+ csprojAction = "updated";
633
+ }
634
+ } else {
635
+ // Not a Code Captain file — don't touch it
636
+ csprojAction = "skipped";
637
+ }
638
+ }
639
+
640
+ // Register in .sln / .slnx
641
+ const slnFiles = await this.detectSlnFiles();
642
+ let slnAction = "no-sln";
643
+ if (slnFiles.length > 0) {
644
+ const slnFile = slnFiles[0];
645
+ if (slnFile.endsWith(".slnx")) {
646
+ slnAction = await this.addProjectToSlnx(slnFile, targetPath);
647
+ } else {
648
+ slnAction = await this.addProjectToSln(slnFile, targetPath);
649
+ }
650
+ }
651
+
652
+ // Migrate: remove Code Captain section from Directory.Build.props if present
653
+ const migrationAction = await this.migrateDirectoryBuildProps();
654
+
655
+ return { csprojAction, slnAction, migrationAction };
656
+ }
657
+
457
658
  // Auto-detect IDE preference
458
659
  detectIDE() {
459
660
  const detections = [];
@@ -553,7 +754,7 @@ class CodeCaptainInstaller {
553
754
  type: "confirm",
554
755
  name: "enableVsSolution",
555
756
  message:
556
- "Install VS Solution View (Directory.Build.props) to make Code Captain files visible in Visual Studio Solution Explorer?",
757
+ "Install VS Solution View (code-captain.csproj) to make Code Captain files visible in Visual Studio Solution Explorer?",
557
758
  default: true,
558
759
  },
559
760
  ]);
@@ -726,7 +927,7 @@ class CodeCaptainInstaller {
726
927
  { name: "Copilot Agents", value: "agents", checked: true },
727
928
  { name: "Copilot Prompts", value: "prompts", checked: true },
728
929
  {
729
- name: "VS Solution View (Directory.Build.props)",
930
+ name: "VS Solution View (code-captain.csproj)",
730
931
  value: "vs-solution",
731
932
  checked: false,
732
933
  },
@@ -916,7 +1117,7 @@ class CodeCaptainInstaller {
916
1117
 
917
1118
  // Extract the ItemGroup block from template
918
1119
  const itemGroupMatch = templateContent.match(
919
- /[ \t]*<ItemGroup Label="Code Captain">[\s\S]*?<\/ItemGroup>/
1120
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
920
1121
  );
921
1122
  if (!itemGroupMatch) {
922
1123
  throw new Error("Template missing Code Captain ItemGroup");
@@ -937,7 +1138,7 @@ class CodeCaptainInstaller {
937
1138
  if (existingContent.includes(marker)) {
938
1139
  // Case 3: Already has Code Captain content — replace if changed
939
1140
  const existingBlock = existingContent.match(
940
- /[ \t]*<ItemGroup Label="Code Captain">[\s\S]*?<\/ItemGroup>/
1141
+ /[ \t]*<ItemGroup Label="Code Captain"[^>]*>[\s\S]*?<\/ItemGroup>/
941
1142
  );
942
1143
  if (existingBlock && existingBlock[0] === itemGroupBlock) {
943
1144
  return { action: "up-to-date" };
@@ -1145,7 +1346,7 @@ class CodeCaptainInstaller {
1145
1346
  spinner.text = `Installing files... (${completed}/${files.length})`;
1146
1347
  }
1147
1348
 
1148
- // Handle Directory.Build.props for vs-solution component
1349
+ // Handle vs-solution component (code-captain.csproj + .sln registration)
1149
1350
  let vsSolutionResult = null;
1150
1351
  const shouldInstallVsSolution =
1151
1352
  (installOptions.installAll && installOptions.installVsSolution) ||
@@ -1154,8 +1355,8 @@ class CodeCaptainInstaller {
1154
1355
  selectedComponents.includes("vs-solution"));
1155
1356
 
1156
1357
  if (shouldInstallVsSolution) {
1157
- spinner.text = "Installing Directory.Build.props...";
1158
- vsSolutionResult = await this.installDirectoryBuildProps();
1358
+ spinner.text = "Installing code-captain.csproj...";
1359
+ vsSolutionResult = await this.installCodeCaptainProject();
1159
1360
  }
1160
1361
 
1161
1362
  spinner.succeed(
@@ -1303,16 +1504,27 @@ class CodeCaptainInstaller {
1303
1504
  break;
1304
1505
  }
1305
1506
 
1306
- // Show Directory.Build.props result if applicable
1507
+ // Show VS Solution View result if applicable
1307
1508
  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",
1509
+ const { csprojAction, slnAction, migrationAction } =
1510
+ installResult.vsSolutionResult;
1511
+ const csprojMessages = {
1512
+ created: "Created code-captain.csproj",
1513
+ updated: "Updated code-captain.csproj (backup saved as .backup)",
1514
+ "up-to-date": "code-captain.csproj is already up to date",
1515
+ skipped: "code-captain.csproj skipped (file exists but was not created by Code Captain)",
1314
1516
  };
1315
- console.log(chalk.cyan("\n📁 VS Solution View:"), messages[action]);
1517
+ const slnMessages = {
1518
+ updated: "registered in solution (.sln backup saved as .backup)",
1519
+ "up-to-date": "already registered in solution",
1520
+ "no-sln": "no .sln/.slnx file found — add code-captain.csproj to your solution manually in Visual Studio",
1521
+ };
1522
+ let message = csprojMessages[csprojAction] || csprojAction;
1523
+ if (slnMessages[slnAction]) message += ` — ${slnMessages[slnAction]}`;
1524
+ if (migrationAction === "migrated") {
1525
+ message += " — removed Code Captain section from Directory.Build.props (backup saved)";
1526
+ }
1527
+ console.log(chalk.cyan("\n📁 VS Solution View:"), message);
1316
1528
  }
1317
1529
 
1318
1530
  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.1",
4
4
  "description": "Unified AI Development Agent System with intelligent change detection",
5
5
  "type": "module",
6
6
  "bin": {