@akshar5/skillsync 0.1.2 → 0.2.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/LICENSE +21 -0
- package/README.md +30 -5
- package/package.json +2 -1
- package/src/cli.js +194 -41
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akshar Patel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npm install -g @akshar5/skillsync
|
|
|
15
15
|
Connect this device to an existing private skills vault:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
skillsync setup --repo
|
|
18
|
+
skillsync setup --repo OWNER/skills
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Add a local skill folder to the vault:
|
|
@@ -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
|
|
@@ -86,7 +88,7 @@ npm install -g @akshar5/skillsync
|
|
|
86
88
|
Connect to an existing vault:
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
|
-
skillsync setup --repo
|
|
91
|
+
skillsync setup --repo OWNER/skills
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
Or create/select a vault repo interactively:
|
|
@@ -97,7 +99,7 @@ skillsync setup
|
|
|
97
99
|
|
|
98
100
|
If you run plain `skillsync setup` in an interactive terminal, it first asks which vault type to use:
|
|
99
101
|
|
|
100
|
-
- Choose `Use an existing GitHub repo`, then enter a repo like `
|
|
102
|
+
- Choose `Use an existing GitHub repo`, then enter a repo like `OWNER/skills`.
|
|
101
103
|
- Choose `Create or use OWNER/<name>`, then enter a repo name like `skills`.
|
|
102
104
|
|
|
103
105
|
`setup --name skills` is the non-interactive form of the second option. It creates `OWNER/skills` as a private GitHub repo if it does not exist. If it exists, SkillSync verifies it is private before using it.
|
|
@@ -105,7 +107,7 @@ If you run plain `skillsync setup` in an interactive terminal, it first asks whi
|
|
|
105
107
|
You can also run commands without a global install:
|
|
106
108
|
|
|
107
109
|
```bash
|
|
108
|
-
npx @akshar5/skillsync setup --repo
|
|
110
|
+
npx @akshar5/skillsync setup --repo OWNER/skills
|
|
109
111
|
npx @akshar5/skillsync add https://github.com/example-org/example-skill --skill example-skill
|
|
110
112
|
```
|
|
111
113
|
|
|
@@ -113,7 +115,7 @@ If an older SkillSync version failed with `git@github.com: Permission denied (pu
|
|
|
113
115
|
|
|
114
116
|
```bash
|
|
115
117
|
npm install -g @akshar5/skillsync@latest
|
|
116
|
-
skillsync setup --repo
|
|
118
|
+
skillsync setup --repo OWNER/skills
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
## Common workflows
|
|
@@ -124,6 +126,22 @@ 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
|
+
|
|
137
|
+
View skills installed on this device:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
skillsync installed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In the interactive UI, choose `Installed on this device` to select installed skills, update them from the vault, or uninstall them from the current device.
|
|
144
|
+
|
|
127
145
|
Check current vault/device state:
|
|
128
146
|
|
|
129
147
|
```bash
|
|
@@ -170,6 +188,8 @@ Sync the vault and reapply local links:
|
|
|
170
188
|
skillsync sync
|
|
171
189
|
```
|
|
172
190
|
|
|
191
|
+
If an installed skill changes in the vault, `skillsync sync` pulls the vault and reapplies local projections. Symlink targets point at the current vault copy automatically; copy targets are refreshed when links are reapplied.
|
|
192
|
+
|
|
173
193
|
Scan configured target folders for already-installed local skills:
|
|
174
194
|
|
|
175
195
|
```bash
|
|
@@ -185,6 +205,7 @@ skillsync setup --name skills
|
|
|
185
205
|
skillsync
|
|
186
206
|
skillsync status
|
|
187
207
|
skillsync list
|
|
208
|
+
skillsync installed
|
|
188
209
|
skillsync add <skill-folder-or-git-url> --skill <name>
|
|
189
210
|
skillsync add https://github.com/example-org/example-skill --skill example-skill
|
|
190
211
|
skillsync import hermes
|
|
@@ -204,6 +225,10 @@ skillsync daemon
|
|
|
204
225
|
- `skillsync uninstall <skill>` removes the skill from the current device only.
|
|
205
226
|
- `skillsync delete <skill>` removes the skill from the vault and all device manifests.
|
|
206
227
|
|
|
228
|
+
## Skill names
|
|
229
|
+
|
|
230
|
+
Skill names are vault-wide identifiers. Installing `my-skill` on two devices means both devices refer to the same vault skill. You can install the same skill on many devices, but you should not use the same name for two different skills in one vault; adding a skill with an existing name updates/replaces that vault entry.
|
|
231
|
+
|
|
207
232
|
## Detected versus managed skills
|
|
208
233
|
|
|
209
234
|
`installed` skills are SkillSync-managed projections into a target folder. `detected` skills are already present in a local agent's skill tree, such as bundled Hermes skills under `~/.hermes/skills`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akshar5/skillsync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first GitHub-backed skill vault sync client for AI agent skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"skillsync": "src/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
+
"LICENSE",
|
|
18
19
|
"src",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
package/src/cli.js
CHANGED
|
@@ -34,6 +34,8 @@ async function main() {
|
|
|
34
34
|
return status();
|
|
35
35
|
case 'list':
|
|
36
36
|
return listSkills();
|
|
37
|
+
case 'installed':
|
|
38
|
+
return installedCommand();
|
|
37
39
|
case 'add':
|
|
38
40
|
return addSkill(rest);
|
|
39
41
|
case 'import':
|
|
@@ -84,6 +86,30 @@ function hasFlag(rest, flag) {
|
|
|
84
86
|
return rest.includes(flag);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
async function promptWithEscape(promptPromise, escapeValue = null) {
|
|
90
|
+
if (!process.stdin.isTTY) return promptPromise;
|
|
91
|
+
const onKeypress = (_value, key) => {
|
|
92
|
+
if (key?.name === 'escape' && typeof promptPromise.cancel === 'function') {
|
|
93
|
+
promptPromise.cancel();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
process.stdin.on('keypress', onKeypress);
|
|
97
|
+
try {
|
|
98
|
+
return await promptPromise;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error?.name === 'CancelPromptError') return escapeValue;
|
|
101
|
+
throw error;
|
|
102
|
+
} finally {
|
|
103
|
+
process.stdin.off('keypress', onKeypress);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function promptPageSize(itemCount, { min = 8, max = 28, reservedRows = 6 } = {}) {
|
|
108
|
+
const rows = Number(process.stdout.rows) || 30;
|
|
109
|
+
const availableRows = Math.max(min, rows - reservedRows);
|
|
110
|
+
return Math.max(1, Math.min(itemCount, max, availableRows));
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
async function configured() {
|
|
88
114
|
const config = await loadConfig();
|
|
89
115
|
if (!config.repoPath || !await exists(config.repoPath)) {
|
|
@@ -142,13 +168,16 @@ async function setup(rest) {
|
|
|
142
168
|
if (nameArg || yes || !process.stdin.isTTY) {
|
|
143
169
|
repo = await ensureOwnedVaultRepo(owner, nameArg || 'skills');
|
|
144
170
|
} else {
|
|
145
|
-
const setupMode = await select({
|
|
171
|
+
const setupMode = await promptWithEscape(select({
|
|
146
172
|
message: 'Which skills vault do you want to use?',
|
|
173
|
+
loop: false,
|
|
174
|
+
pageSize: 2,
|
|
147
175
|
choices: [
|
|
148
176
|
{ name: 'Use an existing GitHub repo', value: 'existing' },
|
|
149
177
|
{ name: `Create or use ${owner}/<name>`, value: 'owned' },
|
|
150
178
|
],
|
|
151
|
-
});
|
|
179
|
+
}));
|
|
180
|
+
if (!setupMode) return;
|
|
152
181
|
if (setupMode === 'existing') {
|
|
153
182
|
const existingRepo = await input({ message: 'Existing vault repo (owner/repo or URL):', default: `${owner}/skills` });
|
|
154
183
|
repo = await resolveRepoCloneUrl(existingRepo);
|
|
@@ -274,6 +303,37 @@ async function listSkills() {
|
|
|
274
303
|
}
|
|
275
304
|
}
|
|
276
305
|
|
|
306
|
+
function installedSkillEntries(device, registry) {
|
|
307
|
+
return Object.entries(device.installed || {})
|
|
308
|
+
.map(([name, targets]) => ({
|
|
309
|
+
name,
|
|
310
|
+
targets: Array.isArray(targets) ? targets : [],
|
|
311
|
+
inVault: Boolean(registry.skills[name]),
|
|
312
|
+
}))
|
|
313
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function installedSkillLabel(skill) {
|
|
317
|
+
const targetLabel = skill.targets.length ? skill.targets.join(', ') : 'no targets';
|
|
318
|
+
return `${skill.name} [${targetLabel}]${skill.inVault ? '' : ' missing from vault'}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function installedCommand() {
|
|
322
|
+
const config = await configured();
|
|
323
|
+
await refreshChangedRegistryEntries(config.repoPath);
|
|
324
|
+
const registry = await loadRegistry(config.repoPath);
|
|
325
|
+
const device = await loadDevice(config.repoPath, config.deviceId);
|
|
326
|
+
const installed = installedSkillEntries(device, registry);
|
|
327
|
+
if (!installed.length) {
|
|
328
|
+
console.log('No SkillSync-managed skills installed on this device.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
console.log(`Installed on ${device.display_name}:`);
|
|
332
|
+
for (const skill of installed) {
|
|
333
|
+
console.log(`- ${installedSkillLabel(skill)}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
277
337
|
async function addSkill(rest) {
|
|
278
338
|
const source = rest[0];
|
|
279
339
|
if (!source) throw new Error('Usage: skillsync add <skill-folder-or-git-url> [--skill name] [--target target]');
|
|
@@ -434,10 +494,13 @@ async function chooseInstallTargets(config, rest) {
|
|
|
434
494
|
if (hasFlag(rest, '--no-install') || hasFlag(rest, '--yes') || hasFlag(rest, '-y') || !process.stdin.isTTY || !available.length) return [];
|
|
435
495
|
const shouldInstall = await confirm({ message: 'Install on this device now?', default: true });
|
|
436
496
|
if (!shouldInstall) return [];
|
|
437
|
-
return checkbox({
|
|
497
|
+
return promptWithEscape(checkbox({
|
|
438
498
|
message: 'Choose local targets',
|
|
499
|
+
loop: false,
|
|
500
|
+
pageSize: promptPageSize(available.length, { min: 6, max: 18 }),
|
|
439
501
|
choices: available.map((target) => ({ name: target, value: target, checked: true })),
|
|
440
|
-
|
|
502
|
+
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
503
|
+
}), []);
|
|
441
504
|
}
|
|
442
505
|
|
|
443
506
|
async function target(rest) {
|
|
@@ -561,21 +624,26 @@ async function runUi() {
|
|
|
561
624
|
|
|
562
625
|
while (true) {
|
|
563
626
|
await refreshChangedRegistryEntries(config.repoPath);
|
|
564
|
-
const
|
|
627
|
+
const menuChoices = [
|
|
628
|
+
{ name: 'Browse/install skills', value: 'skills', description: 'Select multiple vault skills to install or remove here.' },
|
|
629
|
+
{ name: 'Installed on this device', value: 'installed', description: 'View, update, or uninstall SkillSync-managed local skills.' },
|
|
630
|
+
{ name: 'Devices', value: 'devices', description: 'Show devices known to the vault.' },
|
|
631
|
+
{ name: 'Targets', value: 'targets', description: 'Manage local agent skill folders.' },
|
|
632
|
+
{ name: 'Add skill from folder', value: 'add', description: 'Copy a local SKILL.md folder into the vault.' },
|
|
633
|
+
{ name: 'Import Hermes skills', value: 'import-hermes', description: 'Import detected Hermes skills into the vault.' },
|
|
634
|
+
{ name: 'Scan local targets', value: 'scan', description: 'Refresh detected local skills.' },
|
|
635
|
+
{ name: 'Sync now', value: 'sync', description: 'Pull, link, scan, commit, and push vault changes.' },
|
|
636
|
+
{ name: 'Quit', value: 'quit' },
|
|
637
|
+
];
|
|
638
|
+
const choice = await promptWithEscape(select({
|
|
565
639
|
message: 'SkillSync',
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
{ name: 'Import Hermes skills', value: 'import-hermes' },
|
|
572
|
-
{ name: 'Scan local targets', value: 'scan' },
|
|
573
|
-
{ name: 'Sync now', value: 'sync' },
|
|
574
|
-
{ name: 'Quit', value: 'quit' },
|
|
575
|
-
],
|
|
576
|
-
});
|
|
577
|
-
if (choice === 'quit') return;
|
|
640
|
+
loop: false,
|
|
641
|
+
pageSize: promptPageSize(menuChoices.length, { min: 8, max: 12 }),
|
|
642
|
+
choices: menuChoices,
|
|
643
|
+
}));
|
|
644
|
+
if (!choice || choice === 'quit') return;
|
|
578
645
|
if (choice === 'skills') await skillsScreen(config);
|
|
646
|
+
if (choice === 'installed') await installedScreen(config);
|
|
579
647
|
if (choice === 'devices') await devicesScreen(config);
|
|
580
648
|
if (choice === 'targets') await targetsScreen(config);
|
|
581
649
|
if (choice === 'add') await addSkill([await input({ message: 'Skill folder path:' })]);
|
|
@@ -593,41 +661,121 @@ async function skillsScreen(config) {
|
|
|
593
661
|
console.log('\nNo skills in vault yet. Use Add/import first.\n');
|
|
594
662
|
return;
|
|
595
663
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
664
|
+
const installedNames = new Set(Object.keys(device.installed || {}));
|
|
665
|
+
const selected = await promptWithEscape(checkbox({
|
|
666
|
+
message: `Install skills on ${device.display_name}`,
|
|
667
|
+
loop: false,
|
|
668
|
+
pageSize: promptPageSize(names.length, { min: 10, max: 32, reservedRows: 5 }),
|
|
669
|
+
instructions: 'Space toggles skills. Enter applies changes. Esc goes back.',
|
|
670
|
+
choices: names.map((name) => {
|
|
671
|
+
const targets = device.installed[name] || [];
|
|
672
|
+
const targetLabel = targets.length ? ` [${targets.join(', ')}]` : '';
|
|
673
|
+
return {
|
|
674
|
+
name: `${name}${targetLabel}`,
|
|
675
|
+
short: name,
|
|
676
|
+
value: name,
|
|
677
|
+
checked: installedNames.has(name),
|
|
678
|
+
};
|
|
679
|
+
}),
|
|
680
|
+
}));
|
|
681
|
+
if (!selected) return;
|
|
682
|
+
|
|
683
|
+
const selectedNames = new Set(selected);
|
|
684
|
+
const toInstall = names.filter((name) => selectedNames.has(name) && !installedNames.has(name));
|
|
685
|
+
const toUninstall = names.filter((name) => !selectedNames.has(name) && installedNames.has(name));
|
|
686
|
+
if (!toInstall.length && !toUninstall.length) {
|
|
687
|
+
console.log('\nNo install changes.\n');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let targets = [];
|
|
692
|
+
if (toInstall.length) {
|
|
693
|
+
targets = await chooseTargets(config);
|
|
694
|
+
if (!targets.length) return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for (const skillName of toInstall) {
|
|
698
|
+
await installSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName, targets });
|
|
699
|
+
}
|
|
700
|
+
for (const skillName of toUninstall) {
|
|
701
|
+
await uninstallSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName });
|
|
702
|
+
}
|
|
703
|
+
await applyLinks({ vaultPath: config.repoPath, deviceId: config.deviceId });
|
|
704
|
+
await syncVault({ vaultPath: config.repoPath, deviceId: config.deviceId, pull: false });
|
|
705
|
+
|
|
706
|
+
const installedText = toInstall.length ? `Installed ${toInstall.join(', ')} to ${targets.join(', ')}.` : '';
|
|
707
|
+
const removedText = toUninstall.length ? `Removed ${toUninstall.join(', ')} from this device.` : '';
|
|
708
|
+
console.log(`\n${[installedText, removedText].filter(Boolean).join(' ')}\n`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function installedScreen(config) {
|
|
712
|
+
await refreshChangedRegistryEntries(config.repoPath);
|
|
713
|
+
const registry = await loadRegistry(config.repoPath);
|
|
714
|
+
const device = await loadDevice(config.repoPath, config.deviceId);
|
|
715
|
+
const installed = installedSkillEntries(device, registry);
|
|
716
|
+
if (!installed.length) {
|
|
717
|
+
console.log('\nNo SkillSync-managed skills installed on this device.\n');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const selected = await promptWithEscape(checkbox({
|
|
722
|
+
message: `Installed skills on ${device.display_name}`,
|
|
723
|
+
loop: false,
|
|
724
|
+
pageSize: promptPageSize(installed.length, { min: 8, max: 28, reservedRows: 5 }),
|
|
725
|
+
instructions: 'Space selects skills. Enter chooses an action. Esc goes back.',
|
|
726
|
+
choices: installed.map((skill) => ({
|
|
727
|
+
name: installedSkillLabel(skill),
|
|
728
|
+
short: skill.name,
|
|
729
|
+
value: skill.name,
|
|
730
|
+
checked: false,
|
|
731
|
+
})),
|
|
732
|
+
}), []);
|
|
733
|
+
if (!selected.length) return;
|
|
734
|
+
|
|
735
|
+
const action = await promptWithEscape(select({
|
|
736
|
+
message: `Manage ${selected.length} selected skill${selected.length === 1 ? '' : 's'}`,
|
|
737
|
+
loop: false,
|
|
738
|
+
pageSize: 3,
|
|
606
739
|
choices: [
|
|
607
|
-
{ name:
|
|
608
|
-
{ name: '
|
|
740
|
+
{ name: 'Update from vault', value: 'update', description: 'Pull the vault and reapply local installed links.' },
|
|
741
|
+
{ name: 'Uninstall from this device', value: 'uninstall', description: 'Remove selected skills from this device only.' },
|
|
609
742
|
{ name: 'Back', value: 'back' },
|
|
610
743
|
],
|
|
744
|
+
}));
|
|
745
|
+
if (!action || action === 'back') return;
|
|
746
|
+
|
|
747
|
+
if (action === 'update') {
|
|
748
|
+
await syncVault({ vaultPath: config.repoPath, deviceId: config.deviceId, pull: true });
|
|
749
|
+
console.log(`\nUpdated installed skills from the vault. Requested: ${selected.join(', ')}.\n`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const confirmed = await confirm({
|
|
754
|
+
message: `Uninstall ${selected.join(', ')} from this device?`,
|
|
755
|
+
default: false,
|
|
611
756
|
});
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
await install([skill, '--target', targets.join(',')]);
|
|
617
|
-
}
|
|
757
|
+
if (!confirmed) return;
|
|
758
|
+
|
|
759
|
+
for (const skillName of selected) {
|
|
760
|
+
await uninstallSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName });
|
|
618
761
|
}
|
|
619
|
-
|
|
762
|
+
await applyLinks({ vaultPath: config.repoPath, deviceId: config.deviceId });
|
|
763
|
+
await syncVault({ vaultPath: config.repoPath, deviceId: config.deviceId, pull: false });
|
|
764
|
+
console.log(`\nRemoved ${selected.join(', ')} from this device.\n`);
|
|
620
765
|
}
|
|
621
766
|
|
|
622
767
|
async function chooseTargets(config) {
|
|
623
768
|
const device = await loadDevice(config.repoPath, config.deviceId);
|
|
624
769
|
const targetNames = Object.keys(device.targets);
|
|
625
770
|
if (!targetNames.length) throw new Error('No targets configured. Add one from the Targets screen.');
|
|
626
|
-
return checkbox({
|
|
771
|
+
return promptWithEscape(checkbox({
|
|
627
772
|
message: 'Install into which targets?',
|
|
773
|
+
loop: false,
|
|
774
|
+
pageSize: promptPageSize(targetNames.length, { min: 6, max: 18 }),
|
|
628
775
|
choices: targetNames.map((name) => ({ name, value: name, checked: true })),
|
|
629
776
|
required: true,
|
|
630
|
-
|
|
777
|
+
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
778
|
+
}), []);
|
|
631
779
|
}
|
|
632
780
|
|
|
633
781
|
async function devicesScreen(config) {
|
|
@@ -651,8 +799,13 @@ async function targetsScreen(config) {
|
|
|
651
799
|
{ name: 'Add target', value: 'add' },
|
|
652
800
|
{ name: 'Back', value: 'back' },
|
|
653
801
|
]);
|
|
654
|
-
const choice = await select({
|
|
655
|
-
|
|
802
|
+
const choice = await promptWithEscape(select({
|
|
803
|
+
message: 'Targets on this device',
|
|
804
|
+
choices,
|
|
805
|
+
loop: false,
|
|
806
|
+
pageSize: promptPageSize(choices.length, { min: 6, max: 18 }),
|
|
807
|
+
}));
|
|
808
|
+
if (!choice || choice === 'back') return;
|
|
656
809
|
if (choice === 'add') {
|
|
657
810
|
const name = await input({ message: 'Target name (codex, claude, hermes, custom):' });
|
|
658
811
|
const targetPath = await input({ message: 'Target skill directory path:' });
|
|
@@ -665,5 +818,5 @@ async function targetsScreen(config) {
|
|
|
665
818
|
}
|
|
666
819
|
|
|
667
820
|
function help() {
|
|
668
|
-
console.log(`SkillSync\n\nUsage:\n skillsync setup [--name skills] [--repo owner/repo|url]\n skillsync Open TUI\n skillsync list\n skillsync add <skill-folder-or-git-url> [--skill name] [--target target]\n skillsync import hermes\n skillsync install <skill> [--target codex,claude]\n skillsync uninstall <skill>\n skillsync delete <skill>\n skillsync target add <name> <path> [--mode symlink|copy] [--scan-path path]\n skillsync scan\n skillsync sync\n skillsync service install\n skillsync daemon\n`);
|
|
821
|
+
console.log(`SkillSync\n\nUsage:\n skillsync setup [--name skills] [--repo owner/repo|url]\n skillsync Open TUI\n skillsync list\n skillsync installed\n skillsync add <skill-folder-or-git-url> [--skill name] [--target target]\n skillsync import hermes\n skillsync install <skill> [--target codex,claude]\n skillsync uninstall <skill>\n skillsync delete <skill>\n skillsync target add <name> <path> [--mode symlink|copy] [--scan-path path]\n skillsync scan\n skillsync sync\n skillsync service install\n skillsync daemon\n`);
|
|
669
822
|
}
|