@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 +229 -17
- package/copilot/code-captain.csproj +14 -0
- package/manifest.json +11 -3
- package/package.json +1 -1
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(
|
|
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 (
|
|
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 (
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
1158
|
-
vsSolutionResult = await this.
|
|
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
|
|
1507
|
+
// Show VS Solution View result if applicable
|
|
1307
1508
|
if (installResult.vsSolutionResult) {
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
updated: "Updated
|
|
1313
|
-
"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
|
-
|
|
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
|
-
"timestamp": "2026-02-
|
|
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,
|