@akshar5/skillsync 0.1.2 → 0.1.3
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 +10 -0
- package/package.json +1 -1
- package/src/cli.js +92 -41
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ Open the interactive UI:
|
|
|
43
43
|
skillsync
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
In the interactive UI, use arrow keys to move, Space to toggle checklist items, Enter to apply, and Esc to go back.
|
|
47
|
+
|
|
46
48
|
## Requirements
|
|
47
49
|
|
|
48
50
|
- Node.js 20 or newer
|
|
@@ -124,6 +126,14 @@ List available skills:
|
|
|
124
126
|
skillsync list
|
|
125
127
|
```
|
|
126
128
|
|
|
129
|
+
Browse and install multiple vault skills at once:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
skillsync
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Choose `Browse/install skills`, press Space to select every skill you want installed on this device, then press Enter to apply the changes.
|
|
136
|
+
|
|
127
137
|
Check current vault/device state:
|
|
128
138
|
|
|
129
139
|
```bash
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -84,6 +84,24 @@ function hasFlag(rest, flag) {
|
|
|
84
84
|
return rest.includes(flag);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
async function promptWithEscape(promptPromise, escapeValue = null) {
|
|
88
|
+
if (!process.stdin.isTTY) return promptPromise;
|
|
89
|
+
const onKeypress = (_value, key) => {
|
|
90
|
+
if (key?.name === 'escape' && typeof promptPromise.cancel === 'function') {
|
|
91
|
+
promptPromise.cancel();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
process.stdin.on('keypress', onKeypress);
|
|
95
|
+
try {
|
|
96
|
+
return await promptPromise;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error?.name === 'CancelPromptError') return escapeValue;
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
process.stdin.off('keypress', onKeypress);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
async function configured() {
|
|
88
106
|
const config = await loadConfig();
|
|
89
107
|
if (!config.repoPath || !await exists(config.repoPath)) {
|
|
@@ -142,13 +160,16 @@ async function setup(rest) {
|
|
|
142
160
|
if (nameArg || yes || !process.stdin.isTTY) {
|
|
143
161
|
repo = await ensureOwnedVaultRepo(owner, nameArg || 'skills');
|
|
144
162
|
} else {
|
|
145
|
-
const setupMode = await select({
|
|
163
|
+
const setupMode = await promptWithEscape(select({
|
|
146
164
|
message: 'Which skills vault do you want to use?',
|
|
165
|
+
loop: false,
|
|
166
|
+
pageSize: 2,
|
|
147
167
|
choices: [
|
|
148
168
|
{ name: 'Use an existing GitHub repo', value: 'existing' },
|
|
149
169
|
{ name: `Create or use ${owner}/<name>`, value: 'owned' },
|
|
150
170
|
],
|
|
151
|
-
});
|
|
171
|
+
}));
|
|
172
|
+
if (!setupMode) return;
|
|
152
173
|
if (setupMode === 'existing') {
|
|
153
174
|
const existingRepo = await input({ message: 'Existing vault repo (owner/repo or URL):', default: `${owner}/skills` });
|
|
154
175
|
repo = await resolveRepoCloneUrl(existingRepo);
|
|
@@ -434,10 +455,13 @@ async function chooseInstallTargets(config, rest) {
|
|
|
434
455
|
if (hasFlag(rest, '--no-install') || hasFlag(rest, '--yes') || hasFlag(rest, '-y') || !process.stdin.isTTY || !available.length) return [];
|
|
435
456
|
const shouldInstall = await confirm({ message: 'Install on this device now?', default: true });
|
|
436
457
|
if (!shouldInstall) return [];
|
|
437
|
-
return checkbox({
|
|
458
|
+
return promptWithEscape(checkbox({
|
|
438
459
|
message: 'Choose local targets',
|
|
460
|
+
loop: false,
|
|
461
|
+
pageSize: Math.min(12, available.length),
|
|
439
462
|
choices: available.map((target) => ({ name: target, value: target, checked: true })),
|
|
440
|
-
|
|
463
|
+
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
464
|
+
}), []);
|
|
441
465
|
}
|
|
442
466
|
|
|
443
467
|
async function target(rest) {
|
|
@@ -561,20 +585,22 @@ async function runUi() {
|
|
|
561
585
|
|
|
562
586
|
while (true) {
|
|
563
587
|
await refreshChangedRegistryEntries(config.repoPath);
|
|
564
|
-
const choice = await select({
|
|
588
|
+
const choice = await promptWithEscape(select({
|
|
565
589
|
message: 'SkillSync',
|
|
590
|
+
loop: false,
|
|
591
|
+
pageSize: 8,
|
|
566
592
|
choices: [
|
|
567
|
-
{ name: 'Browse/install skills', value: 'skills' },
|
|
568
|
-
{ name: 'Devices', value: 'devices' },
|
|
569
|
-
{ name: 'Targets', value: 'targets' },
|
|
570
|
-
{ name: 'Add skill from folder', value: 'add' },
|
|
571
|
-
{ name: 'Import Hermes skills', value: 'import-hermes' },
|
|
572
|
-
{ name: 'Scan local targets', value: 'scan' },
|
|
573
|
-
{ name: 'Sync now', value: 'sync' },
|
|
593
|
+
{ name: 'Browse/install skills', value: 'skills', description: 'Select multiple vault skills to install or remove here.' },
|
|
594
|
+
{ name: 'Devices', value: 'devices', description: 'Show devices known to the vault.' },
|
|
595
|
+
{ name: 'Targets', value: 'targets', description: 'Manage local agent skill folders.' },
|
|
596
|
+
{ name: 'Add skill from folder', value: 'add', description: 'Copy a local SKILL.md folder into the vault.' },
|
|
597
|
+
{ name: 'Import Hermes skills', value: 'import-hermes', description: 'Import detected Hermes skills into the vault.' },
|
|
598
|
+
{ name: 'Scan local targets', value: 'scan', description: 'Refresh detected local skills.' },
|
|
599
|
+
{ name: 'Sync now', value: 'sync', description: 'Pull, link, scan, commit, and push vault changes.' },
|
|
574
600
|
{ name: 'Quit', value: 'quit' },
|
|
575
601
|
],
|
|
576
|
-
});
|
|
577
|
-
if (choice === 'quit') return;
|
|
602
|
+
}));
|
|
603
|
+
if (!choice || choice === 'quit') return;
|
|
578
604
|
if (choice === 'skills') await skillsScreen(config);
|
|
579
605
|
if (choice === 'devices') await devicesScreen(config);
|
|
580
606
|
if (choice === 'targets') await targetsScreen(config);
|
|
@@ -593,41 +619,66 @@ async function skillsScreen(config) {
|
|
|
593
619
|
console.log('\nNo skills in vault yet. Use Add/import first.\n');
|
|
594
620
|
return;
|
|
595
621
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
622
|
+
const installedNames = new Set(Object.keys(device.installed || {}));
|
|
623
|
+
const selected = await promptWithEscape(checkbox({
|
|
624
|
+
message: `Install skills on ${device.display_name}`,
|
|
625
|
+
loop: false,
|
|
626
|
+
pageSize: Math.min(14, Math.max(7, names.length)),
|
|
627
|
+
instructions: 'Space toggles skills. Enter applies changes. Esc goes back.',
|
|
628
|
+
choices: names.map((name) => {
|
|
629
|
+
const targets = device.installed[name] || [];
|
|
630
|
+
const targetLabel = targets.length ? ` [${targets.join(', ')}]` : '';
|
|
631
|
+
return {
|
|
632
|
+
name: `${name}${targetLabel}`,
|
|
633
|
+
short: name,
|
|
634
|
+
value: name,
|
|
635
|
+
checked: installedNames.has(name),
|
|
636
|
+
description: targets.length ? `Installed in ${targets.join(', ')}` : 'Not installed on this device',
|
|
637
|
+
};
|
|
638
|
+
}),
|
|
639
|
+
}));
|
|
640
|
+
if (!selected) return;
|
|
641
|
+
|
|
642
|
+
const selectedNames = new Set(selected);
|
|
643
|
+
const toInstall = names.filter((name) => selectedNames.has(name) && !installedNames.has(name));
|
|
644
|
+
const toUninstall = names.filter((name) => !selectedNames.has(name) && installedNames.has(name));
|
|
645
|
+
if (!toInstall.length && !toUninstall.length) {
|
|
646
|
+
console.log('\nNo install changes.\n');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let targets = [];
|
|
651
|
+
if (toInstall.length) {
|
|
652
|
+
targets = await chooseTargets(config);
|
|
653
|
+
if (!targets.length) return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const skillName of toInstall) {
|
|
657
|
+
await installSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName, targets });
|
|
618
658
|
}
|
|
619
|
-
|
|
659
|
+
for (const skillName of toUninstall) {
|
|
660
|
+
await uninstallSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName });
|
|
661
|
+
}
|
|
662
|
+
await applyLinks({ vaultPath: config.repoPath, deviceId: config.deviceId });
|
|
663
|
+
await syncVault({ vaultPath: config.repoPath, deviceId: config.deviceId, pull: false });
|
|
664
|
+
|
|
665
|
+
const installedText = toInstall.length ? `Installed ${toInstall.join(', ')} to ${targets.join(', ')}.` : '';
|
|
666
|
+
const removedText = toUninstall.length ? `Removed ${toUninstall.join(', ')} from this device.` : '';
|
|
667
|
+
console.log(`\n${[installedText, removedText].filter(Boolean).join(' ')}\n`);
|
|
620
668
|
}
|
|
621
669
|
|
|
622
670
|
async function chooseTargets(config) {
|
|
623
671
|
const device = await loadDevice(config.repoPath, config.deviceId);
|
|
624
672
|
const targetNames = Object.keys(device.targets);
|
|
625
673
|
if (!targetNames.length) throw new Error('No targets configured. Add one from the Targets screen.');
|
|
626
|
-
return checkbox({
|
|
674
|
+
return promptWithEscape(checkbox({
|
|
627
675
|
message: 'Install into which targets?',
|
|
676
|
+
loop: false,
|
|
677
|
+
pageSize: Math.min(12, targetNames.length),
|
|
628
678
|
choices: targetNames.map((name) => ({ name, value: name, checked: true })),
|
|
629
679
|
required: true,
|
|
630
|
-
|
|
680
|
+
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
681
|
+
}), []);
|
|
631
682
|
}
|
|
632
683
|
|
|
633
684
|
async function devicesScreen(config) {
|
|
@@ -651,8 +702,8 @@ async function targetsScreen(config) {
|
|
|
651
702
|
{ name: 'Add target', value: 'add' },
|
|
652
703
|
{ name: 'Back', value: 'back' },
|
|
653
704
|
]);
|
|
654
|
-
const choice = await select({ message: 'Targets on this device', choices });
|
|
655
|
-
if (choice === 'back') return;
|
|
705
|
+
const choice = await promptWithEscape(select({ message: 'Targets on this device', choices, loop: false, pageSize: Math.min(10, choices.length) }));
|
|
706
|
+
if (!choice || choice === 'back') return;
|
|
656
707
|
if (choice === 'add') {
|
|
657
708
|
const name = await input({ message: 'Target name (codex, claude, hermes, custom):' });
|
|
658
709
|
const targetPath = await input({ message: 'Target skill directory path:' });
|