compound-workflow 1.7.0 → 1.7.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/package.json
CHANGED
package/scripts/install-cli.mjs
CHANGED
|
@@ -42,13 +42,14 @@ When Cursor is detected (~/.cursor), registers the plugin so skills/commands app
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function parseArgs(argv) {
|
|
45
|
-
const out = { root: process.cwd(), dryRun: false, noConfig: false, noRegisterCursor: false, registerCursor: false };
|
|
45
|
+
const out = { root: process.cwd(), dryRun: false, noConfig: false, noRegisterCursor: false, registerCursor: false, verify: false };
|
|
46
46
|
for (let i = 2; i < argv.length; i++) {
|
|
47
47
|
const arg = argv[i];
|
|
48
48
|
if (arg === "--dry-run") out.dryRun = true;
|
|
49
49
|
else if (arg === "--no-config") out.noConfig = true;
|
|
50
50
|
else if (arg === "--no-register-cursor") out.noRegisterCursor = true;
|
|
51
51
|
else if (arg === "--register-cursor") out.registerCursor = true;
|
|
52
|
+
else if (arg === "--verify") out.verify = true;
|
|
52
53
|
else if (arg === "--root") {
|
|
53
54
|
const value = argv[i + 1];
|
|
54
55
|
if (!value) usage(1);
|
|
@@ -504,10 +505,307 @@ function syncCursorSkills(targetRoot, dryRun, isSelfInstall) {
|
|
|
504
505
|
console.log("Synced", skillDirs.length, "skills to .cursor/skills/");
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Cursor discovers commands from .cursor/commands.
|
|
510
|
+
* Populate .cursor/commands/ with symlinks to the package commands so Cursor finds them.
|
|
511
|
+
*/
|
|
512
|
+
function syncCursorCommands(targetRoot, dryRun, isSelfInstall) {
|
|
513
|
+
const packageCommandsAbs = isSelfInstall
|
|
514
|
+
? path.join(PACKAGE_ROOT, "src", ".agents", "commands")
|
|
515
|
+
: path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "commands");
|
|
516
|
+
if (!fs.existsSync(packageCommandsAbs)) return;
|
|
517
|
+
|
|
518
|
+
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
519
|
+
let entries;
|
|
520
|
+
try {
|
|
521
|
+
entries = fs.readdirSync(packageCommandsAbs, { withFileTypes: true });
|
|
522
|
+
} catch {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Filter .md files that are commands
|
|
527
|
+
const commandFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
528
|
+
if (commandFiles.length === 0) return;
|
|
529
|
+
|
|
530
|
+
if (dryRun) {
|
|
531
|
+
console.log("[dry-run] Would symlink", commandFiles.length, "commands into .cursor/commands/");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fs.mkdirSync(cursorCommandsDir, { recursive: true });
|
|
536
|
+
const packageCommandsReal = realpathSafe(packageCommandsAbs);
|
|
537
|
+
const commandSet = new Set(commandFiles);
|
|
538
|
+
|
|
539
|
+
// Prune: remove symlinks that point at our package but are no longer in the package
|
|
540
|
+
try {
|
|
541
|
+
for (const entry of fs.readdirSync(cursorCommandsDir, { withFileTypes: true })) {
|
|
542
|
+
if (!entry.isSymbolicLink()) continue;
|
|
543
|
+
const linkPath = path.join(cursorCommandsDir, entry.name);
|
|
544
|
+
try {
|
|
545
|
+
const resolved = realpathSafe(linkPath);
|
|
546
|
+
if (!resolved.startsWith(packageCommandsReal + path.sep) && resolved !== packageCommandsReal) continue;
|
|
547
|
+
const base = path.basename(resolved);
|
|
548
|
+
if (commandSet.has(base)) continue;
|
|
549
|
+
fs.rmSync(linkPath);
|
|
550
|
+
} catch {
|
|
551
|
+
/* ignore broken symlinks or permission errors */
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch {
|
|
555
|
+
/* .cursor/commands not readable */
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const name of commandFiles) {
|
|
559
|
+
const linkPath = path.join(cursorCommandsDir, name);
|
|
560
|
+
const targetPath = path.join(packageCommandsAbs, name);
|
|
561
|
+
try {
|
|
562
|
+
if (fs.existsSync(linkPath)) {
|
|
563
|
+
const stat = fs.lstatSync(linkPath);
|
|
564
|
+
if (!stat.isSymbolicLink()) continue;
|
|
565
|
+
try {
|
|
566
|
+
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
567
|
+
} catch {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
fs.rmSync(linkPath);
|
|
571
|
+
}
|
|
572
|
+
fs.symlinkSync(targetPath, linkPath, "file");
|
|
573
|
+
} catch (err) {
|
|
574
|
+
console.warn("[cursor] Could not symlink command", name, err.message);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
console.log("Synced", commandFiles.length, "commands to .cursor/commands/");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Cursor discovers agents from .cursor/agents.
|
|
582
|
+
* Populate .cursor/agents/ with symlinks to the package agents so Cursor finds them.
|
|
583
|
+
* Preserves subdirectory structure (research/, workflow/, review/).
|
|
584
|
+
*/
|
|
585
|
+
function syncCursorAgents(targetRoot, dryRun, isSelfInstall) {
|
|
586
|
+
const packageAgentsAbs = isSelfInstall
|
|
587
|
+
? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
|
|
588
|
+
: path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
|
|
589
|
+
if (!fs.existsSync(packageAgentsAbs)) return;
|
|
590
|
+
|
|
591
|
+
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
592
|
+
|
|
593
|
+
// Get all agent files from manifest (these include subdir paths like "research/repo-research-analyst.md")
|
|
594
|
+
const agentRels = GENERATED_MANIFEST.agents.map((a) => a.rel);
|
|
595
|
+
if (agentRels.length === 0) return;
|
|
596
|
+
|
|
597
|
+
if (dryRun) {
|
|
598
|
+
console.log("[dry-run] Would symlink", agentRels.length, "agents into .cursor/agents/");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fs.mkdirSync(cursorAgentsDir, { recursive: true });
|
|
603
|
+
const packageAgentsReal = realpathSafe(packageAgentsAbs);
|
|
604
|
+
const agentSet = new Set(agentRels);
|
|
605
|
+
|
|
606
|
+
// Build set of valid subdirectories to preserve structure
|
|
607
|
+
const validSubdirs = new Set();
|
|
608
|
+
for (const rel of agentRels) {
|
|
609
|
+
const subdir = path.dirname(rel);
|
|
610
|
+
if (subdir !== ".") validSubdirs.add(subdir);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Prune: remove symlinks that point at our package but are no longer in the manifest
|
|
614
|
+
try {
|
|
615
|
+
for (const entry of fs.readdirSync(cursorAgentsDir, { withFileTypes: true })) {
|
|
616
|
+
if (entry.isDirectory()) {
|
|
617
|
+
// Check if this subdir is still valid
|
|
618
|
+
if (!validSubdirs.has(entry.name)) {
|
|
619
|
+
// Remove the entire stale subdirectory
|
|
620
|
+
fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
// Prune stale symlinks within valid subdirectories
|
|
624
|
+
const subdirPath = path.join(cursorAgentsDir, entry.name);
|
|
625
|
+
for (const subEntry of fs.readdirSync(subdirPath, { withFileTypes: true })) {
|
|
626
|
+
if (!subEntry.isSymbolicLink()) continue;
|
|
627
|
+
const linkPath = path.join(subdirPath, subEntry.name);
|
|
628
|
+
try {
|
|
629
|
+
const resolved = realpathSafe(linkPath);
|
|
630
|
+
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
631
|
+
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
632
|
+
if (agentSet.has(relFromPackage)) continue;
|
|
633
|
+
fs.rmSync(linkPath);
|
|
634
|
+
} catch {
|
|
635
|
+
/* ignore broken symlinks */
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else if (entry.isSymbolicLink()) {
|
|
639
|
+
// Handle flat symlinks (if any were created at root level)
|
|
640
|
+
const linkPath = path.join(cursorAgentsDir, entry.name);
|
|
641
|
+
try {
|
|
642
|
+
const resolved = realpathSafe(linkPath);
|
|
643
|
+
if (!resolved.startsWith(packageAgentsReal + path.sep) && resolved !== packageAgentsReal) continue;
|
|
644
|
+
const relFromPackage = path.relative(packageAgentsAbs, resolved);
|
|
645
|
+
if (agentSet.has(relFromPackage)) continue;
|
|
646
|
+
fs.rmSync(linkPath);
|
|
647
|
+
} catch {
|
|
648
|
+
/* ignore broken symlinks */
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
/* .cursor/agents not readable */
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Create symlinks preserving subdirectory structure
|
|
657
|
+
for (const rel of agentRels) {
|
|
658
|
+
const targetPath = path.join(packageAgentsAbs, rel);
|
|
659
|
+
const linkPath = path.join(cursorAgentsDir, rel);
|
|
660
|
+
|
|
661
|
+
// Ensure subdirectory exists
|
|
662
|
+
const subdir = path.dirname(rel);
|
|
663
|
+
if (subdir !== ".") {
|
|
664
|
+
fs.mkdirSync(path.join(cursorAgentsDir, subdir), { recursive: true });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
if (fs.existsSync(linkPath)) {
|
|
669
|
+
const stat = fs.lstatSync(linkPath);
|
|
670
|
+
if (!stat.isSymbolicLink()) continue;
|
|
671
|
+
try {
|
|
672
|
+
if (realpathSafe(linkPath) !== realpathSafe(targetPath)) continue;
|
|
673
|
+
} catch {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
fs.rmSync(linkPath);
|
|
677
|
+
}
|
|
678
|
+
fs.symlinkSync(targetPath, linkPath, "file");
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.warn("[cursor] Could not symlink agent", rel, err.message);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
console.log("Synced", agentRels.length, "agents to .cursor/agents/");
|
|
684
|
+
}
|
|
685
|
+
|
|
507
686
|
function cursorDetected() {
|
|
508
687
|
return fs.existsSync(path.join(os.homedir(), ".cursor"));
|
|
509
688
|
}
|
|
510
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Verifies plugin integrity by checking all symlinks in .cursor/ directories
|
|
692
|
+
* match the expected state from GENERATED_MANIFEST.
|
|
693
|
+
* Returns { ok: boolean, issues: string[] }
|
|
694
|
+
*/
|
|
695
|
+
function verifyPluginIntegrity(targetRoot, isSelfInstall) {
|
|
696
|
+
const issues = [];
|
|
697
|
+
const packageRoot = isSelfInstall
|
|
698
|
+
? PACKAGE_ROOT
|
|
699
|
+
: path.join(targetRoot, "node_modules", "compound-workflow");
|
|
700
|
+
|
|
701
|
+
// Verify skills
|
|
702
|
+
const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
|
|
703
|
+
const packageSkillsDir = path.join(packageRoot, "src", ".agents", "skills");
|
|
704
|
+
if (fs.existsSync(packageSkillsDir)) {
|
|
705
|
+
const expectedSkills = new Set(
|
|
706
|
+
fs.readdirSync(packageSkillsDir, { withFileTypes: true })
|
|
707
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsDir, e.name, "SKILL.md")))
|
|
708
|
+
.map((e) => e.name)
|
|
709
|
+
);
|
|
710
|
+
if (fs.existsSync(cursorSkillsDir)) {
|
|
711
|
+
const actualSkills = fs.readdirSync(cursorSkillsDir, { withFileTypes: true })
|
|
712
|
+
.filter((e) => e.isSymbolicLink())
|
|
713
|
+
.map((e) => e.name);
|
|
714
|
+
for (const skill of expectedSkills) {
|
|
715
|
+
if (!actualSkills.includes(skill)) {
|
|
716
|
+
issues.push(`Missing skill symlink: .cursor/skills/${skill}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const skill of actualSkills) {
|
|
720
|
+
if (!expectedSkills.has(skill)) {
|
|
721
|
+
issues.push(`Stale skill symlink: .cursor/skills/${skill}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} else if (expectedSkills.size > 0) {
|
|
725
|
+
issues.push(`Missing .cursor/skills/ directory (${expectedSkills.size} expected)`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Verify commands
|
|
730
|
+
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
731
|
+
const packageCommandsDir = path.join(packageRoot, "src", ".agents", "commands");
|
|
732
|
+
if (fs.existsSync(packageCommandsDir)) {
|
|
733
|
+
const expectedCommands = new Set(
|
|
734
|
+
fs.readdirSync(packageCommandsDir, { withFileTypes: true })
|
|
735
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
736
|
+
.map((e) => e.name)
|
|
737
|
+
);
|
|
738
|
+
if (fs.existsSync(cursorCommandsDir)) {
|
|
739
|
+
const actualCommands = fs.readdirSync(cursorCommandsDir, { withFileTypes: true })
|
|
740
|
+
.filter((e) => e.isSymbolicLink())
|
|
741
|
+
.map((e) => e.name);
|
|
742
|
+
for (const cmd of expectedCommands) {
|
|
743
|
+
if (!actualCommands.includes(cmd)) {
|
|
744
|
+
issues.push(`Missing command symlink: .cursor/commands/${cmd}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const cmd of actualCommands) {
|
|
748
|
+
if (!expectedCommands.has(cmd)) {
|
|
749
|
+
issues.push(`Stale command symlink: .cursor/commands/${cmd}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} else if (expectedCommands.size > 0) {
|
|
753
|
+
issues.push(`Missing .cursor/commands/ directory (${expectedCommands.size} expected)`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Verify agents
|
|
758
|
+
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
759
|
+
const packageAgentsDir = path.join(packageRoot, "src", ".agents", "agents");
|
|
760
|
+
if (fs.existsSync(packageAgentsDir)) {
|
|
761
|
+
// Recursively get all .md files from package agents dir
|
|
762
|
+
const expectedAgents = [];
|
|
763
|
+
function collectAgents(dir, prefix = "") {
|
|
764
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
765
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
766
|
+
if (entry.isDirectory()) {
|
|
767
|
+
collectAgents(path.join(dir, entry.name), relPath);
|
|
768
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
769
|
+
expectedAgents.push(relPath);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
collectAgents(packageAgentsDir);
|
|
774
|
+
const expectedSet = new Set(expectedAgents);
|
|
775
|
+
|
|
776
|
+
if (fs.existsSync(cursorAgentsDir)) {
|
|
777
|
+
const actualAgents = [];
|
|
778
|
+
function collectActualAgents(dir, prefix = "") {
|
|
779
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
780
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
781
|
+
if (entry.isDirectory()) {
|
|
782
|
+
collectActualAgents(path.join(dir, entry.name), relPath);
|
|
783
|
+
} else if (entry.isSymbolicLink()) {
|
|
784
|
+
actualAgents.push(relPath);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
collectActualAgents(cursorAgentsDir);
|
|
789
|
+
const actualSet = new Set(actualAgents);
|
|
790
|
+
|
|
791
|
+
for (const agent of expectedAgents) {
|
|
792
|
+
if (!actualSet.has(agent)) {
|
|
793
|
+
issues.push(`Missing agent symlink: .cursor/agents/${agent}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const agent of actualAgents) {
|
|
797
|
+
if (!expectedSet.has(agent)) {
|
|
798
|
+
issues.push(`Stale agent symlink: .cursor/agents/${agent}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} else if (expectedAgents.length > 0) {
|
|
802
|
+
issues.push(`Missing .cursor/agents/ directory (${expectedAgents.length} expected)`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { ok: issues.length === 0, issues };
|
|
807
|
+
}
|
|
808
|
+
|
|
511
809
|
function applyCursorRegistration(targetRoot, dryRun, noRegisterCursor, forceRegister, isSelfInstall) {
|
|
512
810
|
const claudePluginsDir = path.join(os.homedir(), ".claude", "plugins");
|
|
513
811
|
const installedPath = path.join(claudePluginsDir, "installed_plugins.json");
|
|
@@ -610,6 +908,31 @@ function main() {
|
|
|
610
908
|
const args = parseArgs(process.argv);
|
|
611
909
|
const targetRoot = realpathSafe(args.root);
|
|
612
910
|
|
|
911
|
+
// Handle verification mode early (no manifest needed)
|
|
912
|
+
if (args.verify) {
|
|
913
|
+
console.log("Verifying plugin integrity...");
|
|
914
|
+
const isSelfInstall = realpathSafe(targetRoot) === realpathSafe(PACKAGE_ROOT);
|
|
915
|
+
// Try to read manifest for accurate verification, but continue without it
|
|
916
|
+
try {
|
|
917
|
+
GENERATED_MANIFEST = readGeneratedManifest();
|
|
918
|
+
} catch {
|
|
919
|
+
console.warn("Warning: Could not read generated manifest, using filesystem scan only");
|
|
920
|
+
GENERATED_MANIFEST = { commands: [], agents: [] };
|
|
921
|
+
}
|
|
922
|
+
const result = verifyPluginIntegrity(targetRoot, isSelfInstall);
|
|
923
|
+
if (result.ok) {
|
|
924
|
+
console.log("Plugin integrity: OK (all symlinks present and valid)");
|
|
925
|
+
process.exit(0);
|
|
926
|
+
} else {
|
|
927
|
+
console.error("Plugin integrity issues found:");
|
|
928
|
+
for (const issue of result.issues) {
|
|
929
|
+
console.error(` - ${issue}`);
|
|
930
|
+
}
|
|
931
|
+
console.error("\nRun 'npx compound-workflow install' to fix.");
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
613
936
|
const genScript = path.join(PACKAGE_ROOT, "scripts", "generate-platform-artifacts.mjs");
|
|
614
937
|
if (fs.existsSync(genScript)) {
|
|
615
938
|
console.log("[compound-workflow] Regenerating manifest from package source...");
|
|
@@ -653,6 +976,8 @@ function main() {
|
|
|
653
976
|
writeOpenCodeJson(targetRoot, args.dryRun, isSelfInstall);
|
|
654
977
|
writePluginManifests(targetRoot, args.dryRun, isSelfInstall);
|
|
655
978
|
syncCursorSkills(targetRoot, args.dryRun, isSelfInstall);
|
|
979
|
+
syncCursorCommands(targetRoot, args.dryRun, isSelfInstall);
|
|
980
|
+
syncCursorAgents(targetRoot, args.dryRun, isSelfInstall);
|
|
656
981
|
applyCursorRegistration(targetRoot, args.dryRun, args.noRegisterCursor, args.registerCursor, isSelfInstall);
|
|
657
982
|
reportOpenCodeIntegration(targetRoot, args.dryRun);
|
|
658
983
|
writeAgentsMd(targetRoot, args.dryRun);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: presentation-composability
|
|
3
|
+
description: Enforce folder-per-component structure for tapesv3 presentation modules. Use when creating or refactoring components under src/features/tapesv3/presentation/.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Presentation Composability
|
|
7
|
+
|
|
8
|
+
Enforce folder-per-component structure for tapesv3 presentation modules.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- Adding new presentation components to tapesv3
|
|
13
|
+
- Refactoring existing tapesv3 presentation into composable pieces
|
|
14
|
+
- Aligning new UI with established tapesv3 patterns
|
|
15
|
+
|
|
16
|
+
## Structure Rules
|
|
17
|
+
|
|
18
|
+
### One folder per composable
|
|
19
|
+
|
|
20
|
+
Each logical UI element gets its own subfolder (PascalCase):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
presentation/FeatureName/
|
|
24
|
+
├── index.ts # barrel
|
|
25
|
+
├── SubComponentA/
|
|
26
|
+
│ ├── index.tsx
|
|
27
|
+
│ └── styles.ts
|
|
28
|
+
├── SubComponentB/
|
|
29
|
+
│ ├── index.tsx
|
|
30
|
+
│ └── styles.ts
|
|
31
|
+
└── SubComponentC/
|
|
32
|
+
└── index.tsx # styles.ts optional if no styled components
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Barrel (`index.ts`)
|
|
36
|
+
|
|
37
|
+
Re-export from subfolders. Two patterns:
|
|
38
|
+
|
|
39
|
+
**Flat exports** (simple feature, independent components):
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export { SubComponentA } from "./SubComponentA";
|
|
43
|
+
export { SubComponentB } from "./SubComponentB";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Compound component** (Root + subcomponents used together):
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export const FeatureName = Object.assign(RootComponent, {
|
|
50
|
+
SubA: SubComponentA,
|
|
51
|
+
SubB: SubComponentB,
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Component + styles
|
|
56
|
+
|
|
57
|
+
- `index.tsx`: component logic and JSX
|
|
58
|
+
- `styles.ts`: styled-components (or emotion) only
|
|
59
|
+
- Import: `import * as S from "./styles"` or `import { Root, Item } from "./styles"`
|
|
60
|
+
|
|
61
|
+
### Reference implementations
|
|
62
|
+
|
|
63
|
+
- `SharedTabsHeader` – compound pattern (Root.Action, Root.BackgroundPicker)
|
|
64
|
+
- `PanelClipsArtist`, `FolderSelector` – compound pattern
|
|
65
|
+
- `MultiSourceThreads` – flat exports (FilterBar, Message, Input, List)
|
|
66
|
+
- `TapesTabBarTabOptions` – flat exports
|
|
67
|
+
|
|
68
|
+
## Anti-patterns
|
|
69
|
+
|
|
70
|
+
- Single file with multiple components and inline styles
|
|
71
|
+
- `ComponentName.styles.ts` – use `styles.ts` in the component folder
|
|
72
|
+
- Barrel re-exporting from sibling files instead of subfolders
|
package/src/AGENTS.md
CHANGED
|
@@ -172,7 +172,7 @@ worktree_bootstrap_notes:
|
|
|
172
172
|
## Implemented Components (Current Scope)
|
|
173
173
|
|
|
174
174
|
- Commands: `workflow:brainstorm`, `workflow:plan`, `workflow:triage`, `workflow:work`, `workflow:review`, `workflow:tech-review`, `workflow:compound` (under `.agents/commands/` as `workflow-*.md`), plus `test-browser`, `metrics`, `assess`, `install` (root commands)
|
|
175
|
-
- Skills: `brainstorming`, `document-review`, `technical-review`, `compound-docs` (alias: `compound_doc`), `capture-skill`, `file-todos`, `agent-browser`, `git-worktree`, `process-metrics`, `react-ddd-mvc-frontend`, `xstate-actor-orchestration`, `standards`, `pii-protection-prisma`, `financial-workflow-integrity`, `audit-traceability`, `data-foundations`
|
|
175
|
+
- Skills: `brainstorming`, `document-review`, `technical-review`, `compound-docs` (alias: `compound_doc`), `capture-skill`, `file-todos`, `agent-browser`, `git-worktree`, `process-metrics`, `react-ddd-mvc-frontend`, `xstate-actor-orchestration`, `standards`, `pii-protection-prisma`, `financial-workflow-integrity`, `audit-traceability`, `data-foundations`, `presentation-composability`
|
|
176
176
|
- Agents:
|
|
177
177
|
- `repo-research-analyst`
|
|
178
178
|
- `learnings-researcher`
|
|
@@ -237,6 +237,7 @@ Maintenance:
|
|
|
237
237
|
| `react-ddd-mvc-frontend` | You need React frontend architecture guidance (DDD + MVC hybrid) during planning or review to enforce feature structure, layer boundaries, composable pure components, container/controller responsibilities, and maintainable patterns. |
|
|
238
238
|
| `xstate-actor-orchestration` | You are evaluating complexity and need explicit state orchestration: React container-as-orchestrator for UI flows, or actor/state-machine orchestration for backend/internal workflows (especially multi-step async branching, retries/timeouts/cancellation, receptionist/child-actor coordination, or boolean-flag sprawl). |
|
|
239
239
|
| `standards` | You need Altai coding standards for implementation and refactoring, including domain entity patterns, XState conventions, type usage, and feature code organization. |
|
|
240
|
+
| `presentation-composability` | You are creating or refactoring presentation components in tapesv3 and need folder-per-component structure with barrel exports and styles separation. |
|
|
240
241
|
|
|
241
242
|
### Reference standards (guardrails)
|
|
242
243
|
|