@supercorks/skills-installer 1.5.0 → 1.7.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
@@ -16,9 +16,9 @@ npx @supercorks/skills-installer install
16
16
 
17
17
  ## What it does
18
18
 
19
- 1. **Choose installation path** - Select where skills should be installed:
20
- - `.github/skills/` (GitHub Copilot default)
21
- - `.codex/skills/` (Codex)
19
+ 1. **Choose installation path(s)** - Select one or more locations where skills should be installed:
20
+ - `.github/skills/` (Copilot)
21
+ - `~/.codex/skills/` (Codex)
22
22
  - `.claude/skills/` (Claude)
23
23
  - Custom path of your choice
24
24
 
package/bin/install.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { existsSync, appendFileSync, readFileSync, writeFileSync } from 'fs';
11
11
  import { resolve, join } from 'path';
12
+ import { homedir } from 'os';
12
13
  import {
13
14
  promptInstallType,
14
15
  promptInstallPath,
@@ -40,8 +41,14 @@ const require = createRequire(import.meta.url);
40
41
  const { version: VERSION } = require('../package.json');
41
42
 
42
43
  // Common installation paths to check for existing installations
43
- const SKILL_PATHS = ['.github/skills/', '.codex/skills/', '.claude/skills/'];
44
- const AGENT_PATHS = ['.github/agents/', '.claude/agents/'];
44
+ const SKILL_PATHS = ['.github/skills/', '~/.codex/skills/', '.claude/skills/'];
45
+ const AGENT_PATHS = ['.github/agents/', '.agents/agents/', '.claude/agents/'];
46
+
47
+ function resolveInstallPath(path) {
48
+ if (path === '~') return homedir();
49
+ if (path.startsWith('~/')) return resolve(homedir(), path.slice(2));
50
+ return resolve(process.cwd(), path);
51
+ }
45
52
 
46
53
  /**
47
54
  * Detect existing skill installations in common paths
@@ -51,7 +58,7 @@ async function detectExistingSkillInstallations() {
51
58
  const installations = [];
52
59
 
53
60
  for (const path of SKILL_PATHS) {
54
- const absolutePath = resolve(process.cwd(), path);
61
+ const absolutePath = resolveInstallPath(path);
55
62
  const gitDir = join(absolutePath, '.git');
56
63
 
57
64
  if (existsSync(gitDir)) {
@@ -79,7 +86,7 @@ async function detectExistingAgentInstallations() {
79
86
  const installations = [];
80
87
 
81
88
  for (const path of AGENT_PATHS) {
82
- const absolutePath = resolve(process.cwd(), path);
89
+ const absolutePath = resolveInstallPath(path);
83
90
  const gitDir = join(absolutePath, '.git');
84
91
 
85
92
  if (existsSync(gitDir)) {
@@ -208,8 +215,26 @@ async function runSkillsInstall() {
208
215
  const existingInstalls = await detectExistingSkillInstallations();
209
216
 
210
217
  // Ask where to install (showing existing installations if any)
211
- const { path: installPath, isExisting } = await promptInstallPath(existingInstalls);
212
- const absoluteInstallPath = resolve(process.cwd(), installPath);
218
+ const installTargets = await promptInstallPath(existingInstalls);
219
+
220
+ for (let i = 0; i < installTargets.length; i++) {
221
+ const target = installTargets[i];
222
+ if (installTargets.length > 1) {
223
+ console.log(`\nšŸ“ Skills target ${i + 1}/${installTargets.length}: ${target.path}`);
224
+ }
225
+ await runSkillsInstallForTarget(skills, existingInstalls, target);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Install/update skills for a specific target path
231
+ * @param {Array<{name: string, description: string, folder: string}>} skills
232
+ * @param {Array<{path: string, skillCount: number, skills: string[]}>} existingInstalls
233
+ * @param {{path: string, isExisting: boolean}} target
234
+ */
235
+ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
236
+ const { path: installPath, isExisting } = target;
237
+ const absoluteInstallPath = resolveInstallPath(installPath);
213
238
 
214
239
  // Get currently installed skills if managing existing installation
215
240
  let installedSkills = [];
@@ -248,7 +273,7 @@ async function runSkillsInstall() {
248
273
 
249
274
  // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
250
275
  let shouldGitignore = false;
251
- const gitignorePath = resolve(process.cwd(), '.gitignore');
276
+ const gitignorePath = resolveInstallPath('.gitignore');
252
277
  if (!isManageMode && !isInGitignore(gitignorePath, installPath)) {
253
278
  shouldGitignore = await promptGitignore(installPath);
254
279
  }
@@ -337,8 +362,26 @@ async function runSubagentsInstall() {
337
362
  const existingInstalls = await detectExistingAgentInstallations();
338
363
 
339
364
  // Ask where to install (showing existing installations if any)
340
- const { path: installPath, isExisting } = await promptAgentInstallPath(existingInstalls);
341
- const absoluteInstallPath = resolve(process.cwd(), installPath);
365
+ const installTargets = await promptAgentInstallPath(existingInstalls);
366
+
367
+ for (let i = 0; i < installTargets.length; i++) {
368
+ const target = installTargets[i];
369
+ if (installTargets.length > 1) {
370
+ console.log(`\nšŸ“ Subagents target ${i + 1}/${installTargets.length}: ${target.path}`);
371
+ }
372
+ await runSubagentsInstallForTarget(subagents, existingInstalls, target);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Install/update subagents for a specific target path
378
+ * @param {Array<{name: string, description: string, filename: string}>} subagents
379
+ * @param {Array<{path: string, agentCount: number, agents: string[]}>} existingInstalls
380
+ * @param {{path: string, isExisting: boolean}} target
381
+ */
382
+ async function runSubagentsInstallForTarget(subagents, existingInstalls, target) {
383
+ const { path: installPath, isExisting } = target;
384
+ const absoluteInstallPath = resolveInstallPath(installPath);
342
385
 
343
386
  // Get currently installed subagents if managing existing installation
344
387
  let installedAgents = [];
@@ -377,7 +420,7 @@ async function runSubagentsInstall() {
377
420
 
378
421
  // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
379
422
  let shouldGitignore = false;
380
- const gitignorePath = resolve(process.cwd(), '.gitignore');
423
+ const gitignorePath = resolveInstallPath('.gitignore');
381
424
  if (!isManageMode && !isInGitignore(gitignorePath, installPath)) {
382
425
  shouldGitignore = await promptGitignore(installPath);
383
426
  }
package/lib/git.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { execSync, spawn } from 'child_process';
6
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, appendFileSync } from 'fs';
6
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from 'fs';
7
7
  import { join, resolve } from 'path';
8
8
  import { getRepoUrl } from './skills.js';
9
9
  import { getSubagentsRepoUrl } from './subagents.js';
@@ -71,6 +71,10 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
71
71
  if (existsSync(gitDir)) {
72
72
  throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
73
73
  }
74
+ const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
75
+ if (entries.length > 0) {
76
+ throw new Error(`Directory "${targetPath}" already exists and is not empty. Choose an empty directory or an existing managed installation.`);
77
+ }
74
78
  }
75
79
 
76
80
  // Create target directory
@@ -328,6 +332,10 @@ export async function sparseCloneSubagents(targetPath, agentFilenames, onProgres
328
332
  if (existsSync(gitDir)) {
329
333
  throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
330
334
  }
335
+ const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
336
+ if (entries.length > 0) {
337
+ throw new Error(`Directory "${targetPath}" already exists and is not empty. Choose an empty directory or an existing managed installation.`);
338
+ }
331
339
  }
332
340
 
333
341
  // Create target directory
package/lib/prompts.js CHANGED
@@ -7,13 +7,14 @@ import * as readline from 'readline';
7
7
 
8
8
  const SKILL_PATH_CHOICES = {
9
9
  GITHUB: '.github/skills/',
10
- CODEX: '.codex/skills/',
10
+ CODEX_HOME: '~/.codex/skills/',
11
11
  CLAUDE: '.claude/skills/',
12
12
  CUSTOM: '__custom__'
13
13
  };
14
14
 
15
15
  const AGENT_PATH_CHOICES = {
16
16
  GITHUB: '.github/agents/',
17
+ CODEX: '.agents/agents/',
17
18
  CLAUDE: '.claude/agents/',
18
19
  CUSTOM: '__custom__'
19
20
  };
@@ -58,48 +59,67 @@ export async function promptInstallType() {
58
59
  }
59
60
 
60
61
  /**
61
- * Prompt user to select installation path, showing existing installations
62
+ * Prompt user to select one or more installation paths, showing existing installations
62
63
  * @param {Array<{path: string, skillCount: number}>} existingInstalls - Detected existing installations
63
- * @returns {Promise<{path: string, isExisting: boolean}>} The selected path and whether it's existing
64
+ * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
64
65
  */
65
66
  export async function promptInstallPath(existingInstalls = []) {
66
67
  const choices = [];
68
+ const existingPaths = existingInstalls.map(i => i.path);
67
69
 
68
70
  // Add existing installations at the top
69
71
  if (existingInstalls.length > 0) {
70
72
  existingInstalls.forEach(install => {
71
73
  choices.push({
72
74
  name: `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
73
- value: { path: install.path, isExisting: true }
75
+ value: install.path
74
76
  });
75
77
  });
76
78
  choices.push(new inquirer.Separator('── New installation ──'));
77
79
  }
78
80
 
79
81
  // Standard path options
80
- const standardPaths = [SKILL_PATH_CHOICES.GITHUB, SKILL_PATH_CHOICES.CODEX, SKILL_PATH_CHOICES.CLAUDE];
81
- const existingPaths = existingInstalls.map(i => i.path);
82
+ const standardPaths = [
83
+ { path: SKILL_PATH_CHOICES.GITHUB, label: `${SKILL_PATH_CHOICES.GITHUB} (Copilot)` },
84
+ { path: SKILL_PATH_CHOICES.CODEX_HOME, label: `${SKILL_PATH_CHOICES.CODEX_HOME} (Codex)` },
85
+ { path: SKILL_PATH_CHOICES.CLAUDE, label: `${SKILL_PATH_CHOICES.CLAUDE} (Claude)` }
86
+ ];
82
87
 
83
- standardPaths.forEach(path => {
88
+ standardPaths.forEach(({ path, label }) => {
84
89
  if (!existingPaths.includes(path)) {
85
- choices.push({ name: path, value: { path, isExisting: false } });
90
+ choices.push({ name: label, value: path });
86
91
  }
87
92
  });
88
93
 
89
- choices.push({ name: 'Custom path...', value: { path: SKILL_PATH_CHOICES.CUSTOM, isExisting: false } });
94
+ choices.push({ name: 'Custom path...', value: SKILL_PATH_CHOICES.CUSTOM });
90
95
 
91
- const { pathChoice } = await inquirer.prompt([
96
+ const { pathChoices } = await inquirer.prompt([
92
97
  {
93
- type: 'list',
94
- name: 'pathChoice',
98
+ type: 'checkbox',
99
+ name: 'pathChoices',
95
100
  message: existingInstalls.length > 0
96
- ? 'Select an existing installation to manage, or choose a new location:'
97
- : 'Where would you like to install the skills?',
98
- choices
101
+ ? 'Select one or more installations to manage, or choose new locations:'
102
+ : 'Where would you like to install the skills? (Select one or more)',
103
+ choices,
104
+ validate: (input) => {
105
+ if (!input || input.length === 0) {
106
+ return 'Please select at least one installation path';
107
+ }
108
+ return true;
109
+ }
99
110
  }
100
111
  ]);
101
112
 
102
- if (pathChoice.path === SKILL_PATH_CHOICES.CUSTOM) {
113
+ const selected = [];
114
+ const selectedSet = new Set(pathChoices);
115
+
116
+ pathChoices
117
+ .filter(path => path !== SKILL_PATH_CHOICES.CUSTOM)
118
+ .forEach(path => {
119
+ selected.push({ path, isExisting: existingPaths.includes(path) });
120
+ });
121
+
122
+ if (selectedSet.has(SKILL_PATH_CHOICES.CUSTOM)) {
103
123
  const { customPath } = await inquirer.prompt([
104
124
  {
105
125
  type: 'input',
@@ -113,55 +133,76 @@ export async function promptInstallPath(existingInstalls = []) {
113
133
  }
114
134
  }
115
135
  ]);
116
- return { path: customPath.trim(), isExisting: false };
136
+ selected.push({ path: customPath.trim(), isExisting: false });
117
137
  }
118
138
 
119
- return pathChoice;
139
+ const deduped = new Map();
140
+ selected.forEach(entry => deduped.set(entry.path, entry));
141
+ return Array.from(deduped.values());
120
142
  }
121
143
 
122
144
  /**
123
- * Prompt user to select subagent installation path
145
+ * Prompt user to select one or more subagent installation paths
124
146
  * @param {Array<{path: string, agentCount: number}>} existingInstalls - Detected existing installations
125
- * @returns {Promise<{path: string, isExisting: boolean}>} The selected path and whether it's existing
147
+ * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
126
148
  */
127
149
  export async function promptAgentInstallPath(existingInstalls = []) {
128
150
  const choices = [];
151
+ const existingPaths = existingInstalls.map(i => i.path);
129
152
 
130
153
  // Add existing installations at the top
131
154
  if (existingInstalls.length > 0) {
132
155
  existingInstalls.forEach(install => {
133
156
  choices.push({
134
157
  name: `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
135
- value: { path: install.path, isExisting: true }
158
+ value: install.path
136
159
  });
137
160
  });
138
161
  choices.push(new inquirer.Separator('── New installation ──'));
139
162
  }
140
163
 
141
164
  // Standard path options
142
- const standardPaths = [AGENT_PATH_CHOICES.GITHUB, AGENT_PATH_CHOICES.CLAUDE];
143
- const existingPaths = existingInstalls.map(i => i.path);
165
+ const standardPaths = [
166
+ { path: AGENT_PATH_CHOICES.GITHUB, label: `${AGENT_PATH_CHOICES.GITHUB} (Copilot)` },
167
+ { path: AGENT_PATH_CHOICES.CODEX, label: `${AGENT_PATH_CHOICES.CODEX} (Codex)` },
168
+ { path: AGENT_PATH_CHOICES.CLAUDE, label: `${AGENT_PATH_CHOICES.CLAUDE} (Claude)` }
169
+ ];
144
170
 
145
- standardPaths.forEach(path => {
171
+ standardPaths.forEach(({ path, label }) => {
146
172
  if (!existingPaths.includes(path)) {
147
- choices.push({ name: path, value: { path, isExisting: false } });
173
+ choices.push({ name: label, value: path });
148
174
  }
149
175
  });
150
176
 
151
- choices.push({ name: 'Custom path...', value: { path: AGENT_PATH_CHOICES.CUSTOM, isExisting: false } });
177
+ choices.push({ name: 'Custom path...', value: AGENT_PATH_CHOICES.CUSTOM });
152
178
 
153
- const { pathChoice } = await inquirer.prompt([
179
+ const { pathChoices } = await inquirer.prompt([
154
180
  {
155
- type: 'list',
156
- name: 'pathChoice',
181
+ type: 'checkbox',
182
+ name: 'pathChoices',
157
183
  message: existingInstalls.length > 0
158
- ? 'Select an existing installation to manage, or choose a new location:'
159
- : 'Where would you like to install the subagents?',
160
- choices
184
+ ? 'Select one or more installations to manage, or choose new locations:'
185
+ : 'Where would you like to install the subagents? (Select one or more)',
186
+ choices,
187
+ validate: (input) => {
188
+ if (!input || input.length === 0) {
189
+ return 'Please select at least one installation path';
190
+ }
191
+ return true;
192
+ }
161
193
  }
162
194
  ]);
163
195
 
164
- if (pathChoice.path === AGENT_PATH_CHOICES.CUSTOM) {
196
+ const selected = [];
197
+ const selectedSet = new Set(pathChoices);
198
+
199
+ pathChoices
200
+ .filter(path => path !== AGENT_PATH_CHOICES.CUSTOM)
201
+ .forEach(path => {
202
+ selected.push({ path, isExisting: existingPaths.includes(path) });
203
+ });
204
+
205
+ if (selectedSet.has(AGENT_PATH_CHOICES.CUSTOM)) {
165
206
  const { customPath } = await inquirer.prompt([
166
207
  {
167
208
  type: 'input',
@@ -175,10 +216,12 @@ export async function promptAgentInstallPath(existingInstalls = []) {
175
216
  }
176
217
  }
177
218
  ]);
178
- return { path: customPath.trim(), isExisting: false };
219
+ selected.push({ path: customPath.trim(), isExisting: false });
179
220
  }
180
221
 
181
- return pathChoice;
222
+ const deduped = new Map();
223
+ selected.forEach(entry => deduped.set(entry.path, entry));
224
+ return Array.from(deduped.values());
182
225
  }
183
226
 
184
227
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {