@supercorks/skills-installer 1.10.0 → 1.11.1
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 +1 -1
- package/bin/install.js +15 -2
- package/lib/git.js +140 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ npx @supercorks/skills-installer install
|
|
|
27
27
|
- `.claude/agents/` (Claude)
|
|
28
28
|
- Custom path of your choice
|
|
29
29
|
|
|
30
|
-
3. **Gitignore option** -
|
|
30
|
+
3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
|
|
31
31
|
|
|
32
32
|
4. **Select skills/subagents** - Interactive checkbox to pick what to install:
|
|
33
33
|
- Use `↑`/`↓` to navigate
|
package/bin/install.js
CHANGED
|
@@ -27,6 +27,7 @@ import { fetchAvailableSubagents, fetchSubagentMetadata } from '../lib/subagents
|
|
|
27
27
|
import {
|
|
28
28
|
sparseCloneSkills,
|
|
29
29
|
isGitAvailable,
|
|
30
|
+
isInsideGitWorkTree,
|
|
30
31
|
listCheckedOutSkills,
|
|
31
32
|
updateSparseCheckout,
|
|
32
33
|
sparseCloneSubagents,
|
|
@@ -279,8 +280,14 @@ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
|
|
|
279
280
|
|
|
280
281
|
// Ask about .gitignore (only for fresh installs and if not already in .gitignore)
|
|
281
282
|
let shouldGitignore = false;
|
|
283
|
+
const isInGitWorkTree = isInsideGitWorkTree();
|
|
282
284
|
const gitignorePath = resolveInstallPath('.gitignore');
|
|
283
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
isInGitWorkTree &&
|
|
287
|
+
!isManageMode &&
|
|
288
|
+
!isHomePath(installPath) &&
|
|
289
|
+
!isInGitignore(gitignorePath, installPath)
|
|
290
|
+
) {
|
|
284
291
|
shouldGitignore = await promptGitignore(installPath);
|
|
285
292
|
}
|
|
286
293
|
|
|
@@ -433,8 +440,14 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
433
440
|
|
|
434
441
|
// Ask about .gitignore (only for fresh installs and if not already in .gitignore)
|
|
435
442
|
let shouldGitignore = false;
|
|
443
|
+
const isInGitWorkTree = isInsideGitWorkTree();
|
|
436
444
|
const gitignorePath = resolveInstallPath('.gitignore');
|
|
437
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
isInGitWorkTree &&
|
|
447
|
+
!isManageMode &&
|
|
448
|
+
!isHomePath(installPath) &&
|
|
449
|
+
!isInGitignore(gitignorePath, installPath)
|
|
450
|
+
) {
|
|
438
451
|
shouldGitignore = await promptGitignore(installPath);
|
|
439
452
|
}
|
|
440
453
|
|
package/lib/git.js
CHANGED
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
import { execSync, spawn } from 'child_process';
|
|
6
6
|
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, appendFileSync, readdirSync } from 'fs';
|
|
7
7
|
import { join, resolve } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
8
9
|
import { getRepoUrl } from './skills.js';
|
|
9
10
|
import { getSubagentsRepoUrl } from './subagents.js';
|
|
10
11
|
|
|
12
|
+
function resolvePath(path) {
|
|
13
|
+
if (path === '~') return homedir();
|
|
14
|
+
if (path.startsWith('~/')) return resolve(homedir(), path.slice(2));
|
|
15
|
+
return resolve(path);
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Check if git is available
|
|
13
20
|
* @returns {boolean}
|
|
@@ -21,6 +28,24 @@ export function isGitAvailable() {
|
|
|
21
28
|
}
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Check whether a directory is inside a git work tree
|
|
33
|
+
* @param {string} path - Directory to check
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export function isInsideGitWorkTree(path = process.cwd()) {
|
|
37
|
+
const absolutePath = resolvePath(path);
|
|
38
|
+
try {
|
|
39
|
+
const output = execSync('git rev-parse --is-inside-work-tree', {
|
|
40
|
+
cwd: absolutePath,
|
|
41
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
42
|
+
}).toString().trim();
|
|
43
|
+
return output === 'true';
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
/**
|
|
25
50
|
* Execute a git command and return the result
|
|
26
51
|
* @param {string[]} args - Git command arguments
|
|
@@ -52,6 +77,44 @@ function runGitCommand(args, cwd) {
|
|
|
52
77
|
});
|
|
53
78
|
}
|
|
54
79
|
|
|
80
|
+
async function getDefaultRemoteBranch(cwd) {
|
|
81
|
+
try {
|
|
82
|
+
const output = await runGitCommand(['ls-remote', '--symref', 'origin', 'HEAD'], cwd);
|
|
83
|
+
const match = output.match(/ref:\s+refs\/heads\/([^\s]+)\s+HEAD/);
|
|
84
|
+
return match?.[1] || 'main';
|
|
85
|
+
} catch {
|
|
86
|
+
return 'main';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function configureSparseCheckout(cwd, patterns) {
|
|
91
|
+
await runGitCommand(['sparse-checkout', 'init', '--no-cone'], cwd);
|
|
92
|
+
const sparseCheckoutPath = join(cwd, '.git', 'info', 'sparse-checkout');
|
|
93
|
+
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function initializeRepoInExistingDirectory(cwd, repoUrl, patterns, onProgress) {
|
|
97
|
+
onProgress('Initializing git repository in existing directory...');
|
|
98
|
+
await runGitCommand(['init'], cwd);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await runGitCommand(['remote', 'set-url', 'origin', repoUrl], cwd);
|
|
102
|
+
} catch {
|
|
103
|
+
await runGitCommand(['remote', 'add', 'origin', repoUrl], cwd);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onProgress('Fetching repository...');
|
|
107
|
+
await runGitCommand(['fetch', 'origin'], cwd);
|
|
108
|
+
|
|
109
|
+
onProgress('Configuring sparse-checkout...');
|
|
110
|
+
await configureSparseCheckout(cwd, patterns);
|
|
111
|
+
|
|
112
|
+
const defaultBranch = await getDefaultRemoteBranch(cwd);
|
|
113
|
+
|
|
114
|
+
onProgress('Checking out files...');
|
|
115
|
+
await runGitCommand(['checkout', '-B', defaultBranch, `origin/${defaultBranch}`], cwd);
|
|
116
|
+
}
|
|
117
|
+
|
|
55
118
|
/**
|
|
56
119
|
* Perform a sparse clone of the repository with only selected skills
|
|
57
120
|
* This method preserves full git history and allows push capability
|
|
@@ -63,17 +126,20 @@ function runGitCommand(args, cwd) {
|
|
|
63
126
|
*/
|
|
64
127
|
export async function sparseCloneSkills(targetPath, skillFolders, onProgress = () => {}) {
|
|
65
128
|
const repoUrl = getRepoUrl();
|
|
66
|
-
const absolutePath =
|
|
129
|
+
const absolutePath = resolvePath(targetPath);
|
|
130
|
+
const existedBefore = existsSync(absolutePath);
|
|
131
|
+
const patterns = skillFolders.map(folder => `/${folder}/`).join('\n');
|
|
132
|
+
let shouldAdoptExistingDirectory = false;
|
|
67
133
|
|
|
68
134
|
// Check if target already exists and has content
|
|
69
|
-
if (
|
|
135
|
+
if (existedBefore) {
|
|
70
136
|
const gitDir = join(absolutePath, '.git');
|
|
71
137
|
if (existsSync(gitDir)) {
|
|
72
138
|
throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
|
|
73
139
|
}
|
|
74
140
|
const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
|
|
75
141
|
if (entries.length > 0) {
|
|
76
|
-
|
|
142
|
+
shouldAdoptExistingDirectory = true;
|
|
77
143
|
}
|
|
78
144
|
}
|
|
79
145
|
|
|
@@ -81,43 +147,39 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
|
|
|
81
147
|
mkdirSync(absolutePath, { recursive: true });
|
|
82
148
|
|
|
83
149
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const patterns = skillFolders.map(folder => `/${folder}/`).join('\n');
|
|
108
|
-
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
109
|
-
|
|
110
|
-
// Checkout the files
|
|
111
|
-
onProgress('Checking out files...');
|
|
112
|
-
await runGitCommand(['checkout'], absolutePath);
|
|
150
|
+
if (shouldAdoptExistingDirectory) {
|
|
151
|
+
await initializeRepoInExistingDirectory(absolutePath, repoUrl, patterns, onProgress);
|
|
152
|
+
} else {
|
|
153
|
+
// Clone with blob filter for minimal download, no checkout yet
|
|
154
|
+
onProgress('Initializing sparse clone...');
|
|
155
|
+
await runGitCommand([
|
|
156
|
+
'clone',
|
|
157
|
+
'--filter=blob:none',
|
|
158
|
+
'--no-checkout',
|
|
159
|
+
'--sparse',
|
|
160
|
+
repoUrl,
|
|
161
|
+
'.'
|
|
162
|
+
], absolutePath);
|
|
163
|
+
|
|
164
|
+
// Use non-cone mode for precise control over what's checked out
|
|
165
|
+
// This prevents root-level files (README.md, etc.) from being included
|
|
166
|
+
onProgress('Configuring sparse-checkout...');
|
|
167
|
+
await configureSparseCheckout(absolutePath, patterns);
|
|
168
|
+
|
|
169
|
+
// Checkout the files
|
|
170
|
+
onProgress('Checking out files...');
|
|
171
|
+
await runGitCommand(['checkout'], absolutePath);
|
|
172
|
+
}
|
|
113
173
|
|
|
114
174
|
onProgress('Done!');
|
|
115
175
|
} catch (error) {
|
|
116
176
|
// Clean up on failure
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
177
|
+
if (!existedBefore) {
|
|
178
|
+
try {
|
|
179
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
180
|
+
} catch {
|
|
181
|
+
// Ignore cleanup errors
|
|
182
|
+
}
|
|
121
183
|
}
|
|
122
184
|
throw error;
|
|
123
185
|
}
|
|
@@ -132,7 +194,7 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
|
|
|
132
194
|
* @returns {Promise<void>}
|
|
133
195
|
*/
|
|
134
196
|
export async function updateSparseCheckout(repoPath, skillFolders, onProgress = () => {}) {
|
|
135
|
-
const absolutePath =
|
|
197
|
+
const absolutePath = resolvePath(repoPath);
|
|
136
198
|
|
|
137
199
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
138
200
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -166,7 +228,7 @@ export async function updateSparseCheckout(repoPath, skillFolders, onProgress =
|
|
|
166
228
|
* @returns {Promise<void>}
|
|
167
229
|
*/
|
|
168
230
|
export async function addSkillsToSparseCheckout(repoPath, skillFolders) {
|
|
169
|
-
const absolutePath =
|
|
231
|
+
const absolutePath = resolvePath(repoPath);
|
|
170
232
|
|
|
171
233
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
172
234
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -187,7 +249,7 @@ export async function addSkillsToSparseCheckout(repoPath, skillFolders) {
|
|
|
187
249
|
* @returns {Promise<string[]>} List of skill folder names
|
|
188
250
|
*/
|
|
189
251
|
export async function listCheckedOutSkills(repoPath) {
|
|
190
|
-
const absolutePath =
|
|
252
|
+
const absolutePath = resolvePath(repoPath);
|
|
191
253
|
|
|
192
254
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
193
255
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -214,7 +276,7 @@ export async function listCheckedOutSkills(repoPath) {
|
|
|
214
276
|
* @returns {Promise<string>}
|
|
215
277
|
*/
|
|
216
278
|
export async function pullUpdates(repoPath) {
|
|
217
|
-
const absolutePath =
|
|
279
|
+
const absolutePath = resolvePath(repoPath);
|
|
218
280
|
return runGitCommand(['pull'], absolutePath);
|
|
219
281
|
}
|
|
220
282
|
|
|
@@ -225,7 +287,7 @@ export async function pullUpdates(repoPath) {
|
|
|
225
287
|
* @returns {Promise<Set<string>>} Set of skill folder names that need updates
|
|
226
288
|
*/
|
|
227
289
|
export async function checkSkillsForUpdates(repoPath, skillFolders) {
|
|
228
|
-
const absolutePath =
|
|
290
|
+
const absolutePath = resolvePath(repoPath);
|
|
229
291
|
const needsUpdate = new Set();
|
|
230
292
|
|
|
231
293
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
@@ -272,7 +334,7 @@ export async function checkSkillsForUpdates(repoPath, skillFolders) {
|
|
|
272
334
|
* @returns {Promise<Set<string>>} Set of agent filenames that need updates
|
|
273
335
|
*/
|
|
274
336
|
export async function checkSubagentsForUpdates(repoPath, agentFilenames) {
|
|
275
|
-
const absolutePath =
|
|
337
|
+
const absolutePath = resolvePath(repoPath);
|
|
276
338
|
const needsUpdate = new Set();
|
|
277
339
|
|
|
278
340
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
@@ -324,17 +386,20 @@ export async function checkSubagentsForUpdates(repoPath, agentFilenames) {
|
|
|
324
386
|
*/
|
|
325
387
|
export async function sparseCloneSubagents(targetPath, agentFilenames, onProgress = () => {}) {
|
|
326
388
|
const repoUrl = getSubagentsRepoUrl();
|
|
327
|
-
const absolutePath =
|
|
389
|
+
const absolutePath = resolvePath(targetPath);
|
|
390
|
+
const existedBefore = existsSync(absolutePath);
|
|
391
|
+
const patterns = agentFilenames.map(filename => `/${filename}`).join('\n');
|
|
392
|
+
let shouldAdoptExistingDirectory = false;
|
|
328
393
|
|
|
329
394
|
// Check if target already exists and has content
|
|
330
|
-
if (
|
|
395
|
+
if (existedBefore) {
|
|
331
396
|
const gitDir = join(absolutePath, '.git');
|
|
332
397
|
if (existsSync(gitDir)) {
|
|
333
398
|
throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
|
|
334
399
|
}
|
|
335
400
|
const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
|
|
336
401
|
if (entries.length > 0) {
|
|
337
|
-
|
|
402
|
+
shouldAdoptExistingDirectory = true;
|
|
338
403
|
}
|
|
339
404
|
}
|
|
340
405
|
|
|
@@ -342,41 +407,38 @@ export async function sparseCloneSubagents(targetPath, agentFilenames, onProgres
|
|
|
342
407
|
mkdirSync(absolutePath, { recursive: true });
|
|
343
408
|
|
|
344
409
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
'
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
'
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
368
|
-
|
|
369
|
-
// Checkout the files
|
|
370
|
-
onProgress('Checking out files...');
|
|
371
|
-
await runGitCommand(['checkout'], absolutePath);
|
|
410
|
+
if (shouldAdoptExistingDirectory) {
|
|
411
|
+
await initializeRepoInExistingDirectory(absolutePath, repoUrl, patterns, onProgress);
|
|
412
|
+
} else {
|
|
413
|
+
// Clone with blob filter for minimal download, no checkout yet
|
|
414
|
+
onProgress('Initializing sparse clone...');
|
|
415
|
+
await runGitCommand([
|
|
416
|
+
'clone',
|
|
417
|
+
'--filter=blob:none',
|
|
418
|
+
'--no-checkout',
|
|
419
|
+
'--sparse',
|
|
420
|
+
repoUrl,
|
|
421
|
+
'.'
|
|
422
|
+
], absolutePath);
|
|
423
|
+
|
|
424
|
+
// Use non-cone mode for precise control over what's checked out
|
|
425
|
+
onProgress('Configuring sparse-checkout...');
|
|
426
|
+
await configureSparseCheckout(absolutePath, patterns);
|
|
427
|
+
|
|
428
|
+
// Checkout the files
|
|
429
|
+
onProgress('Checking out files...');
|
|
430
|
+
await runGitCommand(['checkout'], absolutePath);
|
|
431
|
+
}
|
|
372
432
|
|
|
373
433
|
onProgress('Done!');
|
|
374
434
|
} catch (error) {
|
|
375
435
|
// Clean up on failure
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
436
|
+
if (!existedBefore) {
|
|
437
|
+
try {
|
|
438
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
439
|
+
} catch {
|
|
440
|
+
// Ignore cleanup errors
|
|
441
|
+
}
|
|
380
442
|
}
|
|
381
443
|
throw error;
|
|
382
444
|
}
|
|
@@ -390,7 +452,7 @@ export async function sparseCloneSubagents(targetPath, agentFilenames, onProgres
|
|
|
390
452
|
* @returns {Promise<void>}
|
|
391
453
|
*/
|
|
392
454
|
export async function updateSubagentsSparseCheckout(repoPath, agentFilenames, onProgress = () => {}) {
|
|
393
|
-
const absolutePath =
|
|
455
|
+
const absolutePath = resolvePath(repoPath);
|
|
394
456
|
|
|
395
457
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
396
458
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -423,7 +485,7 @@ export async function updateSubagentsSparseCheckout(repoPath, agentFilenames, on
|
|
|
423
485
|
* @returns {Promise<string[]>} List of agent filenames
|
|
424
486
|
*/
|
|
425
487
|
export async function listCheckedOutSubagents(repoPath) {
|
|
426
|
-
const absolutePath =
|
|
488
|
+
const absolutePath = resolvePath(repoPath);
|
|
427
489
|
|
|
428
490
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
429
491
|
throw new Error(`"${repoPath}" is not a git repository`);
|