@skild/core 0.2.8 → 0.4.0
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 +25 -1
- package/dist/index.js +248 -174
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -140,6 +140,24 @@ declare function initSkill(name: string, options?: InitOptions): string;
|
|
|
140
140
|
|
|
141
141
|
declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
142
142
|
|
|
143
|
+
declare function normalizeAlias(input: unknown): string | null;
|
|
144
|
+
declare function isValidAlias(input: string): boolean;
|
|
145
|
+
declare function assertValidAlias(input: string): void;
|
|
146
|
+
|
|
147
|
+
declare function materializeSourceToDir(input: {
|
|
148
|
+
source: string;
|
|
149
|
+
targetDir: string;
|
|
150
|
+
materializedDir?: string | null;
|
|
151
|
+
}): Promise<{
|
|
152
|
+
sourceType: SourceType;
|
|
153
|
+
materializedFrom: string;
|
|
154
|
+
}>;
|
|
155
|
+
declare function materializeSourceToTemp(source: string): Promise<{
|
|
156
|
+
dir: string;
|
|
157
|
+
cleanup: () => void;
|
|
158
|
+
sourceType: SourceType;
|
|
159
|
+
}>;
|
|
160
|
+
|
|
143
161
|
declare const DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
144
162
|
interface RegistrySpecifier {
|
|
145
163
|
canonicalName: string;
|
|
@@ -179,6 +197,12 @@ declare function downloadAndExtractTarball(resolved: RegistryResolvedVersion, te
|
|
|
179
197
|
interface InstallInput {
|
|
180
198
|
source: string;
|
|
181
199
|
nameOverride?: string;
|
|
200
|
+
/**
|
|
201
|
+
* Optional local directory containing the already-fetched source content.
|
|
202
|
+
* When provided, Skild will copy from this directory but still record `source`
|
|
203
|
+
* and `sourceType` based on the original `source` string (useful for multi-skill installs).
|
|
204
|
+
*/
|
|
205
|
+
materializedDir?: string;
|
|
182
206
|
}
|
|
183
207
|
declare function installSkill(input: InstallInput, options?: InstallOptions): Promise<InstallRecord>;
|
|
184
208
|
declare function installRegistrySkill(input: {
|
|
@@ -211,4 +235,4 @@ declare function uninstallSkill(name: string, options?: InstallOptions & {
|
|
|
211
235
|
}): void;
|
|
212
236
|
declare function updateSkill(name?: string, options?: UpdateOptions): Promise<InstallRecord[]>;
|
|
213
237
|
|
|
214
|
-
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, resolveRegistryAlias, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
|
238
|
+
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, assertValidAlias, canonicalNameToInstallDirName, clearRegistryAuth, downloadAndExtractTarball, fetchWithTimeout, getSkillInfo, getSkillInstallDir, getSkillsDir, initSkill, installRegistrySkill, installSkill, isValidAlias, listAllSkills, listSkills, loadOrCreateGlobalConfig, loadRegistryAuth, materializeSourceToDir, materializeSourceToTemp, normalizeAlias, parseRegistrySpecifier, resolveRegistryAlias, resolveRegistryUrl, resolveRegistryVersion, saveRegistryAuth, searchRegistrySkills, splitCanonicalName, uninstallSkill, updateSkill, validateSkill, validateSkillDir };
|
package/dist/index.js
CHANGED
|
@@ -262,10 +262,226 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 1e4) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// src/
|
|
265
|
+
// src/alias.ts
|
|
266
|
+
function normalizeAlias(input) {
|
|
267
|
+
if (typeof input !== "string") return null;
|
|
268
|
+
const v = input.trim();
|
|
269
|
+
return v ? v : null;
|
|
270
|
+
}
|
|
271
|
+
function isValidAlias(input) {
|
|
272
|
+
const alias = input.trim();
|
|
273
|
+
if (!alias) return false;
|
|
274
|
+
if (alias.length < 3 || alias.length > 64) return false;
|
|
275
|
+
if (alias.includes("--")) return false;
|
|
276
|
+
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(alias);
|
|
277
|
+
}
|
|
278
|
+
function assertValidAlias(input) {
|
|
279
|
+
const alias = input.trim();
|
|
280
|
+
if (!alias) throw new SkildError("INVALID_SOURCE", "Missing alias.");
|
|
281
|
+
if (alias.length < 3 || alias.length > 64) {
|
|
282
|
+
throw new SkildError("INVALID_SOURCE", "Alias length must be between 3 and 64.");
|
|
283
|
+
}
|
|
284
|
+
if (alias.includes("--")) {
|
|
285
|
+
throw new SkildError("INVALID_SOURCE", "Invalid alias format: consecutive hyphens are not allowed.");
|
|
286
|
+
}
|
|
287
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(alias)) {
|
|
288
|
+
throw new SkildError("INVALID_SOURCE", "Invalid alias format. Use lowercase letters, numbers, and hyphens.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/materialize.ts
|
|
293
|
+
import fs6 from "fs";
|
|
294
|
+
import os2 from "os";
|
|
295
|
+
import path7 from "path";
|
|
296
|
+
import degit from "degit";
|
|
297
|
+
|
|
298
|
+
// src/source.ts
|
|
299
|
+
import path6 from "path";
|
|
300
|
+
|
|
301
|
+
// src/fs.ts
|
|
266
302
|
import fs5 from "fs";
|
|
267
303
|
import path5 from "path";
|
|
268
304
|
import crypto from "crypto";
|
|
305
|
+
function pathExists(filePath) {
|
|
306
|
+
return fs5.existsSync(filePath);
|
|
307
|
+
}
|
|
308
|
+
function isDirectory(filePath) {
|
|
309
|
+
try {
|
|
310
|
+
return fs5.statSync(filePath).isDirectory();
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function isDirEmpty(dir) {
|
|
316
|
+
try {
|
|
317
|
+
const entries = fs5.readdirSync(dir);
|
|
318
|
+
return entries.length === 0;
|
|
319
|
+
} catch {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function copyDir(src, dest) {
|
|
324
|
+
fs5.cpSync(src, dest, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
function removeDir(dir) {
|
|
327
|
+
if (fs5.existsSync(dir)) fs5.rmSync(dir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
function sanitizeForPathSegment(value) {
|
|
330
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
331
|
+
}
|
|
332
|
+
function createTempDir(parentDir, prefix) {
|
|
333
|
+
ensureDir(parentDir);
|
|
334
|
+
const safePrefix = sanitizeForPathSegment(prefix || "tmp");
|
|
335
|
+
const template = path5.join(parentDir, `.skild-${safePrefix}-`);
|
|
336
|
+
return fs5.mkdtempSync(template);
|
|
337
|
+
}
|
|
338
|
+
function replaceDirAtomic(sourceDir, destDir) {
|
|
339
|
+
const backupDir = fs5.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
|
|
340
|
+
try {
|
|
341
|
+
if (backupDir) fs5.renameSync(destDir, backupDir);
|
|
342
|
+
fs5.renameSync(sourceDir, destDir);
|
|
343
|
+
if (backupDir) removeDir(backupDir);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
try {
|
|
346
|
+
if (!fs5.existsSync(destDir) && backupDir && fs5.existsSync(backupDir)) {
|
|
347
|
+
fs5.renameSync(backupDir, destDir);
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
if (fs5.existsSync(sourceDir)) removeDir(sourceDir);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function listFilesRecursive(rootDir) {
|
|
359
|
+
const results = [];
|
|
360
|
+
const stack = [rootDir];
|
|
361
|
+
while (stack.length) {
|
|
362
|
+
const current = stack.pop();
|
|
363
|
+
const entries = fs5.readdirSync(current, { withFileTypes: true });
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
if (entry.name === ".skild") continue;
|
|
366
|
+
if (entry.name === ".git") continue;
|
|
367
|
+
const full = path5.join(current, entry.name);
|
|
368
|
+
if (entry.isDirectory()) stack.push(full);
|
|
369
|
+
else if (entry.isFile()) results.push(full);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
results.sort();
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
375
|
+
function hashDirectoryContent(rootDir) {
|
|
376
|
+
const files = listFilesRecursive(rootDir);
|
|
377
|
+
const h = crypto.createHash("sha256");
|
|
378
|
+
for (const filePath of files) {
|
|
379
|
+
const rel = path5.relative(rootDir, filePath);
|
|
380
|
+
h.update(rel);
|
|
381
|
+
h.update("\0");
|
|
382
|
+
h.update(fs5.readFileSync(filePath));
|
|
383
|
+
h.update("\0");
|
|
384
|
+
}
|
|
385
|
+
return h.digest("hex");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/source.ts
|
|
389
|
+
function resolveLocalPath(source) {
|
|
390
|
+
const resolved = path6.resolve(source);
|
|
391
|
+
return pathExists(resolved) ? resolved : null;
|
|
392
|
+
}
|
|
393
|
+
function classifySource(source) {
|
|
394
|
+
const local = resolveLocalPath(source);
|
|
395
|
+
if (local) return "local";
|
|
396
|
+
if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
|
|
397
|
+
if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
|
|
398
|
+
throw new SkildError(
|
|
399
|
+
"INVALID_SOURCE",
|
|
400
|
+
`Unsupported source "${source}". Use a Git URL (e.g. https://github.com/owner/repo/tree/<branch>/<subdir>) or degit shorthand (e.g. owner/repo[/subdir][#ref]) or a local directory.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
function extractSkillName(source) {
|
|
404
|
+
const local = resolveLocalPath(source);
|
|
405
|
+
if (local) return path6.basename(local) || "unknown-skill";
|
|
406
|
+
const cleaned = source.replace(/[#?].*$/, "");
|
|
407
|
+
const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
|
|
408
|
+
if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
|
|
409
|
+
const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
|
|
410
|
+
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
411
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
412
|
+
if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
|
|
413
|
+
return cleaned || "unknown-skill";
|
|
414
|
+
}
|
|
415
|
+
function toDegitPath(url) {
|
|
416
|
+
const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
|
|
417
|
+
if (treeMatch) {
|
|
418
|
+
const [, owner, repo, branch, subpath] = treeMatch;
|
|
419
|
+
return `${owner}/${repo}/${subpath}#${branch}`;
|
|
420
|
+
}
|
|
421
|
+
const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
422
|
+
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
423
|
+
return url;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/materialize.ts
|
|
427
|
+
function ensureInstallableDir(sourcePath) {
|
|
428
|
+
if (!isDirectory(sourcePath)) {
|
|
429
|
+
throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function cloneRemote(degitSrc, targetPath) {
|
|
433
|
+
const emitter = degit(degitSrc, { force: true, verbose: false });
|
|
434
|
+
await emitter.clone(targetPath);
|
|
435
|
+
}
|
|
436
|
+
async function materializeSourceToDir(input) {
|
|
437
|
+
const sourceType = classifySource(input.source);
|
|
438
|
+
const targetDir = path7.resolve(input.targetDir);
|
|
439
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
440
|
+
const overridden = input.materializedDir?.trim() ? path7.resolve(input.materializedDir.trim()) : null;
|
|
441
|
+
if (overridden) {
|
|
442
|
+
ensureInstallableDir(overridden);
|
|
443
|
+
copyDir(overridden, targetDir);
|
|
444
|
+
return { sourceType, materializedFrom: overridden };
|
|
445
|
+
}
|
|
446
|
+
const localPath = resolveLocalPath(input.source);
|
|
447
|
+
if (localPath) {
|
|
448
|
+
ensureInstallableDir(localPath);
|
|
449
|
+
copyDir(localPath, targetDir);
|
|
450
|
+
return { sourceType: "local", materializedFrom: localPath };
|
|
451
|
+
}
|
|
452
|
+
const degitPath = toDegitPath(input.source);
|
|
453
|
+
await cloneRemote(degitPath, targetDir);
|
|
454
|
+
return { sourceType, materializedFrom: degitPath };
|
|
455
|
+
}
|
|
456
|
+
async function materializeSourceToTemp(source) {
|
|
457
|
+
const sourceType = classifySource(source);
|
|
458
|
+
const tempParent = path7.join(os2.tmpdir(), "skild-materialize");
|
|
459
|
+
const tempRoot = createTempDir(tempParent, extractSkillName(source));
|
|
460
|
+
const dir = path7.join(tempRoot, "staging");
|
|
461
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
462
|
+
try {
|
|
463
|
+
await materializeSourceToDir({ source, targetDir: dir });
|
|
464
|
+
if (isDirEmpty(dir)) {
|
|
465
|
+
throw new SkildError(
|
|
466
|
+
"EMPTY_INSTALL_DIR",
|
|
467
|
+
`Installed directory is empty for source: ${source}
|
|
468
|
+
Source likely does not point to a valid subdirectory.
|
|
469
|
+
Try: https://github.com/<owner>/<repo>/tree/<branch>/skills/<skill-name>
|
|
470
|
+
Example: https://github.com/anthropics/skills/tree/main/skills/pdf`,
|
|
471
|
+
{ source }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return { dir, sourceType, cleanup: () => removeDir(tempRoot) };
|
|
475
|
+
} catch (e) {
|
|
476
|
+
removeDir(tempRoot);
|
|
477
|
+
throw e;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/registry.ts
|
|
482
|
+
import fs7 from "fs";
|
|
483
|
+
import path8 from "path";
|
|
484
|
+
import crypto2 from "crypto";
|
|
269
485
|
import * as tar from "tar";
|
|
270
486
|
import semver from "semver";
|
|
271
487
|
var DEFAULT_REGISTRY_URL = "https://registry.skild.sh";
|
|
@@ -342,7 +558,7 @@ async function resolveRegistryVersion(registryUrl, spec) {
|
|
|
342
558
|
return { canonicalName: spec.canonicalName, version: json.version, integrity: json.integrity, tarballUrl, publishedAt: json.publishedAt };
|
|
343
559
|
}
|
|
344
560
|
function sha256Hex(buffer) {
|
|
345
|
-
const h =
|
|
561
|
+
const h = crypto2.createHash("sha256");
|
|
346
562
|
h.update(buffer);
|
|
347
563
|
return h.digest("hex");
|
|
348
564
|
}
|
|
@@ -395,9 +611,9 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
395
611
|
`Integrity mismatch for ${resolved.canonicalName}@${resolved.version}. Expected ${resolved.integrity}, got ${computed}.`
|
|
396
612
|
);
|
|
397
613
|
}
|
|
398
|
-
const tarballPath =
|
|
399
|
-
|
|
400
|
-
|
|
614
|
+
const tarballPath = path8.join(tempRoot, "skill.tgz");
|
|
615
|
+
fs7.mkdirSync(stagingDir, { recursive: true });
|
|
616
|
+
fs7.writeFileSync(tarballPath, buf);
|
|
401
617
|
try {
|
|
402
618
|
await tar.x({ file: tarballPath, cwd: stagingDir, gzip: true });
|
|
403
619
|
} catch (error) {
|
|
@@ -406,139 +622,8 @@ async function downloadAndExtractTarball(resolved, tempRoot, stagingDir) {
|
|
|
406
622
|
}
|
|
407
623
|
|
|
408
624
|
// src/lifecycle.ts
|
|
409
|
-
import
|
|
410
|
-
import
|
|
411
|
-
import degit from "degit";
|
|
412
|
-
|
|
413
|
-
// src/source.ts
|
|
414
|
-
import path7 from "path";
|
|
415
|
-
|
|
416
|
-
// src/fs.ts
|
|
417
|
-
import fs6 from "fs";
|
|
418
|
-
import path6 from "path";
|
|
419
|
-
import crypto2 from "crypto";
|
|
420
|
-
function pathExists(filePath) {
|
|
421
|
-
return fs6.existsSync(filePath);
|
|
422
|
-
}
|
|
423
|
-
function isDirectory(filePath) {
|
|
424
|
-
try {
|
|
425
|
-
return fs6.statSync(filePath).isDirectory();
|
|
426
|
-
} catch {
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
function isDirEmpty(dir) {
|
|
431
|
-
try {
|
|
432
|
-
const entries = fs6.readdirSync(dir);
|
|
433
|
-
return entries.length === 0;
|
|
434
|
-
} catch {
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
function copyDir(src, dest) {
|
|
439
|
-
fs6.cpSync(src, dest, { recursive: true });
|
|
440
|
-
}
|
|
441
|
-
function removeDir(dir) {
|
|
442
|
-
if (fs6.existsSync(dir)) fs6.rmSync(dir, { recursive: true, force: true });
|
|
443
|
-
}
|
|
444
|
-
function sanitizeForPathSegment(value) {
|
|
445
|
-
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
446
|
-
}
|
|
447
|
-
function createTempDir(parentDir, prefix) {
|
|
448
|
-
ensureDir(parentDir);
|
|
449
|
-
const safePrefix = sanitizeForPathSegment(prefix || "tmp");
|
|
450
|
-
const template = path6.join(parentDir, `.skild-${safePrefix}-`);
|
|
451
|
-
return fs6.mkdtempSync(template);
|
|
452
|
-
}
|
|
453
|
-
function replaceDirAtomic(sourceDir, destDir) {
|
|
454
|
-
const backupDir = fs6.existsSync(destDir) ? `${destDir}.bak-${Date.now()}` : null;
|
|
455
|
-
try {
|
|
456
|
-
if (backupDir) fs6.renameSync(destDir, backupDir);
|
|
457
|
-
fs6.renameSync(sourceDir, destDir);
|
|
458
|
-
if (backupDir) removeDir(backupDir);
|
|
459
|
-
} catch (error) {
|
|
460
|
-
try {
|
|
461
|
-
if (!fs6.existsSync(destDir) && backupDir && fs6.existsSync(backupDir)) {
|
|
462
|
-
fs6.renameSync(backupDir, destDir);
|
|
463
|
-
}
|
|
464
|
-
} catch {
|
|
465
|
-
}
|
|
466
|
-
try {
|
|
467
|
-
if (fs6.existsSync(sourceDir)) removeDir(sourceDir);
|
|
468
|
-
} catch {
|
|
469
|
-
}
|
|
470
|
-
throw error;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
function listFilesRecursive(rootDir) {
|
|
474
|
-
const results = [];
|
|
475
|
-
const stack = [rootDir];
|
|
476
|
-
while (stack.length) {
|
|
477
|
-
const current = stack.pop();
|
|
478
|
-
const entries = fs6.readdirSync(current, { withFileTypes: true });
|
|
479
|
-
for (const entry of entries) {
|
|
480
|
-
if (entry.name === ".skild") continue;
|
|
481
|
-
if (entry.name === ".git") continue;
|
|
482
|
-
const full = path6.join(current, entry.name);
|
|
483
|
-
if (entry.isDirectory()) stack.push(full);
|
|
484
|
-
else if (entry.isFile()) results.push(full);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
results.sort();
|
|
488
|
-
return results;
|
|
489
|
-
}
|
|
490
|
-
function hashDirectoryContent(rootDir) {
|
|
491
|
-
const files = listFilesRecursive(rootDir);
|
|
492
|
-
const h = crypto2.createHash("sha256");
|
|
493
|
-
for (const filePath of files) {
|
|
494
|
-
const rel = path6.relative(rootDir, filePath);
|
|
495
|
-
h.update(rel);
|
|
496
|
-
h.update("\0");
|
|
497
|
-
h.update(fs6.readFileSync(filePath));
|
|
498
|
-
h.update("\0");
|
|
499
|
-
}
|
|
500
|
-
return h.digest("hex");
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// src/source.ts
|
|
504
|
-
function resolveLocalPath(source) {
|
|
505
|
-
const resolved = path7.resolve(source);
|
|
506
|
-
return pathExists(resolved) ? resolved : null;
|
|
507
|
-
}
|
|
508
|
-
function classifySource(source) {
|
|
509
|
-
const local = resolveLocalPath(source);
|
|
510
|
-
if (local) return "local";
|
|
511
|
-
if (/^https?:\/\//i.test(source) || source.includes("github.com")) return "github-url";
|
|
512
|
-
if (/^[^/]+\/[^/]+/.test(source)) return "degit-shorthand";
|
|
513
|
-
throw new SkildError(
|
|
514
|
-
"INVALID_SOURCE",
|
|
515
|
-
`Unsupported source "${source}". Use a Git URL (e.g. https://github.com/owner/repo/tree/<branch>/<subdir>) or degit shorthand (e.g. owner/repo[/subdir][#ref]) or a local directory.`
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
function extractSkillName(source) {
|
|
519
|
-
const local = resolveLocalPath(source);
|
|
520
|
-
if (local) return path7.basename(local) || "unknown-skill";
|
|
521
|
-
const cleaned = source.replace(/[#?].*$/, "");
|
|
522
|
-
const treeMatch = cleaned.match(/\/tree\/[^/]+\/(.+?)(?:\/)?$/);
|
|
523
|
-
if (treeMatch) return treeMatch[1].split("/").pop() || "unknown-skill";
|
|
524
|
-
const repoMatch = cleaned.match(/github\.com\/[^/]+\/([^/]+)/);
|
|
525
|
-
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
526
|
-
const parts = cleaned.split("/").filter(Boolean);
|
|
527
|
-
if (parts.length >= 2) return parts[parts.length - 1] || "unknown-skill";
|
|
528
|
-
return cleaned || "unknown-skill";
|
|
529
|
-
}
|
|
530
|
-
function toDegitPath(url) {
|
|
531
|
-
const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)(?:\/)?$/);
|
|
532
|
-
if (treeMatch) {
|
|
533
|
-
const [, owner, repo, branch, subpath] = treeMatch;
|
|
534
|
-
return `${owner}/${repo}/${subpath}#${branch}`;
|
|
535
|
-
}
|
|
536
|
-
const repoMatch = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
537
|
-
if (repoMatch) return repoMatch[1].replace(/\.git$/, "");
|
|
538
|
-
return url;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// src/lifecycle.ts
|
|
625
|
+
import path9 from "path";
|
|
626
|
+
import fs8 from "fs";
|
|
542
627
|
function normalizeDependencies(raw) {
|
|
543
628
|
if (raw === void 0 || raw === null) return [];
|
|
544
629
|
if (!Array.isArray(raw)) {
|
|
@@ -564,9 +649,9 @@ function isRelativeDependency(dep) {
|
|
|
564
649
|
return dep.startsWith("./") || dep.startsWith("../");
|
|
565
650
|
}
|
|
566
651
|
function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
567
|
-
const resolved =
|
|
568
|
-
const relToRoot =
|
|
569
|
-
if (relToRoot.startsWith("..") ||
|
|
652
|
+
const resolved = path9.resolve(baseDir, raw);
|
|
653
|
+
const relToRoot = path9.relative(rootDir, resolved);
|
|
654
|
+
if (relToRoot.startsWith("..") || path9.isAbsolute(relToRoot)) {
|
|
570
655
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency path escapes the skill root: ${raw}`);
|
|
571
656
|
}
|
|
572
657
|
if (!isDirectory(resolved)) {
|
|
@@ -576,11 +661,11 @@ function resolveInlineDependency(raw, baseDir, rootDir) {
|
|
|
576
661
|
if (!validation.ok) {
|
|
577
662
|
throw new SkildError("INVALID_DEPENDENCY", `Inline dependency is not a valid skill: ${raw}`, { issues: validation.issues });
|
|
578
663
|
}
|
|
579
|
-
const normalizedInlinePath = relToRoot.split(
|
|
664
|
+
const normalizedInlinePath = relToRoot.split(path9.sep).join("/");
|
|
580
665
|
return {
|
|
581
666
|
sourceType: "inline",
|
|
582
667
|
source: raw,
|
|
583
|
-
name:
|
|
668
|
+
name: path9.basename(resolved) || raw,
|
|
584
669
|
inlinePath: normalizedInlinePath,
|
|
585
670
|
inlineDir: resolved
|
|
586
671
|
};
|
|
@@ -650,15 +735,6 @@ function dedupeInstalledDependencies(entries) {
|
|
|
650
735
|
}
|
|
651
736
|
return out;
|
|
652
737
|
}
|
|
653
|
-
async function cloneRemote(degitSrc, targetPath) {
|
|
654
|
-
const emitter = degit(degitSrc, { force: true, verbose: false });
|
|
655
|
-
await emitter.clone(targetPath);
|
|
656
|
-
}
|
|
657
|
-
function ensureInstallableLocalDir(sourcePath) {
|
|
658
|
-
if (!isDirectory(sourcePath)) {
|
|
659
|
-
throw new SkildError("NOT_A_DIRECTORY", `Source path is not a directory: ${sourcePath}`, { sourcePath });
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
738
|
function assertNonEmptyInstall(stagingDir, source) {
|
|
663
739
|
if (isDirEmpty(stagingDir)) {
|
|
664
740
|
throw new SkildError(
|
|
@@ -686,23 +762,16 @@ async function installSkillBase(input, options = {}) {
|
|
|
686
762
|
ensureDir(skillsDir);
|
|
687
763
|
const skillName = input.nameOverride || extractSkillName(source);
|
|
688
764
|
const installDir = getSkillInstallDir(platform, scope, skillName);
|
|
689
|
-
if (
|
|
765
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
690
766
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${skillName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
691
767
|
skillName,
|
|
692
768
|
installDir
|
|
693
769
|
});
|
|
694
770
|
}
|
|
695
771
|
const tempRoot = createTempDir(skillsDir, skillName);
|
|
696
|
-
const stagingDir =
|
|
772
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
697
773
|
try {
|
|
698
|
-
|
|
699
|
-
if (localPath) {
|
|
700
|
-
ensureInstallableLocalDir(localPath);
|
|
701
|
-
copyDir(localPath, stagingDir);
|
|
702
|
-
} else {
|
|
703
|
-
const degitPath = toDegitPath(source);
|
|
704
|
-
await cloneRemote(degitPath, stagingDir);
|
|
705
|
-
}
|
|
774
|
+
await materializeSourceToDir({ source, targetDir: stagingDir, materializedDir: input.materializedDir });
|
|
706
775
|
assertNonEmptyInstall(stagingDir, source);
|
|
707
776
|
replaceDirAtomic(stagingDir, installDir);
|
|
708
777
|
const contentHash = hashDirectoryContent(installDir);
|
|
@@ -717,7 +786,7 @@ async function installSkillBase(input, options = {}) {
|
|
|
717
786
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
718
787
|
installDir,
|
|
719
788
|
contentHash,
|
|
720
|
-
hasSkillMd:
|
|
789
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
721
790
|
skill: {
|
|
722
791
|
frontmatter: validation.frontmatter,
|
|
723
792
|
validation
|
|
@@ -750,14 +819,14 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
750
819
|
ensureDir(skillsDir);
|
|
751
820
|
const installName = input.nameOverride || canonicalNameToInstallDirName(canonicalName);
|
|
752
821
|
const installDir = getSkillInstallDir(platform, scope, installName);
|
|
753
|
-
if (
|
|
822
|
+
if (fs8.existsSync(installDir) && !options.force) {
|
|
754
823
|
throw new SkildError("ALREADY_INSTALLED", `Skill "${canonicalName}" is already installed at ${installDir}. Use --force, or uninstall first.`, {
|
|
755
824
|
skillName: canonicalName,
|
|
756
825
|
installDir
|
|
757
826
|
});
|
|
758
827
|
}
|
|
759
828
|
const tempRoot = createTempDir(skillsDir, installName);
|
|
760
|
-
const stagingDir =
|
|
829
|
+
const stagingDir = path9.join(tempRoot, "staging");
|
|
761
830
|
try {
|
|
762
831
|
const resolvedVersion = resolved || await resolveRegistryVersion(registryUrl, spec);
|
|
763
832
|
await downloadAndExtractTarball(resolvedVersion, tempRoot, stagingDir);
|
|
@@ -778,7 +847,7 @@ async function installRegistrySkillBase(input, options = {}, resolved) {
|
|
|
778
847
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
779
848
|
installDir,
|
|
780
849
|
contentHash,
|
|
781
|
-
hasSkillMd:
|
|
850
|
+
hasSkillMd: fs8.existsSync(path9.join(installDir, "SKILL.md")),
|
|
782
851
|
skill: { validation, frontmatter: validation.frontmatter }
|
|
783
852
|
};
|
|
784
853
|
writeInstallRecord(installDir, record);
|
|
@@ -827,7 +896,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
827
896
|
const resolved = await resolveRegistryVersion(registryUrl, spec);
|
|
828
897
|
const installName2 = canonicalNameToInstallDirName(spec.canonicalName);
|
|
829
898
|
const installDir2 = getSkillInstallDir(ctx.platform, ctx.scope, installName2);
|
|
830
|
-
if (
|
|
899
|
+
if (fs8.existsSync(installDir2) && !ctx.force) {
|
|
831
900
|
const existing = readInstallRecord(installDir2);
|
|
832
901
|
if (!existing) {
|
|
833
902
|
throw new SkildError(
|
|
@@ -869,7 +938,7 @@ async function ensureExternalDependencyInstalled(dep, ctx, dependerName) {
|
|
|
869
938
|
}
|
|
870
939
|
const installName = extractSkillName(dep.source);
|
|
871
940
|
const installDir = getSkillInstallDir(ctx.platform, ctx.scope, installName);
|
|
872
|
-
if (
|
|
941
|
+
if (fs8.existsSync(installDir) && !ctx.force) {
|
|
873
942
|
const existing = readInstallRecord(installDir);
|
|
874
943
|
if (!existing) {
|
|
875
944
|
throw new SkildError(
|
|
@@ -992,11 +1061,11 @@ async function installRegistrySkill(input, options = {}) {
|
|
|
992
1061
|
function listSkills(options = {}) {
|
|
993
1062
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
994
1063
|
const skillsDir = getSkillsDir(platform, scope);
|
|
995
|
-
if (!
|
|
996
|
-
const entries =
|
|
1064
|
+
if (!fs8.existsSync(skillsDir)) return [];
|
|
1065
|
+
const entries = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
997
1066
|
return entries.map((e) => {
|
|
998
|
-
const dir =
|
|
999
|
-
const hasSkillMd =
|
|
1067
|
+
const dir = path9.join(skillsDir, e.name);
|
|
1068
|
+
const hasSkillMd = fs8.existsSync(path9.join(dir, "SKILL.md"));
|
|
1000
1069
|
const record = readInstallRecord(dir);
|
|
1001
1070
|
return { name: e.name, installDir: dir, hasSkillMd, record };
|
|
1002
1071
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -1014,7 +1083,7 @@ function listAllSkills(options = {}) {
|
|
|
1014
1083
|
function getSkillInfo(name, options = {}) {
|
|
1015
1084
|
const { platform, scope } = resolvePlatformAndScope(options);
|
|
1016
1085
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1017
|
-
if (!
|
|
1086
|
+
if (!fs8.existsSync(installDir)) {
|
|
1018
1087
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1019
1088
|
}
|
|
1020
1089
|
const record = readInstallRecord(installDir);
|
|
@@ -1038,7 +1107,7 @@ function uninstallSkillInternal(name, options, visited) {
|
|
|
1038
1107
|
if (visited.has(key)) return;
|
|
1039
1108
|
visited.add(key);
|
|
1040
1109
|
const installDir = getSkillInstallDir(platform, scope, name);
|
|
1041
|
-
if (!
|
|
1110
|
+
if (!fs8.existsSync(installDir)) {
|
|
1042
1111
|
throw new SkildError("SKILL_NOT_FOUND", `Skill "${name}" not found in ${getSkillsDir(platform, scope)}`, { name, platform, scope });
|
|
1043
1112
|
}
|
|
1044
1113
|
const record = readInstallRecord(installDir);
|
|
@@ -1084,6 +1153,7 @@ export {
|
|
|
1084
1153
|
DEFAULT_REGISTRY_URL,
|
|
1085
1154
|
PLATFORMS,
|
|
1086
1155
|
SkildError,
|
|
1156
|
+
assertValidAlias,
|
|
1087
1157
|
canonicalNameToInstallDirName,
|
|
1088
1158
|
clearRegistryAuth,
|
|
1089
1159
|
downloadAndExtractTarball,
|
|
@@ -1094,10 +1164,14 @@ export {
|
|
|
1094
1164
|
initSkill,
|
|
1095
1165
|
installRegistrySkill,
|
|
1096
1166
|
installSkill,
|
|
1167
|
+
isValidAlias,
|
|
1097
1168
|
listAllSkills,
|
|
1098
1169
|
listSkills,
|
|
1099
1170
|
loadOrCreateGlobalConfig,
|
|
1100
1171
|
loadRegistryAuth,
|
|
1172
|
+
materializeSourceToDir,
|
|
1173
|
+
materializeSourceToTemp,
|
|
1174
|
+
normalizeAlias,
|
|
1101
1175
|
parseRegistrySpecifier,
|
|
1102
1176
|
resolveRegistryAlias,
|
|
1103
1177
|
resolveRegistryUrl,
|