@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 +59 -10
- package/lib/git.js +94 -0
- package/lib/prompts.js +23 -11
- package/package.json +1 -1
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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 = () => {
|