@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 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/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 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 = crypto.createHash("sha256");
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 = path5.join(tempRoot, "skill.tgz");
426
- fs5.mkdirSync(stagingDir, { recursive: true });
427
- fs5.writeFileSync(tarballPath, buf);
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 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
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 = path8.resolve(baseDir, raw);
595
- const relToRoot = path8.relative(rootDir, resolved);
596
- if (relToRoot.startsWith("..") || path8.isAbsolute(relToRoot)) {
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(path8.sep).join("/");
664
+ const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
607
665
  return {
608
666
  sourceType: "inline",
609
667
  source: raw,
610
- name: path8.basename(resolved) || raw,
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 (fs7.existsSync(installDir) && !options.force) {
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 = path8.join(tempRoot, "staging");
772
+ const stagingDir = path9.join(tempRoot, "staging");
724
773
  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
- }
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: fs7.existsSync(path8.join(installDir, "SKILL.md")),
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 (fs7.existsSync(installDir) && !options.force) {
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 = path8.join(tempRoot, "staging");
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: fs7.existsSync(path8.join(installDir, "SKILL.md")),
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 (fs7.existsSync(installDir2) && !ctx.force) {
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 (fs7.existsSync(installDir) && !ctx.force) {
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 (!fs7.existsSync(skillsDir)) return [];
1023
- const entries = fs7.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
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 = path8.join(skillsDir, e.name);
1026
- const hasSkillMd = fs7.existsSync(path8.join(dir, "SKILL.md"));
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 (!fs7.existsSync(installDir)) {
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 (!fs7.existsSync(installDir)) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skild/core",
3
- "version": "0.2.9",
3
+ "version": "0.4.0",
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",