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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-workflow",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Clarify -> plan -> execute -> verify -> capture workflow for Cursor",
5
5
  "author": {
6
6
  "name": "Compound Workflow"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-workflow",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Clarify → plan → execute → verify → capture. One Install action for Cursor, Claude, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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; install syncs
13
- * each skill into .cursor/skills/ (symlinks) so Cursor discovers them. Prune removes stale symlinks.
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] [--no-register-cursor] [--register-cursor]
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, and prompts for Repo Config Block (unless --no-config).
32
- When Cursor is detected (~/.cursor), registers the plugin so skills/commands appear.
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, noRegisterCursor: false, registerCursor: false, verify: 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 supports full manifest with commands/agents/skills path overrides.
346
- // For commands/agents, use .cursor/ directories where symlinks are created
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: "./.cursor/commands",
351
- agents: "./.cursor/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 only from .agents/skills, .cursor/skills, ~/.cursor/skills.
433
- * Populate .cursor/skills/ with symlinks to the package skills so Cursor finds them.
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 syncCursorSkills(targetRoot, dryRun, isSelfInstall) {
436
- const packageSkillsAbs = isSelfInstall
437
- ? path.join(PACKAGE_ROOT, "src", ".agents", "skills")
438
- : path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "skills");
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
- let entries;
443
- try {
444
- entries = fs.readdirSync(packageSkillsAbs, { withFileTypes: true });
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 symlink", skillDirs.length, "skills into .cursor/skills/");
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: remove symlinks that point at our package but are no longer in the package
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.isSymbolicLink()) continue;
465
- const linkPath = path.join(cursorSkillsDir, entry.name);
466
- try {
467
- const resolved = realpathSafe(linkPath);
468
- if (!resolved.startsWith(packageSkillsReal + path.sep) && resolved !== packageSkillsReal) continue;
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 linkPath = path.join(cursorSkillsDir, name);
482
- const targetPath = path.join(packageSkillsAbs, name);
483
- try {
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("Synced", skillDirs.length, "skills to .cursor/skills/");
486
+ console.log("Copied", skillDirs.length, "skills to .cursor/skills/");
508
487
  }
509
488
 
510
489
  /**
511
- * Cursor discovers commands from .cursor/commands.
512
- * Populate .cursor/commands/ with symlinks to the package commands so Cursor finds them.
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 syncCursorCommands(targetRoot, dryRun, isSelfInstall) {
515
- const packageCommandsAbs = isSelfInstall
516
- ? path.join(PACKAGE_ROOT, "src", ".agents", "commands")
517
- : path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "commands");
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
- let entries;
522
- try {
523
- entries = fs.readdirSync(packageCommandsAbs, { withFileTypes: true });
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 symlink", commandFiles.length, "commands into .cursor/commands/");
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: remove symlinks that point at our package but are no longer in the package
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.isSymbolicLink()) continue;
545
- const linkPath = path.join(cursorCommandsDir, entry.name);
546
- try {
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
- const linkPath = path.join(cursorCommandsDir, name);
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("Synced", commandFiles.length, "commands to .cursor/commands/");
527
+ console.log("Copied", commandFiles.length, "commands to .cursor/commands/");
580
528
  }
581
529
 
582
530
  /**
583
- * Cursor discovers agents from .cursor/agents.
584
- * Populate .cursor/agents/ with symlinks to the package agents so Cursor finds them.
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 syncCursorAgents(targetRoot, dryRun, isSelfInstall) {
588
- const packageAgentsAbs = isSelfInstall
589
- ? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
590
- : path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
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 symlink", agentRels.length, "agents into .cursor/agents/");
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
- // Build set of valid subdirectories to preserve structure
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: remove symlinks that point at our package but are no longer in the manifest
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
- // Check if this subdir is still valid
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 targetPath = path.join(packageAgentsAbs, rel);
661
- const linkPath = path.join(cursorAgentsDir, rel);
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("Synced", agentRels.length, "agents to .cursor/agents/");
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
- * Verifies plugin integrity by checking all symlinks in .cursor/ directories
694
- * match the expected state from GENERATED_MANIFEST.
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 verifyPluginIntegrity(targetRoot, isSelfInstall) {
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: ".claude-plugin/marketplace.json" },
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
- syncCursorSkills(targetRoot, args.dryRun, isSelfInstall);
981
- syncCursorCommands(targetRoot, args.dryRun, isSelfInstall);
982
- syncCursorAgents(targetRoot, args.dryRun, isSelfInstall);
983
- applyCursorRegistration(targetRoot, args.dryRun, args.noRegisterCursor, args.registerCursor, isSelfInstall);
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);