@skild/core 0.1.3 → 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 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 path7 from "path";
212
- import fs6 from "fs";
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 path6 from "path";
362
+ import path7 from "path";
217
363
 
218
364
  // src/fs.ts
219
- import fs5 from "fs";
220
- import path5 from "path";
221
- import crypto from "crypto";
365
+ import fs6 from "fs";
366
+ import path6 from "path";
367
+ import crypto2 from "crypto";
222
368
  function pathExists(filePath) {
223
- return fs5.existsSync(filePath);
369
+ return fs6.existsSync(filePath);
224
370
  }
225
371
  function isDirectory(filePath) {
226
372
  try {
227
- return fs5.statSync(filePath).isDirectory();
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 = fs5.readdirSync(dir);
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
- fs5.cpSync(src, dest, { recursive: true });
387
+ fs6.cpSync(src, dest, { recursive: true });
242
388
  }
243
389
  function removeDir(dir) {
244
- if (fs5.existsSync(dir)) fs5.rmSync(dir, { recursive: true, force: true });
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 = path5.join(parentDir, `.skild-${safePrefix}-`);
253
- return fs5.mkdtempSync(template);
398
+ const template = path6.join(parentDir, `.skild-${safePrefix}-`);
399
+ return fs6.mkdtempSync(template);
254
400
  }
255
401
  function replaceDirAtomic(sourceDir, destDir) {
256
- const backupDir = fs5.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
402
+ const backupDir = fs6.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
257
403
  try {
258
- if (backupDir) fs5.renameSync(destDir, backupDir);
259
- fs5.renameSync(sourceDir, destDir);
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 (!fs5.existsSync(destDir) && backupDir && fs5.existsSync(backupDir)) {
264
- fs5.renameSync(backupDir, destDir);
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 (fs5.existsSync(sourceDir)) removeDir(sourceDir);
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 = fs5.readdirSync(current, { withFileTypes: true });
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 = path5.join(current, entry.name);
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 = crypto.createHash("sha256");
440
+ const h = crypto2.createHash("sha256");
295
441
  for (const filePath of files) {
296
- const rel = path5.relative(rootDir, filePath);
442
+ const rel = path6.relative(rootDir, filePath);
297
443
  h.update(rel);
298
444
  h.update("\0");
299
- h.update(fs5.readFileSync(filePath));
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 = path6.resolve(source);
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 path6.basename(local) || "unknown-skill";
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 (fs6.existsSync(installDir) && !options.force) {
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 = path7.join(tempRoot, "staging");
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: fs6.existsSync(path7.join(installDir, "SKILL.md")),
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 (!fs6.existsSync(skillsDir)) return [];
439
- const entries = fs6.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
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 = path7.join(skillsDir, e.name);
442
- const hasSkillMd = fs6.existsSync(path7.join(dir, "SKILL.md"));
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 (!fs6.existsSync(installDir)) {
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 (!fs6.existsSync(installDir)) {
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 updated = await installSkill({ source: record.source, nameOverride: record.name }, { platform, scope, force: true });
494
- updated.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
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",
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",