dsh-skills-manager 0.1.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/lib/index.js ADDED
@@ -0,0 +1,700 @@
1
+ import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { execFile } from "node:child_process";
5
+ import { tmpdir } from "node:os";
6
+ import { promisify } from "node:util";
7
+ import Schema from "@deepseek-ai/schemastery";
8
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
+ import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
10
+ import { parse } from "yaml";
11
+
12
+ //#region lib/types/state.js
13
+ const EMPTY_STATE = {
14
+ version: 1,
15
+ skills: {},
16
+ globalEnabled: [],
17
+ projectEnabled: {}
18
+ };
19
+ function normalizeState(value) {
20
+ if (!isRecord$1(value) || value.version !== 1 || !isRecord$1(value.skills) || !Array.isArray(value.globalEnabled) || !isRecord$1(value.projectEnabled)) throw new Error("skill-manager state.json has an unsupported shape");
21
+ const skills = {};
22
+ for (const [name$1, entry] of Object.entries(value.skills)) {
23
+ if (!isRecord$1(entry) || entry.name !== name$1 || typeof entry.description !== "string" || entry.layout !== "directory" && entry.layout !== "file" || typeof entry.installedAt !== "string") throw new Error(`skill-manager state.json has an invalid skill entry: ${name$1}`);
24
+ skills[name$1] = {
25
+ name: name$1,
26
+ description: entry.description,
27
+ layout: entry.layout,
28
+ installedAt: entry.installedAt,
29
+ ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
30
+ ...entry.sourceType === "local" || entry.sourceType === "git" ? { sourceType: entry.sourceType } : {},
31
+ ...typeof entry.sourcePath === "string" && isAbsolute(entry.sourcePath) ? { sourcePath: entry.sourcePath } : {},
32
+ ...typeof entry.sourceUrl === "string" ? { sourceUrl: entry.sourceUrl } : {},
33
+ ...typeof entry.sourceSubpath === "string" ? { sourceSubpath: entry.sourceSubpath } : {},
34
+ ...typeof entry.contentHash === "string" ? { contentHash: entry.contentHash } : {}
35
+ };
36
+ }
37
+ return {
38
+ version: 1,
39
+ skills,
40
+ globalEnabled: cleanNames(value.globalEnabled, skills),
41
+ projectEnabled: cleanProjects(value.projectEnabled, skills)
42
+ };
43
+ }
44
+ function setEnabled(state, name$1, scope, projectRoot, enabled) {
45
+ if (!(name$1 in state.skills)) throw new Error(`skill is not installed: ${name$1}`);
46
+ if (scope === "project" && !projectRoot) throw new Error("projectRoot is required for project scope");
47
+ if (scope === "global") return {
48
+ ...state,
49
+ globalEnabled: updateNames(state.globalEnabled, name$1, enabled)
50
+ };
51
+ const next = updateNames(state.projectEnabled[projectRoot] ?? [], name$1, enabled);
52
+ const projectEnabled = { ...state.projectEnabled };
53
+ if (next.length === 0) delete projectEnabled[projectRoot];
54
+ else projectEnabled[projectRoot] = next;
55
+ return {
56
+ ...state,
57
+ projectEnabled
58
+ };
59
+ }
60
+ function removeSkill(state, name$1) {
61
+ if (!(name$1 in state.skills)) throw new Error(`skill is not installed: ${name$1}`);
62
+ const skills = { ...state.skills };
63
+ delete skills[name$1];
64
+ return {
65
+ version: 1,
66
+ skills,
67
+ globalEnabled: state.globalEnabled.filter((item) => item !== name$1),
68
+ projectEnabled: Object.fromEntries(Object.entries(state.projectEnabled).map(([root, names]) => [root, names.filter((item) => item !== name$1)]).filter(([, names]) => names.length > 0))
69
+ };
70
+ }
71
+ function isPathWithin(root, target) {
72
+ if (!isAbsolute(root) || !isAbsolute(target)) return false;
73
+ const child = relative(root, target);
74
+ return child === "" || child !== ".." && !child.startsWith(`..${sep}`);
75
+ }
76
+ function isEnabledFor(state, name$1, cwd) {
77
+ if (state.globalEnabled.includes(name$1)) return true;
78
+ if (!cwd) return false;
79
+ return Object.entries(state.projectEnabled).some(([root, names]) => names.includes(name$1) && isPathWithin(root, cwd));
80
+ }
81
+ function updateNames(names, name$1, enabled) {
82
+ const next = new Set(names);
83
+ if (enabled) next.add(name$1);
84
+ else next.delete(name$1);
85
+ return [...next].sort();
86
+ }
87
+ function cleanNames(value, skills) {
88
+ return [...new Set(value.filter((item) => typeof item === "string" && item in skills))].sort();
89
+ }
90
+ function cleanProjects(value, skills) {
91
+ const result = {};
92
+ for (const [root, names] of Object.entries(value)) {
93
+ if (!isAbsolute(root) || !Array.isArray(names)) continue;
94
+ const clean = cleanNames(names, skills);
95
+ if (clean.length > 0) result[root] = clean;
96
+ }
97
+ return result;
98
+ }
99
+ function isRecord$1(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ }
102
+
103
+ //#endregion
104
+ //#region lib/types/index.js
105
+ const name = "dsh-skills-manager";
106
+ const inject = ["skills", "connection"];
107
+ const execFileAsync = promisify(execFile);
108
+ const Config = Schema.object({ storageDir: Schema.string().default("") });
109
+ var SkillManager = class {
110
+ ctx;
111
+ root;
112
+ library;
113
+ stateFile;
114
+ state = EMPTY_STATE;
115
+ loaded;
116
+ writeQueue = Promise.resolve();
117
+ updateChecks = /* @__PURE__ */ new Map();
118
+ providerControl;
119
+ filesystemProvider;
120
+ constructor(ctx, config) {
121
+ this.ctx = ctx;
122
+ const storageDir = config.storageDir?.trim();
123
+ this.root = storageDir ? resolve(storageDir) : join(resolveDshHome(), "skill-manager");
124
+ this.library = join(this.root, "library");
125
+ this.stateFile = join(this.root, "state.json");
126
+ this.loaded = this.load();
127
+ }
128
+ provider(control) {
129
+ this.providerControl = control;
130
+ this.filesystemProvider = new FileSystemSkillProvider(this.ctx, control, {
131
+ providerName: "skill-manager-library",
132
+ includeDefaultRoots: false,
133
+ customSkillDirs: [this.library],
134
+ watch: true
135
+ });
136
+ return {
137
+ name: "skill-manager",
138
+ list: async (options) => {
139
+ await this.loaded;
140
+ const discovered = await this.filesystemProvider.list(options);
141
+ return (Array.isArray(discovered) ? discovered : discovered.candidates).filter((candidate) => isEnabledFor(this.state, candidate.name, options.cwd)).map((candidate) => this.managedCandidate(candidate));
142
+ },
143
+ get: async (candidate, options) => {
144
+ await this.loaded;
145
+ const locator = candidate.locator;
146
+ return this.filesystemProvider.get(locator.candidate, options);
147
+ }
148
+ };
149
+ }
150
+ async catalog(projectRoot) {
151
+ await this.loaded;
152
+ return {
153
+ entries: Object.values(this.state.skills).sort((a, b) => a.name.localeCompare(b.name)).map((skill) => {
154
+ const check = this.updateChecks.get(skill.name);
155
+ return {
156
+ ...skill,
157
+ globalEnabled: this.state.globalEnabled.includes(skill.name),
158
+ projectEnabled: projectRoot !== void 0 && (this.state.projectEnabled[projectRoot] ?? []).includes(skill.name),
159
+ effectiveEnabled: isEnabledFor(this.state, skill.name, projectRoot),
160
+ updateSupported: Boolean(skill.sourcePath || skill.sourceUrl),
161
+ updateAvailable: check?.available ?? false,
162
+ ...check?.error ? { updateError: check.error } : {}
163
+ };
164
+ }),
165
+ ...projectRoot ? { projectRoot } : {}
166
+ };
167
+ }
168
+ async install(sourcePath, projectRoot) {
169
+ const sources = await discoverSources(sourcePath);
170
+ return this.installSources(sources, (source) => ({
171
+ sourceType: "local",
172
+ sourcePath: source.path
173
+ }), projectRoot);
174
+ }
175
+ async installRemote(input, projectRoot) {
176
+ const sourceUrl = normalizeGitSource(input);
177
+ const checkout = await cloneGitRepository(sourceUrl);
178
+ try {
179
+ const sources = await discoverRepositorySources(checkout);
180
+ return await this.installSources(sources, (source) => ({
181
+ sourceType: "git",
182
+ sourceUrl,
183
+ sourceSubpath: relative(checkout, source.path).split(sep).join("/")
184
+ }), projectRoot);
185
+ } finally {
186
+ await rm(checkout, {
187
+ recursive: true,
188
+ force: true
189
+ });
190
+ }
191
+ }
192
+ async installSources(sources, originFor, projectRoot) {
193
+ await this.loaded;
194
+ const duplicate = sources.find((source) => source.name in this.state.skills);
195
+ if (duplicate) throw new Error(`skill is already installed: ${duplicate.name}`);
196
+ await mkdir(this.library, { recursive: true });
197
+ const installed = {};
198
+ const staged = [];
199
+ const previousState = this.state;
200
+ try {
201
+ for (const source of sources) {
202
+ const target = join(this.library, source.layout === "file" ? `${source.name}.md` : source.name);
203
+ if (await pathExists(target)) throw new Error(`skill target already exists: ${target}`);
204
+ const temp = `${target}.tmp-${randomUUID()}`;
205
+ await cp(source.path, temp, {
206
+ recursive: source.layout === "directory",
207
+ errorOnExist: true,
208
+ force: false,
209
+ verbatimSymlinks: true
210
+ });
211
+ staged.push({
212
+ temp,
213
+ target,
214
+ layout: source.layout,
215
+ committed: false
216
+ });
217
+ const now = (/* @__PURE__ */ new Date()).toISOString();
218
+ installed[source.name] = {
219
+ name: source.name,
220
+ description: source.description,
221
+ layout: source.layout,
222
+ installedAt: now,
223
+ updatedAt: now,
224
+ ...originFor(source),
225
+ contentHash: await hashSkillSource(temp)
226
+ };
227
+ }
228
+ for (const item of staged) {
229
+ await rename(item.temp, item.target);
230
+ item.committed = true;
231
+ }
232
+ this.state = {
233
+ ...this.state,
234
+ skills: {
235
+ ...this.state.skills,
236
+ ...installed
237
+ }
238
+ };
239
+ await this.persist();
240
+ for (const skillName of Object.keys(installed)) this.updateChecks.set(skillName, { available: false });
241
+ this.providerControl?.invalidate();
242
+ return this.catalog(projectRoot);
243
+ } catch (error) {
244
+ this.state = previousState;
245
+ await Promise.all(staged.flatMap((item) => [rm(item.temp, {
246
+ recursive: item.layout === "directory",
247
+ force: true
248
+ }), ...item.committed ? [rm(item.target, {
249
+ recursive: item.layout === "directory",
250
+ force: true
251
+ })] : []]));
252
+ throw error;
253
+ }
254
+ }
255
+ async checkUpdates(projectRoot) {
256
+ await this.loaded;
257
+ const checkouts = [];
258
+ const checkoutCache = /* @__PURE__ */ new Map();
259
+ const checkoutFor = (sourceUrl) => {
260
+ let checkout = checkoutCache.get(sourceUrl);
261
+ if (!checkout) {
262
+ checkout = cloneGitRepository(sourceUrl).then((path) => {
263
+ checkouts.push(path);
264
+ return path;
265
+ });
266
+ checkoutCache.set(sourceUrl, checkout);
267
+ }
268
+ return checkout;
269
+ };
270
+ try {
271
+ for (const skill of Object.values(this.state.skills)) {
272
+ if (!skill.contentHash || !skill.sourcePath && !skill.sourceUrl) {
273
+ this.updateChecks.delete(skill.name);
274
+ continue;
275
+ }
276
+ try {
277
+ const source = await resolveTrackedSource(skill, checkoutFor);
278
+ this.updateChecks.set(skill.name, { available: await hashSkillSource(source.path) !== skill.contentHash });
279
+ } catch (error) {
280
+ this.updateChecks.set(skill.name, {
281
+ available: false,
282
+ error: messageOf(error)
283
+ });
284
+ }
285
+ }
286
+ return this.catalog(projectRoot);
287
+ } finally {
288
+ await Promise.all(checkouts.map((path) => rm(path, {
289
+ recursive: true,
290
+ force: true
291
+ })));
292
+ }
293
+ }
294
+ async update(name$1, sourcePath, projectRoot) {
295
+ await this.loaded;
296
+ const current = this.state.skills[name$1];
297
+ if (!current) throw new Error(`skill is not installed: ${name$1}`);
298
+ const resolvedSource = sourcePath ? {
299
+ source: await findUpdateSource(name$1, sourcePath),
300
+ origin: {
301
+ sourceType: "local",
302
+ sourcePath: resolve(sourcePath)
303
+ }
304
+ } : await prepareTrackedUpdate(current);
305
+ try {
306
+ return await this.replaceSkill(name$1, current, resolvedSource.source, resolvedSource.origin, projectRoot);
307
+ } finally {
308
+ await resolvedSource.cleanup?.();
309
+ }
310
+ }
311
+ async replaceSkill(name$1, current, source, origin, projectRoot) {
312
+ const currentTarget = join(this.library, current.layout === "file" ? `${name$1}.md` : name$1);
313
+ const nextTarget = join(this.library, source.layout === "file" ? `${name$1}.md` : name$1);
314
+ if (currentTarget !== nextTarget && await pathExists(nextTarget)) throw new Error(`cannot change skill layout because the target already exists: ${nextTarget}`);
315
+ const suffix = randomUUID();
316
+ const temp = `${nextTarget}.tmp-${suffix}`;
317
+ const backup = `${currentTarget}.bak-${suffix}`;
318
+ const previousState = this.state;
319
+ let currentMoved = false;
320
+ let nextMoved = false;
321
+ try {
322
+ await cp(source.path, temp, {
323
+ recursive: source.layout === "directory",
324
+ errorOnExist: true,
325
+ force: false,
326
+ verbatimSymlinks: true
327
+ });
328
+ const contentHash = await hashSkillSource(temp);
329
+ await rename(currentTarget, backup);
330
+ currentMoved = true;
331
+ await rename(temp, nextTarget);
332
+ nextMoved = true;
333
+ this.state = {
334
+ ...this.state,
335
+ skills: {
336
+ ...this.state.skills,
337
+ [name$1]: {
338
+ ...current,
339
+ description: source.description,
340
+ layout: source.layout,
341
+ sourceType: origin.sourceType,
342
+ sourcePath: origin.sourcePath,
343
+ sourceUrl: origin.sourceUrl,
344
+ sourceSubpath: origin.sourceSubpath,
345
+ contentHash,
346
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
347
+ }
348
+ }
349
+ };
350
+ await this.persist();
351
+ await rm(backup, {
352
+ recursive: current.layout === "directory",
353
+ force: true
354
+ }).catch(() => void 0);
355
+ this.updateChecks.set(name$1, { available: false });
356
+ this.providerControl?.invalidate();
357
+ return this.catalog(projectRoot);
358
+ } catch (error) {
359
+ this.state = previousState;
360
+ if (nextMoved) await rm(nextTarget, {
361
+ recursive: source.layout === "directory",
362
+ force: true
363
+ });
364
+ if (currentMoved) await rename(backup, currentTarget);
365
+ await rm(temp, {
366
+ recursive: source.layout === "directory",
367
+ force: true
368
+ });
369
+ throw error;
370
+ }
371
+ }
372
+ async setEnabled(name$1, scope, projectRoot, enabled) {
373
+ await this.loaded;
374
+ this.state = setEnabled(this.state, name$1, scope, projectRoot, enabled);
375
+ await this.persist();
376
+ this.providerControl?.invalidate();
377
+ return this.catalog(projectRoot);
378
+ }
379
+ async remove(name$1, projectRoot) {
380
+ await this.loaded;
381
+ const skill = this.state.skills[name$1];
382
+ if (!skill) throw new Error(`skill is not installed: ${name$1}`);
383
+ await rm(join(this.library, skill.layout === "file" ? `${name$1}.md` : name$1), {
384
+ recursive: skill.layout === "directory",
385
+ force: true
386
+ });
387
+ this.state = removeSkill(this.state, name$1);
388
+ await this.persist();
389
+ this.updateChecks.delete(name$1);
390
+ this.providerControl?.invalidate();
391
+ return this.catalog(projectRoot);
392
+ }
393
+ async load() {
394
+ await mkdir(this.library, { recursive: true });
395
+ try {
396
+ this.state = normalizeState(JSON.parse(await readFile(this.stateFile, "utf8")));
397
+ } catch (error) {
398
+ if (isNodeError(error) && error.code === "ENOENT") {
399
+ this.state = EMPTY_STATE;
400
+ await this.persist();
401
+ return;
402
+ }
403
+ throw error;
404
+ }
405
+ }
406
+ async persist() {
407
+ const operation = this.writeQueue.then(async () => {
408
+ await mkdir(this.root, { recursive: true });
409
+ const temp = `${this.stateFile}.tmp-${randomUUID()}`;
410
+ await writeFile(temp, `${JSON.stringify(this.state, null, 2)}\n`, "utf8");
411
+ await rename(temp, this.stateFile);
412
+ });
413
+ this.writeQueue = operation.catch(() => void 0);
414
+ await operation;
415
+ }
416
+ managedCandidate(candidate) {
417
+ return {
418
+ ...candidate,
419
+ provider: "skill-manager",
420
+ source: "skill-manager",
421
+ rank: 50,
422
+ locator: { candidate }
423
+ };
424
+ }
425
+ };
426
+ function apply(ctx, config) {
427
+ if (config.storageDir?.trim() && !isAbsolute(config.storageDir)) throw new Error("storageDir must resolve to an absolute path");
428
+ const manager = new SkillManager(ctx, config);
429
+ ctx.skills.registerProvider((control) => manager.provider(control));
430
+ const host = ctx;
431
+ ctx.effect(() => host.connection.rpc.handle("/skill-manager", async (endpoint, payload) => handleRpc(manager, endpoint, payload), { authority: "loopback" }), "dsh-skill-manager: rpc");
432
+ }
433
+ async function handleRpc(manager, endpoint, payload) {
434
+ const args = isRecord(payload) ? payload : {};
435
+ switch (endpoint) {
436
+ case "list": return {
437
+ ok: true,
438
+ value: await manager.catalog(asOptionalString(args.projectRoot))
439
+ };
440
+ case "install": return {
441
+ ok: true,
442
+ value: await manager.install(requiredString(args.path, "path"), asOptionalString(args.projectRoot))
443
+ };
444
+ case "install-remote": return {
445
+ ok: true,
446
+ value: await manager.installRemote(requiredString(args.repository, "repository"), asOptionalString(args.projectRoot))
447
+ };
448
+ case "check-updates": return {
449
+ ok: true,
450
+ value: await manager.checkUpdates(asOptionalString(args.projectRoot))
451
+ };
452
+ case "update": return {
453
+ ok: true,
454
+ value: await manager.update(requiredString(args.name, "name"), asOptionalString(args.path), asOptionalString(args.projectRoot))
455
+ };
456
+ case "set-enabled": return {
457
+ ok: true,
458
+ value: await manager.setEnabled(requiredString(args.name, "name"), requiredScope(args.scope), asOptionalString(args.projectRoot), requiredBoolean(args.enabled, "enabled"))
459
+ };
460
+ case "remove": return {
461
+ ok: true,
462
+ value: await manager.remove(requiredString(args.name, "name"), asOptionalString(args.projectRoot))
463
+ };
464
+ default: throw new Error(`unknown skill-manager endpoint: ${endpoint}`);
465
+ }
466
+ }
467
+ async function discoverSources(inputPath) {
468
+ const path = resolve(inputPath.trim());
469
+ const info = await stat(path);
470
+ if (info.isFile()) {
471
+ if (extname(path).toLowerCase() !== ".md") throw new Error("a skill file must use the .md extension");
472
+ return [await readSkillSource(path, "file")];
473
+ }
474
+ if (!info.isDirectory()) throw new Error("skill source must be a directory or Markdown file");
475
+ if (await isFile(join(path, "SKILL.md"))) return [await readSkillSource(join(path, "SKILL.md"), "directory", path)];
476
+ const entries = await readdir(path, { withFileTypes: true });
477
+ const sources = [];
478
+ for (const entry of entries) {
479
+ const child = join(path, entry.name);
480
+ if (entry.isDirectory() && await isFile(join(child, "SKILL.md"))) sources.push(await readSkillSource(join(child, "SKILL.md"), "directory", child));
481
+ else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") sources.push(await readSkillSource(child, "file"));
482
+ }
483
+ if (sources.length === 0) throw new Error("no SKILL.md or Markdown skill file was found in the selected folder");
484
+ return sources;
485
+ }
486
+ async function discoverRepositorySources(root) {
487
+ if (await isFile(join(root, "SKILL.md"))) return [await readSkillSource(join(root, "SKILL.md"), "directory", root)];
488
+ const searchRoots = [
489
+ root,
490
+ join(root, "skills"),
491
+ join(root, ".agents", "skills")
492
+ ];
493
+ const sources = /* @__PURE__ */ new Map();
494
+ for (const searchRoot of searchRoots) {
495
+ let entries;
496
+ try {
497
+ entries = await readdir(searchRoot, { withFileTypes: true });
498
+ } catch (error) {
499
+ if (isNodeError(error) && error.code === "ENOENT") continue;
500
+ throw error;
501
+ }
502
+ for (const entry of entries) {
503
+ const child = join(searchRoot, entry.name);
504
+ if (entry.isDirectory() && await isFile(join(child, "SKILL.md"))) {
505
+ const source = await readSkillSource(join(child, "SKILL.md"), "directory", child);
506
+ sources.set(source.name, source);
507
+ } else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md" && entry.name.toLowerCase() !== "readme.md") try {
508
+ const source = await readSkillSource(child, "file");
509
+ sources.set(source.name, source);
510
+ } catch (error) {
511
+ if ((await readFile(child, "utf8")).startsWith("---")) throw error;
512
+ }
513
+ }
514
+ }
515
+ if (sources.size === 0) throw new Error("the repository does not contain a root skill or skills/* entries");
516
+ return [...sources.values()];
517
+ }
518
+ function normalizeGitSource(input) {
519
+ const value = input.trim();
520
+ if (!value || value.startsWith("-") || /[\r\n\0]/.test(value)) throw new Error("repository must be a valid Git URL or owner/repo");
521
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value)) return `https://github.com/${value}.git`;
522
+ if (/^[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+:[^\s]+$/.test(value)) return value;
523
+ let url;
524
+ try {
525
+ url = new URL(value);
526
+ } catch {
527
+ throw new Error("repository must be a GitHub owner/repo, HTTPS URL, SSH URL, or file URL");
528
+ }
529
+ if (![
530
+ "https:",
531
+ "http:",
532
+ "ssh:",
533
+ "git:",
534
+ "file:"
535
+ ].includes(url.protocol)) throw new Error(`unsupported Git protocol: ${url.protocol}`);
536
+ if (url.password || (url.protocol === "https:" || url.protocol === "http:") && url.username) throw new Error("do not embed credentials in the repository URL; use a Git credential helper or SSH");
537
+ if (url.hostname === "github.com" && /\/tree\//.test(url.pathname)) throw new Error("use the repository root URL, not a GitHub /tree/ URL");
538
+ return value;
539
+ }
540
+ async function cloneGitRepository(sourceUrl) {
541
+ const checkout = await mkdtemp(join(tmpdir(), "dsh-skill-manager-git-"));
542
+ try {
543
+ await execFileAsync("git", [
544
+ "clone",
545
+ "--depth",
546
+ "1",
547
+ "--",
548
+ sourceUrl,
549
+ checkout
550
+ ], {
551
+ timeout: 12e4,
552
+ maxBuffer: 1024 * 1024
553
+ });
554
+ return checkout;
555
+ } catch {
556
+ await rm(checkout, {
557
+ recursive: true,
558
+ force: true
559
+ });
560
+ throw new Error("failed to clone Git repository; check the URL and local Git credentials");
561
+ }
562
+ }
563
+ async function resolveTrackedSource(skill, checkoutFor) {
564
+ if (skill.sourceUrl) {
565
+ if (typeof skill.sourceSubpath !== "string") throw new Error("the repository skill path is missing; import it again");
566
+ const checkout = await checkoutFor(skill.sourceUrl);
567
+ return findUpdateSource(skill.name, resolveRepositorySubpath(checkout, skill.sourceSubpath));
568
+ }
569
+ return findUpdateSource(skill.name, skill.sourcePath);
570
+ }
571
+ async function prepareTrackedUpdate(skill) {
572
+ if (!skill.sourceUrl) return {
573
+ source: await findUpdateSource(skill.name, skill.sourcePath),
574
+ origin: {
575
+ sourceType: "local",
576
+ sourcePath: skill.sourcePath
577
+ }
578
+ };
579
+ const checkout = await cloneGitRepository(skill.sourceUrl);
580
+ try {
581
+ return {
582
+ source: await resolveTrackedSource(skill, async () => checkout),
583
+ origin: {
584
+ sourceType: "git",
585
+ sourceUrl: skill.sourceUrl,
586
+ sourceSubpath: skill.sourceSubpath
587
+ },
588
+ cleanup: () => rm(checkout, {
589
+ recursive: true,
590
+ force: true
591
+ })
592
+ };
593
+ } catch (error) {
594
+ await rm(checkout, {
595
+ recursive: true,
596
+ force: true
597
+ });
598
+ throw error;
599
+ }
600
+ }
601
+ function resolveRepositorySubpath(root, subpath) {
602
+ const target = resolve(root, subpath);
603
+ const child = relative(root, target);
604
+ if (child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) throw new Error("repository skill path escapes the checkout");
605
+ return target;
606
+ }
607
+ async function findUpdateSource(name$1, sourcePath) {
608
+ if (!sourcePath) throw new Error("the original source is unknown; select the skill source folder to update it");
609
+ const source = (await discoverSources(sourcePath)).find((candidate) => candidate.name === name$1);
610
+ if (!source) throw new Error(`the selected source does not contain skill: ${name$1}`);
611
+ return source;
612
+ }
613
+ async function hashSkillSource(root) {
614
+ const hash = createHash("sha256");
615
+ const visit = async (path) => {
616
+ const info = await lstat(path);
617
+ const name$1 = relative(root, path).split(sep).join("/");
618
+ if (info.isSymbolicLink()) {
619
+ const target = await readlink(path);
620
+ const child = relative(root, resolve(dirname(path), target));
621
+ if (isAbsolute(target) || child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) throw new Error(`skill symlink escapes its source root: ${path}`);
622
+ hash.update(`link\0${name$1}\0${target}\0`);
623
+ return;
624
+ }
625
+ if (info.isFile()) {
626
+ hash.update(`file\0${name$1}\0`);
627
+ hash.update(await readFile(path));
628
+ hash.update("\0");
629
+ return;
630
+ }
631
+ if (!info.isDirectory()) throw new Error(`unsupported skill entry: ${path}`);
632
+ hash.update(`directory\0${name$1}\0`);
633
+ const entries = await readdir(path);
634
+ entries.sort();
635
+ for (const entry of entries) await visit(join(path, entry));
636
+ };
637
+ await visit(root);
638
+ return hash.digest("hex");
639
+ }
640
+ async function pathExists(path) {
641
+ try {
642
+ await lstat(path);
643
+ return true;
644
+ } catch (error) {
645
+ if (isNodeError(error) && error.code === "ENOENT") return false;
646
+ throw error;
647
+ }
648
+ }
649
+ async function readSkillSource(markdownPath, layout, directoryPath = markdownPath) {
650
+ return {
651
+ path: layout === "directory" ? directoryPath : markdownPath,
652
+ layout,
653
+ ...await parseSkillFrontmatter(markdownPath)
654
+ };
655
+ }
656
+ async function parseSkillFrontmatter(path) {
657
+ const text = await readFile(path, "utf8");
658
+ const match = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/.exec(text);
659
+ if (!match) throw new Error(`missing YAML frontmatter: ${path}`);
660
+ const metadata = parse(match[1]);
661
+ if (!isRecord(metadata) || typeof metadata.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(metadata.name) || typeof metadata.description !== "string" || !metadata.description.trim()) throw new Error(`invalid skill frontmatter: ${path}`);
662
+ return {
663
+ name: metadata.name,
664
+ description: metadata.description.trim()
665
+ };
666
+ }
667
+ async function isFile(path) {
668
+ try {
669
+ return (await stat(path)).isFile();
670
+ } catch {
671
+ return false;
672
+ }
673
+ }
674
+ function requiredString(value, field) {
675
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} must be a non-empty string`);
676
+ return value.trim();
677
+ }
678
+ function asOptionalString(value) {
679
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
680
+ }
681
+ function requiredBoolean(value, field) {
682
+ if (typeof value !== "boolean") throw new Error(`${field} must be boolean`);
683
+ return value;
684
+ }
685
+ function requiredScope(value) {
686
+ if (value !== "global" && value !== "project") throw new Error("scope must be global or project");
687
+ return value;
688
+ }
689
+ function isRecord(value) {
690
+ return typeof value === "object" && value !== null && !Array.isArray(value);
691
+ }
692
+ function messageOf(value) {
693
+ return value instanceof Error ? value.message : String(value);
694
+ }
695
+ function isNodeError(value) {
696
+ return value instanceof Error && "code" in value;
697
+ }
698
+
699
+ //#endregion
700
+ export { Config, SkillManager, apply, inject, name };
@@ -0,0 +1,3 @@
1
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
2
+ export declare const inject: string[];
3
+ export declare function apply(ctx: ClientContext): void;