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.
- package/README.md +1 -1
- package/dist/bin/wangchuan.js +1 -1
- package/dist/src/agents/openclaw.d.ts.map +1 -1
- package/dist/src/agents/openclaw.js +1 -0
- package/dist/src/agents/openclaw.js.map +1 -1
- package/dist/src/agents/workbuddy.d.ts.map +1 -1
- package/dist/src/agents/workbuddy.js +1 -0
- package/dist/src/agents/workbuddy.js.map +1 -1
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.js +8 -94
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/core/sync.d.ts +13 -1
- package/dist/src/core/sync.d.ts.map +1 -1
- package/dist/src/core/sync.js +351 -153
- package/dist/src/core/sync.js.map +1 -1
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +9 -0
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/types.d.ts +15 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/test/sync.test.js +51 -14
- package/dist/test/sync.test.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
package/dist/src/core/sync.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
//
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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
|
-
|
|
590
|
-
for
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
//
|
|
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 >
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
*/
|