@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -5
  3. package/package.json +2 -1
  4. 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 AksharP5/skills
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 AksharP5/skills
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 `AksharP5/skills`.
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 AksharP5/skills
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 AksharP5/skills
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.1.3",
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: Math.min(12, available.length),
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: Math.min(14, Math.max(7, names.length)),
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: Math.min(12, targetNames.length),
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({ message: 'Targets on this device', choices, loop: false, pageSize: Math.min(10, choices.length) }));
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
  }