compound-workflow 1.7.2 → 1.8.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/.cursor-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/install-cli.mjs +95 -394
package/package.json
CHANGED
package/scripts/install-cli.mjs
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* - Commands: add/remove .md under src/.agents/commands/ (frontmatter: invocation, name, description).
|
|
10
10
|
* Registry (src/.agents/registry.json) + generate-platform-artifacts → opencode.managed.json → install.
|
|
11
11
|
* - Agents: add/remove .md under src/.agents/agents/ (frontmatter: name, description). Same pipeline.
|
|
12
|
-
* - Skills: add/remove dir src/.agents/skills/<name>/SKILL.md. OpenCode uses skills path
|
|
13
|
-
*
|
|
12
|
+
* - Skills: add/remove dir src/.agents/skills/<name>/SKILL.md. OpenCode uses skills path.
|
|
13
|
+
*
|
|
14
|
+
* PLATFORM STRATEGY:
|
|
15
|
+
* - Cursor: copies files into .cursor/skills/, .cursor/commands/, .cursor/agents/ for native discovery.
|
|
16
|
+
* - Claude Code: marketplace flow via .claude-plugin/ + project .claude/settings.json.
|
|
17
|
+
* - OpenCode: opencode.json with paths into package source.
|
|
14
18
|
* Run install (or npm install compound-workflow) after any change; no other registration needed.
|
|
15
19
|
*/
|
|
16
20
|
import fs from "node:fs";
|
|
@@ -25,31 +29,26 @@ function usage(exitCode = 0) {
|
|
|
25
29
|
const msg = `
|
|
26
30
|
Usage:
|
|
27
31
|
(automatic) npm install compound-workflow # runs install via postinstall; no npx needed
|
|
28
|
-
(manual) npx compound-workflow install [--root <projectDir>] [--dry-run] [--no-config]
|
|
32
|
+
(manual) npx compound-workflow install [--root <projectDir>] [--dry-run] [--no-config]
|
|
29
33
|
|
|
30
34
|
Install writes opencode.json (from package), merges AGENTS.md, creates standard
|
|
31
|
-
docs/todos directories,
|
|
32
|
-
|
|
35
|
+
docs/todos directories, copies skills/commands/agents into .cursor/ for Cursor,
|
|
36
|
+
and registers the Claude Code plugin (project-scoped).
|
|
33
37
|
|
|
34
38
|
--root <dir> Project directory (default: cwd)
|
|
35
39
|
--dry-run Print planned changes only
|
|
36
40
|
--no-config Skip Repo Config Block reminder
|
|
37
|
-
--no-register-cursor Do not register plugin with Cursor (skip apply to ~/.claude/)
|
|
38
|
-
--register-cursor Force registration with Cursor even if ~/.cursor not found
|
|
39
41
|
`;
|
|
40
42
|
(exitCode === 0 ? console.log : console.error)(msg.trimStart());
|
|
41
43
|
process.exit(exitCode);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function parseArgs(argv) {
|
|
45
|
-
const out = { root: process.cwd(), dryRun: false, noConfig: false
|
|
47
|
+
const out = { root: process.cwd(), dryRun: false, noConfig: false };
|
|
46
48
|
for (let i = 2; i < argv.length; i++) {
|
|
47
49
|
const arg = argv[i];
|
|
48
50
|
if (arg === "--dry-run") out.dryRun = true;
|
|
49
51
|
else if (arg === "--no-config") out.noConfig = true;
|
|
50
|
-
else if (arg === "--no-register-cursor") out.noRegisterCursor = true;
|
|
51
|
-
else if (arg === "--register-cursor") out.registerCursor = true;
|
|
52
|
-
else if (arg === "--verify") out.verify = true;
|
|
53
52
|
else if (arg === "--root") {
|
|
54
53
|
const value = argv[i + 1];
|
|
55
54
|
if (!value) usage(1);
|
|
@@ -342,13 +341,12 @@ function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
|
|
|
342
341
|
const claudeManifest = readJsonMaybe(claudeSrc);
|
|
343
342
|
if (!cursorManifest || !claudeManifest) return;
|
|
344
343
|
|
|
345
|
-
// Cursor
|
|
346
|
-
//
|
|
347
|
-
// so Cursor can discover them properly. Skills still use source path.
|
|
344
|
+
// All Cursor paths point directly at the package source — no symlink indirection.
|
|
345
|
+
// This ensures frontmatter (descriptions) is parsed correctly by Cursor for all components.
|
|
348
346
|
const cursorOut = {
|
|
349
347
|
...cursorManifest,
|
|
350
|
-
commands:
|
|
351
|
-
agents:
|
|
348
|
+
commands: `${pathsBase}/commands`,
|
|
349
|
+
agents: `${pathsBase}/agents`,
|
|
352
350
|
skills: `${pathsBase}/skills`,
|
|
353
351
|
};
|
|
354
352
|
// Claude Code only accepts name, description, author in plugin.json.
|
|
@@ -428,261 +426,151 @@ function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
|
|
|
428
426
|
console.log("Wrote: .cursor-plugin/plugin.json, .claude-plugin/plugin.json, .cursor-plugin/registration.json" + (isSelfInstall ? "" : ", .claude-plugin/marketplace.json"));
|
|
429
427
|
}
|
|
430
428
|
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
432
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
433
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
434
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
435
|
+
const destPath = path.join(destDir, entry.name);
|
|
436
|
+
if (entry.isDirectory()) {
|
|
437
|
+
copyDirRecursive(srcPath, destPath);
|
|
438
|
+
} else if (entry.isFile()) {
|
|
439
|
+
fs.copyFileSync(srcPath, destPath);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
431
444
|
/**
|
|
432
|
-
* Cursor discovers skills
|
|
433
|
-
*
|
|
445
|
+
* Cursor discovers skills from .cursor/skills/ (each subdir with SKILL.md).
|
|
446
|
+
* Copy package skills so Cursor finds them with full frontmatter metadata.
|
|
434
447
|
*/
|
|
435
|
-
function
|
|
436
|
-
const packageSkillsAbs =
|
|
437
|
-
? path.join(
|
|
438
|
-
|
|
448
|
+
function copyCursorSkills(targetRoot, dryRun, isSelfInstall) {
|
|
449
|
+
const packageSkillsAbs = path.join(
|
|
450
|
+
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
451
|
+
"src", ".agents", "skills"
|
|
452
|
+
);
|
|
439
453
|
if (!fs.existsSync(packageSkillsAbs)) return;
|
|
440
454
|
|
|
441
455
|
const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
} catch {
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const skillDirs = entries.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsAbs, e.name, "SKILL.md"))).map((e) => e.name);
|
|
456
|
+
const skillDirs = fs.readdirSync(packageSkillsAbs, { withFileTypes: true })
|
|
457
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsAbs, e.name, "SKILL.md")))
|
|
458
|
+
.map((e) => e.name);
|
|
450
459
|
if (skillDirs.length === 0) return;
|
|
451
460
|
|
|
452
461
|
if (dryRun) {
|
|
453
|
-
console.log("[dry-run] Would
|
|
462
|
+
console.log("[dry-run] Would copy", skillDirs.length, "skills into .cursor/skills/");
|
|
454
463
|
return;
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
fs.mkdirSync(cursorSkillsDir, { recursive: true });
|
|
458
|
-
const packageSkillsReal = realpathSafe(packageSkillsAbs);
|
|
459
467
|
const skillSet = new Set(skillDirs);
|
|
460
468
|
|
|
461
|
-
// Prune
|
|
469
|
+
// Prune stale entries that we manage (contain SKILL.md) but are no longer in the package
|
|
462
470
|
try {
|
|
463
471
|
for (const entry of fs.readdirSync(cursorSkillsDir, { withFileTypes: true })) {
|
|
464
|
-
if (!entry.
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const base = path.basename(resolved);
|
|
470
|
-
if (skillSet.has(base)) continue;
|
|
471
|
-
fs.rmSync(linkPath);
|
|
472
|
-
} catch {
|
|
473
|
-
/* ignore broken symlinks or permission errors */
|
|
472
|
+
if (!entry.isDirectory()) continue;
|
|
473
|
+
const skillMd = path.join(cursorSkillsDir, entry.name, "SKILL.md");
|
|
474
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
475
|
+
if (!skillSet.has(entry.name)) {
|
|
476
|
+
fs.rmSync(path.join(cursorSkillsDir, entry.name), { recursive: true, force: true });
|
|
474
477
|
}
|
|
475
478
|
}
|
|
476
|
-
} catch {
|
|
477
|
-
/* .cursor/skills not readable */
|
|
478
|
-
}
|
|
479
|
+
} catch { /* ignore */ }
|
|
479
480
|
|
|
480
481
|
for (const name of skillDirs) {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (fs.existsSync(linkPath)) {
|
|
485
|
-
const stat = fs.lstatSync(linkPath);
|
|
486
|
-
if (!stat.isSymbolicLink()) continue;
|
|
487
|
-
try {
|
|
488
|
-
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
489
|
-
} catch {
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
fs.rmSync(linkPath);
|
|
493
|
-
}
|
|
494
|
-
fs.symlinkSync(targetPath, linkPath, "dir");
|
|
495
|
-
} catch (err) {
|
|
496
|
-
if (err.code === "EPERM" && process.platform === "win32") {
|
|
497
|
-
try {
|
|
498
|
-
fs.symlinkSync(targetPath, linkPath, "junction");
|
|
499
|
-
} catch {
|
|
500
|
-
console.warn("[cursor] Could not symlink skill", name, err.message);
|
|
501
|
-
}
|
|
502
|
-
} else {
|
|
503
|
-
console.warn("[cursor] Could not symlink skill", name, err.message);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
482
|
+
const dest = path.join(cursorSkillsDir, name);
|
|
483
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
484
|
+
copyDirRecursive(path.join(packageSkillsAbs, name), dest);
|
|
506
485
|
}
|
|
507
|
-
console.log("
|
|
486
|
+
console.log("Copied", skillDirs.length, "skills to .cursor/skills/");
|
|
508
487
|
}
|
|
509
488
|
|
|
510
489
|
/**
|
|
511
|
-
* Cursor discovers commands from .cursor/commands.
|
|
512
|
-
*
|
|
490
|
+
* Cursor discovers commands from .cursor/commands/ (.md files).
|
|
491
|
+
* Copy package commands so Cursor finds them with full frontmatter metadata.
|
|
513
492
|
*/
|
|
514
|
-
function
|
|
515
|
-
const packageCommandsAbs =
|
|
516
|
-
? path.join(
|
|
517
|
-
|
|
493
|
+
function copyCursorCommands(targetRoot, dryRun, isSelfInstall) {
|
|
494
|
+
const packageCommandsAbs = path.join(
|
|
495
|
+
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
496
|
+
"src", ".agents", "commands"
|
|
497
|
+
);
|
|
518
498
|
if (!fs.existsSync(packageCommandsAbs)) return;
|
|
519
499
|
|
|
520
500
|
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
} catch {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Filter .md files that are commands
|
|
529
|
-
const commandFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
501
|
+
const commandFiles = fs.readdirSync(packageCommandsAbs, { withFileTypes: true })
|
|
502
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
503
|
+
.map((e) => e.name);
|
|
530
504
|
if (commandFiles.length === 0) return;
|
|
531
505
|
|
|
532
506
|
if (dryRun) {
|
|
533
|
-
console.log("[dry-run] Would
|
|
507
|
+
console.log("[dry-run] Would copy", commandFiles.length, "commands into .cursor/commands/");
|
|
534
508
|
return;
|
|
535
509
|
}
|
|
536
510
|
|
|
537
511
|
fs.mkdirSync(cursorCommandsDir, { recursive: true });
|
|
538
|
-
const packageCommandsReal = realpathSafe(packageCommandsAbs);
|
|
539
512
|
const commandSet = new Set(commandFiles);
|
|
540
513
|
|
|
541
|
-
// Prune
|
|
514
|
+
// Prune stale .md files and old symlinks that we previously managed
|
|
542
515
|
try {
|
|
543
516
|
for (const entry of fs.readdirSync(cursorCommandsDir, { withFileTypes: true })) {
|
|
544
|
-
if (!entry.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const resolved = realpathSafe(linkPath);
|
|
548
|
-
if (!resolved.startsWith(packageCommandsReal + path.sep) && resolved !== packageCommandsReal) continue;
|
|
549
|
-
const base = path.basename(resolved);
|
|
550
|
-
if (commandSet.has(base)) continue;
|
|
551
|
-
fs.rmSync(linkPath);
|
|
552
|
-
} catch {
|
|
553
|
-
/* ignore broken symlinks or permission errors */
|
|
517
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
518
|
+
if (entry.isSymbolicLink() || !commandSet.has(entry.name)) {
|
|
519
|
+
fs.rmSync(path.join(cursorCommandsDir, entry.name), { force: true });
|
|
554
520
|
}
|
|
555
521
|
}
|
|
556
|
-
} catch {
|
|
557
|
-
/* .cursor/commands not readable */
|
|
558
|
-
}
|
|
522
|
+
} catch { /* ignore */ }
|
|
559
523
|
|
|
560
524
|
for (const name of commandFiles) {
|
|
561
|
-
|
|
562
|
-
const targetPath = path.join(packageCommandsAbs, name);
|
|
563
|
-
try {
|
|
564
|
-
if (fs.existsSync(linkPath)) {
|
|
565
|
-
const stat = fs.lstatSync(linkPath);
|
|
566
|
-
if (!stat.isSymbolicLink()) continue;
|
|
567
|
-
try {
|
|
568
|
-
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
569
|
-
} catch {
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
fs.rmSync(linkPath);
|
|
573
|
-
}
|
|
574
|
-
fs.symlinkSync(targetPath, linkPath, "file");
|
|
575
|
-
} catch (err) {
|
|
576
|
-
console.warn("[cursor] Could not symlink command", name, err.message);
|
|
577
|
-
}
|
|
525
|
+
fs.copyFileSync(path.join(packageCommandsAbs, name), path.join(cursorCommandsDir, name));
|
|
578
526
|
}
|
|
579
|
-
console.log("
|
|
527
|
+
console.log("Copied", commandFiles.length, "commands to .cursor/commands/");
|
|
580
528
|
}
|
|
581
529
|
|
|
582
530
|
/**
|
|
583
|
-
* Cursor discovers agents from .cursor/agents.
|
|
584
|
-
*
|
|
585
|
-
* Preserves subdirectory structure (research/, workflow/, review/).
|
|
531
|
+
* Cursor discovers agents from .cursor/agents/ (.md files, supports subdirs).
|
|
532
|
+
* Copy package agents preserving subdirectory structure (research/, review/, workflow/).
|
|
586
533
|
*/
|
|
587
|
-
function
|
|
588
|
-
const packageAgentsAbs =
|
|
589
|
-
? path.join(
|
|
590
|
-
|
|
534
|
+
function copyCursorAgents(targetRoot, dryRun, isSelfInstall) {
|
|
535
|
+
const packageAgentsAbs = path.join(
|
|
536
|
+
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
537
|
+
"src", ".agents", "agents"
|
|
538
|
+
);
|
|
591
539
|
if (!fs.existsSync(packageAgentsAbs)) return;
|
|
592
540
|
|
|
593
541
|
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
594
|
-
|
|
595
|
-
// Get all agent files from manifest (these include subdir paths like "research/repo-research-analyst.md")
|
|
596
542
|
const agentRels = GENERATED_MANIFEST.agents.map((a) => a.rel);
|
|
597
543
|
if (agentRels.length === 0) return;
|
|
598
544
|
|
|
599
545
|
if (dryRun) {
|
|
600
|
-
console.log("[dry-run] Would
|
|
546
|
+
console.log("[dry-run] Would copy", agentRels.length, "agents into .cursor/agents/");
|
|
601
547
|
return;
|
|
602
548
|
}
|
|
603
549
|
|
|
604
550
|
fs.mkdirSync(cursorAgentsDir, { recursive: true });
|
|
605
|
-
const packageAgentsReal = realpathSafe(packageAgentsAbs);
|
|
606
|
-
const agentSet = new Set(agentRels);
|
|
607
551
|
|
|
608
|
-
//
|
|
552
|
+
// Collect valid subdirectories from manifest
|
|
609
553
|
const validSubdirs = new Set();
|
|
610
554
|
for (const rel of agentRels) {
|
|
611
555
|
const subdir = path.dirname(rel);
|
|
612
556
|
if (subdir !== ".") validSubdirs.add(subdir);
|
|
613
557
|
}
|
|
614
558
|
|
|
615
|
-
// Prune
|
|
559
|
+
// Prune stale subdirs and files
|
|
616
560
|
try {
|
|
617
561
|
for (const entry of fs.readdirSync(cursorAgentsDir, { withFileTypes: true })) {
|
|
618
|
-
if (entry.isDirectory()) {
|
|
619
|
-
|
|
620
|
-
if (!validSubdirs.has(entry.name)) {
|
|
621
|
-
// Remove the entire stale subdirectory
|
|
622
|
-
fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
// Prune stale symlinks within valid subdirectories
|
|
626
|
-
const subdirPath = path.join(cursorAgentsDir, entry.name);
|
|
627
|
-
for (const subEntry of fs.readdirSync(subdirPath, { withFileTypes: true })) {
|
|
628
|
-
if (!subEntry.isSymbolicLink()) continue;
|
|
629
|
-
const linkPath = path.join(subdirPath, subEntry.name);
|
|
630
|
-
try {
|
|
631
|
-
const resolved = realpathSafe(linkPath);
|
|
632
|
-
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
633
|
-
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
634
|
-
if (agentSet.has(relFromPackage)) continue;
|
|
635
|
-
fs.rmSync(linkPath);
|
|
636
|
-
} catch {
|
|
637
|
-
/* ignore broken symlinks */
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
} else if (entry.isSymbolicLink()) {
|
|
641
|
-
// Handle flat symlinks (if any were created at root level)
|
|
642
|
-
const linkPath = path.join(cursorAgentsDir, entry.name);
|
|
643
|
-
try {
|
|
644
|
-
const resolved = realpathSafe(linkPath);
|
|
645
|
-
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
646
|
-
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
647
|
-
if (agentSet.has(relFromPackage)) continue;
|
|
648
|
-
fs.rmSync(linkPath);
|
|
649
|
-
} catch {
|
|
650
|
-
/* ignore broken symlinks */
|
|
651
|
-
}
|
|
562
|
+
if (entry.isDirectory() && !validSubdirs.has(entry.name)) {
|
|
563
|
+
fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
|
|
652
564
|
}
|
|
653
565
|
}
|
|
654
|
-
} catch {
|
|
655
|
-
/* .cursor/agents not readable */
|
|
656
|
-
}
|
|
566
|
+
} catch { /* ignore */ }
|
|
657
567
|
|
|
658
|
-
// Create symlinks preserving subdirectory structure
|
|
659
568
|
for (const rel of agentRels) {
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// Ensure subdirectory exists
|
|
664
|
-
const subdir = path.dirname(rel);
|
|
665
|
-
if (subdir !== ".") {
|
|
666
|
-
fs.mkdirSync(path.join(cursorAgentsDir, subdir), { recursive: true });
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
if (fs.existsSync(linkPath)) {
|
|
671
|
-
const stat = fs.lstatSync(linkPath);
|
|
672
|
-
if (!stat.isSymbolicLink()) continue;
|
|
673
|
-
try {
|
|
674
|
-
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
675
|
-
} catch {
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
fs.rmSync(linkPath);
|
|
679
|
-
}
|
|
680
|
-
fs.symlinkSync(targetPath, linkPath, "file");
|
|
681
|
-
} catch (err) {
|
|
682
|
-
console.warn("[cursor] Could not symlink agent", rel, err.message);
|
|
683
|
-
}
|
|
569
|
+
const destPath = path.join(cursorAgentsDir, rel);
|
|
570
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
571
|
+
fs.copyFileSync(path.join(packageAgentsAbs, rel), destPath);
|
|
684
572
|
}
|
|
685
|
-
console.log("
|
|
573
|
+
console.log("Copied", agentRels.length, "agents to .cursor/agents/");
|
|
686
574
|
}
|
|
687
575
|
|
|
688
576
|
function cursorDetected() {
|
|
@@ -690,149 +578,18 @@ function cursorDetected() {
|
|
|
690
578
|
}
|
|
691
579
|
|
|
692
580
|
/**
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* Returns { ok: boolean, issues: string[] }
|
|
581
|
+
* Register compound-workflow with Claude Code (project-scoped only).
|
|
582
|
+
* Cursor discovery is handled separately via copyCursor* functions.
|
|
696
583
|
*/
|
|
697
|
-
function
|
|
698
|
-
const issues = [];
|
|
699
|
-
const packageRoot = isSelfInstall
|
|
700
|
-
? PACKAGE_ROOT
|
|
701
|
-
: path.join(targetRoot, "node_modules", "compound-workflow");
|
|
702
|
-
|
|
703
|
-
// Verify skills
|
|
704
|
-
const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
|
|
705
|
-
const packageSkillsDir = path.join(packageRoot, "src", ".agents", "skills");
|
|
706
|
-
if (fs.existsSync(packageSkillsDir)) {
|
|
707
|
-
const expectedSkills = new Set(
|
|
708
|
-
fs.readdirSync(packageSkillsDir, { withFileTypes: true })
|
|
709
|
-
.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsDir, e.name, "SKILL.md")))
|
|
710
|
-
.map((e) => e.name)
|
|
711
|
-
);
|
|
712
|
-
if (fs.existsSync(cursorSkillsDir)) {
|
|
713
|
-
const actualSkills = fs.readdirSync(cursorSkillsDir, { withFileTypes: true })
|
|
714
|
-
.filter((e) => e.isSymbolicLink())
|
|
715
|
-
.map((e) => e.name);
|
|
716
|
-
for (const skill of expectedSkills) {
|
|
717
|
-
if (!actualSkills.includes(skill)) {
|
|
718
|
-
issues.push(`Missing skill symlink: .cursor/skills/${skill}`);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
for (const skill of actualSkills) {
|
|
722
|
-
if (!expectedSkills.has(skill)) {
|
|
723
|
-
issues.push(`Stale skill symlink: .cursor/skills/${skill}`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
} else if (expectedSkills.size > 0) {
|
|
727
|
-
issues.push(`Missing .cursor/skills/ directory (${expectedSkills.size} expected)`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Verify commands
|
|
732
|
-
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
733
|
-
const packageCommandsDir = path.join(packageRoot, "src", ".agents", "commands");
|
|
734
|
-
if (fs.existsSync(packageCommandsDir)) {
|
|
735
|
-
const expectedCommands = new Set(
|
|
736
|
-
fs.readdirSync(packageCommandsDir, { withFileTypes: true })
|
|
737
|
-
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
738
|
-
.map((e) => e.name)
|
|
739
|
-
);
|
|
740
|
-
if (fs.existsSync(cursorCommandsDir)) {
|
|
741
|
-
const actualCommands = fs.readdirSync(cursorCommandsDir, { withFileTypes: true })
|
|
742
|
-
.filter((e) => e.isSymbolicLink())
|
|
743
|
-
.map((e) => e.name);
|
|
744
|
-
for (const cmd of expectedCommands) {
|
|
745
|
-
if (!actualCommands.includes(cmd)) {
|
|
746
|
-
issues.push(`Missing command symlink: .cursor/commands/${cmd}`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
for (const cmd of actualCommands) {
|
|
750
|
-
if (!expectedCommands.has(cmd)) {
|
|
751
|
-
issues.push(`Stale command symlink: .cursor/commands/${cmd}`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
} else if (expectedCommands.size > 0) {
|
|
755
|
-
issues.push(`Missing .cursor/commands/ directory (${expectedCommands.size} expected)`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Verify agents
|
|
760
|
-
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
761
|
-
const packageAgentsDir = path.join(packageRoot, "src", ".agents", "agents");
|
|
762
|
-
if (fs.existsSync(packageAgentsDir)) {
|
|
763
|
-
// Recursively get all .md files from package agents dir
|
|
764
|
-
const expectedAgents = [];
|
|
765
|
-
function collectAgents(dir, prefix = "") {
|
|
766
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
767
|
-
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
768
|
-
if (entry.isDirectory()) {
|
|
769
|
-
collectAgents(path.join(dir, entry.name), relPath);
|
|
770
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
771
|
-
expectedAgents.push(relPath);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
collectAgents(packageAgentsDir);
|
|
776
|
-
const expectedSet = new Set(expectedAgents);
|
|
777
|
-
|
|
778
|
-
if (fs.existsSync(cursorAgentsDir)) {
|
|
779
|
-
const actualAgents = [];
|
|
780
|
-
function collectActualAgents(dir, prefix = "") {
|
|
781
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
782
|
-
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
783
|
-
if (entry.isDirectory()) {
|
|
784
|
-
collectActualAgents(path.join(dir, entry.name), relPath);
|
|
785
|
-
} else if (entry.isSymbolicLink()) {
|
|
786
|
-
actualAgents.push(relPath);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
collectActualAgents(cursorAgentsDir);
|
|
791
|
-
const actualSet = new Set(actualAgents);
|
|
792
|
-
|
|
793
|
-
for (const agent of expectedAgents) {
|
|
794
|
-
if (!actualSet.has(agent)) {
|
|
795
|
-
issues.push(`Missing agent symlink: .cursor/agents/${agent}`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
for (const agent of actualAgents) {
|
|
799
|
-
if (!expectedSet.has(agent)) {
|
|
800
|
-
issues.push(`Stale agent symlink: .cursor/agents/${agent}`);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
} else if (expectedAgents.length > 0) {
|
|
804
|
-
issues.push(`Missing .cursor/agents/ directory (${expectedAgents.length} expected)`);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
return { ok: issues.length === 0, issues };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegister, isSelfInstall) {
|
|
812
|
-
const claudePluginsDir = path.join(os.homedir(), ".claude", "plugins");
|
|
813
|
-
const installedPath = path.join(claudePluginsDir, "installed_plugins.json");
|
|
814
|
-
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
815
|
-
|
|
816
|
-
const pluginVersion = (() => {
|
|
817
|
-
try {
|
|
818
|
-
const pkgPath = path.join(PACKAGE_ROOT, "package.json");
|
|
819
|
-
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version || "0.0.0";
|
|
820
|
-
} catch {
|
|
821
|
-
return "0.0.0";
|
|
822
|
-
}
|
|
823
|
-
})();
|
|
824
|
-
|
|
584
|
+
function applyClaudeRegistration(targetRoot, dryRun, isSelfInstall) {
|
|
825
585
|
const projectRoot = isSelfInstall ? PACKAGE_ROOT : targetRoot;
|
|
826
|
-
const pluginId = "compound-workflow@local";
|
|
586
|
+
const pluginId = "compound-workflow@compound-workflow-local";
|
|
827
587
|
|
|
828
588
|
if (dryRun) {
|
|
829
589
|
console.log("[dry-run] Would register Claude plugin (project-scoped) at:", projectRoot);
|
|
830
590
|
return;
|
|
831
591
|
}
|
|
832
592
|
|
|
833
|
-
// Registration is always project-scoped: write only to <project>/.claude/settings.json.
|
|
834
|
-
// Claude Code manages ~/.claude/plugins/installed_plugins.json itself via marketplace flow;
|
|
835
|
-
// writing to user-level files causes "unregistered local marketplace" errors on startup.
|
|
836
593
|
const projectSettingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
837
594
|
let projectSettings = {};
|
|
838
595
|
if (fs.existsSync(projectSettingsPath)) {
|
|
@@ -840,52 +597,21 @@ function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegi
|
|
|
840
597
|
}
|
|
841
598
|
projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
|
|
842
599
|
projectSettings.enabledPlugins[pluginId] = true;
|
|
843
|
-
// Remove stale/invalid marketplace keys left by earlier install methods
|
|
844
600
|
if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
|
|
845
601
|
delete projectSettings.extraKnownMarketplaces["compound-workflow"];
|
|
846
602
|
}
|
|
847
603
|
projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
|
|
848
604
|
projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
|
|
849
|
-
source: { source: "file", path: ".
|
|
605
|
+
source: { source: "file", path: "." },
|
|
850
606
|
};
|
|
851
607
|
fs.mkdirSync(path.join(projectRoot, ".claude"), { recursive: true });
|
|
852
608
|
fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
|
|
853
609
|
|
|
854
|
-
// Clean up any stale user-level enabledPlugins entries left by previous install versions.
|
|
855
|
-
// These cause "unregistered local marketplace" errors on every Claude Code startup.
|
|
856
|
-
if (fs.existsSync(settingsPath)) {
|
|
857
|
-
try {
|
|
858
|
-
let userSettings = readJsonMaybe(settingsPath) ?? {};
|
|
859
|
-
const staleIds = ["compound-workflow@local", "compound-workflow@compound-workflow-local"];
|
|
860
|
-
let changed = false;
|
|
861
|
-
for (const id of staleIds) {
|
|
862
|
-
if (userSettings?.enabledPlugins?.[id] !== undefined) {
|
|
863
|
-
delete userSettings.enabledPlugins[id];
|
|
864
|
-
changed = true;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (changed) {
|
|
868
|
-
fs.writeFileSync(settingsPath, JSON.stringify(userSettings, null, 2) + "\n", "utf8");
|
|
869
|
-
console.log("Cleaned up stale compound-workflow entries from ~/.claude/settings.json");
|
|
870
|
-
}
|
|
871
|
-
} catch { /* ignore */ }
|
|
872
|
-
}
|
|
873
|
-
|
|
874
610
|
console.log("Registered compound-workflow with Claude Code (project-scoped).");
|
|
875
611
|
if (!isSelfInstall) {
|
|
876
612
|
console.log(" Claude Code 2.1+: open /plugin, go to Discover; install 'compound-workflow' from marketplace 'compound-workflow-local', or run: claude --plugin-dir ./node_modules/compound-workflow");
|
|
877
613
|
}
|
|
878
614
|
console.log(" Restart Claude Code; enable 'Include third-party Plugins, Skills, and other configs' in Settings if needed.");
|
|
879
|
-
|
|
880
|
-
if (noRegisterCursor && !forceRegister) return;
|
|
881
|
-
const shouldApply = forceRegister || (cursorDetected() && !noRegisterCursor);
|
|
882
|
-
if (!shouldApply) {
|
|
883
|
-
console.log("[cursor] Cursor not detected; skipped Cursor plugin registration. Use --register-cursor to force.");
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
const registrationPath = path.join(targetRoot, ".cursor-plugin", "registration.json");
|
|
887
|
-
if (!fs.existsSync(registrationPath)) return;
|
|
888
|
-
console.log("Registered compound-workflow with Cursor. Restart Cursor; enable 'Include third-party Plugins, Skills, and other configs' in Settings if needed.");
|
|
889
615
|
}
|
|
890
616
|
|
|
891
617
|
function reportOpenCodeIntegration(targetRoot, dryRun) {
|
|
@@ -910,31 +636,6 @@ function main() {
|
|
|
910
636
|
const args = parseArgs(process.argv);
|
|
911
637
|
const targetRoot = realpathSafe(args.root);
|
|
912
638
|
|
|
913
|
-
// Handle verification mode early (no manifest needed)
|
|
914
|
-
if (args.verify) {
|
|
915
|
-
console.log("Verifying plugin integrity...");
|
|
916
|
-
const isSelfInstall = realpathSafe(targetRoot) === realpathSafe(PACKAGE_ROOT);
|
|
917
|
-
// Try to read manifest for accurate verification, but continue without it
|
|
918
|
-
try {
|
|
919
|
-
GENERATED_MANIFEST = readGeneratedManifest();
|
|
920
|
-
} catch {
|
|
921
|
-
console.warn("Warning: Could not read generated manifest, using filesystem scan only");
|
|
922
|
-
GENERATED_MANIFEST = { commands: [], agents: [] };
|
|
923
|
-
}
|
|
924
|
-
const result = verifyPluginIntegrity(targetRoot, isSelfInstall);
|
|
925
|
-
if (result.ok) {
|
|
926
|
-
console.log("Plugin integrity: OK (all symlinks present and valid)");
|
|
927
|
-
process.exit(0);
|
|
928
|
-
} else {
|
|
929
|
-
console.error("Plugin integrity issues found:");
|
|
930
|
-
for (const issue of result.issues) {
|
|
931
|
-
console.error(` - ${issue}`);
|
|
932
|
-
}
|
|
933
|
-
console.error("\nRun 'npx compound-workflow install' to fix.");
|
|
934
|
-
process.exit(1);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
639
|
const genScript = path.join(PACKAGE_ROOT, "scripts", "generate-platform-artifacts.mjs");
|
|
939
640
|
if (fs.existsSync(genScript)) {
|
|
940
641
|
console.log("[compound-workflow] Regenerating manifest from package source...");
|
|
@@ -977,10 +678,10 @@ function main() {
|
|
|
977
678
|
|
|
978
679
|
writeOpenCodeJson(targetRoot, args.dryRun, isSelfInstall);
|
|
979
680
|
writePluginManifests(targetRoot, args.dryRun, isSelfInstall);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
681
|
+
applyClaudeRegistration(targetRoot, args.dryRun, isSelfInstall);
|
|
682
|
+
copyCursorSkills(targetRoot, args.dryRun, isSelfInstall);
|
|
683
|
+
copyCursorCommands(targetRoot, args.dryRun, isSelfInstall);
|
|
684
|
+
copyCursorAgents(targetRoot, args.dryRun, isSelfInstall);
|
|
984
685
|
reportOpenCodeIntegration(targetRoot, args.dryRun);
|
|
985
686
|
writeAgentsMd(targetRoot, args.dryRun);
|
|
986
687
|
ensureDirs(targetRoot, args.dryRun);
|