@skild/core 0.2.1 → 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 +19 -2
- package/dist/index.js +361 -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,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 (
|
|
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
|
|
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}@${
|
|
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}@${
|
|
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
|
|
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
|
|
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
|
|
600
|
-
await downloadAndExtractTarball(
|
|
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.
|
|
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
|
},
|