@akshar5/skillsync 0.1.3 → 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 +20 -5
- package/package.json +2 -1
- package/src/cli.js +119 -17
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:
|
|
@@ -88,7 +88,7 @@ npm install -g @akshar5/skillsync
|
|
|
88
88
|
Connect to an existing vault:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
skillsync setup --repo
|
|
91
|
+
skillsync setup --repo OWNER/skills
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
Or create/select a vault repo interactively:
|
|
@@ -99,7 +99,7 @@ skillsync setup
|
|
|
99
99
|
|
|
100
100
|
If you run plain `skillsync setup` in an interactive terminal, it first asks which vault type to use:
|
|
101
101
|
|
|
102
|
-
- 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`.
|
|
103
103
|
- Choose `Create or use OWNER/<name>`, then enter a repo name like `skills`.
|
|
104
104
|
|
|
105
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.
|
|
@@ -107,7 +107,7 @@ If you run plain `skillsync setup` in an interactive terminal, it first asks whi
|
|
|
107
107
|
You can also run commands without a global install:
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
npx @akshar5/skillsync setup --repo
|
|
110
|
+
npx @akshar5/skillsync setup --repo OWNER/skills
|
|
111
111
|
npx @akshar5/skillsync add https://github.com/example-org/example-skill --skill example-skill
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -115,7 +115,7 @@ If an older SkillSync version failed with `git@github.com: Permission denied (pu
|
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
npm install -g @akshar5/skillsync@latest
|
|
118
|
-
skillsync setup --repo
|
|
118
|
+
skillsync setup --repo OWNER/skills
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
## Common workflows
|
|
@@ -134,6 +134,14 @@ skillsync
|
|
|
134
134
|
|
|
135
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
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
|
+
|
|
137
145
|
Check current vault/device state:
|
|
138
146
|
|
|
139
147
|
```bash
|
|
@@ -180,6 +188,8 @@ Sync the vault and reapply local links:
|
|
|
180
188
|
skillsync sync
|
|
181
189
|
```
|
|
182
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
|
+
|
|
183
193
|
Scan configured target folders for already-installed local skills:
|
|
184
194
|
|
|
185
195
|
```bash
|
|
@@ -195,6 +205,7 @@ skillsync setup --name skills
|
|
|
195
205
|
skillsync
|
|
196
206
|
skillsync status
|
|
197
207
|
skillsync list
|
|
208
|
+
skillsync installed
|
|
198
209
|
skillsync add <skill-folder-or-git-url> --skill <name>
|
|
199
210
|
skillsync add https://github.com/example-org/example-skill --skill example-skill
|
|
200
211
|
skillsync import hermes
|
|
@@ -214,6 +225,10 @@ skillsync daemon
|
|
|
214
225
|
- `skillsync uninstall <skill>` removes the skill from the current device only.
|
|
215
226
|
- `skillsync delete <skill>` removes the skill from the vault and all device manifests.
|
|
216
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
|
+
|
|
217
232
|
## Detected versus managed skills
|
|
218
233
|
|
|
219
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':
|
|
@@ -102,6 +104,12 @@ async function promptWithEscape(promptPromise, escapeValue = null) {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
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
|
+
|
|
105
113
|
async function configured() {
|
|
106
114
|
const config = await loadConfig();
|
|
107
115
|
if (!config.repoPath || !await exists(config.repoPath)) {
|
|
@@ -295,6 +303,37 @@ async function listSkills() {
|
|
|
295
303
|
}
|
|
296
304
|
}
|
|
297
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
|
+
|
|
298
337
|
async function addSkill(rest) {
|
|
299
338
|
const source = rest[0];
|
|
300
339
|
if (!source) throw new Error('Usage: skillsync add <skill-folder-or-git-url> [--skill name] [--target target]');
|
|
@@ -458,7 +497,7 @@ async function chooseInstallTargets(config, rest) {
|
|
|
458
497
|
return promptWithEscape(checkbox({
|
|
459
498
|
message: 'Choose local targets',
|
|
460
499
|
loop: false,
|
|
461
|
-
pageSize:
|
|
500
|
+
pageSize: promptPageSize(available.length, { min: 6, max: 18 }),
|
|
462
501
|
choices: available.map((target) => ({ name: target, value: target, checked: true })),
|
|
463
502
|
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
464
503
|
}), []);
|
|
@@ -585,23 +624,26 @@ async function runUi() {
|
|
|
585
624
|
|
|
586
625
|
while (true) {
|
|
587
626
|
await refreshChangedRegistryEntries(config.repoPath);
|
|
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
|
+
];
|
|
588
638
|
const choice = await promptWithEscape(select({
|
|
589
639
|
message: 'SkillSync',
|
|
590
640
|
loop: false,
|
|
591
|
-
pageSize: 8,
|
|
592
|
-
choices:
|
|
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.' },
|
|
600
|
-
{ name: 'Quit', value: 'quit' },
|
|
601
|
-
],
|
|
641
|
+
pageSize: promptPageSize(menuChoices.length, { min: 8, max: 12 }),
|
|
642
|
+
choices: menuChoices,
|
|
602
643
|
}));
|
|
603
644
|
if (!choice || choice === 'quit') return;
|
|
604
645
|
if (choice === 'skills') await skillsScreen(config);
|
|
646
|
+
if (choice === 'installed') await installedScreen(config);
|
|
605
647
|
if (choice === 'devices') await devicesScreen(config);
|
|
606
648
|
if (choice === 'targets') await targetsScreen(config);
|
|
607
649
|
if (choice === 'add') await addSkill([await input({ message: 'Skill folder path:' })]);
|
|
@@ -623,7 +665,7 @@ async function skillsScreen(config) {
|
|
|
623
665
|
const selected = await promptWithEscape(checkbox({
|
|
624
666
|
message: `Install skills on ${device.display_name}`,
|
|
625
667
|
loop: false,
|
|
626
|
-
pageSize:
|
|
668
|
+
pageSize: promptPageSize(names.length, { min: 10, max: 32, reservedRows: 5 }),
|
|
627
669
|
instructions: 'Space toggles skills. Enter applies changes. Esc goes back.',
|
|
628
670
|
choices: names.map((name) => {
|
|
629
671
|
const targets = device.installed[name] || [];
|
|
@@ -633,7 +675,6 @@ async function skillsScreen(config) {
|
|
|
633
675
|
short: name,
|
|
634
676
|
value: name,
|
|
635
677
|
checked: installedNames.has(name),
|
|
636
|
-
description: targets.length ? `Installed in ${targets.join(', ')}` : 'Not installed on this device',
|
|
637
678
|
};
|
|
638
679
|
}),
|
|
639
680
|
}));
|
|
@@ -667,6 +708,62 @@ async function skillsScreen(config) {
|
|
|
667
708
|
console.log(`\n${[installedText, removedText].filter(Boolean).join(' ')}\n`);
|
|
668
709
|
}
|
|
669
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,
|
|
739
|
+
choices: [
|
|
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.' },
|
|
742
|
+
{ name: 'Back', value: 'back' },
|
|
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,
|
|
756
|
+
});
|
|
757
|
+
if (!confirmed) return;
|
|
758
|
+
|
|
759
|
+
for (const skillName of selected) {
|
|
760
|
+
await uninstallSkill({ vaultPath: config.repoPath, deviceId: config.deviceId, skillName });
|
|
761
|
+
}
|
|
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`);
|
|
765
|
+
}
|
|
766
|
+
|
|
670
767
|
async function chooseTargets(config) {
|
|
671
768
|
const device = await loadDevice(config.repoPath, config.deviceId);
|
|
672
769
|
const targetNames = Object.keys(device.targets);
|
|
@@ -674,7 +771,7 @@ async function chooseTargets(config) {
|
|
|
674
771
|
return promptWithEscape(checkbox({
|
|
675
772
|
message: 'Install into which targets?',
|
|
676
773
|
loop: false,
|
|
677
|
-
pageSize:
|
|
774
|
+
pageSize: promptPageSize(targetNames.length, { min: 6, max: 18 }),
|
|
678
775
|
choices: targetNames.map((name) => ({ name, value: name, checked: true })),
|
|
679
776
|
required: true,
|
|
680
777
|
instructions: 'Space toggles targets. Enter confirms. Esc cancels.',
|
|
@@ -702,7 +799,12 @@ async function targetsScreen(config) {
|
|
|
702
799
|
{ name: 'Add target', value: 'add' },
|
|
703
800
|
{ name: 'Back', value: 'back' },
|
|
704
801
|
]);
|
|
705
|
-
const choice = await promptWithEscape(select({
|
|
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
|
+
}));
|
|
706
808
|
if (!choice || choice === 'back') return;
|
|
707
809
|
if (choice === 'add') {
|
|
708
810
|
const name = await input({ message: 'Target name (codex, claude, hermes, custom):' });
|
|
@@ -716,5 +818,5 @@ async function targetsScreen(config) {
|
|
|
716
818
|
}
|
|
717
819
|
|
|
718
820
|
function help() {
|
|
719
|
-
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`);
|
|
720
822
|
}
|