@skild/core 0.2.8 → 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
@@ -140,6 +140,24 @@ declare function initSkill(name: string, options?: InitOptions): string;
140
140
 
141
141
  declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
142
142
 
143
+ declare function normalizeAlias(input: unknown): string | null;
144
+ declare function isValidAlias(input: string): boolean;
145
+ declare function assertValidAlias(input: string): void;
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
+
143
161
  declare const DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
144
162
  interface RegistrySpecifier {
145
163
  canonicalName: string;
@@ -179,6 +197,12 @@ declare function downloadAndExtractTarball(resolved: RegistryResolvedVersion, te
179
197
  interface InstallInput {
180
198
  source: string;
181
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;
182
206
  }
183
207
  declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
184
208
  declare function installRegistrySkill(input: {
@@ -211,4 +235,4 @@ declare function uninstallSkill(name: string, options?: InstallOptions & {
211
235
  }): void;
212
236
  declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
213
237
 
214
- 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, canonicalNameToInstallDirName, clearRegistryAuth, downloadAndExtractTarball, fetchWithTimeout, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installRegistrySkill, installSkill, listAllSkills, listSkills, loadOrCreateGlobalConfig, loadRegistryAuth, 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
@@ -262,10 +262,226 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 1e4) {
262
262
  }
263
263
  }
264
264
 
265
- // src/registry.ts
265
+ // src/alias.ts
266
+ function normalizeAlias(input) {
267
+ if (typeof input !== "string") return null;
268
+ const v = input.trim();
269
+ return v ? v : null;
270
+ }
271
+ function isValidAlias(input) {
272
+ const alias = input.trim();
273
+ if (!alias) return false;
274
+ if (alias.length < 3 || alias.length > 64) return false;
275
+ if (alias.includes("--")) return false;
276
+ return /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(alias);
277
+ }
278
+ function assertValidAlias(input) {
279
+ const alias = input.trim();
280
+ if (!alias) throw new SkildError("INVALID_SOURCE", "Missing alias.");
281
+ if (alias.length < 3 || alias.length > 64) {
282
+ throw new SkildError("INVALID_SOURCE", "Alias length must be between 3 and 64.");
283
+ }
284
+ if (alias.includes("--")) {
285
+ throw new SkildError("INVALID_SOURCE", "Invalid alias format: consecutive hyphens are not allowed.");
286
+ }
287
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(alias)) {
288
+ throw new SkildError("INVALID_SOURCE", "Invalid alias format. Use lowercase letters, numbers, and hyphens.");
289
+ }
290
+ }
291
+
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
266
302
  import fs5 from "fs";
267
303
  import path5 from "path";
268
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";
269
485
  import * as tar from "tar";
270
486
  import semver from "semver";
271
487
  var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
@@ -342,7 +558,7 @@ async function resolveRegistryVersion(registryUrl, spec) {
342
558
  return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
343
559
  }
344
560
  function sha256Hex(buffer) {
345
- const h = crypto.createHash("sha256");
561
+ const h = crypto2.createHash("sha256");
346
562
  h.update(buffer);
347
563
  return h.digest("hex");
348
564
  }
@@ -395,9 +611,9 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
395
611
  `Integrity mismatch for ${resolved.canonicalName}@${resolved.version}. Expected ${resolved.integrity}, got ${computed}.`
396
612
  );
397
613
  }
398
- const tarballPath = path5.join(tempRoot, "skill.tgz");
399
- fs5.mkdirSync(stagingDir, { recursive: true });
400
- fs5.writeFileSync(tarballPath, buf);
614
+ const tarballPath = path8.join(tempRoot, "skill.tgz");
615
+ fs7.mkdirSync(stagingDir, { recursive: true });
616
+ fs7.writeFileSync(tarballPath, buf);
401
617
  try {
402
618
  await tar.x({ file: tarballPath, cwd: stagingDir, gzip: true });
403
619
  } catch (error) {
@@ -406,139 +622,8 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
406
622
  }
407
623
 
408
624
  // src/lifecycle.ts
409
- import path8 from "path";
410
- import fs7 from "fs";
411
- import degit from "degit";
412
-
413
- // src/source.ts
414
- import path7 from "path";
415
-
416
- // src/fs.ts
417
- import fs6 from "fs";
418
- import path6 from "path";
419
- import crypto2 from "crypto";
420
- function pathExists(filePath) {
421
- return fs6.existsSync(filePath);
422
- }
423
- function isDirectory(filePath) {
424
- try {
425
- return fs6.statSync(filePath).isDirectory();
426
- } catch {
427
- return false;
428
- }
429
- }
430
- function isDirEmpty(dir) {
431
- try {
432
- const entries = fs6.readdirSync(dir);
433
- return entries.length === 0;
434
- } catch {
435
- return true;
436
- }
437
- }
438
- function copyDir(src, dest) {
439
- fs6.cpSync(src, dest, { recursive: true });
440
- }
441
- function removeDir(dir) {
442
- if (fs6.existsSync(dir)) fs6.rmSync(dir, { recursive: true, force: true });
443
- }
444
- function sanitizeForPathSegment(value) {
445
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
446
- }
447
- function createTempDir(parentDir, prefix) {
448
- ensureDir(parentDir);
449
- const safePrefix = sanitizeForPathSegment(prefix || "tmp");
450
- const template = path6.join(parentDir, `.skild-${safePrefix}-`);
451
- return fs6.mkdtempSync(template);
452
- }
453
- function replaceDirAtomic(sourceDir, destDir) {
454
- const backupDir = fs6.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
455
- try {
456
- if (backupDir) fs6.renameSync(destDir, backupDir);
457
- fs6.renameSync(sourceDir, destDir);
458
- if (backupDir) removeDir(backupDir);
459
- } catch (error) {
460
- try {
461
- if (!fs6.existsSync(destDir) && backupDir && fs6.existsSync(backupDir)) {
462
- fs6.renameSync(backupDir, destDir);
463
- }
464
- } catch {
465
- }
466
- try {
467
- if (fs6.existsSync(sourceDir)) removeDir(sourceDir);
468
- } catch {
469
- }
470
- throw error;
471
- }
472
- }
473
- function listFilesRecursive(rootDir) {
474
- const results = [];
475
- const stack = [rootDir];
476
- while (stack.length) {
477
- const current = stack.pop();
478
- const entries = fs6.readdirSync(current, { withFileTypes: true });
479
- for (const entry of entries) {
480
- if (entry.name === ".skild") continue;
481
- if (entry.name === ".git") continue;
482
- const full = path6.join(current, entry.name);
483
- if (entry.isDirectory()) stack.push(full);
484
- else if (entry.isFile()) results.push(full);
485
- }
486
- }
487
- results.sort();
488
- return results;
489
- }
490
- function hashDirectoryContent(rootDir) {
491
- const files = listFilesRecursive(rootDir);
492
- const h = crypto2.createHash("sha256");
493
- for (const filePath of files) {
494
- const rel = path6.relative(rootDir, filePath);
495
- h.update(rel);
496
- h.update("\0");
497
- h.update(fs6.readFileSync(filePath));
498
- h.update("\0");
499
- }
500
- return h.digest("hex");
501
- }
502
-
503
- // src/source.ts
504
- function resolveLocalPath(source) {
505
- const resolved = path7.resolve(source);
506
- return pathExists(resolved) ? resolved : null;
507
- }
508
- function classifySource(source) {
509
- const local = resolveLocalPath(source);
510
- if (local) return "local";
511
- if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
512
- if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
513
- throw new SkildError(
514
- "INVALID_SOURCE",
515
- `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.`
516
- );
517
- }
518
- function extractSkillName(source) {
519
- const local = resolveLocalPath(source);
520
- if (local) return path7.basename(local) || "unknown-skill";
521
- const cleaned = source.replace(/[#?].*$/, "");
522
- const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
523
- if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
524
- const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
525
- if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
526
- const parts = cleaned.split("/").filter(Boolean);
527
- if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
528
- return cleaned || "unknown-skill";
529
- }
530
- function toDegitPath(url) {
531
- const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
532
- if (treeMatch) {
533
- const [, owner, repo, branch, subpath] = treeMatch;
534
- return `${owner}/${repo}/${subpath}#${branch}`;
535
- }
536
- const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
537
- if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
538
- return url;
539
- }
540
-
541
- // src/lifecycle.ts
625
+ import path9 from "path";
626
+ import fs8 from "fs";
542
627
  function normalizeDependencies(raw) {
543
628
  if (raw === void 0 || raw === null) return [];
544
629
  if (!Array.isArray(raw)) {
@@ -564,9 +649,9 @@ function isRelativeDependency(dep) {
564
649
  return dep.startsWith("./") || dep.startsWith("../");
565
650
  }
566
651
  function resolveInlineDependency(raw, baseDir, rootDir) {
567
- const resolved = path8.resolve(baseDir, raw);
568
- const relToRoot = path8.relative(rootDir, resolved);
569
- 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)) {
570
655
  throw new SkildError("INVALID_DEPENDENCY", `Inline dependency path escapes the skill root: ${raw}`);
571
656
  }
572
657
  if (!isDirectory(resolved)) {
@@ -576,11 +661,11 @@ function resolveInlineDependency(raw, baseDir, rootDir) {
576
661
  if (!validation.ok) {
577
662
  throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a valid skill: ${raw}`, { issues: validation.issues });
578
663
  }
579
- const normalizedInlinePath = relToRoot.split(path8.sep).join("/");
664
+ const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
580
665
  return {
581
666
  sourceType: "inline",
582
667
  source: raw,
583
- name: path8.basename(resolved) || raw,
668
+ name: path9.basename(resolved) || raw,
584
669
  inlinePath: normalizedInlinePath,
585
670
  inlineDir: resolved
586
671
  };
@@ -650,15 +735,6 @@ function dedupeInstalledDependencies(entries) {
650
735
  }
651
736
  return out;
652
737
  }
653
- async function cloneRemote(degitSrc, targetPath) {
654
- const emitter = degit(degitSrc, { force: true, verbose: false });
655
- await emitter.clone(targetPath);
656
- }
657
- function ensureInstallableLocalDir(sourcePath) {
658
- if (!isDirectory(sourcePath)) {
659
- throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
660
- }
661
- }
662
738
  function assertNonEmptyInstall(stagingDir, source) {
663
739
  if (isDirEmpty(stagingDir)) {
664
740
  throw new SkildError(
@@ -686,23 +762,16 @@ async function installSkillBase(input, options = {}) {
686
762
  ensureDir(skillsDir);
687
763
  const skillName = input.nameOverride || extractSkillName(source);
688
764
  const installDir = getSkillInstallDir(platform, scope, skillName);
689
- if (fs7.existsSync(installDir) && !options.force) {
765
+ if (fs8.existsSync(installDir) && !options.force) {
690
766
  throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
691
767
  skillName,
692
768
  installDir
693
769
  });
694
770
  }
695
771
  const tempRoot = createTempDir(skillsDir, skillName);
696
- const stagingDir = path8.join(tempRoot, "staging");
772
+ const stagingDir = path9.join(tempRoot, "staging");
697
773
  try {
698
- const localPath = resolveLocalPath(source);
699
- if (localPath) {
700
- ensureInstallableLocalDir(localPath);
701
- copyDir(localPath, stagingDir);
702
- } else {
703
- const degitPath = toDegitPath(source);
704
- await cloneRemote(degitPath, stagingDir);
705
- }
774
+ await materializeSourceToDir({ source, targetDir: stagingDir, materializedDir: input.materializedDir });
706
775
  assertNonEmptyInstall(stagingDir, source);
707
776
  replaceDirAtomic(stagingDir, installDir);
708
777
  const contentHash = hashDirectoryContent(installDir);
@@ -717,7 +786,7 @@ async function installSkillBase(input, options = {}) {
717
786
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
718
787
  installDir,
719
788
  contentHash,
720
- hasSkillMd: fs7.existsSync(path8.join(installDir, "SKILL.md")),
789
+ hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
721
790
  skill: {
722
791
  frontmatter: validation.frontmatter,
723
792
  validation
@@ -750,14 +819,14 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
750
819
  ensureDir(skillsDir);
751
820
  const installName = input.nameOverride || canonicalNameToInstallDirName(canonicalName);
752
821
  const installDir = getSkillInstallDir(platform, scope, installName);
753
- if (fs7.existsSync(installDir) && !options.force) {
822
+ if (fs8.existsSync(installDir) && !options.force) {
754
823
  throw new SkildError("ALREADY_INSTALLED", `Skill "${canonicalName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
755
824
  skillName: canonicalName,
756
825
  installDir
757
826
  });
758
827
  }
759
828
  const tempRoot = createTempDir(skillsDir, installName);
760
- const stagingDir = path8.join(tempRoot, "staging");
829
+ const stagingDir = path9.join(tempRoot, "staging");
761
830
  try {
762
831
  const resolvedVersion = resolved || await resolveRegistryVersion(registryUrl, spec);
763
832
  await downloadAndExtractTarball(resolvedVersion, tempRoot, stagingDir);
@@ -778,7 +847,7 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
778
847
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
779
848
  installDir,
780
849
  contentHash,
781
- hasSkillMd: fs7.existsSync(path8.join(installDir, "SKILL.md")),
850
+ hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
782
851
  skill: { validation, frontmatter: validation.frontmatter }
783
852
  };
784
853
  writeInstallRecord(installDir, record);
@@ -827,7 +896,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
827
896
  const resolved = await resolveRegistryVersion(registryUrl, spec);
828
897
  const installName2 = canonicalNameToInstallDirName(spec.canonicalName);
829
898
  const installDir2 = getSkillInstallDir(ctx.platform, ctx.scope, installName2);
830
- if (fs7.existsSync(installDir2) && !ctx.force) {
899
+ if (fs8.existsSync(installDir2) && !ctx.force) {
831
900
  const existing = readInstallRecord(installDir2);
832
901
  if (!existing) {
833
902
  throw new SkildError(
@@ -869,7 +938,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
869
938
  }
870
939
  const installName = extractSkillName(dep.source);
871
940
  const installDir = getSkillInstallDir(ctx.platform, ctx.scope, installName);
872
- if (fs7.existsSync(installDir) && !ctx.force) {
941
+ if (fs8.existsSync(installDir) && !ctx.force) {
873
942
  const existing = readInstallRecord(installDir);
874
943
  if (!existing) {
875
944
  throw new SkildError(
@@ -992,11 +1061,11 @@ async function installRegistrySkill(input, options = {}) {
992
1061
  function listSkills(options = {}) {
993
1062
  const { platform, scope } = resolvePlatformAndScope(options);
994
1063
  const skillsDir = getSkillsDir(platform, scope);
995
- if (!fs7.existsSync(skillsDir)) return [];
996
- 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("."));
997
1066
  return entries.map((e) => {
998
- const dir = path8.join(skillsDir, e.name);
999
- 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"));
1000
1069
  const record = readInstallRecord(dir);
1001
1070
  return { name: e.name, installDir: dir, hasSkillMd, record };
1002
1071
  }).sort((a, b) => a.name.localeCompare(b.name));
@@ -1014,7 +1083,7 @@ function listAllSkills(options = {}) {
1014
1083
  function getSkillInfo(name, options = {}) {
1015
1084
  const { platform, scope } = resolvePlatformAndScope(options);
1016
1085
  const installDir = getSkillInstallDir(platform, scope, name);
1017
- if (!fs7.existsSync(installDir)) {
1086
+ if (!fs8.existsSync(installDir)) {
1018
1087
  throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
1019
1088
  }
1020
1089
  const record = readInstallRecord(installDir);
@@ -1038,7 +1107,7 @@ function uninstallSkillInternal(name, options, visited) {
1038
1107
  if (visited.has(key)) return;
1039
1108
  visited.add(key);
1040
1109
  const installDir = getSkillInstallDir(platform, scope, name);
1041
- if (!fs7.existsSync(installDir)) {
1110
+ if (!fs8.existsSync(installDir)) {
1042
1111
  throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
1043
1112
  }
1044
1113
  const record = readInstallRecord(installDir);
@@ -1084,6 +1153,7 @@ export {
1084
1153
  DEFAULT_REGISTRY_URL,
1085
1154
  PLATFORMS,
1086
1155
  SkildError,
1156
+ assertValidAlias,
1087
1157
  canonicalNameToInstallDirName,
1088
1158
  clearRegistryAuth,
1089
1159
  downloadAndExtractTarball,
@@ -1094,10 +1164,14 @@ export {
1094
1164
  initSkill,
1095
1165
  installRegistrySkill,
1096
1166
  installSkill,
1167
+ isValidAlias,
1097
1168
  listAllSkills,
1098
1169
  listSkills,
1099
1170
  loadOrCreateGlobalConfig,
1100
1171
  loadRegistryAuth,
1172
+ materializeSourceToDir,
1173
+ materializeSourceToTemp,
1174
+ normalizeAlias,
1101
1175
  parseRegistrySpecifier,
1102
1176
  resolveRegistryAlias,
1103
1177
  resolveRegistryUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skild/core",
3
- "version": "0.2.8",
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",