@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 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/registry.ts
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 = crypto.createHash("sha256");
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 = path5.join(tempRoot, "skill.tgz");
426
- fs5.mkdirSync(stagingDir, { recursive: true });
427
- fs5.writeFileSync(tarballPath, buf);
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 path8 from "path";
437
- import fs7 from "fs";
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 = path8.resolve(baseDir, raw);
595
- const relToRoot = path8.relative(rootDir, resolved);
596
- if (relToRoot.startsWith("..") || path8.isAbsolute(relToRoot)) {
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(path8.sep).join("/");
680
+ const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
607
681
  return {
608
682
  sourceType: "inline",
609
683
  source: raw,
610
- name: path8.basename(resolved) || raw,
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 (fs7.existsSync(installDir) && !options.force) {
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 = path8.join(tempRoot, "staging");
788
+ const stagingDir = path9.join(tempRoot, "staging");
724
789
  try {
725
- const localPath = resolveLocalPath(source);
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: fs7.existsSync(path8.join(installDir, "SKILL.md")),
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 (fs7.existsSync(installDir) && !options.force) {
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 = path8.join(tempRoot, "staging");
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: fs7.existsSync(path8.join(installDir, "SKILL.md")),
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 (fs7.existsSync(installDir2) && !ctx.force) {
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 (fs7.existsSync(installDir) && !ctx.force) {
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 (!fs7.existsSync(skillsDir)) return [];
1023
- const entries = fs7.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
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 = path8.join(skillsDir, e.name);
1026
- const hasSkillMd = fs7.existsSync(path8.join(dir, "SKILL.md"));
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 (!fs7.existsSync(installDir)) {
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 (!fs7.existsSync(installDir)) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skild/core",
3
- "version": "0.2.9",
3
+ "version": "0.4.1",
4
4
  "description": "Skild core library (headless) for installing, validating, and managing Agent Skills locally.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",