@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 +24 -2
- package/dist/index.js +363 -8
- package/package.json +3 -1
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 (
|
|
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
|
|
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}@${
|
|
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}@${
|
|
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
|
|
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
|
|
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
|
|
600
|
-
await downloadAndExtractTarball(
|
|
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.
|
|
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
|
},
|