@skild/core 0.2.0 → 0.2.3

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
@@ -1,4 +1,4 @@
1
- type SkildErrorCode = 'INVALID_SOURCE' | 'NOT_A_DIRECTORY' | 'EMPTY_INSTALL_DIR' | 'ALREADY_INSTALLED' | 'SKILL_NOT_FOUND' | 'MISSING_METADATA' | 'INVALID_SKILL' | 'MISSING_REGISTRY_CONFIG' | 'REGISTRY_RESOLVE_FAILED' | 'REGISTRY_DOWNLOAD_FAILED' | 'INTEGRITY_MISMATCH' | 'NETWORK_TIMEOUT';
1
+ type SkildErrorCode = 'INVALID_SOURCE' | 'INVALID_DEPENDENCY' | 'INVALID_DEPENDENCIES' | 'NOT_A_DIRECTORY' | 'EMPTY_INSTALL_DIR' | 'ALREADY_INSTALLED' | 'SKILL_NOT_FOUND' | 'MISSING_METADATA' | 'INVALID_SKILL' | 'DEPENDENCY_CONFLICT' | 'DEPENDENCY_CYCLE' | 'VERSION_CONFLICT' | 'MISSING_REGISTRY_CONFIG' | 'REGISTRY_RESOLVE_FAILED' | 'REGISTRY_DOWNLOAD_FAILED' | 'INTEGRITY_MISMATCH' | 'NETWORK_TIMEOUT';
2
2
  declare class SkildError extends Error {
3
3
  readonly code: SkildErrorCode;
4
4
  readonly details?: Record<string, unknown>;
@@ -9,6 +9,7 @@ declare const PLATFORMS: readonly ["claude", "codex", "copilot"];
9
9
  type Platform = (typeof PLATFORMS)[number];
10
10
  type InstallScope = 'global' | 'project';
11
11
  type SourceType = 'local' | 'github-url' | 'degit-shorthand' | 'registry';
12
+ type DependencySourceType = SourceType | 'inline';
12
13
  interface InstallOptions {
13
14
  platform?: Platform;
14
15
  scope?: InstallScope;
@@ -27,6 +28,8 @@ interface SkillFrontmatter {
27
28
  name: string;
28
29
  description: string;
29
30
  version?: string;
31
+ dependencies?: string[];
32
+ skillset?: boolean;
30
33
  [key: string]: unknown;
31
34
  }
32
35
  interface SkillValidationIssue {
@@ -52,6 +55,7 @@ interface InstallRecord {
52
55
  * Example: "https://registry.skild.sh"
53
56
  */
54
57
  registryUrl?: string;
58
+ resolvedVersion?: string;
55
59
  platform: Platform;
56
60
  scope: InstallScope;
57
61
  source: string;
@@ -61,11 +65,23 @@ interface InstallRecord {
61
65
  installDir: string;
62
66
  contentHash: string;
63
67
  hasSkillMd: boolean;
68
+ skillset?: boolean;
69
+ dependencies?: string[];
70
+ installedDependencies?: InstalledDependency[];
71
+ dependedBy?: string[];
64
72
  skill?: {
65
73
  frontmatter?: SkillFrontmatter;
66
74
  validation?: SkillValidationResult;
67
75
  };
68
76
  }
77
+ interface InstalledDependency {
78
+ name: string;
79
+ source: string;
80
+ sourceType: DependencySourceType;
81
+ canonicalName?: string;
82
+ installDir?: string;
83
+ inlinePath?: string;
84
+ }
69
85
  interface LockEntry {
70
86
  name: string;
71
87
  platform: Platform;
@@ -179,7 +195,8 @@ declare function validateSkill(nameOrPath: string, options?: {
179
195
  }): SkillValidationResult;
180
196
  declare function uninstallSkill(name: string, options?: InstallOptions & {
181
197
  allowMissingMetadata?: boolean;
198
+ withDeps?: boolean;
182
199
  }): void;
183
200
  declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
184
201
 
185
- export { DEFAULT_REGISTRY_URL, type GlobalConfig, type InstallOptions, type InstallRecord, type InstallScope, 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, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
202
+ 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, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
package/dist/index.js CHANGED
@@ -183,6 +183,15 @@ function validateSkillDir(skillDir) {
183
183
  if (!frontmatter.description || typeof frontmatter.description !== "string") {
184
184
  issues.push({ level: "error", message: 'Frontmatter "description" is required and must be a string' });
185
185
  }
186
+ if (frontmatter.skillset !== void 0 && typeof frontmatter.skillset !== "boolean") {
187
+ issues.push({ level: "error", message: 'Frontmatter "skillset" must be a boolean when provided' });
188
+ }
189
+ if (frontmatter.dependencies !== void 0) {
190
+ const deps = frontmatter.dependencies;
191
+ if (!Array.isArray(deps) || deps.some((dep) => typeof dep !== "string")) {
192
+ issues.push({ level: "error", message: 'Frontmatter "dependencies" must be an array of strings when provided' });
193
+ }
194
+ }
186
195
  return { ok: issues.every((i) => i.level !== "error"), issues, frontmatter };
187
196
  }
188
197
 
@@ -254,6 +263,7 @@ import fs5 from "fs";
254
263
  import path5 from "path";
255
264
  import crypto from "crypto";
256
265
  import * as tar from "tar";
266
+ import semver from "semver";
257
267
  var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
258
268
  function parseRegistrySpecifier(input) {
259
269
  const raw = input.trim();
@@ -268,7 +278,7 @@ function parseRegistrySpecifier(input) {
268
278
  if (!/^@[a-z0-9][a-z0-9-]{1,31}\/[a-z0-9][a-z0-9-]{1,63}$/.test(canonicalName)) {
269
279
  throw new SkildError("INVALID_SOURCE", `Invalid skill name "${canonicalName}". Expected @publisher/skill (lowercase letters/digits/dashes).`);
270
280
  }
271
- if (!/^[A-Za-z0-9][A-Za-z0-9.+-]*$/.test(versionOrTag)) {
281
+ if (!versionOrTag || /\s/.test(versionOrTag)) {
272
282
  throw new SkildError("INVALID_SOURCE", `Invalid version or tag "${versionOrTag}".`);
273
283
  }
274
284
  return { canonicalName, versionOrTag };
@@ -294,15 +304,35 @@ function resolveRegistryUrl(explicit) {
294
304
  }
295
305
  async function resolveRegistryVersion(registryUrl, spec) {
296
306
  const { scope, name } = splitCanonicalName(spec.canonicalName);
297
- const url = `${registryUrl}/skills/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/versions/${encodeURIComponent(spec.versionOrTag)}`;
307
+ const versionOrTag = spec.versionOrTag;
308
+ const range = semver.validRange(versionOrTag);
309
+ if (range && !semver.valid(versionOrTag)) {
310
+ const metaUrl = `${registryUrl}/skills/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`;
311
+ const metaRes = await fetchWithTimeout(metaUrl, { headers: { accept: "application/json" } }, 1e4);
312
+ if (!metaRes.ok) {
313
+ const text = await metaRes.text().catch(() => "");
314
+ throw new SkildError("REGISTRY_RESOLVE_FAILED", `Failed to resolve ${spec.canonicalName}@${versionOrTag} (${metaRes.status}). ${text}`.trim());
315
+ }
316
+ const meta = await metaRes.json();
317
+ if (!meta?.ok || !Array.isArray(meta.versions)) {
318
+ throw new SkildError("REGISTRY_RESOLVE_FAILED", `Invalid registry response for ${spec.canonicalName}@${versionOrTag}.`);
319
+ }
320
+ const versions = meta.versions.map((v) => v.version).filter((v) => semver.valid(v));
321
+ const matched = semver.maxSatisfying(versions, range);
322
+ if (!matched) {
323
+ throw new SkildError("REGISTRY_RESOLVE_FAILED", `No published version satisfies ${spec.canonicalName}@${versionOrTag}.`);
324
+ }
325
+ return resolveRegistryVersion(registryUrl, { canonicalName: spec.canonicalName, versionOrTag: matched });
326
+ }
327
+ const url = `${registryUrl}/skills/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionOrTag)}`;
298
328
  const res = await fetchWithTimeout(url, { headers: { accept: "application/json" } }, 1e4);
299
329
  if (!res.ok) {
300
330
  const text = await res.text().catch(() => "");
301
- throw new SkildError("REGISTRY_RESOLVE_FAILED", `Failed to resolve ${spec.canonicalName}@${spec.versionOrTag} (${res.status}). ${text}`.trim());
331
+ throw new SkildError("REGISTRY_RESOLVE_FAILED", `Failed to resolve ${spec.canonicalName}@${versionOrTag} (${res.status}). ${text}`.trim());
302
332
  }
303
333
  const json = await res.json();
304
334
  if (!json?.ok || !json.tarballUrl || !json.integrity || !json.version) {
305
- throw new SkildError("REGISTRY_RESOLVE_FAILED", `Invalid registry response for ${spec.canonicalName}@${spec.versionOrTag}.`);
335
+ throw new SkildError("REGISTRY_RESOLVE_FAILED", `Invalid registry response for ${spec.canonicalName}@${versionOrTag}.`);
306
336
  }
307
337
  const tarballUrl = json.tarballUrl.startsWith("http") ? json.tarballUrl : `${registryUrl}${json.tarballUrl}`;
308
338
  return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
@@ -487,6 +517,117 @@ function toDegitPath(url) {
487
517
  }
488
518
 
489
519
  // src/lifecycle.ts
520
+ function normalizeDependencies(raw) {
521
+ if (raw === void 0 || raw === null) return [];
522
+ if (!Array.isArray(raw)) {
523
+ throw new SkildError("INVALID_DEPENDENCIES", 'Frontmatter "dependencies" must be an array of strings.');
524
+ }
525
+ const out = [];
526
+ const seen = /* @__PURE__ */ new Set();
527
+ for (const item of raw) {
528
+ if (typeof item !== "string") {
529
+ throw new SkildError("INVALID_DEPENDENCIES", 'Frontmatter "dependencies" must be an array of strings.');
530
+ }
531
+ const trimmed = item.trim();
532
+ if (!trimmed) {
533
+ throw new SkildError("INVALID_DEPENDENCIES", "Dependencies cannot contain empty values.");
534
+ }
535
+ if (seen.has(trimmed)) continue;
536
+ seen.add(trimmed);
537
+ out.push(trimmed);
538
+ }
539
+ return out;
540
+ }
541
+ function isRelativeDependency(dep) {
542
+ return dep.startsWith("./") || dep.startsWith("../");
543
+ }
544
+ function resolveInlineDependency(raw, baseDir, rootDir) {
545
+ const resolved = path8.resolve(baseDir, raw);
546
+ const relToRoot = path8.relative(rootDir, resolved);
547
+ if (relToRoot.startsWith("..") || path8.isAbsolute(relToRoot)) {
548
+ throw new SkildError("INVALID_DEPENDENCY", `Inline dependency path escapes the skill root: ${raw}`);
549
+ }
550
+ if (!isDirectory(resolved)) {
551
+ throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a directory: ${raw}`);
552
+ }
553
+ const validation = validateSkillDir(resolved);
554
+ if (!validation.ok) {
555
+ throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a valid skill: ${raw}`, { issues: validation.issues });
556
+ }
557
+ const normalizedInlinePath = relToRoot.split(path8.sep).join("/");
558
+ return {
559
+ sourceType: "inline",
560
+ source: raw,
561
+ name: path8.basename(resolved) || raw,
562
+ inlinePath: normalizedInlinePath,
563
+ inlineDir: resolved
564
+ };
565
+ }
566
+ function normalizeDependencyInput(raw) {
567
+ if (!raw.toLowerCase().startsWith("github:")) return raw;
568
+ const trimmed = raw.slice("github:".length).trim().replace(/^\/+/, "");
569
+ if (!trimmed) {
570
+ throw new SkildError("INVALID_DEPENDENCY", 'Invalid GitHub dependency: missing repo after "github:".');
571
+ }
572
+ return trimmed;
573
+ }
574
+ function parseDependency(raw, baseDir, rootDir) {
575
+ const cleaned = normalizeDependencyInput(raw).trim();
576
+ if (!cleaned) {
577
+ throw new SkildError("INVALID_DEPENDENCY", "Dependencies cannot contain empty values.");
578
+ }
579
+ if (isRelativeDependency(cleaned)) {
580
+ return resolveInlineDependency(cleaned, baseDir, rootDir);
581
+ }
582
+ if (cleaned.startsWith("@")) {
583
+ return { sourceType: "registry", source: cleaned, spec: parseRegistrySpecifier(cleaned) };
584
+ }
585
+ if (/^https?:\/\//i.test(cleaned) || cleaned.includes("github.com")) {
586
+ return { sourceType: "github-url", source: cleaned };
587
+ }
588
+ if (/^[^/]+\/[^/]+/.test(cleaned)) {
589
+ return { sourceType: "degit-shorthand", source: cleaned };
590
+ }
591
+ throw new SkildError("INVALID_DEPENDENCY", `Unsupported dependency source "${raw}".`);
592
+ }
593
+ function getFrontmatterFromDir(dir) {
594
+ const skillMd = readSkillMd(dir);
595
+ if (!skillMd) return null;
596
+ return parseSkillFrontmatter(skillMd);
597
+ }
598
+ function getDependencyKeyFromRecord(record) {
599
+ if (record.sourceType === "registry") {
600
+ return `registry:${record.canonicalName || record.source}`;
601
+ }
602
+ if (record.sourceType === "local") {
603
+ const resolved = resolveLocalPath(record.source);
604
+ return `local:${resolved || record.source}`;
605
+ }
606
+ return `${record.sourceType}:${record.source}`;
607
+ }
608
+ function addDependedBy(record, dependerName) {
609
+ const current = new Set(record.dependedBy || []);
610
+ current.add(dependerName);
611
+ record.dependedBy = Array.from(current).sort();
612
+ writeInstallRecord(record.installDir, record);
613
+ }
614
+ function removeDependedBy(record, dependerName) {
615
+ if (!record.dependedBy) return;
616
+ const next = record.dependedBy.filter((name) => name !== dependerName);
617
+ record.dependedBy = next.length ? next : void 0;
618
+ writeInstallRecord(record.installDir, record);
619
+ }
620
+ function dedupeInstalledDependencies(entries) {
621
+ const seen = /* @__PURE__ */ new Set();
622
+ const out = [];
623
+ for (const entry of entries) {
624
+ const key = entry.sourceType === "inline" ? `inline:${entry.inlinePath || entry.name}` : `external:${entry.installDir || entry.source}`;
625
+ if (seen.has(key)) continue;
626
+ seen.add(key);
627
+ out.push(entry);
628
+ }
629
+ return out;
630
+ }
490
631
  async function cloneRemote(degitSrc, targetPath) {
491
632
  const emitter = degit(degitSrc, { force: true, verbose: false });
492
633
  await emitter.clone(targetPath);
@@ -515,7 +656,7 @@ function resolvePlatformAndScope(options) {
515
656
  scope: options.scope || config.defaultScope
516
657
  };
517
658
  }
518
- async function installSkill(input, options = {}) {
659
+ async function installSkillBase(input, options = {}) {
519
660
  const { platform, scope } = resolvePlatformAndScope(options);
520
661
  const source = input.source;
521
662
  const sourceType = classifySource(source);
@@ -578,7 +719,7 @@ async function installSkill(input, options = {}) {
578
719
  removeDir(tempRoot);
579
720
  }
580
721
  }
581
- async function installRegistrySkill(input, options = {}) {
722
+ async function installRegistrySkillBase(input, options = {}, resolved) {
582
723
  const { platform, scope } = resolvePlatformAndScope(options);
583
724
  const registryUrl = resolveRegistryUrl(input.registryUrl);
584
725
  const spec = parseRegistrySpecifier(input.spec);
@@ -596,8 +737,8 @@ async function installRegistrySkill(input, options = {}) {
596
737
  const tempRoot = createTempDir(skillsDir, installName);
597
738
  const stagingDir = path8.join(tempRoot, "staging");
598
739
  try {
599
- const resolved = await resolveRegistryVersion(registryUrl, spec);
600
- await downloadAndExtractTarball(resolved, tempRoot, stagingDir);
740
+ const resolvedVersion = resolved || await resolveRegistryVersion(registryUrl, spec);
741
+ await downloadAndExtractTarball(resolvedVersion, tempRoot, stagingDir);
601
742
  assertNonEmptyInstall(stagingDir, input.spec);
602
743
  replaceDirAtomic(stagingDir, installDir);
603
744
  const contentHash = hashDirectoryContent(installDir);
@@ -611,6 +752,7 @@ async function installRegistrySkill(input, options = {}) {
611
752
  scope,
612
753
  source: input.spec,
613
754
  sourceType: "registry",
755
+ resolvedVersion: resolvedVersion.version,
614
756
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
615
757
  installDir,
616
758
  contentHash,
@@ -635,6 +777,196 @@ async function installRegistrySkill(input, options = {}) {
635
777
  removeDir(tempRoot);
636
778
  }
637
779
  }
780
+ function createInstallContext(options, registryUrl) {
781
+ const { platform, scope } = resolvePlatformAndScope(options);
782
+ return {
783
+ platform,
784
+ scope,
785
+ force: Boolean(options.force),
786
+ registryUrl,
787
+ active: /* @__PURE__ */ new Set(),
788
+ inlineActive: /* @__PURE__ */ new Set()
789
+ };
790
+ }
791
+ function formatVersionConflict(input) {
792
+ const lines = [
793
+ "Version conflict detected",
794
+ ` Installed: ${input.name}@${input.installedVersion || "unknown"} (required by ${input.installedBy})`,
795
+ ` Requested: ${input.name}@${input.requested} (required by ${input.requestedBy})`,
796
+ "",
797
+ "Please resolve manually."
798
+ ];
799
+ return lines.join("\n");
800
+ }
801
+ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
802
+ if (dep.sourceType === "registry") {
803
+ const registryUrl = resolveRegistryUrl(ctx.registryUrl);
804
+ const spec = dep.spec || parseRegistrySpecifier(dep.source);
805
+ const resolved = await resolveRegistryVersion(registryUrl, spec);
806
+ const installName2 = canonicalNameToInstallDirName(spec.canonicalName);
807
+ const installDir2 = getSkillInstallDir(ctx.platform, ctx.scope, installName2);
808
+ if (fs7.existsSync(installDir2) && !ctx.force) {
809
+ const existing = readInstallRecord(installDir2);
810
+ if (!existing) {
811
+ throw new SkildError(
812
+ "MISSING_METADATA",
813
+ `Skill "${spec.canonicalName}" is missing install metadata (.skild/install.json). Use --force to reinstall.`,
814
+ { installDir: installDir2 }
815
+ );
816
+ }
817
+ const installedVersion = existing.resolvedVersion || existing.skill?.frontmatter?.version;
818
+ if (installedVersion && installedVersion !== resolved.version) {
819
+ const installedBy = existing.dependedBy?.length ? existing.dependedBy.join(", ") : "unknown";
820
+ throw new SkildError(
821
+ "VERSION_CONFLICT",
822
+ formatVersionConflict({
823
+ name: spec.canonicalName,
824
+ installedVersion,
825
+ requested: spec.versionOrTag,
826
+ installedBy,
827
+ requestedBy: dependerName
828
+ }),
829
+ { installDir: installDir2 }
830
+ );
831
+ }
832
+ addDependedBy(existing, dependerName);
833
+ await processDependenciesForSkill(existing, ctx);
834
+ return existing;
835
+ }
836
+ const record2 = await installRegistrySkillBase(
837
+ { spec: dep.source, registryUrl },
838
+ { platform: ctx.platform, scope: ctx.scope, force: ctx.force },
839
+ resolved
840
+ );
841
+ addDependedBy(record2, dependerName);
842
+ await processDependenciesForSkill(record2, ctx);
843
+ return record2;
844
+ }
845
+ if (dep.sourceType === "local") {
846
+ throw new SkildError("INVALID_DEPENDENCY", `Dependencies cannot reference local absolute paths: ${dep.source}`);
847
+ }
848
+ const installName = extractSkillName(dep.source);
849
+ const installDir = getSkillInstallDir(ctx.platform, ctx.scope, installName);
850
+ if (fs7.existsSync(installDir) && !ctx.force) {
851
+ const existing = readInstallRecord(installDir);
852
+ if (!existing) {
853
+ throw new SkildError(
854
+ "MISSING_METADATA",
855
+ `Skill "${installName}" is missing install metadata (.skild/install.json). Use --force to reinstall.`,
856
+ { installDir }
857
+ );
858
+ }
859
+ if (existing.source !== dep.source) {
860
+ throw new SkildError(
861
+ "DEPENDENCY_CONFLICT",
862
+ `Dependency conflict detected for "${installName}". Installed from ${existing.source}, requested ${dep.source}.`,
863
+ { installDir }
864
+ );
865
+ }
866
+ addDependedBy(existing, dependerName);
867
+ await processDependenciesForSkill(existing, ctx);
868
+ return existing;
869
+ }
870
+ const record = await installSkillBase(
871
+ { source: dep.source },
872
+ { platform: ctx.platform, scope: ctx.scope, force: ctx.force }
873
+ );
874
+ addDependedBy(record, dependerName);
875
+ await processDependenciesForSkill(record, ctx);
876
+ return record;
877
+ }
878
+ async function processInlineDependencies(inlineDir, rootDir, ctx, dependerName) {
879
+ const key = `inline:${inlineDir}`;
880
+ if (ctx.inlineActive.has(key)) {
881
+ throw new SkildError("DEPENDENCY_CYCLE", `Inline dependency cycle detected at ${inlineDir}`);
882
+ }
883
+ ctx.inlineActive.add(key);
884
+ try {
885
+ const frontmatter = getFrontmatterFromDir(inlineDir);
886
+ if (!frontmatter) return [];
887
+ const dependencies = normalizeDependencies(frontmatter.dependencies);
888
+ const installedDeps = [];
889
+ for (const depRaw of dependencies) {
890
+ const parsed = parseDependency(depRaw, inlineDir, rootDir);
891
+ if (parsed.sourceType === "inline") {
892
+ installedDeps.push({
893
+ name: parsed.name,
894
+ source: depRaw,
895
+ sourceType: "inline",
896
+ inlinePath: parsed.inlinePath
897
+ });
898
+ const nested = await processInlineDependencies(parsed.inlineDir, rootDir, ctx, dependerName);
899
+ installedDeps.push(...nested);
900
+ } else {
901
+ const depRecord = await ensureExternalDependencyInstalled(parsed, ctx, dependerName);
902
+ installedDeps.push({
903
+ name: depRecord.name,
904
+ source: depRaw,
905
+ sourceType: depRecord.sourceType,
906
+ canonicalName: depRecord.canonicalName,
907
+ installDir: depRecord.installDir
908
+ });
909
+ }
910
+ }
911
+ return installedDeps;
912
+ } finally {
913
+ ctx.inlineActive.delete(key);
914
+ }
915
+ }
916
+ async function processDependenciesForSkill(record, ctx) {
917
+ const key = getDependencyKeyFromRecord(record);
918
+ if (ctx.active.has(key)) {
919
+ throw new SkildError("DEPENDENCY_CYCLE", `Dependency cycle detected for ${record.name}`);
920
+ }
921
+ ctx.active.add(key);
922
+ try {
923
+ const frontmatter = record.skill?.frontmatter || getFrontmatterFromDir(record.installDir);
924
+ if (!frontmatter) return;
925
+ const dependencies = normalizeDependencies(frontmatter.dependencies);
926
+ const skillset = frontmatter.skillset === true;
927
+ const installedDeps = [];
928
+ for (const depRaw of dependencies) {
929
+ const parsed = parseDependency(depRaw, record.installDir, record.installDir);
930
+ if (parsed.sourceType === "inline") {
931
+ installedDeps.push({
932
+ name: parsed.name,
933
+ source: depRaw,
934
+ sourceType: "inline",
935
+ inlinePath: parsed.inlinePath
936
+ });
937
+ const nested = await processInlineDependencies(parsed.inlineDir, record.installDir, ctx, record.name);
938
+ installedDeps.push(...nested);
939
+ } else {
940
+ const depRecord = await ensureExternalDependencyInstalled(parsed, ctx, record.name);
941
+ installedDeps.push({
942
+ name: depRecord.name,
943
+ source: depRaw,
944
+ sourceType: depRecord.sourceType,
945
+ canonicalName: depRecord.canonicalName,
946
+ installDir: depRecord.installDir
947
+ });
948
+ }
949
+ }
950
+ record.skillset = skillset ? true : void 0;
951
+ record.dependencies = dependencies.length ? dependencies : void 0;
952
+ record.installedDependencies = installedDeps.length ? dedupeInstalledDependencies(installedDeps) : void 0;
953
+ writeInstallRecord(record.installDir, record);
954
+ } finally {
955
+ ctx.active.delete(key);
956
+ }
957
+ }
958
+ async function installSkill(input, options = {}) {
959
+ const ctx = createInstallContext(options);
960
+ const record = await installSkillBase(input, options);
961
+ await processDependenciesForSkill(record, ctx);
962
+ return record;
963
+ }
964
+ async function installRegistrySkill(input, options = {}) {
965
+ const ctx = createInstallContext(options, input.registryUrl);
966
+ const record = await installRegistrySkillBase(input, options);
967
+ await processDependenciesForSkill(record, ctx);
968
+ return record;
969
+ }
638
970
  function listSkills(options = {}) {
639
971
  const { platform, scope } = resolvePlatformAndScope(options);
640
972
  const skillsDir = getSkillsDir(platform, scope);
@@ -675,7 +1007,14 @@ function validateSkill(nameOrPath, options = {}) {
675
1007
  return validateSkillDir(dir);
676
1008
  }
677
1009
  function uninstallSkill(name, options = {}) {
1010
+ const visited = /* @__PURE__ */ new Set();
1011
+ uninstallSkillInternal(name, options, visited);
1012
+ }
1013
+ function uninstallSkillInternal(name, options, visited) {
678
1014
  const { platform, scope } = resolvePlatformAndScope(options);
1015
+ const key = `${platform}:${scope}:${name}`;
1016
+ if (visited.has(key)) return;
1017
+ visited.add(key);
679
1018
  const installDir = getSkillInstallDir(platform, scope, name);
680
1019
  if (!fs7.existsSync(installDir)) {
681
1020
  throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
@@ -684,6 +1023,19 @@ function uninstallSkill(name, options = {}) {
684
1023
  if (!record && !options.allowMissingMetadata) {
685
1024
  throw new SkildError("MISSING_METADATA", `Skill "${name}" is missing install metadata. Use --force to uninstall anyway.`, { name, installDir });
686
1025
  }
1026
+ const dependerName = record?.name || name;
1027
+ if (record?.installedDependencies?.length) {
1028
+ for (const dep of record.installedDependencies) {
1029
+ if (dep.sourceType === "inline") continue;
1030
+ const depInstallDir = dep.installDir || getSkillInstallDir(platform, scope, dep.name);
1031
+ const depRecord = readInstallRecord(depInstallDir);
1032
+ if (!depRecord) continue;
1033
+ removeDependedBy(depRecord, dependerName);
1034
+ if (options.withDeps && (!depRecord.dependedBy || depRecord.dependedBy.length === 0)) {
1035
+ uninstallSkillInternal(depRecord.name, options, visited);
1036
+ }
1037
+ }
1038
+ }
687
1039
  removeDir(installDir);
688
1040
  removeLockEntry(scope, name);
689
1041
  }
@@ -699,6 +1051,7 @@ async function updateSkill(name, options = {}) {
699
1051
  { platform, scope, force: true }
700
1052
  ) : await installSkill({ source: record.source, nameOverride: record.name }, { platform, scope, force: true });
701
1053
  updated.installedAt = record.installedAt;
1054
+ updated.dependedBy = record.dependedBy;
702
1055
  updated.updatedAt = now;
703
1056
  writeInstallRecord(updated.installDir, updated);
704
1057
  results.push(updated);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skild/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
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",
@@ -19,11 +19,13 @@
19
19
  "dependencies": {
20
20
  "degit": "^2.8.4",
21
21
  "js-yaml": "^4.1.0",
22
+ "semver": "^7.6.3",
22
23
  "tar": "^7.4.3"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/js-yaml": "^4.0.9",
26
27
  "@types/node": "^20.10.0",
28
+ "@types/semver": "^7.5.8",
27
29
  "tsup": "^8.0.0",
28
30
  "typescript": "^5.3.0"
29
31
  },