@supercorks/skills-installer 1.10.0 → 1.11.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/lib/git.js +122 -78
- package/package.json +1 -1
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}
|
|
@@ -52,6 +59,44 @@ function runGitCommand(args, cwd) {
|
|
|
52
59
|
});
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
async function getDefaultRemoteBranch(cwd) {
|
|
63
|
+
try {
|
|
64
|
+
const output = await runGitCommand(['ls-remote', '--symref', 'origin', 'HEAD'], cwd);
|
|
65
|
+
const match = output.match(/ref:\s+refs\/heads\/([^\s]+)\s+HEAD/);
|
|
66
|
+
return match?.[1] || 'main';
|
|
67
|
+
} catch {
|
|
68
|
+
return 'main';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function configureSparseCheckout(cwd, patterns) {
|
|
73
|
+
await runGitCommand(['sparse-checkout', 'init', '--no-cone'], cwd);
|
|
74
|
+
const sparseCheckoutPath = join(cwd, '.git', 'info', 'sparse-checkout');
|
|
75
|
+
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function initializeRepoInExistingDirectory(cwd, repoUrl, patterns, onProgress) {
|
|
79
|
+
onProgress('Initializing git repository in existing directory...');
|
|
80
|
+
await runGitCommand(['init'], cwd);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await runGitCommand(['remote', 'set-url', 'origin', repoUrl], cwd);
|
|
84
|
+
} catch {
|
|
85
|
+
await runGitCommand(['remote', 'add', 'origin', repoUrl], cwd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onProgress('Fetching repository...');
|
|
89
|
+
await runGitCommand(['fetch', 'origin'], cwd);
|
|
90
|
+
|
|
91
|
+
onProgress('Configuring sparse-checkout...');
|
|
92
|
+
await configureSparseCheckout(cwd, patterns);
|
|
93
|
+
|
|
94
|
+
const defaultBranch = await getDefaultRemoteBranch(cwd);
|
|
95
|
+
|
|
96
|
+
onProgress('Checking out files...');
|
|
97
|
+
await runGitCommand(['checkout', '-B', defaultBranch, `origin/${defaultBranch}`], cwd);
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/**
|
|
56
101
|
* Perform a sparse clone of the repository with only selected skills
|
|
57
102
|
* This method preserves full git history and allows push capability
|
|
@@ -63,17 +108,20 @@ function runGitCommand(args, cwd) {
|
|
|
63
108
|
*/
|
|
64
109
|
export async function sparseCloneSkills(targetPath, skillFolders, onProgress = () => {}) {
|
|
65
110
|
const repoUrl = getRepoUrl();
|
|
66
|
-
const absolutePath =
|
|
111
|
+
const absolutePath = resolvePath(targetPath);
|
|
112
|
+
const existedBefore = existsSync(absolutePath);
|
|
113
|
+
const patterns = skillFolders.map(folder => `/${folder}/`).join('\n');
|
|
114
|
+
let shouldAdoptExistingDirectory = false;
|
|
67
115
|
|
|
68
116
|
// Check if target already exists and has content
|
|
69
|
-
if (
|
|
117
|
+
if (existedBefore) {
|
|
70
118
|
const gitDir = join(absolutePath, '.git');
|
|
71
119
|
if (existsSync(gitDir)) {
|
|
72
120
|
throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
|
|
73
121
|
}
|
|
74
122
|
const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
|
|
75
123
|
if (entries.length > 0) {
|
|
76
|
-
|
|
124
|
+
shouldAdoptExistingDirectory = true;
|
|
77
125
|
}
|
|
78
126
|
}
|
|
79
127
|
|
|
@@ -81,43 +129,39 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
|
|
|
81
129
|
mkdirSync(absolutePath, { recursive: true });
|
|
82
130
|
|
|
83
131
|
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);
|
|
132
|
+
if (shouldAdoptExistingDirectory) {
|
|
133
|
+
await initializeRepoInExistingDirectory(absolutePath, repoUrl, patterns, onProgress);
|
|
134
|
+
} else {
|
|
135
|
+
// Clone with blob filter for minimal download, no checkout yet
|
|
136
|
+
onProgress('Initializing sparse clone...');
|
|
137
|
+
await runGitCommand([
|
|
138
|
+
'clone',
|
|
139
|
+
'--filter=blob:none',
|
|
140
|
+
'--no-checkout',
|
|
141
|
+
'--sparse',
|
|
142
|
+
repoUrl,
|
|
143
|
+
'.'
|
|
144
|
+
], absolutePath);
|
|
145
|
+
|
|
146
|
+
// Use non-cone mode for precise control over what's checked out
|
|
147
|
+
// This prevents root-level files (README.md, etc.) from being included
|
|
148
|
+
onProgress('Configuring sparse-checkout...');
|
|
149
|
+
await configureSparseCheckout(absolutePath, patterns);
|
|
150
|
+
|
|
151
|
+
// Checkout the files
|
|
152
|
+
onProgress('Checking out files...');
|
|
153
|
+
await runGitCommand(['checkout'], absolutePath);
|
|
154
|
+
}
|
|
113
155
|
|
|
114
156
|
onProgress('Done!');
|
|
115
157
|
} catch (error) {
|
|
116
158
|
// Clean up on failure
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
if (!existedBefore) {
|
|
160
|
+
try {
|
|
161
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore cleanup errors
|
|
164
|
+
}
|
|
121
165
|
}
|
|
122
166
|
throw error;
|
|
123
167
|
}
|
|
@@ -132,7 +176,7 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
|
|
|
132
176
|
* @returns {Promise<void>}
|
|
133
177
|
*/
|
|
134
178
|
export async function updateSparseCheckout(repoPath, skillFolders, onProgress = () => {}) {
|
|
135
|
-
const absolutePath =
|
|
179
|
+
const absolutePath = resolvePath(repoPath);
|
|
136
180
|
|
|
137
181
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
138
182
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -166,7 +210,7 @@ export async function updateSparseCheckout(repoPath, skillFolders, onProgress =
|
|
|
166
210
|
* @returns {Promise<void>}
|
|
167
211
|
*/
|
|
168
212
|
export async function addSkillsToSparseCheckout(repoPath, skillFolders) {
|
|
169
|
-
const absolutePath =
|
|
213
|
+
const absolutePath = resolvePath(repoPath);
|
|
170
214
|
|
|
171
215
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
172
216
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -187,7 +231,7 @@ export async function addSkillsToSparseCheckout(repoPath, skillFolders) {
|
|
|
187
231
|
* @returns {Promise<string[]>} List of skill folder names
|
|
188
232
|
*/
|
|
189
233
|
export async function listCheckedOutSkills(repoPath) {
|
|
190
|
-
const absolutePath =
|
|
234
|
+
const absolutePath = resolvePath(repoPath);
|
|
191
235
|
|
|
192
236
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
193
237
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -214,7 +258,7 @@ export async function listCheckedOutSkills(repoPath) {
|
|
|
214
258
|
* @returns {Promise<string>}
|
|
215
259
|
*/
|
|
216
260
|
export async function pullUpdates(repoPath) {
|
|
217
|
-
const absolutePath =
|
|
261
|
+
const absolutePath = resolvePath(repoPath);
|
|
218
262
|
return runGitCommand(['pull'], absolutePath);
|
|
219
263
|
}
|
|
220
264
|
|
|
@@ -225,7 +269,7 @@ export async function pullUpdates(repoPath) {
|
|
|
225
269
|
* @returns {Promise<Set<string>>} Set of skill folder names that need updates
|
|
226
270
|
*/
|
|
227
271
|
export async function checkSkillsForUpdates(repoPath, skillFolders) {
|
|
228
|
-
const absolutePath =
|
|
272
|
+
const absolutePath = resolvePath(repoPath);
|
|
229
273
|
const needsUpdate = new Set();
|
|
230
274
|
|
|
231
275
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
@@ -272,7 +316,7 @@ export async function checkSkillsForUpdates(repoPath, skillFolders) {
|
|
|
272
316
|
* @returns {Promise<Set<string>>} Set of agent filenames that need updates
|
|
273
317
|
*/
|
|
274
318
|
export async function checkSubagentsForUpdates(repoPath, agentFilenames) {
|
|
275
|
-
const absolutePath =
|
|
319
|
+
const absolutePath = resolvePath(repoPath);
|
|
276
320
|
const needsUpdate = new Set();
|
|
277
321
|
|
|
278
322
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
@@ -324,17 +368,20 @@ export async function checkSubagentsForUpdates(repoPath, agentFilenames) {
|
|
|
324
368
|
*/
|
|
325
369
|
export async function sparseCloneSubagents(targetPath, agentFilenames, onProgress = () => {}) {
|
|
326
370
|
const repoUrl = getSubagentsRepoUrl();
|
|
327
|
-
const absolutePath =
|
|
371
|
+
const absolutePath = resolvePath(targetPath);
|
|
372
|
+
const existedBefore = existsSync(absolutePath);
|
|
373
|
+
const patterns = agentFilenames.map(filename => `/${filename}`).join('\n');
|
|
374
|
+
let shouldAdoptExistingDirectory = false;
|
|
328
375
|
|
|
329
376
|
// Check if target already exists and has content
|
|
330
|
-
if (
|
|
377
|
+
if (existedBefore) {
|
|
331
378
|
const gitDir = join(absolutePath, '.git');
|
|
332
379
|
if (existsSync(gitDir)) {
|
|
333
380
|
throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
|
|
334
381
|
}
|
|
335
382
|
const entries = readdirSync(absolutePath).filter(name => name !== '.DS_Store');
|
|
336
383
|
if (entries.length > 0) {
|
|
337
|
-
|
|
384
|
+
shouldAdoptExistingDirectory = true;
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
387
|
|
|
@@ -342,41 +389,38 @@ export async function sparseCloneSubagents(targetPath, agentFilenames, onProgres
|
|
|
342
389
|
mkdirSync(absolutePath, { recursive: true });
|
|
343
390
|
|
|
344
391
|
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);
|
|
392
|
+
if (shouldAdoptExistingDirectory) {
|
|
393
|
+
await initializeRepoInExistingDirectory(absolutePath, repoUrl, patterns, onProgress);
|
|
394
|
+
} else {
|
|
395
|
+
// Clone with blob filter for minimal download, no checkout yet
|
|
396
|
+
onProgress('Initializing sparse clone...');
|
|
397
|
+
await runGitCommand([
|
|
398
|
+
'clone',
|
|
399
|
+
'--filter=blob:none',
|
|
400
|
+
'--no-checkout',
|
|
401
|
+
'--sparse',
|
|
402
|
+
repoUrl,
|
|
403
|
+
'.'
|
|
404
|
+
], absolutePath);
|
|
405
|
+
|
|
406
|
+
// Use non-cone mode for precise control over what's checked out
|
|
407
|
+
onProgress('Configuring sparse-checkout...');
|
|
408
|
+
await configureSparseCheckout(absolutePath, patterns);
|
|
409
|
+
|
|
410
|
+
// Checkout the files
|
|
411
|
+
onProgress('Checking out files...');
|
|
412
|
+
await runGitCommand(['checkout'], absolutePath);
|
|
413
|
+
}
|
|
372
414
|
|
|
373
415
|
onProgress('Done!');
|
|
374
416
|
} catch (error) {
|
|
375
417
|
// Clean up on failure
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
418
|
+
if (!existedBefore) {
|
|
419
|
+
try {
|
|
420
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
421
|
+
} catch {
|
|
422
|
+
// Ignore cleanup errors
|
|
423
|
+
}
|
|
380
424
|
}
|
|
381
425
|
throw error;
|
|
382
426
|
}
|
|
@@ -390,7 +434,7 @@ export async function sparseCloneSubagents(targetPath, agentFilenames, onProgres
|
|
|
390
434
|
* @returns {Promise<void>}
|
|
391
435
|
*/
|
|
392
436
|
export async function updateSubagentsSparseCheckout(repoPath, agentFilenames, onProgress = () => {}) {
|
|
393
|
-
const absolutePath =
|
|
437
|
+
const absolutePath = resolvePath(repoPath);
|
|
394
438
|
|
|
395
439
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
396
440
|
throw new Error(`"${repoPath}" is not a git repository`);
|
|
@@ -423,7 +467,7 @@ export async function updateSubagentsSparseCheckout(repoPath, agentFilenames, on
|
|
|
423
467
|
* @returns {Promise<string[]>} List of agent filenames
|
|
424
468
|
*/
|
|
425
469
|
export async function listCheckedOutSubagents(repoPath) {
|
|
426
|
-
const absolutePath =
|
|
470
|
+
const absolutePath = resolvePath(repoPath);
|
|
427
471
|
|
|
428
472
|
if (!existsSync(join(absolutePath, '.git'))) {
|
|
429
473
|
throw new Error(`"${repoPath}" is not a git repository`);
|