wangchuan 4.6.0 → 5.0.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.
@@ -347,136 +347,134 @@ export function buildFileEntries(cfg, repoDirBase, agent, filter) {
347
347
  return applyFilter(deduplicateEntries(entries), filter);
348
348
  }
349
349
  /**
350
- * Distribute shared content (skills, MCP configs) to each agent's local directory.
351
- * Called before push to ensure all agents have the latest shared resources.
350
+ * Distribute shared content (skills, MCP configs, custom agents) to each agent's local directory.
351
+ * Skills and custom agents: collect pending distributions for user confirmation (no files written).
352
+ * MCP configs: distributed automatically (low-risk config merges).
353
+ * Called before push to prepare cross-agent sharing.
352
354
  */
353
355
  function distributeShared(cfg) {
354
356
  const shared = cfg.shared;
355
357
  if (!shared)
356
358
  return;
357
359
  const profiles = cfg.profiles.default;
358
- // ── Distribute skills: collect from each source, only add skills the target is missing ──
359
- // Collect each agent's current skill set
360
- const agentSkills = new Map(); // agent → relPath → absPath
361
- for (const source of shared.skills.sources) {
362
- const p = profiles[source.agent];
363
- if (!p.enabled)
364
- continue;
365
- const skillsDir = path.join(expandHome(p.workspacePath), source.dir);
366
- const skills = new Map();
367
- if (fs.existsSync(skillsDir)) {
368
- for (const relFile of walkDir(skillsDir)) {
369
- if (path.basename(relFile).startsWith('.'))
370
- continue;
371
- skills.set(relFile, path.join(skillsDir, relFile));
360
+ const pendingItems = [];
361
+ // ── Skills: collect pending distributions (no file writes) ──────
362
+ {
363
+ // Collect each agent's current skill set
364
+ const agentSkills = new Map(); // agent → relPath → absPath
365
+ for (const source of shared.skills.sources) {
366
+ const p = profiles[source.agent];
367
+ if (!p.enabled)
368
+ continue;
369
+ const skillsDir = path.join(expandHome(p.workspacePath), source.dir);
370
+ const skills = new Map();
371
+ if (fs.existsSync(skillsDir)) {
372
+ for (const relFile of walkDir(skillsDir)) {
373
+ if (path.basename(relFile).startsWith('.'))
374
+ continue;
375
+ skills.set(relFile, path.join(skillsDir, relFile));
376
+ }
372
377
  }
378
+ agentSkills.set(source.agent, skills);
373
379
  }
374
- agentSkills.set(source.agent, skills);
375
- }
376
- // Merge all agents' skills — for each relPath, pick the NEWEST version (latest mtime)
377
- const allSkills = new Map(); // relPath → absPath (newest)
378
- const allSkillMtimes = new Map(); // relPath → mtime ms
379
- for (const skills of agentSkills.values()) {
380
- for (const [rel, abs] of skills) {
381
- try {
382
- const mtime = fs.statSync(abs).mtimeMs;
383
- if (!allSkills.has(rel) || mtime > allSkillMtimes.get(rel)) {
384
- allSkills.set(rel, abs);
385
- allSkillMtimes.set(rel, mtime);
380
+ // Merge all agents' skills — for each relPath, pick the NEWEST version (latest mtime)
381
+ const allSkills = new Map(); // relPath → absPath (newest)
382
+ const allSkillMtimes = new Map();
383
+ const allSkillOwner = new Map(); // relPath → agent name that owns newest
384
+ for (const [agentName, skills] of agentSkills) {
385
+ for (const [rel, abs] of skills) {
386
+ try {
387
+ const mtime = fs.statSync(abs).mtimeMs;
388
+ if (!allSkills.has(rel) || mtime > allSkillMtimes.get(rel)) {
389
+ allSkills.set(rel, abs);
390
+ allSkillMtimes.set(rel, mtime);
391
+ allSkillOwner.set(rel, agentName);
392
+ }
393
+ }
394
+ catch {
395
+ if (!allSkills.has(rel)) {
396
+ allSkills.set(rel, abs);
397
+ allSkillOwner.set(rel, agentName);
398
+ }
386
399
  }
387
- }
388
- catch {
389
- if (!allSkills.has(rel))
390
- allSkills.set(rel, abs);
391
400
  }
392
401
  }
393
- }
394
- // Detect skills that exist in some agents but were deleted from others.
395
- // These are "pending delete propagations" — user must confirm which agents to delete from.
396
- const deletedFromAgents = [];
397
- for (const source of shared.skills.sources) {
398
- const p = profiles[source.agent];
399
- if (!p.enabled)
400
- continue;
401
- const mySkills = agentSkills.get(source.agent);
402
- if (!mySkills)
403
- continue;
404
- // Check skills that exist globally but this agent doesn't have
405
- // → This agent might have intentionally deleted them
406
- // We DON'T auto-copy them back (respect user's deletion)
407
- }
408
- // Build a set of skills each agent has
409
- const agentHasSkill = new Map(); // relPath → set of agent names that have it
410
- for (const [agentName, skills] of agentSkills) {
411
- for (const rel of skills.keys()) {
412
- if (!agentHasSkill.has(rel))
413
- agentHasSkill.set(rel, new Set());
414
- agentHasSkill.get(rel).add(agentName);
402
+ // Build ownership map: relPath → set of agent names that have it
403
+ const agentHasSkill = new Map();
404
+ for (const [agentName, skills] of agentSkills) {
405
+ for (const rel of skills.keys()) {
406
+ if (!agentHasSkill.has(rel))
407
+ agentHasSkill.set(rel, new Set());
408
+ agentHasSkill.get(rel).add(agentName);
409
+ }
415
410
  }
416
- }
417
- // Distribute: only copy NEW skills and UPDATED existing skills.
418
- // NEVER copy a skill back to an agent that doesn't have it — that agent may have deleted it.
419
- // Only distribute to agents that ALREADY have the skill (update) or have NEVER seen it (new).
420
- for (const source of shared.skills.sources) {
421
- const p = profiles[source.agent];
422
- if (!p.enabled)
423
- continue;
424
- const mySkills = agentSkills.get(source.agent) ?? new Map();
425
- const skillsDir = path.join(expandHome(p.workspacePath), source.dir);
411
+ const allSourceAgents = shared.skills.sources.map(s => s.agent).filter(a => profiles[a].enabled);
412
+ // Detect pending distributions for each skill
426
413
  for (const [relFile, srcAbs] of allSkills) {
427
- const dest = path.join(skillsDir, relFile);
428
- const agentHasIt = mySkills.has(relFile);
429
- if (!agentHasIt) {
430
- // Agent doesn't have this skill. Check if it's a genuinely NEW skill
431
- // (not present in repo yet) vs one the agent intentionally deleted.
432
- // If the skill exists in repo shared/skills/, this agent previously had it
433
- // and may have deleted it DON'T copy back.
434
- // If it's NOT in repo yet, it's brand new — copy it.
435
- const repoSkillPath = path.join(expandHome(profiles[Object.keys(profiles)[0]].workspacePath), '..', '.wangchuan', 'repo', 'shared', 'skills', relFile);
436
- // Simpler check: if the skill only exists in one agent (the source), it's new → distribute
437
- const owners = agentHasSkill.get(relFile);
438
- if (owners && owners.size === 1 && !owners.has(source.agent)) {
439
- // Only one other agent has it, and not us — it's new from that agent, copy it
440
- fs.mkdirSync(path.dirname(dest), { recursive: true });
441
- fs.copyFileSync(srcAbs, dest);
442
- logger.debug(` ${t('sync.distributeSkill', { file: relFile, agent: source.agent })}`);
414
+ const owners = agentHasSkill.get(relFile) ?? new Set();
415
+ const sourceAgent = allSkillOwner.get(relFile) ?? '';
416
+ for (const targetAgent of allSourceAgents) {
417
+ if (targetAgent === sourceAgent)
418
+ continue;
419
+ const targetHasIt = owners.has(targetAgent);
420
+ const targetSkillsDir = path.join(expandHome(profiles[targetAgent].workspacePath), shared.skills.sources.find(s => s.agent === targetAgent).dir);
421
+ const targetPath = path.join(targetSkillsDir, relFile);
422
+ if (!targetHasIt) {
423
+ // Target doesn't have this skill.
424
+ // If only one agent has it → genuinely new skill → "add" pending
425
+ // If multiple agents have it but this one doesn't → likely deleted → "delete" pending
426
+ if (owners.size === 1) {
427
+ pendingItems.push({
428
+ kind: 'skill',
429
+ action: 'add',
430
+ relFile,
431
+ sourceAgent,
432
+ targetAgents: [targetAgent],
433
+ sourceAbs: srcAbs,
434
+ });
435
+ }
436
+ // Multi-owner missing case handled in the delete detection loop below
437
+ }
438
+ else {
439
+ // Target has it — check if content differs (needs update)
440
+ if (path.resolve(targetPath) === path.resolve(srcAbs))
441
+ continue;
442
+ try {
443
+ if (fs.readFileSync(targetPath).equals(fs.readFileSync(srcAbs)))
444
+ continue;
445
+ }
446
+ catch { /* fall through */ }
447
+ pendingItems.push({
448
+ kind: 'skill',
449
+ action: 'update',
450
+ relFile,
451
+ sourceAgent,
452
+ targetAgents: [targetAgent],
453
+ sourceAbs: srcAbs,
454
+ });
443
455
  }
444
- // If multiple agents have it but this one doesn't → user likely deleted it, skip
445
- continue;
446
- }
447
- // Agent already has this skill — check if it needs updating
448
- if (path.resolve(dest) === path.resolve(srcAbs))
449
- continue; // same file
450
- try {
451
- if (fs.readFileSync(dest).equals(fs.readFileSync(srcAbs)))
452
- continue; // same content
453
456
  }
454
- catch { /* fall through to copy */ }
455
- // Content differs — update with newest version
456
- fs.mkdirSync(path.dirname(dest), { recursive: true });
457
- fs.copyFileSync(srcAbs, dest);
458
- logger.debug(` ${t('sync.distributeSkill', { file: relFile, agent: source.agent })}`);
459
457
  }
460
- }
461
- // Record skill deletions that need user confirmation for cross-agent propagation
462
- for (const [relFile, owners] of agentHasSkill) {
463
- const allSourceAgents = shared.skills.sources.map(s => s.agent).filter(a => profiles[a].enabled);
464
- const missingFrom = allSourceAgents.filter(a => !owners.has(a));
465
- if (missingFrom.length > 0 && owners.size > 0) {
466
- // Some agents deleted this skill, others still have it
467
- // Record for user confirmation (handled by the command layer)
468
- for (const agent of missingFrom) {
469
- deletedFromAgents.push({ relFile, deletedFrom: agent, presentIn: [...owners] });
458
+ // Detect delete cases: skill missing from some agents but present in multiple others
459
+ for (const [relFile, owners] of agentHasSkill) {
460
+ const missingFrom = allSourceAgents.filter(a => !owners.has(a));
461
+ if (missingFrom.length > 0 && owners.size > 1) {
462
+ const srcAgent = [...owners][0];
463
+ const srcAbs = agentSkills.get(srcAgent)?.get(relFile) ?? '';
464
+ for (const target of missingFrom) {
465
+ pendingItems.push({
466
+ kind: 'skill',
467
+ action: 'delete',
468
+ relFile,
469
+ sourceAgent: srcAgent,
470
+ targetAgents: [target],
471
+ sourceAbs: srcAbs,
472
+ });
473
+ }
470
474
  }
471
475
  }
472
476
  }
473
- // Save deletion propagation requests if any
474
- if (deletedFromAgents.length > 0) {
475
- const pendingPath = path.join(os.homedir(), '.wangchuan', 'pending-skill-deletions.json');
476
- fs.mkdirSync(path.dirname(pendingPath), { recursive: true });
477
- fs.writeFileSync(pendingPath, JSON.stringify(deletedFromAgents, null, 2), 'utf-8');
478
- }
479
- // ── Distribute MCP configs: extract from each source, merge by newest mtime ──
477
+ // ── Distribute MCP configs: automatic (unchanged) ──────────────
480
478
  const mergedMcp = {};
481
479
  const mcpMtimes = {}; // server key → mtime of source file
482
480
  for (const source of shared.mcp.sources) {
@@ -525,7 +523,6 @@ function distributeShared(cfg) {
525
523
  }
526
524
  else if (JSON.stringify(currentMcp[key]) !== JSON.stringify(val)) {
527
525
  // Existing server with updated config — take the newer version
528
- // (mergedMcp is built by iterating sources in order, last write wins)
529
526
  currentMcp[key] = val;
530
527
  changed = true;
531
528
  }
@@ -540,7 +537,7 @@ function distributeShared(cfg) {
540
537
  catch { /* ignore */ }
541
538
  }
542
539
  }
543
- // ── Distribute custom agents: same pattern as skills ───────────
540
+ // ── Custom agents: collect pending distributions (no file writes) ──
544
541
  if (shared.agents && shared.agents.sources.length > 0) {
545
542
  // Collect each agent's current custom agent files
546
543
  const agentAgents = new Map(); // agent → relPath → absPath
@@ -562,18 +559,22 @@ function distributeShared(cfg) {
562
559
  // Merge all agents' custom agent files — pick NEWEST version by mtime
563
560
  const allAgentFiles = new Map();
564
561
  const allAgentMtimes = new Map();
565
- for (const agents of agentAgents.values()) {
562
+ const allAgentOwner = new Map();
563
+ for (const [agentName, agents] of agentAgents) {
566
564
  for (const [rel, abs] of agents) {
567
565
  try {
568
566
  const mtime = fs.statSync(abs).mtimeMs;
569
567
  if (!allAgentFiles.has(rel) || mtime > allAgentMtimes.get(rel)) {
570
568
  allAgentFiles.set(rel, abs);
571
569
  allAgentMtimes.set(rel, mtime);
570
+ allAgentOwner.set(rel, agentName);
572
571
  }
573
572
  }
574
573
  catch {
575
- if (!allAgentFiles.has(rel))
574
+ if (!allAgentFiles.has(rel)) {
576
575
  allAgentFiles.set(rel, abs);
576
+ allAgentOwner.set(rel, agentName);
577
+ }
577
578
  }
578
579
  }
579
580
  }
@@ -586,55 +587,73 @@ function distributeShared(cfg) {
586
587
  agentHasFile.get(rel).add(agentName);
587
588
  }
588
589
  }
589
- // Distribute: only copy NEW agent files and UPDATE existing ones
590
- for (const source of shared.agents.sources) {
591
- const p = profiles[source.agent];
592
- if (!p.enabled)
593
- continue;
594
- const myAgents = agentAgents.get(source.agent) ?? new Map();
595
- const agentsDir = path.join(expandHome(p.workspacePath), source.dir);
596
- for (const [relFile, srcAbs] of allAgentFiles) {
597
- const dest = path.join(agentsDir, relFile);
598
- const agentHasIt = myAgents.has(relFile);
599
- if (!agentHasIt) {
600
- // Check if genuinely NEW (only one source has it) vs intentionally deleted
601
- const owners = agentHasFile.get(relFile);
602
- if (owners && owners.size === 1 && !owners.has(source.agent)) {
603
- fs.mkdirSync(path.dirname(dest), { recursive: true });
604
- fs.copyFileSync(srcAbs, dest);
605
- logger.debug(` ${t('sync.distributeAgent', { file: relFile, agent: source.agent })}`);
606
- }
590
+ const allSourceAgents = shared.agents.sources.map(s => s.agent).filter(a => profiles[a].enabled);
591
+ // Detect pending distributions for each custom agent file
592
+ for (const [relFile, srcAbs] of allAgentFiles) {
593
+ const owners = agentHasFile.get(relFile) ?? new Set();
594
+ const sourceAgent = allAgentOwner.get(relFile) ?? '';
595
+ for (const targetAgent of allSourceAgents) {
596
+ if (targetAgent === sourceAgent)
607
597
  continue;
598
+ const targetHasIt = owners.has(targetAgent);
599
+ const targetAgentsDir = path.join(expandHome(profiles[targetAgent].workspacePath), shared.agents.sources.find(s => s.agent === targetAgent).dir);
600
+ const targetPath = path.join(targetAgentsDir, relFile);
601
+ if (!targetHasIt) {
602
+ // Only create add if genuinely new (single owner)
603
+ if (owners.size === 1) {
604
+ pendingItems.push({
605
+ kind: 'agent',
606
+ action: 'add',
607
+ relFile,
608
+ sourceAgent,
609
+ targetAgents: [targetAgent],
610
+ sourceAbs: srcAbs,
611
+ });
612
+ }
608
613
  }
609
- // Agent already has this file — check if it needs updating
610
- if (path.resolve(dest) === path.resolve(srcAbs))
611
- continue;
612
- try {
613
- if (fs.readFileSync(dest).equals(fs.readFileSync(srcAbs)))
614
+ else {
615
+ if (path.resolve(targetPath) === path.resolve(srcAbs))
614
616
  continue;
617
+ try {
618
+ if (fs.readFileSync(targetPath).equals(fs.readFileSync(srcAbs)))
619
+ continue;
620
+ }
621
+ catch { /* fall through */ }
622
+ pendingItems.push({
623
+ kind: 'agent',
624
+ action: 'update',
625
+ relFile,
626
+ sourceAgent,
627
+ targetAgents: [targetAgent],
628
+ sourceAbs: srcAbs,
629
+ });
615
630
  }
616
- catch { /* fall through to copy */ }
617
- fs.mkdirSync(path.dirname(dest), { recursive: true });
618
- fs.copyFileSync(srcAbs, dest);
619
- logger.debug(` ${t('sync.distributeAgent', { file: relFile, agent: source.agent })}`);
620
631
  }
621
632
  }
622
- // Record agent deletions that need user confirmation (same as skills)
623
- const deletedAgentFiles = [];
633
+ // Detect delete cases
624
634
  for (const [relFile, owners] of agentHasFile) {
625
- const allSourceAgents = shared.agents.sources.map(s => s.agent).filter(a => profiles[a].enabled);
626
635
  const missingFrom = allSourceAgents.filter(a => !owners.has(a));
627
- if (missingFrom.length > 0 && owners.size > 0) {
628
- for (const agent of missingFrom) {
629
- deletedAgentFiles.push({ relFile, deletedFrom: agent, presentIn: [...owners] });
636
+ if (missingFrom.length > 0 && owners.size > 1) {
637
+ const srcAgent = [...owners][0];
638
+ const srcAbs = agentAgents.get(srcAgent)?.get(relFile) ?? '';
639
+ for (const target of missingFrom) {
640
+ pendingItems.push({
641
+ kind: 'agent',
642
+ action: 'delete',
643
+ relFile,
644
+ sourceAgent: srcAgent,
645
+ targetAgents: [target],
646
+ sourceAbs: srcAbs,
647
+ });
630
648
  }
631
649
  }
632
650
  }
633
- if (deletedAgentFiles.length > 0) {
634
- const pendingPath = path.join(os.homedir(), '.wangchuan', 'pending-agent-deletions.json');
635
- fs.mkdirSync(path.dirname(pendingPath), { recursive: true });
636
- fs.writeFileSync(pendingPath, JSON.stringify(deletedAgentFiles, null, 2), 'utf-8');
637
- }
651
+ }
652
+ // ── Write pending distributions if any ──────────────────────────
653
+ if (pendingItems.length > 0) {
654
+ // Merge same-kind/same-action/same-relFile items by combining targetAgents
655
+ const merged = mergePendingItems(pendingItems);
656
+ savePendingDistributions(merged);
638
657
  }
639
658
  }
640
659
  /**
@@ -686,6 +705,7 @@ export function deleteStaleFiles(repoPath, staleFiles) {
686
705
  }
687
706
  }
688
707
  const PENDING_DELETIONS_PATH = path.join(os.homedir(), '.wangchuan', 'pending-deletions.json');
708
+ const PENDING_DISTRIBUTIONS_PATH = path.join(os.homedir(), '.wangchuan', 'pending-distributions.json');
689
709
  /** Save pending deletions for later user confirmation */
690
710
  function savePendingDeletions(files) {
691
711
  const existing = loadPendingDeletions();
@@ -712,6 +732,181 @@ export function clearPendingDeletions() {
712
732
  }
713
733
  catch { /* */ }
714
734
  }
735
+ /** Merge pending distribution items with same kind/action/relFile by combining targetAgents */
736
+ function mergePendingItems(items) {
737
+ const map = new Map();
738
+ for (const item of items) {
739
+ const key = `${item.kind}:${item.action}:${item.relFile}:${item.sourceAgent}`;
740
+ const existing = map.get(key);
741
+ if (existing) {
742
+ for (const t of item.targetAgents) {
743
+ if (!existing.targetAgents.includes(t))
744
+ existing.targetAgents.push(t);
745
+ }
746
+ }
747
+ else {
748
+ map.set(key, { ...item, targetAgents: [...item.targetAgents] });
749
+ }
750
+ }
751
+ return [...map.values()];
752
+ }
753
+ /** Save pending distributions for user confirmation */
754
+ function savePendingDistributions(items) {
755
+ fs.mkdirSync(path.dirname(PENDING_DISTRIBUTIONS_PATH), { recursive: true });
756
+ fs.writeFileSync(PENDING_DISTRIBUTIONS_PATH, JSON.stringify(items, null, 2), 'utf-8');
757
+ }
758
+ /** Load pending distributions */
759
+ export function loadPendingDistributions() {
760
+ try {
761
+ if (!fs.existsSync(PENDING_DISTRIBUTIONS_PATH))
762
+ return [];
763
+ return JSON.parse(fs.readFileSync(PENDING_DISTRIBUTIONS_PATH, 'utf-8'));
764
+ }
765
+ catch {
766
+ return [];
767
+ }
768
+ }
769
+ /** Clear pending distributions after processing */
770
+ export function clearPendingDistributions() {
771
+ try {
772
+ if (fs.existsSync(PENDING_DISTRIBUTIONS_PATH))
773
+ fs.unlinkSync(PENDING_DISTRIBUTIONS_PATH);
774
+ }
775
+ catch { /* */ }
776
+ }
777
+ /**
778
+ * Process pending distributions interactively.
779
+ * Groups by relFile, prompts user for each, executes the chosen actions.
780
+ */
781
+ export async function processPendingDistributions(cfg) {
782
+ const pending = loadPendingDistributions();
783
+ if (pending.length === 0)
784
+ return;
785
+ const profiles = cfg.profiles.default;
786
+ const shared = cfg.shared;
787
+ if (!shared) {
788
+ clearPendingDistributions();
789
+ return;
790
+ }
791
+ // Group by kind + relFile
792
+ const grouped = new Map();
793
+ for (const item of pending) {
794
+ const key = `${item.kind}:${item.relFile}`;
795
+ if (!grouped.has(key))
796
+ grouped.set(key, []);
797
+ grouped.get(key).push(item);
798
+ }
799
+ logger.info(t('sync.pendingDistributions', { count: pending.length }));
800
+ const rl = await import('readline');
801
+ for (const [, items] of grouped) {
802
+ const first = items[0];
803
+ // Collect all unique target agents across all actions for this file
804
+ const allTargets = [...new Set(items.flatMap(i => [...i.targetAgents]))];
805
+ console.log();
806
+ logger.info(t('sync.distItem', {
807
+ kind: first.kind,
808
+ action: first.action,
809
+ file: first.relFile,
810
+ source: first.sourceAgent,
811
+ }));
812
+ logger.info(t('sync.distPrompt'));
813
+ // Build choices
814
+ const choices = [];
815
+ choices.push(`[0] ${t('sync.distAll')} (${allTargets.join(', ')})`);
816
+ for (let i = 0; i < allTargets.length; i++) {
817
+ choices.push(`[${i + 1}] ${allTargets[i]}`);
818
+ }
819
+ choices.push(`[${allTargets.length + 1}] ${t('sync.distNone')}`);
820
+ for (const c of choices)
821
+ console.log(` ${c}`);
822
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
823
+ const answer = await new Promise(resolve => {
824
+ iface.question(t('sync.distInputPrompt'), (ans) => { iface.close(); resolve(ans.trim()); });
825
+ });
826
+ // Parse selection
827
+ const indices = answer.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
828
+ let selectedAgents = [];
829
+ if (indices.includes(0)) {
830
+ selectedAgents = [...allTargets];
831
+ }
832
+ else if (indices.includes(allTargets.length + 1)) {
833
+ selectedAgents = [];
834
+ }
835
+ else {
836
+ selectedAgents = indices
837
+ .filter(i => i > 0 && i <= allTargets.length)
838
+ .map(i => allTargets[i - 1])
839
+ .filter((a) => a !== undefined);
840
+ }
841
+ // Execute the distribution for selected agents
842
+ for (const targetAgent of selectedAgents) {
843
+ for (const item of items) {
844
+ if (!item.targetAgents.includes(targetAgent))
845
+ continue;
846
+ executeDistribution(item, targetAgent, cfg);
847
+ }
848
+ }
849
+ if (selectedAgents.length === 0) {
850
+ logger.info(t('sync.distSkipped'));
851
+ }
852
+ }
853
+ clearPendingDistributions();
854
+ }
855
+ /** Execute a single distribution action for a target agent */
856
+ function executeDistribution(item, targetAgent, cfg) {
857
+ const profiles = cfg.profiles.default;
858
+ const shared = cfg.shared;
859
+ if (!shared)
860
+ return;
861
+ const p = profiles[targetAgent];
862
+ if (!p)
863
+ return;
864
+ // Resolve target directory based on kind
865
+ let targetDir;
866
+ if (item.kind === 'skill') {
867
+ const source = shared.skills.sources.find(s => s.agent === targetAgent);
868
+ if (!source)
869
+ return;
870
+ targetDir = path.join(expandHome(p.workspacePath), source.dir);
871
+ }
872
+ else {
873
+ const source = shared.agents?.sources.find(s => s.agent === targetAgent);
874
+ if (!source)
875
+ return;
876
+ targetDir = path.join(expandHome(p.workspacePath), source.dir);
877
+ }
878
+ const targetPath = path.join(targetDir, item.relFile);
879
+ if (item.action === 'delete') {
880
+ if (fs.existsSync(targetPath)) {
881
+ fs.unlinkSync(targetPath);
882
+ // Clean up empty parent dirs
883
+ let dir = path.dirname(targetPath);
884
+ while (dir !== targetDir && dir.startsWith(targetDir)) {
885
+ try {
886
+ const remaining = fs.readdirSync(dir);
887
+ if (remaining.length === 0) {
888
+ fs.rmdirSync(dir);
889
+ dir = path.dirname(dir);
890
+ }
891
+ else
892
+ break;
893
+ }
894
+ catch {
895
+ break;
896
+ }
897
+ }
898
+ logger.ok(` ${t('sync.distApplied', { action: 'delete', file: item.relFile, agent: targetAgent })}`);
899
+ }
900
+ }
901
+ else {
902
+ // add or update — copy the source file
903
+ if (!fs.existsSync(item.sourceAbs))
904
+ return;
905
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
906
+ fs.copyFileSync(item.sourceAbs, targetPath);
907
+ logger.ok(` ${t('sync.distApplied', { action: item.action, file: item.relFile, agent: targetAgent })}`);
908
+ }
909
+ }
715
910
  const SYNC_META_FILE = 'sync-meta.json';
716
911
  function writeSyncMeta(repoPath, cfg) {
717
912
  const meta = {
@@ -871,6 +1066,9 @@ export const syncEngine = {
871
1066
  loadPendingDeletions,
872
1067
  clearPendingDeletions,
873
1068
  deleteStaleFiles,
1069
+ loadPendingDistributions,
1070
+ clearPendingDistributions,
1071
+ processPendingDistributions,
874
1072
  /**
875
1073
  * Push: distribute shared content to all agents, then collect files to repo.
876
1074
  */