@supercorks/skills-installer 1.2.0 → 1.4.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/bin/install.js CHANGED
@@ -30,10 +30,12 @@ import {
30
30
  updateSparseCheckout,
31
31
  sparseCloneSubagents,
32
32
  listCheckedOutSubagents,
33
- updateSubagentsSparseCheckout
33
+ updateSubagentsSparseCheckout,
34
+ checkSkillsForUpdates,
35
+ checkSubagentsForUpdates
34
36
  } from '../lib/git.js';
35
37
 
36
- const VERSION = '1.2.0';
38
+ const VERSION = '1.3.0';
37
39
 
38
40
  // Common installation paths to check for existing installations
39
41
  const SKILL_PATHS = ['.github/skills/', '.claude/skills/'];
@@ -113,6 +115,21 @@ Examples:
113
115
  `);
114
116
  }
115
117
 
118
+ /**
119
+ * Check if a path is already in .gitignore
120
+ * @param {string} gitignorePath - Path to .gitignore file
121
+ * @param {string} pathToCheck - Path to check
122
+ * @returns {boolean}
123
+ */
124
+ function isInGitignore(gitignorePath, pathToCheck) {
125
+ if (!existsSync(gitignorePath)) {
126
+ return false;
127
+ }
128
+ const normalizedPath = pathToCheck.replace(/\/$/, '');
129
+ const content = readFileSync(gitignorePath, 'utf-8');
130
+ return content.includes(normalizedPath);
131
+ }
132
+
116
133
  /**
117
134
  * Add a path to .gitignore if not already present
118
135
  * @param {string} gitignorePath - Path to .gitignore file
@@ -211,14 +228,31 @@ async function runSkillsInstall() {
211
228
 
212
229
  const isManageMode = installedSkills.length > 0;
213
230
 
214
- // Ask about .gitignore (only for fresh installs)
231
+ // Check for updates if in manage mode
232
+ let skillsNeedingUpdate = new Set();
233
+ if (isManageMode) {
234
+ const updateSpinner = showSpinner('Checking for available updates...');
235
+ try {
236
+ skillsNeedingUpdate = await checkSkillsForUpdates(absoluteInstallPath, installedSkills);
237
+ if (skillsNeedingUpdate.size > 0) {
238
+ updateSpinner.stop(`✅ Found ${skillsNeedingUpdate.size} skill${skillsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
239
+ } else {
240
+ updateSpinner.stop('✅ All installed skills are up to date');
241
+ }
242
+ } catch {
243
+ updateSpinner.stop('⚠️ Could not check for updates');
244
+ }
245
+ }
246
+
247
+ // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
215
248
  let shouldGitignore = false;
216
- if (!isManageMode) {
249
+ const gitignorePath = resolve(process.cwd(), '.gitignore');
250
+ if (!isManageMode && !isInGitignore(gitignorePath, installPath)) {
217
251
  shouldGitignore = await promptGitignore(installPath);
218
252
  }
219
253
 
220
254
  // Select skills (pre-select installed skills in manage mode)
221
- const selectedSkills = await promptSkillSelection(skills, installedSkills);
255
+ const selectedSkills = await promptSkillSelection(skills, installedSkills, skillsNeedingUpdate);
222
256
 
223
257
  // Perform installation or update
224
258
  console.log('');
@@ -262,7 +296,6 @@ async function runSkillsInstall() {
262
296
 
263
297
  // Update .gitignore if requested
264
298
  if (shouldGitignore) {
265
- const gitignorePath = resolve(process.cwd(), '.gitignore');
266
299
  addToGitignore(gitignorePath, installPath);
267
300
  }
268
301
 
@@ -324,14 +357,31 @@ async function runSubagentsInstall() {
324
357
 
325
358
  const isManageMode = installedAgents.length > 0;
326
359
 
327
- // Ask about .gitignore (only for fresh installs)
360
+ // Check for updates if in manage mode
361
+ let subagentsNeedingUpdate = new Set();
362
+ if (isManageMode) {
363
+ const updateSpinner = showSpinner('Checking for available updates...');
364
+ try {
365
+ subagentsNeedingUpdate = await checkSubagentsForUpdates(absoluteInstallPath, installedAgents);
366
+ if (subagentsNeedingUpdate.size > 0) {
367
+ updateSpinner.stop(`✅ Found ${subagentsNeedingUpdate.size} subagent${subagentsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
368
+ } else {
369
+ updateSpinner.stop('✅ All installed subagents are up to date');
370
+ }
371
+ } catch {
372
+ updateSpinner.stop('⚠️ Could not check for updates');
373
+ }
374
+ }
375
+
376
+ // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
328
377
  let shouldGitignore = false;
329
- if (!isManageMode) {
378
+ const gitignorePath = resolve(process.cwd(), '.gitignore');
379
+ if (!isManageMode && !isInGitignore(gitignorePath, installPath)) {
330
380
  shouldGitignore = await promptGitignore(installPath);
331
381
  }
332
382
 
333
383
  // Select subagents (pre-select installed ones in manage mode)
334
- const selectedAgents = await promptSubagentSelection(subagents, installedAgents);
384
+ const selectedAgents = await promptSubagentSelection(subagents, installedAgents, subagentsNeedingUpdate);
335
385
 
336
386
  // Perform installation or update
337
387
  console.log('');
@@ -375,7 +425,6 @@ async function runSubagentsInstall() {
375
425
 
376
426
  // Update .gitignore if requested
377
427
  if (shouldGitignore) {
378
- const gitignorePath = resolve(process.cwd(), '.gitignore');
379
428
  addToGitignore(gitignorePath, installPath);
380
429
  }
381
430
 
package/lib/git.js CHANGED
@@ -214,6 +214,100 @@ export async function pullUpdates(repoPath) {
214
214
  return runGitCommand(['pull'], absolutePath);
215
215
  }
216
216
 
217
+ /**
218
+ * Check which skills have updates available (local differs from remote)
219
+ * @param {string} repoPath - Path to the sparse-checkout repo
220
+ * @param {string[]} skillFolders - Skill folders to check
221
+ * @returns {Promise<Set<string>>} Set of skill folder names that need updates
222
+ */
223
+ export async function checkSkillsForUpdates(repoPath, skillFolders) {
224
+ const absolutePath = resolve(repoPath);
225
+ const needsUpdate = new Set();
226
+
227
+ if (!existsSync(join(absolutePath, '.git'))) {
228
+ return needsUpdate;
229
+ }
230
+
231
+ try {
232
+ // Fetch latest from remote without modifying working tree
233
+ await runGitCommand(['fetch', 'origin'], absolutePath);
234
+
235
+ // Get current branch
236
+ const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], absolutePath);
237
+
238
+ // For each skill, check if there are differences between local and remote
239
+ for (const folder of skillFolders) {
240
+ try {
241
+ // Check if remote has changes for this folder
242
+ const diff = await runGitCommand([
243
+ 'diff',
244
+ `HEAD..origin/${branch}`,
245
+ '--stat',
246
+ '--',
247
+ folder
248
+ ], absolutePath);
249
+
250
+ if (diff.trim()) {
251
+ needsUpdate.add(folder);
252
+ }
253
+ } catch {
254
+ // If diff fails, skip this folder
255
+ }
256
+ }
257
+ } catch {
258
+ // If fetch fails, return empty set (can't determine updates)
259
+ }
260
+
261
+ return needsUpdate;
262
+ }
263
+
264
+ /**
265
+ * Check which subagents have updates available (local differs from remote)
266
+ * @param {string} repoPath - Path to the sparse-checkout repo
267
+ * @param {string[]} agentFilenames - Agent filenames to check
268
+ * @returns {Promise<Set<string>>} Set of agent filenames that need updates
269
+ */
270
+ export async function checkSubagentsForUpdates(repoPath, agentFilenames) {
271
+ const absolutePath = resolve(repoPath);
272
+ const needsUpdate = new Set();
273
+
274
+ if (!existsSync(join(absolutePath, '.git'))) {
275
+ return needsUpdate;
276
+ }
277
+
278
+ try {
279
+ // Fetch latest from remote without modifying working tree
280
+ await runGitCommand(['fetch', 'origin'], absolutePath);
281
+
282
+ // Get current branch
283
+ const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], absolutePath);
284
+
285
+ // For each agent, check if there are differences between local and remote
286
+ for (const filename of agentFilenames) {
287
+ try {
288
+ // Check if remote has changes for this file
289
+ const diff = await runGitCommand([
290
+ 'diff',
291
+ `HEAD..origin/${branch}`,
292
+ '--stat',
293
+ '--',
294
+ filename
295
+ ], absolutePath);
296
+
297
+ if (diff.trim()) {
298
+ needsUpdate.add(filename);
299
+ }
300
+ } catch {
301
+ // If diff fails, skip this file
302
+ }
303
+ }
304
+ } catch {
305
+ // If fetch fails, return empty set (can't determine updates)
306
+ }
307
+
308
+ return needsUpdate;
309
+ }
310
+
217
311
  // ==================== SUBAGENTS FUNCTIONS ====================
218
312
 
219
313
  /**
package/lib/prompts.js CHANGED
@@ -43,9 +43,9 @@ export async function promptInstallType() {
43
43
  name: 'installType',
44
44
  message: 'What would you like to install?',
45
45
  choices: [
46
+ { name: 'Skills and Agents', value: 'both' },
46
47
  { name: 'Skills only', value: 'skills' },
47
- { name: 'Subagents only', value: 'subagents' },
48
- { name: 'Both skills and subagents', value: 'both' }
48
+ { name: 'Agents only', value: 'subagents' }
49
49
  ]
50
50
  }
51
51
  ]);
@@ -202,13 +202,15 @@ export async function promptGitignore(installPath) {
202
202
  * Prompt user to select skills to install with expand/collapse support
203
203
  * @param {Array<{name: string, description: string, folder: string}>} skills - Available skills
204
204
  * @param {string[]} installedSkills - Already installed skill folder names (will be pre-selected)
205
+ * @param {Set<string>} skillsNeedingUpdate - Skill folder names that have updates available
205
206
  * @returns {Promise<string[]>} Selected skill folder names
206
207
  */
207
- export async function promptSkillSelection(skills, installedSkills = []) {
208
+ export async function promptSkillSelection(skills, installedSkills = [], skillsNeedingUpdate = new Set()) {
208
209
  return promptItemSelection(
209
210
  skills.map(s => ({ id: s.folder, name: s.name, description: s.description })),
210
211
  installedSkills,
211
- '📦 Available Skills'
212
+ '📦 Available Skills',
213
+ skillsNeedingUpdate
212
214
  );
213
215
  }
214
216
 
@@ -216,13 +218,15 @@ export async function promptSkillSelection(skills, installedSkills = []) {
216
218
  * Prompt user to select subagents to install with expand/collapse support
217
219
  * @param {Array<{name: string, description: string, filename: string}>} subagents - Available subagents
218
220
  * @param {string[]} installedSubagents - Already installed subagent filenames (will be pre-selected)
221
+ * @param {Set<string>} subagentsNeedingUpdate - Subagent filenames that have updates available
219
222
  * @returns {Promise<string[]>} Selected subagent filenames
220
223
  */
221
- export async function promptSubagentSelection(subagents, installedSubagents = []) {
224
+ export async function promptSubagentSelection(subagents, installedSubagents = [], subagentsNeedingUpdate = new Set()) {
222
225
  return promptItemSelection(
223
226
  subagents.map(s => ({ id: s.filename, name: s.name, description: s.description })),
224
227
  installedSubagents,
225
- '🤖 Available Subagents'
228
+ '🤖 Available Subagents',
229
+ subagentsNeedingUpdate
226
230
  );
227
231
  }
228
232
 
@@ -231,9 +235,10 @@ export async function promptSubagentSelection(subagents, installedSubagents = []
231
235
  * @param {Array<{id: string, name: string, description: string}>} items - Available items
232
236
  * @param {string[]} installedItems - Already installed item IDs (will be pre-selected)
233
237
  * @param {string} title - Title to display
238
+ * @param {Set<string>} itemsNeedingUpdate - Item IDs that have updates available
234
239
  * @returns {Promise<string[]>} Selected item IDs
235
240
  */
236
- function promptItemSelection(items, installedItems = [], title = '📦 Available Items') {
241
+ function promptItemSelection(items, installedItems = [], title = '📦 Available Items', itemsNeedingUpdate = new Set()) {
237
242
  return new Promise((resolve, reject) => {
238
243
  const rl = readline.createInterface({
239
244
  input: process.stdin,
@@ -247,7 +252,10 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
247
252
  readline.emitKeypressEvents(process.stdin, rl);
248
253
 
249
254
  let cursor = 0;
250
- const selected = new Set(installedItems);
255
+ // If nothing is installed, select all by default; otherwise pre-select installed items
256
+ const selected = installedItems.length > 0
257
+ ? new Set(installedItems)
258
+ : new Set(items.map(item => item.id));
251
259
  const expanded = new Set();
252
260
 
253
261
  const render = () => {
@@ -263,10 +271,12 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
263
271
  const isSelected = selected.has(item.id);
264
272
  const isCursor = i === cursor;
265
273
  const isExpanded = expanded.has(item.id);
274
+ const needsUpdate = itemsNeedingUpdate.has(item.id);
266
275
 
267
276
  const checkbox = isSelected ? '◉' : '○';
268
277
  const pointer = isCursor ? '❯' : ' ';
269
278
  const expandIcon = isExpanded ? '▼' : '▶';
279
+ const updateFlag = needsUpdate ? ' \x1B[33m(update)\x1B[0m' : '';
270
280
 
271
281
  // Highlight current line
272
282
  const highlight = isCursor ? '\x1B[36m' : ''; // Cyan for selected
@@ -275,7 +285,7 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
275
285
  const shortDesc = getFirstSentence(item.description);
276
286
 
277
287
  if (isExpanded) {
278
- console.log(`${highlight}${pointer} ${checkbox} ${item.name}${reset}`);
288
+ console.log(`${highlight}${pointer} ${checkbox} ${item.name}${reset}${updateFlag}`);
279
289
  // Show full description indented
280
290
  const fullDesc = item.description || 'No description available';
281
291
  const lines = fullDesc.match(/.{1,55}/g) || [fullDesc];
@@ -283,12 +293,14 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
283
293
  console.log(` ${highlight}${line}${reset}`);
284
294
  });
285
295
  } else {
286
- console.log(`${highlight}${pointer} ${checkbox} ${item.name} ${expandIcon} ${shortDesc}${reset}`);
296
+ console.log(`${highlight}${pointer} ${checkbox} ${item.name}${reset}${updateFlag} ${highlight}${expandIcon} ${shortDesc}${reset}`);
287
297
  }
288
298
  });
289
299
 
290
300
  const selectedCount = selected.size;
291
- console.log(`\n${selectedCount} item${selectedCount !== 1 ? 's' : ''} selected`);
301
+ const updateCount = Array.from(selected).filter(id => itemsNeedingUpdate.has(id)).length;
302
+ const updateNote = updateCount > 0 ? ` (${updateCount} to update)` : '';
303
+ console.log(`\n${selectedCount} item${selectedCount !== 1 ? 's' : ''} selected${updateNote}`);
292
304
  };
293
305
 
294
306
  const cleanup = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {