@skild/core 0.2.1 → 0.2.4

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