@skild/core 0.2.9 → 0.4.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/dist/index.d.ts +30 -1
- package/dist/index.js +236 -174
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -144,6 +144,29 @@ 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
|
+
|
|
161
|
+
declare function toDegitPath(url: string): string;
|
|
162
|
+
/**
|
|
163
|
+
* Derive a child source spec from a base source and a relative path.
|
|
164
|
+
*
|
|
165
|
+
* - GitHub URLs are converted to degit shorthand so the result is a valid install source.
|
|
166
|
+
* - Keeps `#ref` when present.
|
|
167
|
+
*/
|
|
168
|
+
declare function deriveChildSource(baseSource: string, relPath: string): string;
|
|
169
|
+
|
|
147
170
|
declare const DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
148
171
|
interface RegistrySpecifier {
|
|
149
172
|
canonicalName: string;
|
|
@@ -183,6 +206,12 @@ declare function downloadAndExtractTarball(resolved: RegistryResolvedVersion, te
|
|
|
183
206
|
interface InstallInput {
|
|
184
207
|
source: string;
|
|
185
208
|
nameOverride?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Optional local directory containing the already-fetched source content.
|
|
211
|
+
* When provided, Skild will copy from this directory but still record `source`
|
|
212
|
+
* and `sourceType` based on the original `source` string (useful for multi-skill installs).
|
|
213
|
+
*/
|
|
214
|
+
materializedDir?: string;
|
|
186
215
|
}
|
|
187
216
|
declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
|
|
188
217
|
declare function installRegistrySkill(input: {
|
|
@@ -215,4 +244,4 @@ declare function uninstallSkill(name: string, options?: InstallOptions & {
|
|
|
215
244
|
}): void;
|
|
216
245
|
declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
|
|
217
246
|
|
|
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 };
|
|
247
|
+
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, deriveChildSource, downloadAndExtractTarball, fetchWithTimeout, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installRegistrySkill, installSkill, isValidAlias, listAllSkills, listSkills, loadOrCreateGlobalConfig, loadRegistryAuth, materializeSourceToDir, materializeSourceToTemp, normalizeAlias, parseRegistrySpecifier, resolveRegistryAlias, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, toDegitPath, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
package/dist/index.js
CHANGED
|
@@ -289,10 +289,215 @@ 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 treeRootMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/)?$/);
|
|
422
|
+
if (treeRootMatch) {
|
|
423
|
+
const [, owner, repo, branch] = treeRootMatch;
|
|
424
|
+
return `${owner}/${repo}#${branch}`;
|
|
425
|
+
}
|
|
426
|
+
const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
427
|
+
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
428
|
+
return url;
|
|
429
|
+
}
|
|
430
|
+
function normalizeRelPath(relPath) {
|
|
431
|
+
return relPath.split(path6.sep).join("/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
432
|
+
}
|
|
433
|
+
function deriveChildSource(baseSource, relPath) {
|
|
434
|
+
const baseType = classifySource(baseSource);
|
|
435
|
+
const baseSpec = baseType === "github-url" ? toDegitPath(baseSource) : baseSource;
|
|
436
|
+
const [pathPart, ref] = baseSpec.split("#", 2);
|
|
437
|
+
const clean = normalizeRelPath(relPath);
|
|
438
|
+
const joined = clean ? `${pathPart.replace(/\/+$/, "")}/${clean}` : pathPart;
|
|
439
|
+
return ref ? `${joined}#${ref}` : joined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/materialize.ts
|
|
443
|
+
function ensureInstallableDir(sourcePath) {
|
|
444
|
+
if (!isDirectory(sourcePath)) {
|
|
445
|
+
throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function cloneRemote(degitSrc, targetPath) {
|
|
449
|
+
const emitter = degit(degitSrc, { force: true, verbose: false });
|
|
450
|
+
await emitter.clone(targetPath);
|
|
451
|
+
}
|
|
452
|
+
async function materializeSourceToDir(input) {
|
|
453
|
+
const sourceType = classifySource(input.source);
|
|
454
|
+
const targetDir = path7.resolve(input.targetDir);
|
|
455
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
456
|
+
const overridden = input.materializedDir?.trim() ? path7.resolve(input.materializedDir.trim()) : null;
|
|
457
|
+
if (overridden) {
|
|
458
|
+
ensureInstallableDir(overridden);
|
|
459
|
+
copyDir(overridden, targetDir);
|
|
460
|
+
return { sourceType, materializedFrom: overridden };
|
|
461
|
+
}
|
|
462
|
+
const localPath = resolveLocalPath(input.source);
|
|
463
|
+
if (localPath) {
|
|
464
|
+
ensureInstallableDir(localPath);
|
|
465
|
+
copyDir(localPath, targetDir);
|
|
466
|
+
return { sourceType: "local", materializedFrom: localPath };
|
|
467
|
+
}
|
|
468
|
+
const degitPath = toDegitPath(input.source);
|
|
469
|
+
await cloneRemote(degitPath, targetDir);
|
|
470
|
+
return { sourceType, materializedFrom: degitPath };
|
|
471
|
+
}
|
|
472
|
+
async function materializeSourceToTemp(source) {
|
|
473
|
+
const sourceType = classifySource(source);
|
|
474
|
+
const tempParent = path7.join(os2.tmpdir(), "skild-materialize");
|
|
475
|
+
const tempRoot = createTempDir(tempParent, extractSkillName(source));
|
|
476
|
+
const dir = path7.join(tempRoot, "staging");
|
|
477
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
478
|
+
try {
|
|
479
|
+
await materializeSourceToDir({ source, targetDir: dir });
|
|
480
|
+
if (isDirEmpty(dir)) {
|
|
481
|
+
throw new SkildError(
|
|
482
|
+
"EMPTY_INSTALL_DIR",
|
|
483
|
+
`Installed directory is empty for source: ${source}
|
|
484
|
+
Source likely does not point to a valid subdirectory.
|
|
485
|
+
Try: https://github.com/<owner>/<repo>/tree/<branch>/skills/<skill-name>
|
|
486
|
+
Example: https://github.com/anthropics/skills/tree/main/skills/pdf`,
|
|
487
|
+
{ source }
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return { dir, sourceType, cleanup: () => removeDir(tempRoot) };
|
|
491
|
+
} catch (e) {
|
|
492
|
+
removeDir(tempRoot);
|
|
493
|
+
throw e;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/registry.ts
|
|
498
|
+
import fs7 from "fs";
|
|
499
|
+
import path8 from "path";
|
|
500
|
+
import crypto2 from "crypto";
|
|
296
501
|
import * as tar from "tar";
|
|
297
502
|
import semver from "semver";
|
|
298
503
|
var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
@@ -369,7 +574,7 @@ async function resolveRegistryVersion(registryUrl, spec) {
|
|
|
369
574
|
return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
|
|
370
575
|
}
|
|
371
576
|
function sha256Hex(buffer) {
|
|
372
|
-
const h =
|
|
577
|
+
const h = crypto2.createHash("sha256");
|
|
373
578
|
h.update(buffer);
|
|
374
579
|
return h.digest("hex");
|
|
375
580
|
}
|
|
@@ -422,9 +627,9 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
422
627
|
`Integrity mismatch for ${resolved.canonicalName}@${resolved.version}. Expected ${resolved.integrity}, got ${computed}.`
|
|
423
628
|
);
|
|
424
629
|
}
|
|
425
|
-
const tarballPath =
|
|
426
|
-
|
|
427
|
-
|
|
630
|
+
const tarballPath = path8.join(tempRoot, "skill.tgz");
|
|
631
|
+
fs7.mkdirSync(stagingDir, { recursive: true });
|
|
632
|
+
fs7.writeFileSync(tarballPath, buf);
|
|
428
633
|
try {
|
|
429
634
|
await tar.x({ file: tarballPath, cwd: stagingDir, gzip: true });
|
|
430
635
|
} catch (error) {
|
|
@@ -433,139 +638,8 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
433
638
|
}
|
|
434
639
|
|
|
435
640
|
// 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
|
|
641
|
+
import path9 from "path";
|
|
642
|
+
import fs8 from "fs";
|
|
569
643
|
function normalizeDependencies(raw) {
|
|
570
644
|
if (raw === void 0 || raw === null) return [];
|
|
571
645
|
if (!Array.isArray(raw)) {
|
|
@@ -591,9 +665,9 @@ function isRelativeDependency(dep) {
|
|
|
591
665
|
return dep.startsWith("./") || dep.startsWith("../");
|
|
592
666
|
}
|
|
593
667
|
function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
594
|
-
const resolved =
|
|
595
|
-
const relToRoot =
|
|
596
|
-
if (relToRoot.startsWith("..") ||
|
|
668
|
+
const resolved = path9.resolve(baseDir, raw);
|
|
669
|
+
const relToRoot = path9.relative(rootDir, resolved);
|
|
670
|
+
if (relToRoot.startsWith("..") || path9.isAbsolute(relToRoot)) {
|
|
597
671
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency path escapes the skill root: ${raw}`);
|
|
598
672
|
}
|
|
599
673
|
if (!isDirectory(resolved)) {
|
|
@@ -603,11 +677,11 @@ function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
|
603
677
|
if (!validation.ok) {
|
|
604
678
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a valid skill: ${raw}`, { issues: validation.issues });
|
|
605
679
|
}
|
|
606
|
-
const normalizedInlinePath = relToRoot.split(
|
|
680
|
+
const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
|
|
607
681
|
return {
|
|
608
682
|
sourceType: "inline",
|
|
609
683
|
source: raw,
|
|
610
|
-
name:
|
|
684
|
+
name: path9.basename(resolved) || raw,
|
|
611
685
|
inlinePath: normalizedInlinePath,
|
|
612
686
|
inlineDir: resolved
|
|
613
687
|
};
|
|
@@ -677,15 +751,6 @@ function dedupeInstalledDependencies(entries) {
|
|
|
677
751
|
}
|
|
678
752
|
return out;
|
|
679
753
|
}
|
|
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
754
|
function assertNonEmptyInstall(stagingDir, source) {
|
|
690
755
|
if (isDirEmpty(stagingDir)) {
|
|
691
756
|
throw new SkildError(
|
|
@@ -713,23 +778,16 @@ async function installSkillBase(input, options = {}) {
|
|
|
713
778
|
ensureDir(skillsDir);
|
|
714
779
|
const skillName = input.nameOverride || extractSkillName(source);
|
|
715
780
|
const installDir = getSkillInstallDir(platform, scope, skillName);
|
|
716
|
-
if (
|
|
781
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
717
782
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
718
783
|
skillName,
|
|
719
784
|
installDir
|
|
720
785
|
});
|
|
721
786
|
}
|
|
722
787
|
const tempRoot = createTempDir(skillsDir, skillName);
|
|
723
|
-
const stagingDir =
|
|
788
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
724
789
|
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
|
-
}
|
|
790
|
+
await materializeSourceToDir({ source, targetDir: stagingDir, materializedDir: input.materializedDir });
|
|
733
791
|
assertNonEmptyInstall(stagingDir, source);
|
|
734
792
|
replaceDirAtomic(stagingDir, installDir);
|
|
735
793
|
const contentHash = hashDirectoryContent(installDir);
|
|
@@ -744,7 +802,7 @@ async function installSkillBase(input, options = {}) {
|
|
|
744
802
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
745
803
|
installDir,
|
|
746
804
|
contentHash,
|
|
747
|
-
hasSkillMd:
|
|
805
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
748
806
|
skill: {
|
|
749
807
|
frontmatter: validation.frontmatter,
|
|
750
808
|
validation
|
|
@@ -777,14 +835,14 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
777
835
|
ensureDir(skillsDir);
|
|
778
836
|
const installName = input.nameOverride || canonicalNameToInstallDirName(canonicalName);
|
|
779
837
|
const installDir = getSkillInstallDir(platform, scope, installName);
|
|
780
|
-
if (
|
|
838
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
781
839
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${canonicalName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
782
840
|
skillName: canonicalName,
|
|
783
841
|
installDir
|
|
784
842
|
});
|
|
785
843
|
}
|
|
786
844
|
const tempRoot = createTempDir(skillsDir, installName);
|
|
787
|
-
const stagingDir =
|
|
845
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
788
846
|
try {
|
|
789
847
|
const resolvedVersion = resolved || await resolveRegistryVersion(registryUrl, spec);
|
|
790
848
|
await downloadAndExtractTarball(resolvedVersion, tempRoot, stagingDir);
|
|
@@ -805,7 +863,7 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
805
863
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
806
864
|
installDir,
|
|
807
865
|
contentHash,
|
|
808
|
-
hasSkillMd:
|
|
866
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
809
867
|
skill: { validation, frontmatter: validation.frontmatter }
|
|
810
868
|
};
|
|
811
869
|
writeInstallRecord(installDir, record);
|
|
@@ -854,7 +912,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
854
912
|
const resolved = await resolveRegistryVersion(registryUrl, spec);
|
|
855
913
|
const installName2 = canonicalNameToInstallDirName(spec.canonicalName);
|
|
856
914
|
const installDir2 = getSkillInstallDir(ctx.platform, ctx.scope, installName2);
|
|
857
|
-
if (
|
|
915
|
+
if (fs8.existsSync(installDir2) && !ctx.force) {
|
|
858
916
|
const existing = readInstallRecord(installDir2);
|
|
859
917
|
if (!existing) {
|
|
860
918
|
throw new SkildError(
|
|
@@ -896,7 +954,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
896
954
|
}
|
|
897
955
|
const installName = extractSkillName(dep.source);
|
|
898
956
|
const installDir = getSkillInstallDir(ctx.platform, ctx.scope, installName);
|
|
899
|
-
if (
|
|
957
|
+
if (fs8.existsSync(installDir) && !ctx.force) {
|
|
900
958
|
const existing = readInstallRecord(installDir);
|
|
901
959
|
if (!existing) {
|
|
902
960
|
throw new SkildError(
|
|
@@ -1019,11 +1077,11 @@ async function installRegistrySkill(input, options = {}) {
|
|
|
1019
1077
|
function listSkills(options = {}) {
|
|
1020
1078
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
1021
1079
|
const skillsDir = getSkillsDir(platform, scope);
|
|
1022
|
-
if (!
|
|
1023
|
-
const entries =
|
|
1080
|
+
if (!fs8.existsSync(skillsDir)) return [];
|
|
1081
|
+
const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
1024
1082
|
return entries.map((e) => {
|
|
1025
|
-
const dir =
|
|
1026
|
-
const hasSkillMd =
|
|
1083
|
+
const dir = path9.join(skillsDir, e.name);
|
|
1084
|
+
const hasSkillMd = fs8.existsSync(path9.join(dir, "SKILL.md"));
|
|
1027
1085
|
const record = readInstallRecord(dir);
|
|
1028
1086
|
return { name: e.name, installDir: dir, hasSkillMd, record };
|
|
1029
1087
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -1041,7 +1099,7 @@ function listAllSkills(options = {}) {
|
|
|
1041
1099
|
function getSkillInfo(name, options = {}) {
|
|
1042
1100
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
1043
1101
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1044
|
-
if (!
|
|
1102
|
+
if (!fs8.existsSync(installDir)) {
|
|
1045
1103
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1046
1104
|
}
|
|
1047
1105
|
const record = readInstallRecord(installDir);
|
|
@@ -1065,7 +1123,7 @@ function uninstallSkillInternal(name, options, visited) {
|
|
|
1065
1123
|
if (visited.has(key)) return;
|
|
1066
1124
|
visited.add(key);
|
|
1067
1125
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1068
|
-
if (!
|
|
1126
|
+
if (!fs8.existsSync(installDir)) {
|
|
1069
1127
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1070
1128
|
}
|
|
1071
1129
|
const record = readInstallRecord(installDir);
|
|
@@ -1114,6 +1172,7 @@ export {
|
|
|
1114
1172
|
assertValidAlias,
|
|
1115
1173
|
canonicalNameToInstallDirName,
|
|
1116
1174
|
clearRegistryAuth,
|
|
1175
|
+
deriveChildSource,
|
|
1117
1176
|
downloadAndExtractTarball,
|
|
1118
1177
|
fetchWithTimeout,
|
|
1119
1178
|
getSkillInfo,
|
|
@@ -1127,6 +1186,8 @@ export {
|
|
|
1127
1186
|
listSkills,
|
|
1128
1187
|
loadOrCreateGlobalConfig,
|
|
1129
1188
|
loadRegistryAuth,
|
|
1189
|
+
materializeSourceToDir,
|
|
1190
|
+
materializeSourceToTemp,
|
|
1130
1191
|
normalizeAlias,
|
|
1131
1192
|
parseRegistrySpecifier,
|
|
1132
1193
|
resolveRegistryAlias,
|
|
@@ -1135,6 +1196,7 @@ export {
|
|
|
1135
1196
|
saveRegistryAuth,
|
|
1136
1197
|
searchRegistrySkills,
|
|
1137
1198
|
splitCanonicalName,
|
|
1199
|
+
toDegitPath,
|
|
1138
1200
|
uninstallSkill,
|
|
1139
1201
|
updateSkill,
|
|
1140
1202
|
validateSkill,
|