@skild/core 0.1.2 → 0.1.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 +64 -3
- package/dist/index.js +258 -37
- package/package.json +3 -2
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';
|
|
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';
|
|
2
2
|
declare class SkildError extends Error {
|
|
3
3
|
readonly code: SkildErrorCode;
|
|
4
4
|
readonly details?: Record<string, unknown>;
|
|
@@ -8,7 +8,7 @@ declare class SkildError extends Error {
|
|
|
8
8
|
declare const PLATFORMS: readonly ["claude", "codex", "copilot"];
|
|
9
9
|
type Platform = (typeof PLATFORMS)[number];
|
|
10
10
|
type InstallScope = 'global' | 'project';
|
|
11
|
-
type SourceType = 'local' | 'github-url' | 'degit-shorthand';
|
|
11
|
+
type SourceType = 'local' | 'github-url' | 'degit-shorthand' | 'registry';
|
|
12
12
|
interface InstallOptions {
|
|
13
13
|
platform?: Platform;
|
|
14
14
|
scope?: InstallScope;
|
|
@@ -42,6 +42,16 @@ interface SkillValidationResult {
|
|
|
42
42
|
interface InstallRecord {
|
|
43
43
|
schemaVersion: 1;
|
|
44
44
|
name: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional stable identifier for display and CLI input.
|
|
47
|
+
* Example: "@publisher/skill" (directory name remains filesystem-safe).
|
|
48
|
+
*/
|
|
49
|
+
canonicalName?: string;
|
|
50
|
+
/**
|
|
51
|
+
* For `sourceType === "registry"`, the base registry URL used when resolving this Skill.
|
|
52
|
+
* Example: "https://registry.skild.sh"
|
|
53
|
+
*/
|
|
54
|
+
registryUrl?: string;
|
|
45
55
|
platform: Platform;
|
|
46
56
|
scope: InstallScope;
|
|
47
57
|
source: string;
|
|
@@ -62,6 +72,7 @@ interface LockEntry {
|
|
|
62
72
|
scope: InstallScope;
|
|
63
73
|
source: string;
|
|
64
74
|
sourceType: SourceType;
|
|
75
|
+
registryUrl?: string;
|
|
65
76
|
installedAt: string;
|
|
66
77
|
updatedAt?: string;
|
|
67
78
|
installDir: string;
|
|
@@ -77,8 +88,22 @@ interface GlobalConfig {
|
|
|
77
88
|
defaultPlatform: Platform;
|
|
78
89
|
defaultScope: InstallScope;
|
|
79
90
|
}
|
|
91
|
+
interface RegistryAuth {
|
|
92
|
+
schemaVersion: 1;
|
|
93
|
+
registryUrl: string;
|
|
94
|
+
token: string;
|
|
95
|
+
publisher?: {
|
|
96
|
+
id?: string;
|
|
97
|
+
handle?: string;
|
|
98
|
+
email?: string;
|
|
99
|
+
};
|
|
100
|
+
updatedAt: string;
|
|
101
|
+
}
|
|
80
102
|
|
|
81
103
|
declare function loadOrCreateGlobalConfig(): GlobalConfig;
|
|
104
|
+
declare function loadRegistryAuth(): RegistryAuth | null;
|
|
105
|
+
declare function saveRegistryAuth(auth: RegistryAuth): void;
|
|
106
|
+
declare function clearRegistryAuth(): void;
|
|
82
107
|
|
|
83
108
|
declare function getSkillsDir(platform: Platform, scope: InstallScope): string;
|
|
84
109
|
declare function getSkillInstallDir(platform: Platform, scope: InstallScope, skillName: string): string;
|
|
@@ -92,11 +117,47 @@ interface InitOptions {
|
|
|
92
117
|
}
|
|
93
118
|
declare function initSkill(name: string, options?: InitOptions): string;
|
|
94
119
|
|
|
120
|
+
declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
121
|
+
|
|
122
|
+
declare const DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
123
|
+
interface RegistrySpecifier {
|
|
124
|
+
canonicalName: string;
|
|
125
|
+
versionOrTag: string;
|
|
126
|
+
}
|
|
127
|
+
interface RegistryResolvedVersion {
|
|
128
|
+
canonicalName: string;
|
|
129
|
+
version: string;
|
|
130
|
+
integrity: string;
|
|
131
|
+
tarballUrl: string;
|
|
132
|
+
publishedAt?: string;
|
|
133
|
+
}
|
|
134
|
+
declare function parseRegistrySpecifier(input: string): RegistrySpecifier;
|
|
135
|
+
declare function canonicalNameToInstallDirName(canonicalName: string): string;
|
|
136
|
+
declare function splitCanonicalName(canonicalName: string): {
|
|
137
|
+
scope: string;
|
|
138
|
+
name: string;
|
|
139
|
+
};
|
|
140
|
+
declare function resolveRegistryUrl(explicit?: string): string;
|
|
141
|
+
declare function resolveRegistryVersion(registryUrl: string, spec: RegistrySpecifier): Promise<RegistryResolvedVersion>;
|
|
142
|
+
declare function searchRegistrySkills(registryUrl: string, query: string, limit?: number): Promise<Array<{
|
|
143
|
+
name: string;
|
|
144
|
+
description?: string;
|
|
145
|
+
targets_json?: string;
|
|
146
|
+
created_at?: string;
|
|
147
|
+
updated_at?: string;
|
|
148
|
+
}>>;
|
|
149
|
+
declare function downloadAndExtractTarball(resolved: RegistryResolvedVersion, tempRoot: string, stagingDir: string): Promise<void>;
|
|
150
|
+
|
|
95
151
|
interface InstallInput {
|
|
96
152
|
source: string;
|
|
97
153
|
nameOverride?: string;
|
|
98
154
|
}
|
|
99
155
|
declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
|
|
156
|
+
declare function installRegistrySkill(input: {
|
|
157
|
+
spec: string;
|
|
158
|
+
registryUrl?: string;
|
|
159
|
+
nameOverride?: string;
|
|
160
|
+
}, options?: InstallOptions): Promise<InstallRecord>;
|
|
100
161
|
interface ListedSkill {
|
|
101
162
|
name: string;
|
|
102
163
|
installDir: string;
|
|
@@ -121,4 +182,4 @@ declare function uninstallSkill(name: string, options?: InstallOptions & {
|
|
|
121
182
|
}): void;
|
|
122
183
|
declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
|
|
123
184
|
|
|
124
|
-
export { type GlobalConfig, type InstallOptions, type InstallRecord, type InstallScope, type ListOptions, type Lockfile, PLATFORMS, type Platform, SkildError, type SkillFrontmatter, type SkillValidationIssue, type SkillValidationResult, type UpdateOptions, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installSkill, listAllSkills, listSkills, loadOrCreateGlobalConfig, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,9 @@ function getProjectLockPath() {
|
|
|
52
52
|
function getGlobalConfigPath() {
|
|
53
53
|
return path.join(getSkildGlobalDir(), "config.json");
|
|
54
54
|
}
|
|
55
|
+
function getGlobalRegistryAuthPath() {
|
|
56
|
+
return path.join(getSkildGlobalDir(), "registry-auth.json");
|
|
57
|
+
}
|
|
55
58
|
function getGlobalLockPath() {
|
|
56
59
|
return path.join(getSkildGlobalDir(), "lock.json");
|
|
57
60
|
}
|
|
@@ -74,6 +77,14 @@ function writeJsonFile(filePath, value) {
|
|
|
74
77
|
ensureDir(path2.dirname(filePath));
|
|
75
78
|
fs2.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
76
79
|
}
|
|
80
|
+
function writeJsonFilePrivate(filePath, value) {
|
|
81
|
+
ensureDir(path2.dirname(filePath));
|
|
82
|
+
fs2.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
83
|
+
try {
|
|
84
|
+
fs2.chmodSync(filePath, 384);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
77
88
|
function loadOrCreateGlobalConfig() {
|
|
78
89
|
const filePath = getGlobalConfigPath();
|
|
79
90
|
const existing = readJsonFile(filePath);
|
|
@@ -86,6 +97,16 @@ function loadOrCreateGlobalConfig() {
|
|
|
86
97
|
writeJsonFile(filePath, created);
|
|
87
98
|
return created;
|
|
88
99
|
}
|
|
100
|
+
function loadRegistryAuth() {
|
|
101
|
+
return readJsonFile(getGlobalRegistryAuthPath());
|
|
102
|
+
}
|
|
103
|
+
function saveRegistryAuth(auth) {
|
|
104
|
+
writeJsonFilePrivate(getGlobalRegistryAuthPath(), auth);
|
|
105
|
+
}
|
|
106
|
+
function clearRegistryAuth() {
|
|
107
|
+
const filePath = getGlobalRegistryAuthPath();
|
|
108
|
+
if (fs2.existsSync(filePath)) fs2.rmSync(filePath);
|
|
109
|
+
}
|
|
89
110
|
function loadLockfile(lockPath) {
|
|
90
111
|
return readJsonFile(lockPath);
|
|
91
112
|
}
|
|
@@ -207,41 +228,166 @@ echo "${name}: run script placeholder"
|
|
|
207
228
|
return targetDir;
|
|
208
229
|
}
|
|
209
230
|
|
|
231
|
+
// src/http.ts
|
|
232
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = 1e4) {
|
|
233
|
+
const ms = Math.max(1, timeoutMs);
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const existingSignal = init.signal;
|
|
236
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetch(input, { ...init, signal: controller.signal });
|
|
239
|
+
return res;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (existingSignal?.aborted) throw error;
|
|
242
|
+
const name = error instanceof Error ? error.name : "";
|
|
243
|
+
if (name === "AbortError") {
|
|
244
|
+
throw new SkildError("NETWORK_TIMEOUT", `Request timed out after ${ms}ms.`);
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/registry.ts
|
|
253
|
+
import fs5 from "fs";
|
|
254
|
+
import path5 from "path";
|
|
255
|
+
import crypto from "crypto";
|
|
256
|
+
import * as tar from "tar";
|
|
257
|
+
var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
258
|
+
function parseRegistrySpecifier(input) {
|
|
259
|
+
const raw = input.trim();
|
|
260
|
+
if (!raw.startsWith("@") || !raw.includes("/")) {
|
|
261
|
+
throw new SkildError("INVALID_SOURCE", `Invalid registry specifier "${input}". Expected @publisher/skill[@version].`);
|
|
262
|
+
}
|
|
263
|
+
const slash = raw.indexOf("/");
|
|
264
|
+
const at = raw.lastIndexOf("@");
|
|
265
|
+
const hasVersion = at > slash;
|
|
266
|
+
const canonicalName = hasVersion ? raw.slice(0, at) : raw;
|
|
267
|
+
const versionOrTag = hasVersion ? raw.slice(at + 1) : "latest";
|
|
268
|
+
if (!/^@[a-z0-9][a-z0-9-]{1,31}\/[a-z0-9][a-z0-9-]{1,63}$/.test(canonicalName)) {
|
|
269
|
+
throw new SkildError("INVALID_SOURCE", `Invalid skill name "${canonicalName}". Expected @publisher/skill (lowercase letters/digits/dashes).`);
|
|
270
|
+
}
|
|
271
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.+-]*$/.test(versionOrTag)) {
|
|
272
|
+
throw new SkildError("INVALID_SOURCE", `Invalid version or tag "${versionOrTag}".`);
|
|
273
|
+
}
|
|
274
|
+
return { canonicalName, versionOrTag };
|
|
275
|
+
}
|
|
276
|
+
function canonicalNameToInstallDirName(canonicalName) {
|
|
277
|
+
const match = canonicalName.match(/^@([^/]+)\/(.+)$/);
|
|
278
|
+
if (!match) return canonicalName;
|
|
279
|
+
const [, scope, name] = match;
|
|
280
|
+
return `${scope}__${name}`;
|
|
281
|
+
}
|
|
282
|
+
function splitCanonicalName(canonicalName) {
|
|
283
|
+
const match = canonicalName.match(/^@([^/]+)\/(.+)$/);
|
|
284
|
+
if (!match) {
|
|
285
|
+
throw new SkildError("INVALID_SOURCE", `Invalid skill name "${canonicalName}". Expected @publisher/skill.`);
|
|
286
|
+
}
|
|
287
|
+
return { scope: match[1], name: match[2] };
|
|
288
|
+
}
|
|
289
|
+
function resolveRegistryUrl(explicit) {
|
|
290
|
+
const fromEnv = process.env.SKILD_REGISTRY_URL?.trim();
|
|
291
|
+
if (explicit?.trim()) return explicit.trim().replace(/\/+$/, "");
|
|
292
|
+
if (fromEnv) return fromEnv.replace(/\/+$/, "");
|
|
293
|
+
return DEFAULT_REGISTRY_URL;
|
|
294
|
+
}
|
|
295
|
+
async function resolveRegistryVersion(registryUrl, spec) {
|
|
296
|
+
const { scope, name } = splitCanonicalName(spec.canonicalName);
|
|
297
|
+
const url = `${registryUrl}/skills/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/versions/${encodeURIComponent(spec.versionOrTag)}`;
|
|
298
|
+
const res = await fetchWithTimeout(url, { headers: { accept: "application/json" } }, 1e4);
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
const text = await res.text().catch(() => "");
|
|
301
|
+
throw new SkildError("REGISTRY_RESOLVE_FAILED", `Failed to resolve ${spec.canonicalName}@${spec.versionOrTag} (${res.status}). ${text}`.trim());
|
|
302
|
+
}
|
|
303
|
+
const json = await res.json();
|
|
304
|
+
if (!json?.ok || !json.tarballUrl || !json.integrity || !json.version) {
|
|
305
|
+
throw new SkildError("REGISTRY_RESOLVE_FAILED", `Invalid registry response for ${spec.canonicalName}@${spec.versionOrTag}.`);
|
|
306
|
+
}
|
|
307
|
+
const tarballUrl = json.tarballUrl.startsWith("http") ? json.tarballUrl : `${registryUrl}${json.tarballUrl}`;
|
|
308
|
+
return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
|
|
309
|
+
}
|
|
310
|
+
function sha256Hex(buffer) {
|
|
311
|
+
const h = crypto.createHash("sha256");
|
|
312
|
+
h.update(buffer);
|
|
313
|
+
return h.digest("hex");
|
|
314
|
+
}
|
|
315
|
+
async function searchRegistrySkills(registryUrl, query, limit = 50) {
|
|
316
|
+
const q = query.trim();
|
|
317
|
+
const url = new URL(`${registryUrl}/skills`);
|
|
318
|
+
if (q) url.searchParams.set("q", q);
|
|
319
|
+
url.searchParams.set("limit", String(Math.min(Math.max(limit, 1), 100)));
|
|
320
|
+
const res = await fetchWithTimeout(url.toString(), { headers: { accept: "application/json" } }, 1e4);
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
const text = await res.text().catch(() => "");
|
|
323
|
+
throw new SkildError("REGISTRY_RESOLVE_FAILED", `Failed to search skills (${res.status}). ${text}`.trim());
|
|
324
|
+
}
|
|
325
|
+
const json = await res.json();
|
|
326
|
+
if (!json?.ok || !Array.isArray(json.skills)) {
|
|
327
|
+
throw new SkildError("REGISTRY_RESOLVE_FAILED", "Invalid registry response for /skills.");
|
|
328
|
+
}
|
|
329
|
+
return json.skills;
|
|
330
|
+
}
|
|
331
|
+
async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
332
|
+
const res = await fetchWithTimeout(resolved.tarballUrl, {}, 3e4);
|
|
333
|
+
if (!res.ok) {
|
|
334
|
+
const text = await res.text().catch(() => "");
|
|
335
|
+
throw new SkildError("REGISTRY_DOWNLOAD_FAILED", `Failed to download tarball (${res.status}). ${text}`.trim());
|
|
336
|
+
}
|
|
337
|
+
const arrayBuf = await res.arrayBuffer();
|
|
338
|
+
const buf = Buffer.from(arrayBuf);
|
|
339
|
+
const computed = sha256Hex(buf);
|
|
340
|
+
if (computed !== resolved.integrity) {
|
|
341
|
+
throw new SkildError(
|
|
342
|
+
"INTEGRITY_MISMATCH",
|
|
343
|
+
`Integrity mismatch for ${resolved.canonicalName}@${resolved.version}. Expected ${resolved.integrity}, got ${computed}.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const tarballPath = path5.join(tempRoot, "skill.tgz");
|
|
347
|
+
fs5.mkdirSync(stagingDir, { recursive: true });
|
|
348
|
+
fs5.writeFileSync(tarballPath, buf);
|
|
349
|
+
try {
|
|
350
|
+
await tar.x({ file: tarballPath, cwd: stagingDir, gzip: true });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
210
356
|
// src/lifecycle.ts
|
|
211
|
-
import
|
|
212
|
-
import
|
|
357
|
+
import path8 from "path";
|
|
358
|
+
import fs7 from "fs";
|
|
213
359
|
import degit from "degit";
|
|
214
360
|
|
|
215
361
|
// src/source.ts
|
|
216
|
-
import
|
|
362
|
+
import path7 from "path";
|
|
217
363
|
|
|
218
364
|
// src/fs.ts
|
|
219
|
-
import
|
|
220
|
-
import
|
|
221
|
-
import
|
|
365
|
+
import fs6 from "fs";
|
|
366
|
+
import path6 from "path";
|
|
367
|
+
import crypto2 from "crypto";
|
|
222
368
|
function pathExists(filePath) {
|
|
223
|
-
return
|
|
369
|
+
return fs6.existsSync(filePath);
|
|
224
370
|
}
|
|
225
371
|
function isDirectory(filePath) {
|
|
226
372
|
try {
|
|
227
|
-
return
|
|
373
|
+
return fs6.statSync(filePath).isDirectory();
|
|
228
374
|
} catch {
|
|
229
375
|
return false;
|
|
230
376
|
}
|
|
231
377
|
}
|
|
232
378
|
function isDirEmpty(dir) {
|
|
233
379
|
try {
|
|
234
|
-
const entries =
|
|
380
|
+
const entries = fs6.readdirSync(dir);
|
|
235
381
|
return entries.length === 0;
|
|
236
382
|
} catch {
|
|
237
383
|
return true;
|
|
238
384
|
}
|
|
239
385
|
}
|
|
240
386
|
function copyDir(src, dest) {
|
|
241
|
-
|
|
387
|
+
fs6.cpSync(src, dest, { recursive: true });
|
|
242
388
|
}
|
|
243
389
|
function removeDir(dir) {
|
|
244
|
-
if (
|
|
390
|
+
if (fs6.existsSync(dir)) fs6.rmSync(dir, { recursive: true, force: true });
|
|
245
391
|
}
|
|
246
392
|
function sanitizeForPathSegment(value) {
|
|
247
393
|
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -249,24 +395,24 @@ function sanitizeForPathSegment(value) {
|
|
|
249
395
|
function createTempDir(parentDir, prefix) {
|
|
250
396
|
ensureDir(parentDir);
|
|
251
397
|
const safePrefix = sanitizeForPathSegment(prefix || "tmp");
|
|
252
|
-
const template =
|
|
253
|
-
return
|
|
398
|
+
const template = path6.join(parentDir, `.skild-${safePrefix}-`);
|
|
399
|
+
return fs6.mkdtempSync(template);
|
|
254
400
|
}
|
|
255
401
|
function replaceDirAtomic(sourceDir, destDir) {
|
|
256
|
-
const backupDir =
|
|
402
|
+
const backupDir = fs6.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
|
|
257
403
|
try {
|
|
258
|
-
if (backupDir)
|
|
259
|
-
|
|
404
|
+
if (backupDir) fs6.renameSync(destDir, backupDir);
|
|
405
|
+
fs6.renameSync(sourceDir, destDir);
|
|
260
406
|
if (backupDir) removeDir(backupDir);
|
|
261
407
|
} catch (error) {
|
|
262
408
|
try {
|
|
263
|
-
if (!
|
|
264
|
-
|
|
409
|
+
if (!fs6.existsSync(destDir) && backupDir && fs6.existsSync(backupDir)) {
|
|
410
|
+
fs6.renameSync(backupDir, destDir);
|
|
265
411
|
}
|
|
266
412
|
} catch {
|
|
267
413
|
}
|
|
268
414
|
try {
|
|
269
|
-
if (
|
|
415
|
+
if (fs6.existsSync(sourceDir)) removeDir(sourceDir);
|
|
270
416
|
} catch {
|
|
271
417
|
}
|
|
272
418
|
throw error;
|
|
@@ -277,11 +423,11 @@ function listFilesRecursive(rootDir) {
|
|
|
277
423
|
const stack = [rootDir];
|
|
278
424
|
while (stack.length) {
|
|
279
425
|
const current = stack.pop();
|
|
280
|
-
const entries =
|
|
426
|
+
const entries = fs6.readdirSync(current, { withFileTypes: true });
|
|
281
427
|
for (const entry of entries) {
|
|
282
428
|
if (entry.name === ".skild") continue;
|
|
283
429
|
if (entry.name === ".git") continue;
|
|
284
|
-
const full =
|
|
430
|
+
const full = path6.join(current, entry.name);
|
|
285
431
|
if (entry.isDirectory()) stack.push(full);
|
|
286
432
|
else if (entry.isFile()) results.push(full);
|
|
287
433
|
}
|
|
@@ -291,12 +437,12 @@ function listFilesRecursive(rootDir) {
|
|
|
291
437
|
}
|
|
292
438
|
function hashDirectoryContent(rootDir) {
|
|
293
439
|
const files = listFilesRecursive(rootDir);
|
|
294
|
-
const h =
|
|
440
|
+
const h = crypto2.createHash("sha256");
|
|
295
441
|
for (const filePath of files) {
|
|
296
|
-
const rel =
|
|
442
|
+
const rel = path6.relative(rootDir, filePath);
|
|
297
443
|
h.update(rel);
|
|
298
444
|
h.update("\0");
|
|
299
|
-
h.update(
|
|
445
|
+
h.update(fs6.readFileSync(filePath));
|
|
300
446
|
h.update("\0");
|
|
301
447
|
}
|
|
302
448
|
return h.digest("hex");
|
|
@@ -304,7 +450,7 @@ function hashDirectoryContent(rootDir) {
|
|
|
304
450
|
|
|
305
451
|
// src/source.ts
|
|
306
452
|
function resolveLocalPath(source) {
|
|
307
|
-
const resolved =
|
|
453
|
+
const resolved = path7.resolve(source);
|
|
308
454
|
return pathExists(resolved) ? resolved : null;
|
|
309
455
|
}
|
|
310
456
|
function classifySource(source) {
|
|
@@ -319,7 +465,7 @@ function classifySource(source) {
|
|
|
319
465
|
}
|
|
320
466
|
function extractSkillName(source) {
|
|
321
467
|
const local = resolveLocalPath(source);
|
|
322
|
-
if (local) return
|
|
468
|
+
if (local) return path7.basename(local) || "unknown-skill";
|
|
323
469
|
const cleaned = source.replace(/[#?].*$/, "");
|
|
324
470
|
const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
|
|
325
471
|
if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
|
|
@@ -377,14 +523,14 @@ async function installSkill(input, options = {}) {
|
|
|
377
523
|
ensureDir(skillsDir);
|
|
378
524
|
const skillName = input.nameOverride || extractSkillName(source);
|
|
379
525
|
const installDir = getSkillInstallDir(platform, scope, skillName);
|
|
380
|
-
if (
|
|
526
|
+
if (fs7.existsSync(installDir) && !options.force) {
|
|
381
527
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
382
528
|
skillName,
|
|
383
529
|
installDir
|
|
384
530
|
});
|
|
385
531
|
}
|
|
386
532
|
const tempRoot = createTempDir(skillsDir, skillName);
|
|
387
|
-
const stagingDir =
|
|
533
|
+
const stagingDir = path8.join(tempRoot, "staging");
|
|
388
534
|
try {
|
|
389
535
|
const localPath = resolveLocalPath(source);
|
|
390
536
|
if (localPath) {
|
|
@@ -408,7 +554,7 @@ async function installSkill(input, options = {}) {
|
|
|
408
554
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
409
555
|
installDir,
|
|
410
556
|
contentHash,
|
|
411
|
-
hasSkillMd:
|
|
557
|
+
hasSkillMd: fs7.existsSync(path8.join(installDir, "SKILL.md")),
|
|
412
558
|
skill: {
|
|
413
559
|
frontmatter: validation.frontmatter,
|
|
414
560
|
validation
|
|
@@ -432,14 +578,71 @@ async function installSkill(input, options = {}) {
|
|
|
432
578
|
removeDir(tempRoot);
|
|
433
579
|
}
|
|
434
580
|
}
|
|
581
|
+
async function installRegistrySkill(input, options = {}) {
|
|
582
|
+
const { platform, scope } = resolvePlatformAndScope(options);
|
|
583
|
+
const registryUrl = resolveRegistryUrl(input.registryUrl);
|
|
584
|
+
const spec = parseRegistrySpecifier(input.spec);
|
|
585
|
+
const canonicalName = spec.canonicalName;
|
|
586
|
+
const skillsDir = getSkillsDir(platform, scope);
|
|
587
|
+
ensureDir(skillsDir);
|
|
588
|
+
const installName = input.nameOverride || canonicalNameToInstallDirName(canonicalName);
|
|
589
|
+
const installDir = getSkillInstallDir(platform, scope, installName);
|
|
590
|
+
if (fs7.existsSync(installDir) && !options.force) {
|
|
591
|
+
throw new SkildError("ALREADY_INSTALLED", `Skill "${canonicalName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
592
|
+
skillName: canonicalName,
|
|
593
|
+
installDir
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const tempRoot = createTempDir(skillsDir, installName);
|
|
597
|
+
const stagingDir = path8.join(tempRoot, "staging");
|
|
598
|
+
try {
|
|
599
|
+
const resolved = await resolveRegistryVersion(registryUrl, spec);
|
|
600
|
+
await downloadAndExtractTarball(resolved, tempRoot, stagingDir);
|
|
601
|
+
assertNonEmptyInstall(stagingDir, input.spec);
|
|
602
|
+
replaceDirAtomic(stagingDir, installDir);
|
|
603
|
+
const contentHash = hashDirectoryContent(installDir);
|
|
604
|
+
const validation = validateSkillDir(installDir);
|
|
605
|
+
const record = {
|
|
606
|
+
schemaVersion: 1,
|
|
607
|
+
name: installName,
|
|
608
|
+
canonicalName,
|
|
609
|
+
registryUrl,
|
|
610
|
+
platform,
|
|
611
|
+
scope,
|
|
612
|
+
source: input.spec,
|
|
613
|
+
sourceType: "registry",
|
|
614
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
615
|
+
installDir,
|
|
616
|
+
contentHash,
|
|
617
|
+
hasSkillMd: fs7.existsSync(path8.join(installDir, "SKILL.md")),
|
|
618
|
+
skill: { validation, frontmatter: validation.frontmatter }
|
|
619
|
+
};
|
|
620
|
+
writeInstallRecord(installDir, record);
|
|
621
|
+
const lockEntry = {
|
|
622
|
+
name: installName,
|
|
623
|
+
platform,
|
|
624
|
+
scope,
|
|
625
|
+
source: input.spec,
|
|
626
|
+
sourceType: "registry",
|
|
627
|
+
registryUrl,
|
|
628
|
+
installedAt: record.installedAt,
|
|
629
|
+
installDir: record.installDir,
|
|
630
|
+
contentHash: record.contentHash
|
|
631
|
+
};
|
|
632
|
+
upsertLockEntry(scope, lockEntry);
|
|
633
|
+
return record;
|
|
634
|
+
} finally {
|
|
635
|
+
removeDir(tempRoot);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
435
638
|
function listSkills(options = {}) {
|
|
436
639
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
437
640
|
const skillsDir = getSkillsDir(platform, scope);
|
|
438
|
-
if (!
|
|
439
|
-
const entries =
|
|
641
|
+
if (!fs7.existsSync(skillsDir)) return [];
|
|
642
|
+
const entries = fs7.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
440
643
|
return entries.map((e) => {
|
|
441
|
-
const dir =
|
|
442
|
-
const hasSkillMd =
|
|
644
|
+
const dir = path8.join(skillsDir, e.name);
|
|
645
|
+
const hasSkillMd = fs7.existsSync(path8.join(dir, "SKILL.md"));
|
|
443
646
|
const record = readInstallRecord(dir);
|
|
444
647
|
return { name: e.name, installDir: dir, hasSkillMd, record };
|
|
445
648
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -457,7 +660,7 @@ function listAllSkills(options = {}) {
|
|
|
457
660
|
function getSkillInfo(name, options = {}) {
|
|
458
661
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
459
662
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
460
|
-
if (!
|
|
663
|
+
if (!fs7.existsSync(installDir)) {
|
|
461
664
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
462
665
|
}
|
|
463
666
|
const record = readInstallRecord(installDir);
|
|
@@ -474,7 +677,7 @@ function validateSkill(nameOrPath, options = {}) {
|
|
|
474
677
|
function uninstallSkill(name, options = {}) {
|
|
475
678
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
476
679
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
477
|
-
if (!
|
|
680
|
+
if (!fs7.existsSync(installDir)) {
|
|
478
681
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
479
682
|
}
|
|
480
683
|
const record = readInstallRecord(installDir);
|
|
@@ -490,24 +693,42 @@ async function updateSkill(name, options = {}) {
|
|
|
490
693
|
const results = [];
|
|
491
694
|
for (const target of targets) {
|
|
492
695
|
const record = getSkillInfo(target.name, { platform, scope });
|
|
493
|
-
const
|
|
494
|
-
updated
|
|
696
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
697
|
+
const updated = record.sourceType === "registry" ? await installRegistrySkill(
|
|
698
|
+
{ spec: record.source, nameOverride: record.name, registryUrl: record.registryUrl || loadRegistryAuth()?.registryUrl },
|
|
699
|
+
{ platform, scope, force: true }
|
|
700
|
+
) : await installSkill({ source: record.source, nameOverride: record.name }, { platform, scope, force: true });
|
|
701
|
+
updated.installedAt = record.installedAt;
|
|
702
|
+
updated.updatedAt = now;
|
|
495
703
|
writeInstallRecord(updated.installDir, updated);
|
|
496
704
|
results.push(updated);
|
|
497
705
|
}
|
|
498
706
|
return results;
|
|
499
707
|
}
|
|
500
708
|
export {
|
|
709
|
+
DEFAULT_REGISTRY_URL,
|
|
501
710
|
PLATFORMS,
|
|
502
711
|
SkildError,
|
|
712
|
+
canonicalNameToInstallDirName,
|
|
713
|
+
clearRegistryAuth,
|
|
714
|
+
downloadAndExtractTarball,
|
|
715
|
+
fetchWithTimeout,
|
|
503
716
|
getSkillInfo,
|
|
504
717
|
getSkillInstallDir,
|
|
505
718
|
getSkillsDir,
|
|
506
719
|
initSkill,
|
|
720
|
+
installRegistrySkill,
|
|
507
721
|
installSkill,
|
|
508
722
|
listAllSkills,
|
|
509
723
|
listSkills,
|
|
510
724
|
loadOrCreateGlobalConfig,
|
|
725
|
+
loadRegistryAuth,
|
|
726
|
+
parseRegistrySpecifier,
|
|
727
|
+
resolveRegistryUrl,
|
|
728
|
+
resolveRegistryVersion,
|
|
729
|
+
saveRegistryAuth,
|
|
730
|
+
searchRegistrySkills,
|
|
731
|
+
splitCanonicalName,
|
|
511
732
|
uninstallSkill,
|
|
512
733
|
updateSkill,
|
|
513
734
|
validateSkill,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skild/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"degit": "^2.8.4",
|
|
21
|
-
"js-yaml": "^4.1.0"
|
|
21
|
+
"js-yaml": "^4.1.0",
|
|
22
|
+
"tar": "^7.4.3"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/js-yaml": "^4.0.9",
|