@supercorks/skills-installer 1.12.0 → 1.13.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 CHANGED
@@ -18,28 +18,26 @@ npx @supercorks/skills-installer install
18
18
 
19
19
  1. **Choose installation type** - Install skills, subagents, or both.
20
20
 
21
- 2. **Choose installation path(s)** - Select one or more locations where resources should be installed. The installer labels each option with harness, scope, and available count, for example `.github/skills/ (copilot | local | 24 skills)`.
21
+ 2. **Choose installation path(s)** - Select one or more locations where resources should be installed. Global locations are shown before local locations. The installer labels each option with harness, scope, and available count, for example `~/.agents/skills/ (copilot/codex | global | 24 skills)`.
22
22
 
23
23
  Skills:
24
- - `.github/skills/` (copilot | local)
25
- - `~/.copilot/skills/` (copilot | global)
26
- - `.agents/skills/` (codex | local)
27
- - `~/.agents/skills/` (codex | global)
28
- - `.claude/skills/` (claude | local)
24
+ - `~/.agents/skills/` (copilot/codex | global)
29
25
  - `~/.claude/skills/` (claude | global)
26
+ - `.agents/skills/` (copilot/codex | local)
27
+ - `.claude/skills/` (claude | local)
30
28
 
31
29
  Agents:
32
- - `.github/agents/` (copilot | local)
33
- - `~/.copilot/agents/` (copilot | global)
34
- - `.claude/agents/` (claude | local)
30
+ - `~/.agents/agents/` (copilot | global)
35
31
  - `~/.claude/agents/` (claude | global)
36
- - `.codex/agents/` (codex | local, installed as converted TOML agents)
37
32
  - `~/.codex/agents/` (codex | global, installed as converted TOML agents)
33
+ - `.agents/agents/` (copilot | local)
34
+ - `.claude/agents/` (claude | local)
35
+ - `.codex/agents/` (codex | local, installed as converted TOML agents)
38
36
  - Custom path of your choice
39
37
 
40
38
  3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
41
39
 
42
- 4. **Select skills/subagents** - Interactive checkbox to pick what to install:
40
+ 4. **Select skills/subagents** - Interactive checkbox to pick what to install. If multiple locations are selected, the installer asks once and applies the same selection to every selected location:
43
41
  - Use `↑`/`↓` to navigate
44
42
  - Use `SPACE` to toggle selection
45
43
  - Use `→` to expand and lazy-load descriptions
@@ -60,7 +58,7 @@ npx @supercorks/skills-installer install
60
58
  - **Minimal download** - Uses `git clone --filter=blob:none` for efficient cloning
61
59
  - **Push capable** - The sparse clone preserves the full git history, allowing you to commit and push changes
62
60
  - **Auto-discovery** - Fetches the latest skill list from the repository
63
- - **Global and local targets** - Offers documented project/user locations for Copilot, Codex, and Claude where the resource format is compatible
61
+ - **Global and local targets** - Offers documented project/user locations for Copilot, Codex, and Claude where the resource format is compatible, with shared generic `~/.agents/skills/` and `.agents/skills/` targets for Copilot/Codex skills
64
62
  - **Codex agent conversion** - Converts Markdown subagents into Codex TOML custom agents for `.codex/agents/` targets
65
63
  - **Recursive directory creation** - Custom paths are created automatically
66
64
 
@@ -74,7 +72,7 @@ npx @supercorks/skills-installer install
74
72
  Since the installation uses a sparse git checkout, you can pull updates:
75
73
 
76
74
  ```bash
77
- cd .github/skills # or wherever you installed
75
+ cd .agents/skills # or wherever you installed
78
76
  git pull
79
77
  ```
80
78
 
@@ -83,7 +81,7 @@ git pull
83
81
  You can add more skills to an existing installation:
84
82
 
85
83
  ```bash
86
- cd .github/skills
84
+ cd .agents/skills
87
85
  git sparse-checkout add new-skill-name
88
86
  ```
89
87
 
package/bin/install.js CHANGED
@@ -57,6 +57,14 @@ function isHomePath(path) {
57
57
  return path === '~' || path.startsWith('~/');
58
58
  }
59
59
 
60
+ function uniqueItems(items) {
61
+ return Array.from(new Set(items));
62
+ }
63
+
64
+ function unionSets(sets) {
65
+ return new Set(sets.flatMap(set => Array.from(set)));
66
+ }
67
+
60
68
  /**
61
69
  * Detect existing skill installations in common paths
62
70
  * @returns {Promise<Array<{path: string, skillCount: number, skills: string[]}>>}
@@ -242,22 +250,39 @@ async function runSkillsInstall() {
242
250
  // Ask where to install (showing existing installations if any)
243
251
  const installTargets = await promptInstallPath(existingInstalls, skills.length);
244
252
 
245
- for (let i = 0; i < installTargets.length; i++) {
246
- const target = installTargets[i];
253
+ const targetContexts = [];
254
+ for (const [index, target] of installTargets.entries()) {
247
255
  if (installTargets.length > 1) {
248
- console.log(`\n📍 Skills target ${i + 1}/${installTargets.length}: ${target.path}`);
256
+ console.log(`\n📍 Preparing skills target ${index + 1}/${installTargets.length}: ${target.path}`);
249
257
  }
250
- await runSkillsInstallForTarget(skills, existingInstalls, target);
258
+ targetContexts.push(await prepareSkillsInstallTarget(existingInstalls, target));
259
+ }
260
+
261
+ const installedSkills = uniqueItems(targetContexts.flatMap(context => context.installedSkills));
262
+ const skillsNeedingUpdate = unionSets(targetContexts.map(context => context.skillsNeedingUpdate));
263
+
264
+ const selectedSkills = await promptSkillSelection(
265
+ skills,
266
+ installedSkills,
267
+ skillsNeedingUpdate,
268
+ (skillFolder) => fetchSkillMetadata(skillFolder)
269
+ );
270
+
271
+ for (let i = 0; i < targetContexts.length; i++) {
272
+ if (targetContexts.length > 1) {
273
+ console.log(`\n📍 Skills target ${i + 1}/${targetContexts.length}: ${targetContexts[i].installPath}`);
274
+ }
275
+ await runSkillsInstallForTarget(skills, targetContexts[i], selectedSkills);
251
276
  }
252
277
  }
253
278
 
254
279
  /**
255
- * Install/update skills for a specific target path
256
- * @param {Array<{name: string, description: string, folder: string}>} skills
280
+ * Prepare a specific skills target for installation/update.
257
281
  * @param {Array<{path: string, skillCount: number, skills: string[]}>} existingInstalls
258
282
  * @param {{path: string, isExisting: boolean}} target
283
+ * @returns {Promise<object>}
259
284
  */
260
- async function runSkillsInstallForTarget(skills, existingInstalls, target) {
285
+ async function prepareSkillsInstallTarget(existingInstalls, target) {
261
286
  const { path: installPath, isExisting } = target;
262
287
  const absoluteInstallPath = resolveInstallPath(installPath);
263
288
  const gitDir = join(absoluteInstallPath, '.git');
@@ -311,13 +336,33 @@ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
311
336
  shouldGitignore = await promptGitignore(installPath);
312
337
  }
313
338
 
314
- // Select skills (pre-select installed skills in manage mode)
315
- const selectedSkills = await promptSkillSelection(
316
- skills,
339
+ return {
340
+ ...target,
341
+ installPath,
342
+ absoluteInstallPath,
317
343
  installedSkills,
318
- skillsNeedingUpdate,
319
- (skillFolder) => fetchSkillMetadata(skillFolder)
320
- );
344
+ isManageMode,
345
+ shouldGitignore,
346
+ gitignorePath,
347
+ skillsNeedingUpdate
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Install/update skills for a specific target path
353
+ * @param {Array<{name: string, description: string, folder: string}>} skills
354
+ * @param {object} targetContext
355
+ * @param {string[]} selectedSkills
356
+ */
357
+ async function runSkillsInstallForTarget(skills, targetContext, selectedSkills) {
358
+ const {
359
+ installPath,
360
+ absoluteInstallPath,
361
+ installedSkills,
362
+ isManageMode,
363
+ shouldGitignore,
364
+ gitignorePath
365
+ } = targetContext;
321
366
 
322
367
  // Perform installation or update
323
368
  console.log('');
@@ -402,22 +447,39 @@ async function runSubagentsInstall() {
402
447
  // Ask where to install (showing existing installations if any)
403
448
  const installTargets = await promptAgentInstallPath(existingInstalls, subagents.length);
404
449
 
405
- for (let i = 0; i < installTargets.length; i++) {
406
- const target = installTargets[i];
450
+ const targetContexts = [];
451
+ for (const [index, target] of installTargets.entries()) {
407
452
  if (installTargets.length > 1) {
408
- console.log(`\n📍 Subagents target ${i + 1}/${installTargets.length}: ${target.path}`);
453
+ console.log(`\n📍 Preparing subagents target ${index + 1}/${installTargets.length}: ${target.path}`);
454
+ }
455
+ targetContexts.push(await prepareSubagentsInstallTarget(existingInstalls, target));
456
+ }
457
+
458
+ const installedAgents = uniqueItems(targetContexts.flatMap(context => context.installedAgents));
459
+ const subagentsNeedingUpdate = unionSets(targetContexts.map(context => context.subagentsNeedingUpdate));
460
+
461
+ const selectedAgents = await promptSubagentSelection(
462
+ subagents,
463
+ installedAgents,
464
+ subagentsNeedingUpdate,
465
+ (filename) => fetchSubagentMetadata(filename)
466
+ );
467
+
468
+ for (let i = 0; i < targetContexts.length; i++) {
469
+ if (targetContexts.length > 1) {
470
+ console.log(`\n📍 Subagents target ${i + 1}/${targetContexts.length}: ${targetContexts[i].installPath}`);
409
471
  }
410
- await runSubagentsInstallForTarget(subagents, existingInstalls, target);
472
+ await runSubagentsInstallForTarget(subagents, targetContexts[i], selectedAgents);
411
473
  }
412
474
  }
413
475
 
414
476
  /**
415
- * Install/update subagents for a specific target path
416
- * @param {Array<{name: string, description: string, filename: string}>} subagents
477
+ * Prepare a specific subagent target for installation/update.
417
478
  * @param {Array<{path: string, agentCount: number, agents: string[]}>} existingInstalls
418
479
  * @param {{path: string, isExisting: boolean}} target
480
+ * @returns {Promise<object>}
419
481
  */
420
- async function runSubagentsInstallForTarget(subagents, existingInstalls, target) {
482
+ async function prepareSubagentsInstallTarget(existingInstalls, target) {
421
483
  const { path: installPath, isExisting } = target;
422
484
  const absoluteInstallPath = resolveInstallPath(installPath);
423
485
  const installMode = getAgentInstallMode(installPath);
@@ -483,13 +545,35 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
483
545
  shouldGitignore = await promptGitignore(installPath);
484
546
  }
485
547
 
486
- // Select subagents (pre-select installed ones in manage mode)
487
- const selectedAgents = await promptSubagentSelection(
488
- subagents,
548
+ return {
549
+ ...target,
550
+ installPath,
551
+ absoluteInstallPath,
552
+ installMode,
489
553
  installedAgents,
490
- subagentsNeedingUpdate,
491
- (filename) => fetchSubagentMetadata(filename)
492
- );
554
+ isManageMode,
555
+ shouldGitignore,
556
+ gitignorePath,
557
+ subagentsNeedingUpdate
558
+ };
559
+ }
560
+
561
+ /**
562
+ * Install/update subagents for a specific target path
563
+ * @param {Array<{name: string, description: string, filename: string}>} subagents
564
+ * @param {object} targetContext
565
+ * @param {string[]} selectedAgents
566
+ */
567
+ async function runSubagentsInstallForTarget(subagents, targetContext, selectedAgents) {
568
+ const {
569
+ installPath,
570
+ absoluteInstallPath,
571
+ installMode,
572
+ installedAgents,
573
+ isManageMode,
574
+ shouldGitignore,
575
+ gitignorePath
576
+ } = targetContext;
493
577
 
494
578
  // Perform installation or update
495
579
  console.log('');
@@ -3,15 +3,23 @@
3
3
  */
4
4
 
5
5
  export const SKILL_INSTALL_TARGETS = [
6
- { path: '.github/skills/', harness: 'copilot', scope: 'local' },
7
- { path: '~/.copilot/skills/', harness: 'copilot', scope: 'global' },
8
- { path: '.agents/skills/', harness: 'codex', scope: 'local' },
9
- { path: '~/.agents/skills/', harness: 'codex', scope: 'global' },
10
- { path: '.claude/skills/', harness: 'claude', scope: 'local' },
11
- { path: '~/.claude/skills/', harness: 'claude', scope: 'global' }
6
+ { path: '~/.agents/skills/', harness: 'copilot/codex', scope: 'global' },
7
+ { path: '~/.claude/skills/', harness: 'claude', scope: 'global' },
8
+ { path: '.agents/skills/', harness: 'copilot/codex', scope: 'local' },
9
+ { path: '.claude/skills/', harness: 'claude', scope: 'local' }
12
10
  ];
13
11
 
14
12
  export const LEGACY_SKILL_INSTALL_TARGETS = [
13
+ {
14
+ path: '.github/skills/',
15
+ harness: 'copilot',
16
+ scope: 'legacy local'
17
+ },
18
+ {
19
+ path: '~/.copilot/skills/',
20
+ harness: 'copilot',
21
+ scope: 'legacy global'
22
+ },
15
23
  {
16
24
  path: '~/.codex/skills/',
17
25
  harness: 'codex',
@@ -20,19 +28,24 @@ export const LEGACY_SKILL_INSTALL_TARGETS = [
20
28
  ];
21
29
 
22
30
  export const AGENT_INSTALL_TARGETS = [
23
- { path: '.github/agents/', harness: 'copilot', scope: 'local', installMode: 'sparse-git' },
24
- { path: '~/.copilot/agents/', harness: 'copilot', scope: 'global', installMode: 'sparse-git' },
25
- { path: '.claude/agents/', harness: 'claude', scope: 'local', installMode: 'sparse-git' },
31
+ { path: '~/.agents/agents/', harness: 'copilot', scope: 'global', installMode: 'sparse-git' },
26
32
  { path: '~/.claude/agents/', harness: 'claude', scope: 'global', installMode: 'sparse-git' },
27
- { path: '.codex/agents/', harness: 'codex', scope: 'local', installMode: 'codex-toml' },
28
- { path: '~/.codex/agents/', harness: 'codex', scope: 'global', installMode: 'codex-toml' }
33
+ { path: '~/.codex/agents/', harness: 'codex', scope: 'global', installMode: 'codex-toml' },
34
+ { path: '.agents/agents/', harness: 'copilot', scope: 'local', installMode: 'sparse-git' },
35
+ { path: '.claude/agents/', harness: 'claude', scope: 'local', installMode: 'sparse-git' },
36
+ { path: '.codex/agents/', harness: 'codex', scope: 'local', installMode: 'codex-toml' }
29
37
  ];
30
38
 
31
39
  export const LEGACY_AGENT_INSTALL_TARGETS = [
32
40
  {
33
- path: '.agents/agents/',
34
- harness: 'codex',
41
+ path: '.github/agents/',
42
+ harness: 'copilot',
35
43
  scope: 'legacy local'
44
+ },
45
+ {
46
+ path: '~/.copilot/agents/',
47
+ harness: 'copilot',
48
+ scope: 'legacy global'
36
49
  }
37
50
  ];
38
51
 
@@ -44,6 +57,19 @@ export function allAgentDetectionTargets() {
44
57
  return [...AGENT_INSTALL_TARGETS, ...LEGACY_AGENT_INSTALL_TARGETS];
45
58
  }
46
59
 
60
+ export function orderTargetsGlobalFirst(targets) {
61
+ return [...targets].sort((left, right) => {
62
+ const leftGlobal = left.scope.includes('global') ? 0 : 1;
63
+ const rightGlobal = right.scope.includes('global') ? 0 : 1;
64
+
65
+ if (leftGlobal !== rightGlobal) {
66
+ return leftGlobal - rightGlobal;
67
+ }
68
+
69
+ return targets.indexOf(left) - targets.indexOf(right);
70
+ });
71
+ }
72
+
47
73
  export function getAgentInstallMode(path) {
48
74
  const exactTarget = getTargetByPath(AGENT_INSTALL_TARGETS, path);
49
75
  if (exactTarget?.installMode) {
package/lib/prompts.js CHANGED
@@ -10,7 +10,8 @@ import {
10
10
  allAgentDetectionTargets,
11
11
  allSkillDetectionTargets,
12
12
  formatTargetLabel,
13
- getTargetByPath
13
+ getTargetByPath,
14
+ orderTargetsGlobalFirst
14
15
  } from './install-targets.js';
15
16
 
16
17
  const SKILL_PATH_CHOICES = {
@@ -68,13 +69,29 @@ export async function promptInstallType() {
68
69
  */
69
70
  export async function promptInstallPath(existingInstalls = [], availableSkillCount = 0) {
70
71
  const choices = [];
71
- const existingPaths = existingInstalls.map(i => i.path);
72
72
  const detectionTargets = allSkillDetectionTargets();
73
+ const existingByPath = new Map(existingInstalls.map(install => [install.path, install]));
74
+ const standardPaths = new Set(SKILL_INSTALL_TARGETS.map(target => target.path));
73
75
 
74
- // Add existing installations at the top
75
- if (existingInstalls.length > 0) {
76
- existingInstalls.forEach(install => {
77
- const target = getTargetByPath(detectionTargets, install.path);
76
+ orderTargetsGlobalFirst(SKILL_INSTALL_TARGETS).forEach(target => {
77
+ const install = existingByPath.get(target.path);
78
+ choices.push({
79
+ name: install
80
+ ? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
81
+ : formatTargetLabel(target, availableSkillCount, 'skill'),
82
+ value: target.path
83
+ });
84
+ });
85
+
86
+ const existingLegacyOrCustom = existingInstalls.filter(install => !standardPaths.has(install.path));
87
+ if (existingLegacyOrCustom.length > 0) {
88
+ choices.push(new inquirer.Separator('── Existing legacy/custom installations ──'));
89
+ orderTargetsGlobalFirst(existingLegacyOrCustom.map(install => getTargetByPath(detectionTargets, install.path) || {
90
+ path: install.path,
91
+ harness: 'custom',
92
+ scope: 'existing'
93
+ })).forEach(target => {
94
+ const install = existingByPath.get(target.path);
78
95
  choices.push({
79
96
  name: target
80
97
  ? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
@@ -82,19 +99,9 @@ export async function promptInstallPath(existingInstalls = [], availableSkillCou
82
99
  value: install.path
83
100
  });
84
101
  });
85
- choices.push(new inquirer.Separator('── New installation ──'));
86
102
  }
87
-
88
- // Standard path options
89
- SKILL_INSTALL_TARGETS.forEach(target => {
90
- if (!existingPaths.includes(target.path)) {
91
- choices.push({
92
- name: formatTargetLabel(target, availableSkillCount, 'skill'),
93
- value: target.path
94
- });
95
- }
96
- });
97
-
103
+
104
+ choices.push(new inquirer.Separator('── Other ──'));
98
105
  choices.push({ name: 'Custom path...', value: SKILL_PATH_CHOICES.CUSTOM });
99
106
 
100
107
  const { pathChoices } = await inquirer.prompt([
@@ -120,7 +127,7 @@ export async function promptInstallPath(existingInstalls = [], availableSkillCou
120
127
  pathChoices
121
128
  .filter(path => path !== SKILL_PATH_CHOICES.CUSTOM)
122
129
  .forEach(path => {
123
- selected.push({ path, isExisting: existingPaths.includes(path) });
130
+ selected.push({ path, isExisting: existingByPath.has(path) });
124
131
  });
125
132
 
126
133
  if (selectedSet.has(SKILL_PATH_CHOICES.CUSTOM)) {
@@ -153,13 +160,30 @@ export async function promptInstallPath(existingInstalls = [], availableSkillCou
153
160
  */
154
161
  export async function promptAgentInstallPath(existingInstalls = [], availableAgentCount = 0) {
155
162
  const choices = [];
156
- const existingPaths = existingInstalls.map(i => i.path);
157
163
  const detectionTargets = allAgentDetectionTargets();
164
+ const existingByPath = new Map(existingInstalls.map(install => [install.path, install]));
165
+ const standardPaths = new Set(AGENT_INSTALL_TARGETS.map(target => target.path));
158
166
 
159
- // Add existing installations at the top
160
- if (existingInstalls.length > 0) {
161
- existingInstalls.forEach(install => {
162
- const target = getTargetByPath(detectionTargets, install.path);
167
+ orderTargetsGlobalFirst(AGENT_INSTALL_TARGETS).forEach(target => {
168
+ const install = existingByPath.get(target.path);
169
+ choices.push({
170
+ name: install
171
+ ? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
172
+ : formatTargetLabel(target, availableAgentCount, 'agent'),
173
+ value: target.path,
174
+ disabled: target.disabledReason
175
+ });
176
+ });
177
+
178
+ const existingLegacyOrCustom = existingInstalls.filter(install => !standardPaths.has(install.path));
179
+ if (existingLegacyOrCustom.length > 0) {
180
+ choices.push(new inquirer.Separator('── Existing legacy/custom installations ──'));
181
+ orderTargetsGlobalFirst(existingLegacyOrCustom.map(install => getTargetByPath(detectionTargets, install.path) || {
182
+ path: install.path,
183
+ harness: 'custom',
184
+ scope: 'existing'
185
+ })).forEach(target => {
186
+ const install = existingByPath.get(target.path);
163
187
  choices.push({
164
188
  name: target
165
189
  ? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
@@ -167,20 +191,9 @@ export async function promptAgentInstallPath(existingInstalls = [], availableAge
167
191
  value: install.path
168
192
  });
169
193
  });
170
- choices.push(new inquirer.Separator('── New installation ──'));
171
194
  }
172
-
173
- // Standard path options
174
- AGENT_INSTALL_TARGETS.forEach(target => {
175
- if (!existingPaths.includes(target.path)) {
176
- choices.push({
177
- name: formatTargetLabel(target, availableAgentCount, 'agent'),
178
- value: target.path,
179
- disabled: target.disabledReason
180
- });
181
- }
182
- });
183
-
195
+
196
+ choices.push(new inquirer.Separator('── Other ──'));
184
197
  choices.push({ name: 'Custom path...', value: AGENT_PATH_CHOICES.CUSTOM });
185
198
 
186
199
  const { pathChoices } = await inquirer.prompt([
@@ -206,7 +219,7 @@ export async function promptAgentInstallPath(existingInstalls = [], availableAge
206
219
  pathChoices
207
220
  .filter(path => path !== AGENT_PATH_CHOICES.CUSTOM)
208
221
  .forEach(path => {
209
- selected.push({ path, isExisting: existingPaths.includes(path) });
222
+ selected.push({ path, isExisting: existingByPath.has(path) });
210
223
  });
211
224
 
212
225
  if (selectedSet.has(AGENT_PATH_CHOICES.CUSTOM)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {