@skild/core 0.2.9 → 0.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/dist/index.d.ts +21 -1
- package/dist/index.js +218 -174
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -144,6 +144,20 @@ declare function normalizeAlias(input: unknown): string | null;
|
|
|
144
144
|
declare function isValidAlias(input: string): boolean;
|
|
145
145
|
declare function assertValidAlias(input: string): void;
|
|
146
146
|
|
|
147
|
+
declare function materializeSourceToDir(input: {
|
|
148
|
+
source: string;
|
|
149
|
+
targetDir: string;
|
|
150
|
+
materializedDir?: string | null;
|
|
151
|
+
}): Promise<{
|
|
152
|
+
sourceType: SourceType;
|
|
153
|
+
materializedFrom: string;
|
|
154
|
+
}>;
|
|
155
|
+
declare function materializeSourceToTemp(source: string): Promise<{
|
|
156
|
+
dir: string;
|
|
157
|
+
cleanup: () => void;
|
|
158
|
+
sourceType: SourceType;
|
|
159
|
+
}>;
|
|
160
|
+
|
|
147
161
|
declare const DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
148
162
|
interface RegistrySpecifier {
|
|
149
163
|
canonicalName: string;
|
|
@@ -183,6 +197,12 @@ declare function downloadAndExtractTarball(resolved: RegistryResolvedVersion, te
|
|
|
183
197
|
interface InstallInput {
|
|
184
198
|
source: string;
|
|
185
199
|
nameOverride?: string;
|
|
200
|
+
/**
|
|
201
|
+
* Optional local directory containing the already-fetched source content.
|
|
202
|
+
* When provided, Skild will copy from this directory but still record `source`
|
|
203
|
+
* and `sourceType` based on the original `source` string (useful for multi-skill installs).
|
|
204
|
+
*/
|
|
205
|
+
materializedDir?: string;
|
|
186
206
|
}
|
|
187
207
|
declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
|
|
188
208
|
declare function installRegistrySkill(input: {
|
|
@@ -215,4 +235,4 @@ declare function uninstallSkill(name: string, options?: InstallOptions & {
|
|
|
215
235
|
}): void;
|
|
216
236
|
declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
|
|
217
237
|
|
|
218
|
-
export { DEFAULT_REGISTRY_URL, type DependencySourceType, type GlobalConfig, type InstallOptions, type InstallRecord, type InstallScope, type InstalledDependency, type ListOptions, type Lockfile, PLATFORMS, type Platform, type RegistryAuth, SkildError, type SkillFrontmatter, type SkillValidationIssue, type SkillValidationResult, type UpdateOptions, assertValidAlias, canonicalNameToInstallDirName, clearRegistryAuth, downloadAndExtractTarball, fetchWithTimeout, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installRegistrySkill, installSkill, isValidAlias, listAllSkills, listSkills, loadOrCreateGlobalConfig, loadRegistryAuth, normalizeAlias, parseRegistrySpecifier, resolveRegistryAlias, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
|
238
|
+
export { DEFAULT_REGISTRY_URL, type DependencySourceType, type GlobalConfig, type InstallOptions, type InstallRecord, type InstallScope, type InstalledDependency, type ListOptions, type Lockfile, PLATFORMS, type Platform, type RegistryAuth, SkildError, type SkillFrontmatter, type SkillValidationIssue, type SkillValidationResult, type UpdateOptions, assertValidAlias, canonicalNameToInstallDirName, clearRegistryAuth, downloadAndExtractTarball, fetchWithTimeout, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installRegistrySkill, installSkill, isValidAlias, listAllSkills, listSkills, loadOrCreateGlobalConfig, loadRegistryAuth, materializeSourceToDir, materializeSourceToTemp, normalizeAlias, parseRegistrySpecifier, resolveRegistryAlias, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
package/dist/index.js
CHANGED
|
@@ -289,10 +289,199 @@ function assertValidAlias(input) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
// src/
|
|
292
|
+
// src/materialize.ts
|
|
293
|
+
import fs6 from "fs";
|
|
294
|
+
import os2 from "os";
|
|
295
|
+
import path7 from "path";
|
|
296
|
+
import degit from "degit";
|
|
297
|
+
|
|
298
|
+
// src/source.ts
|
|
299
|
+
import path6 from "path";
|
|
300
|
+
|
|
301
|
+
// src/fs.ts
|
|
293
302
|
import fs5 from "fs";
|
|
294
303
|
import path5 from "path";
|
|
295
304
|
import crypto from "crypto";
|
|
305
|
+
function pathExists(filePath) {
|
|
306
|
+
return fs5.existsSync(filePath);
|
|
307
|
+
}
|
|
308
|
+
function isDirectory(filePath) {
|
|
309
|
+
try {
|
|
310
|
+
return fs5.statSync(filePath).isDirectory();
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function isDirEmpty(dir) {
|
|
316
|
+
try {
|
|
317
|
+
const entries = fs5.readdirSync(dir);
|
|
318
|
+
return entries.length === 0;
|
|
319
|
+
} catch {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function copyDir(src, dest) {
|
|
324
|
+
fs5.cpSync(src, dest, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
function removeDir(dir) {
|
|
327
|
+
if (fs5.existsSync(dir)) fs5.rmSync(dir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
function sanitizeForPathSegment(value) {
|
|
330
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
331
|
+
}
|
|
332
|
+
function createTempDir(parentDir, prefix) {
|
|
333
|
+
ensureDir(parentDir);
|
|
334
|
+
const safePrefix = sanitizeForPathSegment(prefix || "tmp");
|
|
335
|
+
const template = path5.join(parentDir, `.skild-${safePrefix}-`);
|
|
336
|
+
return fs5.mkdtempSync(template);
|
|
337
|
+
}
|
|
338
|
+
function replaceDirAtomic(sourceDir, destDir) {
|
|
339
|
+
const backupDir = fs5.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
|
|
340
|
+
try {
|
|
341
|
+
if (backupDir) fs5.renameSync(destDir, backupDir);
|
|
342
|
+
fs5.renameSync(sourceDir, destDir);
|
|
343
|
+
if (backupDir) removeDir(backupDir);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
try {
|
|
346
|
+
if (!fs5.existsSync(destDir) && backupDir && fs5.existsSync(backupDir)) {
|
|
347
|
+
fs5.renameSync(backupDir, destDir);
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
if (fs5.existsSync(sourceDir)) removeDir(sourceDir);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function listFilesRecursive(rootDir) {
|
|
359
|
+
const results = [];
|
|
360
|
+
const stack = [rootDir];
|
|
361
|
+
while (stack.length) {
|
|
362
|
+
const current = stack.pop();
|
|
363
|
+
const entries = fs5.readdirSync(current, { withFileTypes: true });
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
if (entry.name === ".skild") continue;
|
|
366
|
+
if (entry.name === ".git") continue;
|
|
367
|
+
const full = path5.join(current, entry.name);
|
|
368
|
+
if (entry.isDirectory()) stack.push(full);
|
|
369
|
+
else if (entry.isFile()) results.push(full);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
results.sort();
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
375
|
+
function hashDirectoryContent(rootDir) {
|
|
376
|
+
const files = listFilesRecursive(rootDir);
|
|
377
|
+
const h = crypto.createHash("sha256");
|
|
378
|
+
for (const filePath of files) {
|
|
379
|
+
const rel = path5.relative(rootDir, filePath);
|
|
380
|
+
h.update(rel);
|
|
381
|
+
h.update("\0");
|
|
382
|
+
h.update(fs5.readFileSync(filePath));
|
|
383
|
+
h.update("\0");
|
|
384
|
+
}
|
|
385
|
+
return h.digest("hex");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/source.ts
|
|
389
|
+
function resolveLocalPath(source) {
|
|
390
|
+
const resolved = path6.resolve(source);
|
|
391
|
+
return pathExists(resolved) ? resolved : null;
|
|
392
|
+
}
|
|
393
|
+
function classifySource(source) {
|
|
394
|
+
const local = resolveLocalPath(source);
|
|
395
|
+
if (local) return "local";
|
|
396
|
+
if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
|
|
397
|
+
if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
|
|
398
|
+
throw new SkildError(
|
|
399
|
+
"INVALID_SOURCE",
|
|
400
|
+
`Unsupported source "${source}". Use a Git URL (e.g. https://github.com/owner/repo/tree/<branch>/<subdir>) or degit shorthand (e.g. owner/repo[/subdir][#ref]) or a local directory.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
function extractSkillName(source) {
|
|
404
|
+
const local = resolveLocalPath(source);
|
|
405
|
+
if (local) return path6.basename(local) || "unknown-skill";
|
|
406
|
+
const cleaned = source.replace(/[#?].*$/, "");
|
|
407
|
+
const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
|
|
408
|
+
if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
|
|
409
|
+
const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
|
|
410
|
+
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
411
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
412
|
+
if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
|
|
413
|
+
return cleaned || "unknown-skill";
|
|
414
|
+
}
|
|
415
|
+
function toDegitPath(url) {
|
|
416
|
+
const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
|
|
417
|
+
if (treeMatch) {
|
|
418
|
+
const [, owner, repo, branch, subpath] = treeMatch;
|
|
419
|
+
return `${owner}/${repo}/${subpath}#${branch}`;
|
|
420
|
+
}
|
|
421
|
+
const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
422
|
+
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
423
|
+
return url;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/materialize.ts
|
|
427
|
+
function ensureInstallableDir(sourcePath) {
|
|
428
|
+
if (!isDirectory(sourcePath)) {
|
|
429
|
+
throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function cloneRemote(degitSrc, targetPath) {
|
|
433
|
+
const emitter = degit(degitSrc, { force: true, verbose: false });
|
|
434
|
+
await emitter.clone(targetPath);
|
|
435
|
+
}
|
|
436
|
+
async function materializeSourceToDir(input) {
|
|
437
|
+
const sourceType = classifySource(input.source);
|
|
438
|
+
const targetDir = path7.resolve(input.targetDir);
|
|
439
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
440
|
+
const overridden = input.materializedDir?.trim() ? path7.resolve(input.materializedDir.trim()) : null;
|
|
441
|
+
if (overridden) {
|
|
442
|
+
ensureInstallableDir(overridden);
|
|
443
|
+
copyDir(overridden, targetDir);
|
|
444
|
+
return { sourceType, materializedFrom: overridden };
|
|
445
|
+
}
|
|
446
|
+
const localPath = resolveLocalPath(input.source);
|
|
447
|
+
if (localPath) {
|
|
448
|
+
ensureInstallableDir(localPath);
|
|
449
|
+
copyDir(localPath, targetDir);
|
|
450
|
+
return { sourceType: "local", materializedFrom: localPath };
|
|
451
|
+
}
|
|
452
|
+
const degitPath = toDegitPath(input.source);
|
|
453
|
+
await cloneRemote(degitPath, targetDir);
|
|
454
|
+
return { sourceType, materializedFrom: degitPath };
|
|
455
|
+
}
|
|
456
|
+
async function materializeSourceToTemp(source) {
|
|
457
|
+
const sourceType = classifySource(source);
|
|
458
|
+
const tempParent = path7.join(os2.tmpdir(), "skild-materialize");
|
|
459
|
+
const tempRoot = createTempDir(tempParent, extractSkillName(source));
|
|
460
|
+
const dir = path7.join(tempRoot, "staging");
|
|
461
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
462
|
+
try {
|
|
463
|
+
await materializeSourceToDir({ source, targetDir: dir });
|
|
464
|
+
if (isDirEmpty(dir)) {
|
|
465
|
+
throw new SkildError(
|
|
466
|
+
"EMPTY_INSTALL_DIR",
|
|
467
|
+
`Installed directory is empty for source: ${source}
|
|
468
|
+
Source likely does not point to a valid subdirectory.
|
|
469
|
+
Try: https://github.com/<owner>/<repo>/tree/<branch>/skills/<skill-name>
|
|
470
|
+
Example: https://github.com/anthropics/skills/tree/main/skills/pdf`,
|
|
471
|
+
{ source }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return { dir, sourceType, cleanup: () => removeDir(tempRoot) };
|
|
475
|
+
} catch (e) {
|
|
476
|
+
removeDir(tempRoot);
|
|
477
|
+
throw e;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/registry.ts
|
|
482
|
+
import fs7 from "fs";
|
|
483
|
+
import path8 from "path";
|
|
484
|
+
import crypto2 from "crypto";
|
|
296
485
|
import * as tar from "tar";
|
|
297
486
|
import semver from "semver";
|
|
298
487
|
var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
@@ -369,7 +558,7 @@ async function resolveRegistryVersion(registryUrl, spec) {
|
|
|
369
558
|
return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
|
|
370
559
|
}
|
|
371
560
|
function sha256Hex(buffer) {
|
|
372
|
-
const h =
|
|
561
|
+
const h = crypto2.createHash("sha256");
|
|
373
562
|
h.update(buffer);
|
|
374
563
|
return h.digest("hex");
|
|
375
564
|
}
|
|
@@ -422,9 +611,9 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
422
611
|
`Integrity mismatch for ${resolved.canonicalName}@${resolved.version}. Expected ${resolved.integrity}, got ${computed}.`
|
|
423
612
|
);
|
|
424
613
|
}
|
|
425
|
-
const tarballPath =
|
|
426
|
-
|
|
427
|
-
|
|
614
|
+
const tarballPath = path8.join(tempRoot, "skill.tgz");
|
|
615
|
+
fs7.mkdirSync(stagingDir, { recursive: true });
|
|
616
|
+
fs7.writeFileSync(tarballPath, buf);
|
|
428
617
|
try {
|
|
429
618
|
await tar.x({ file: tarballPath, cwd: stagingDir, gzip: true });
|
|
430
619
|
} catch (error) {
|
|
@@ -433,139 +622,8 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
433
622
|
}
|
|
434
623
|
|
|
435
624
|
// src/lifecycle.ts
|
|
436
|
-
import
|
|
437
|
-
import
|
|
438
|
-
import degit from "degit";
|
|
439
|
-
|
|
440
|
-
// src/source.ts
|
|
441
|
-
import path7 from "path";
|
|
442
|
-
|
|
443
|
-
// src/fs.ts
|
|
444
|
-
import fs6 from "fs";
|
|
445
|
-
import path6 from "path";
|
|
446
|
-
import crypto2 from "crypto";
|
|
447
|
-
function pathExists(filePath) {
|
|
448
|
-
return fs6.existsSync(filePath);
|
|
449
|
-
}
|
|
450
|
-
function isDirectory(filePath) {
|
|
451
|
-
try {
|
|
452
|
-
return fs6.statSync(filePath).isDirectory();
|
|
453
|
-
} catch {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
function isDirEmpty(dir) {
|
|
458
|
-
try {
|
|
459
|
-
const entries = fs6.readdirSync(dir);
|
|
460
|
-
return entries.length === 0;
|
|
461
|
-
} catch {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
function copyDir(src, dest) {
|
|
466
|
-
fs6.cpSync(src, dest, { recursive: true });
|
|
467
|
-
}
|
|
468
|
-
function removeDir(dir) {
|
|
469
|
-
if (fs6.existsSync(dir)) fs6.rmSync(dir, { recursive: true, force: true });
|
|
470
|
-
}
|
|
471
|
-
function sanitizeForPathSegment(value) {
|
|
472
|
-
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
473
|
-
}
|
|
474
|
-
function createTempDir(parentDir, prefix) {
|
|
475
|
-
ensureDir(parentDir);
|
|
476
|
-
const safePrefix = sanitizeForPathSegment(prefix || "tmp");
|
|
477
|
-
const template = path6.join(parentDir, `.skild-${safePrefix}-`);
|
|
478
|
-
return fs6.mkdtempSync(template);
|
|
479
|
-
}
|
|
480
|
-
function replaceDirAtomic(sourceDir, destDir) {
|
|
481
|
-
const backupDir = fs6.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
|
|
482
|
-
try {
|
|
483
|
-
if (backupDir) fs6.renameSync(destDir, backupDir);
|
|
484
|
-
fs6.renameSync(sourceDir, destDir);
|
|
485
|
-
if (backupDir) removeDir(backupDir);
|
|
486
|
-
} catch (error) {
|
|
487
|
-
try {
|
|
488
|
-
if (!fs6.existsSync(destDir) && backupDir && fs6.existsSync(backupDir)) {
|
|
489
|
-
fs6.renameSync(backupDir, destDir);
|
|
490
|
-
}
|
|
491
|
-
} catch {
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
if (fs6.existsSync(sourceDir)) removeDir(sourceDir);
|
|
495
|
-
} catch {
|
|
496
|
-
}
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function listFilesRecursive(rootDir) {
|
|
501
|
-
const results = [];
|
|
502
|
-
const stack = [rootDir];
|
|
503
|
-
while (stack.length) {
|
|
504
|
-
const current = stack.pop();
|
|
505
|
-
const entries = fs6.readdirSync(current, { withFileTypes: true });
|
|
506
|
-
for (const entry of entries) {
|
|
507
|
-
if (entry.name === ".skild") continue;
|
|
508
|
-
if (entry.name === ".git") continue;
|
|
509
|
-
const full = path6.join(current, entry.name);
|
|
510
|
-
if (entry.isDirectory()) stack.push(full);
|
|
511
|
-
else if (entry.isFile()) results.push(full);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
results.sort();
|
|
515
|
-
return results;
|
|
516
|
-
}
|
|
517
|
-
function hashDirectoryContent(rootDir) {
|
|
518
|
-
const files = listFilesRecursive(rootDir);
|
|
519
|
-
const h = crypto2.createHash("sha256");
|
|
520
|
-
for (const filePath of files) {
|
|
521
|
-
const rel = path6.relative(rootDir, filePath);
|
|
522
|
-
h.update(rel);
|
|
523
|
-
h.update("\0");
|
|
524
|
-
h.update(fs6.readFileSync(filePath));
|
|
525
|
-
h.update("\0");
|
|
526
|
-
}
|
|
527
|
-
return h.digest("hex");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/source.ts
|
|
531
|
-
function resolveLocalPath(source) {
|
|
532
|
-
const resolved = path7.resolve(source);
|
|
533
|
-
return pathExists(resolved) ? resolved : null;
|
|
534
|
-
}
|
|
535
|
-
function classifySource(source) {
|
|
536
|
-
const local = resolveLocalPath(source);
|
|
537
|
-
if (local) return "local";
|
|
538
|
-
if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
|
|
539
|
-
if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
|
|
540
|
-
throw new SkildError(
|
|
541
|
-
"INVALID_SOURCE",
|
|
542
|
-
`Unsupported source "${source}". Use a Git URL (e.g. https://github.com/owner/repo/tree/<branch>/<subdir>) or degit shorthand (e.g. owner/repo[/subdir][#ref]) or a local directory.`
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
function extractSkillName(source) {
|
|
546
|
-
const local = resolveLocalPath(source);
|
|
547
|
-
if (local) return path7.basename(local) || "unknown-skill";
|
|
548
|
-
const cleaned = source.replace(/[#?].*$/, "");
|
|
549
|
-
const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
|
|
550
|
-
if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
|
|
551
|
-
const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
|
|
552
|
-
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
553
|
-
const parts = cleaned.split("/").filter(Boolean);
|
|
554
|
-
if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
|
|
555
|
-
return cleaned || "unknown-skill";
|
|
556
|
-
}
|
|
557
|
-
function toDegitPath(url) {
|
|
558
|
-
const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
|
|
559
|
-
if (treeMatch) {
|
|
560
|
-
const [, owner, repo, branch, subpath] = treeMatch;
|
|
561
|
-
return `${owner}/${repo}/${subpath}#${branch}`;
|
|
562
|
-
}
|
|
563
|
-
const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
564
|
-
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
565
|
-
return url;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// src/lifecycle.ts
|
|
625
|
+
import path9 from "path";
|
|
626
|
+
import fs8 from "fs";
|
|
569
627
|
function normalizeDependencies(raw) {
|
|
570
628
|
if (raw === void 0 || raw === null) return [];
|
|
571
629
|
if (!Array.isArray(raw)) {
|
|
@@ -591,9 +649,9 @@ function isRelativeDependency(dep) {
|
|
|
591
649
|
return dep.startsWith("./") || dep.startsWith("../");
|
|
592
650
|
}
|
|
593
651
|
function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
594
|
-
const resolved =
|
|
595
|
-
const relToRoot =
|
|
596
|
-
if (relToRoot.startsWith("..") ||
|
|
652
|
+
const resolved = path9.resolve(baseDir, raw);
|
|
653
|
+
const relToRoot = path9.relative(rootDir, resolved);
|
|
654
|
+
if (relToRoot.startsWith("..") || path9.isAbsolute(relToRoot)) {
|
|
597
655
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency path escapes the skill root: ${raw}`);
|
|
598
656
|
}
|
|
599
657
|
if (!isDirectory(resolved)) {
|
|
@@ -603,11 +661,11 @@ function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
|
603
661
|
if (!validation.ok) {
|
|
604
662
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a valid skill: ${raw}`, { issues: validation.issues });
|
|
605
663
|
}
|
|
606
|
-
const normalizedInlinePath = relToRoot.split(
|
|
664
|
+
const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
|
|
607
665
|
return {
|
|
608
666
|
sourceType: "inline",
|
|
609
667
|
source: raw,
|
|
610
|
-
name:
|
|
668
|
+
name: path9.basename(resolved) || raw,
|
|
611
669
|
inlinePath: normalizedInlinePath,
|
|
612
670
|
inlineDir: resolved
|
|
613
671
|
};
|
|
@@ -677,15 +735,6 @@ function dedupeInstalledDependencies(entries) {
|
|
|
677
735
|
}
|
|
678
736
|
return out;
|
|
679
737
|
}
|
|
680
|
-
async function cloneRemote(degitSrc, targetPath) {
|
|
681
|
-
const emitter = degit(degitSrc, { force: true, verbose: false });
|
|
682
|
-
await emitter.clone(targetPath);
|
|
683
|
-
}
|
|
684
|
-
function ensureInstallableLocalDir(sourcePath) {
|
|
685
|
-
if (!isDirectory(sourcePath)) {
|
|
686
|
-
throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
738
|
function assertNonEmptyInstall(stagingDir, source) {
|
|
690
739
|
if (isDirEmpty(stagingDir)) {
|
|
691
740
|
throw new SkildError(
|
|
@@ -713,23 +762,16 @@ async function installSkillBase(input, options = {}) {
|
|
|
713
762
|
ensureDir(skillsDir);
|
|
714
763
|
const skillName = input.nameOverride || extractSkillName(source);
|
|
715
764
|
const installDir = getSkillInstallDir(platform, scope, skillName);
|
|
716
|
-
if (
|
|
765
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
717
766
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
718
767
|
skillName,
|
|
719
768
|
installDir
|
|
720
769
|
});
|
|
721
770
|
}
|
|
722
771
|
const tempRoot = createTempDir(skillsDir, skillName);
|
|
723
|
-
const stagingDir =
|
|
772
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
724
773
|
try {
|
|
725
|
-
|
|
726
|
-
if (localPath) {
|
|
727
|
-
ensureInstallableLocalDir(localPath);
|
|
728
|
-
copyDir(localPath, stagingDir);
|
|
729
|
-
} else {
|
|
730
|
-
const degitPath = toDegitPath(source);
|
|
731
|
-
await cloneRemote(degitPath, stagingDir);
|
|
732
|
-
}
|
|
774
|
+
await materializeSourceToDir({ source, targetDir: stagingDir, materializedDir: input.materializedDir });
|
|
733
775
|
assertNonEmptyInstall(stagingDir, source);
|
|
734
776
|
replaceDirAtomic(stagingDir, installDir);
|
|
735
777
|
const contentHash = hashDirectoryContent(installDir);
|
|
@@ -744,7 +786,7 @@ async function installSkillBase(input, options = {}) {
|
|
|
744
786
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
745
787
|
installDir,
|
|
746
788
|
contentHash,
|
|
747
|
-
hasSkillMd:
|
|
789
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
748
790
|
skill: {
|
|
749
791
|
frontmatter: validation.frontmatter,
|
|
750
792
|
validation
|
|
@@ -777,14 +819,14 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
777
819
|
ensureDir(skillsDir);
|
|
778
820
|
const installName = input.nameOverride || canonicalNameToInstallDirName(canonicalName);
|
|
779
821
|
const installDir = getSkillInstallDir(platform, scope, installName);
|
|
780
|
-
if (
|
|
822
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
781
823
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${canonicalName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
782
824
|
skillName: canonicalName,
|
|
783
825
|
installDir
|
|
784
826
|
});
|
|
785
827
|
}
|
|
786
828
|
const tempRoot = createTempDir(skillsDir, installName);
|
|
787
|
-
const stagingDir =
|
|
829
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
788
830
|
try {
|
|
789
831
|
const resolvedVersion = resolved || await resolveRegistryVersion(registryUrl, spec);
|
|
790
832
|
await downloadAndExtractTarball(resolvedVersion, tempRoot, stagingDir);
|
|
@@ -805,7 +847,7 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
805
847
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
806
848
|
installDir,
|
|
807
849
|
contentHash,
|
|
808
|
-
hasSkillMd:
|
|
850
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
809
851
|
skill: { validation, frontmatter: validation.frontmatter }
|
|
810
852
|
};
|
|
811
853
|
writeInstallRecord(installDir, record);
|
|
@@ -854,7 +896,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
854
896
|
const resolved = await resolveRegistryVersion(registryUrl, spec);
|
|
855
897
|
const installName2 = canonicalNameToInstallDirName(spec.canonicalName);
|
|
856
898
|
const installDir2 = getSkillInstallDir(ctx.platform, ctx.scope, installName2);
|
|
857
|
-
if (
|
|
899
|
+
if (fs8.existsSync(installDir2) && !ctx.force) {
|
|
858
900
|
const existing = readInstallRecord(installDir2);
|
|
859
901
|
if (!existing) {
|
|
860
902
|
throw new SkildError(
|
|
@@ -896,7 +938,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
896
938
|
}
|
|
897
939
|
const installName = extractSkillName(dep.source);
|
|
898
940
|
const installDir = getSkillInstallDir(ctx.platform, ctx.scope, installName);
|
|
899
|
-
if (
|
|
941
|
+
if (fs8.existsSync(installDir) && !ctx.force) {
|
|
900
942
|
const existing = readInstallRecord(installDir);
|
|
901
943
|
if (!existing) {
|
|
902
944
|
throw new SkildError(
|
|
@@ -1019,11 +1061,11 @@ async function installRegistrySkill(input, options = {}) {
|
|
|
1019
1061
|
function listSkills(options = {}) {
|
|
1020
1062
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
1021
1063
|
const skillsDir = getSkillsDir(platform, scope);
|
|
1022
|
-
if (!
|
|
1023
|
-
const entries =
|
|
1064
|
+
if (!fs8.existsSync(skillsDir)) return [];
|
|
1065
|
+
const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
1024
1066
|
return entries.map((e) => {
|
|
1025
|
-
const dir =
|
|
1026
|
-
const hasSkillMd =
|
|
1067
|
+
const dir = path9.join(skillsDir, e.name);
|
|
1068
|
+
const hasSkillMd = fs8.existsSync(path9.join(dir, "SKILL.md"));
|
|
1027
1069
|
const record = readInstallRecord(dir);
|
|
1028
1070
|
return { name: e.name, installDir: dir, hasSkillMd, record };
|
|
1029
1071
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -1041,7 +1083,7 @@ function listAllSkills(options = {}) {
|
|
|
1041
1083
|
function getSkillInfo(name, options = {}) {
|
|
1042
1084
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
1043
1085
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1044
|
-
if (!
|
|
1086
|
+
if (!fs8.existsSync(installDir)) {
|
|
1045
1087
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1046
1088
|
}
|
|
1047
1089
|
const record = readInstallRecord(installDir);
|
|
@@ -1065,7 +1107,7 @@ function uninstallSkillInternal(name, options, visited) {
|
|
|
1065
1107
|
if (visited.has(key)) return;
|
|
1066
1108
|
visited.add(key);
|
|
1067
1109
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1068
|
-
if (!
|
|
1110
|
+
if (!fs8.existsSync(installDir)) {
|
|
1069
1111
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1070
1112
|
}
|
|
1071
1113
|
const record = readInstallRecord(installDir);
|
|
@@ -1127,6 +1169,8 @@ export {
|
|
|
1127
1169
|
listSkills,
|
|
1128
1170
|
loadOrCreateGlobalConfig,
|
|
1129
1171
|
loadRegistryAuth,
|
|
1172
|
+
materializeSourceToDir,
|
|
1173
|
+
materializeSourceToTemp,
|
|
1130
1174
|
normalizeAlias,
|
|
1131
1175
|
parseRegistrySpecifier,
|
|
1132
1176
|
resolveRegistryAlias,
|