dsh-skill-picker 0.5.22 → 0.5.23

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.js
2
- import { mkdir, readFile as readFile2, readdir as readdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
2
+ import { mkdir as mkdir2, readFile as readFile3, readdir as readdir3, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
3
3
  import os2 from "node:os";
4
- import path3 from "node:path";
4
+ import path4 from "node:path";
5
5
 
6
6
  // src/dir-entry.js
7
7
  import { stat } from "node:fs/promises";
@@ -242,17 +242,160 @@ async function healUiSkillPatches() {
242
242
  return { files: filesReport, errors };
243
243
  }
244
244
 
245
+ // src/skill-ops.js
246
+ import { spawn } from "node:child_process";
247
+ import { mkdir, readFile as readFile2, readdir as readdir2, rename as rename2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
248
+ import path3 from "node:path";
249
+ var ENTRY = "SKILL.md";
250
+ var DISABLED_ENTRY = `${ENTRY}.disabled`;
251
+ var CLIENT_HEADER = "x-dsh-skill-picker";
252
+ function skillBackupDir() {
253
+ return path3.join(dshHome(), "skill-backups");
254
+ }
255
+ function resolveSkillPath(candidate, roots) {
256
+ if (typeof candidate !== "string" || candidate === "") return void 0;
257
+ const list = Array.isArray(roots) ? roots : [];
258
+ const resolved = path3.resolve(candidate);
259
+ for (const root of list) {
260
+ const base = path3.resolve(root);
261
+ const relative = path3.relative(base, resolved);
262
+ if (relative === "" || relative.startsWith("..") || path3.isAbsolute(relative)) continue;
263
+ if (relative.split(path3.sep).filter(Boolean).length !== 1) continue;
264
+ return resolved;
265
+ }
266
+ return void 0;
267
+ }
268
+ async function pathKind(target) {
269
+ try {
270
+ return (await stat2(target)).isDirectory() ? "dir" : "file";
271
+ } catch {
272
+ return void 0;
273
+ }
274
+ }
275
+ async function exists(candidate) {
276
+ try {
277
+ await stat2(candidate);
278
+ return true;
279
+ } catch {
280
+ return false;
281
+ }
282
+ }
283
+ async function setSkillDisabled(target, disabled) {
284
+ const kind = await pathKind(target);
285
+ if (kind === void 0) throw new Error(`skill not found: ${target}`);
286
+ const dir = kind === "dir" ? target : path3.dirname(target);
287
+ const base = kind === "dir" ? ENTRY : path3.basename(target);
288
+ const plain = path3.join(dir, base);
289
+ const off = path3.join(dir, `${base}.disabled`);
290
+ const hasPlain = await exists(plain);
291
+ const hasOff = await exists(off);
292
+ if (disabled) {
293
+ if (hasOff && !hasPlain) throw new Error("already disabled");
294
+ if (!hasPlain) throw new Error(`cannot disable: ${base} is not there`);
295
+ if (hasOff) throw new Error(`cannot disable: ${base} and ${base}.disabled both exist`);
296
+ await rename2(plain, off);
297
+ return { entry: `${base}.disabled`, disabled: true };
298
+ }
299
+ if (hasPlain && !hasOff) throw new Error("already enabled");
300
+ if (!hasOff) throw new Error(`cannot enable: ${base}.disabled is not there`);
301
+ if (hasPlain) throw new Error(`cannot enable: ${base} and ${base}.disabled both exist`);
302
+ await rename2(off, plain);
303
+ return { entry: base, disabled: false };
304
+ }
305
+ async function uninstallSkill(target) {
306
+ const kind = await pathKind(target);
307
+ if (kind === void 0) throw new Error(`skill not found: ${target}`);
308
+ const name = path3.basename(target);
309
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
310
+ const backupPath = path3.join(skillBackupDir(), `${stamp}-${name}`);
311
+ await mkdir(backupPath, { recursive: true });
312
+ await rename2(target, path3.join(backupPath, name));
313
+ await writeFile2(path3.join(backupPath, "manifest.json"), `${JSON.stringify({
314
+ name,
315
+ kind,
316
+ originalPath: target,
317
+ movedAt: (/* @__PURE__ */ new Date()).toISOString()
318
+ }, null, 2)}
319
+ `, "utf8");
320
+ return { backupPath, restoreTarget: target };
321
+ }
322
+ async function listSkillBackups(roots) {
323
+ let entries;
324
+ try {
325
+ entries = await readdir2(skillBackupDir(), { withFileTypes: true });
326
+ } catch {
327
+ return [];
328
+ }
329
+ const backups = [];
330
+ for (const entry of entries) {
331
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
332
+ const dir = path3.join(skillBackupDir(), entry.name);
333
+ try {
334
+ const manifest = JSON.parse(await readFile2(path3.join(dir, "manifest.json"), "utf8"));
335
+ backups.push({
336
+ id: entry.name,
337
+ name: typeof manifest.name === "string" ? manifest.name : entry.name,
338
+ originalPath: typeof manifest.originalPath === "string" ? manifest.originalPath : void 0,
339
+ movedAt: typeof manifest.movedAt === "string" ? manifest.movedAt : void 0,
340
+ restorable: resolveSkillPath(manifest.originalPath, roots) !== void 0
341
+ });
342
+ } catch {
343
+ }
344
+ }
345
+ return backups.sort((a, b) => String(b.movedAt ?? "").localeCompare(String(a.movedAt ?? "")));
346
+ }
347
+ async function restoreSkillBackup(id, roots) {
348
+ if (typeof id !== "string" || id === "" || id.includes("/") || id.includes("\\") || id.includes("..")) {
349
+ throw new Error("invalid backup id");
350
+ }
351
+ const dir = path3.join(skillBackupDir(), id);
352
+ let manifest;
353
+ try {
354
+ manifest = JSON.parse(await readFile2(path3.join(dir, "manifest.json"), "utf8"));
355
+ } catch {
356
+ throw new Error("that backup is gone");
357
+ }
358
+ const original = resolveSkillPath(manifest.originalPath, roots);
359
+ if (original === void 0) throw new Error("that backup no longer points inside a skill root");
360
+ if (await exists(original)) {
361
+ throw new Error(`${path3.basename(original)} is occupied again \u2014 move it away first`);
362
+ }
363
+ await rename2(path3.join(dir, path3.basename(original)), original);
364
+ const done = path3.join(skillBackupDir(), ".restored");
365
+ await mkdir(done, { recursive: true });
366
+ await rename2(dir, path3.join(done, id)).catch(() => {
367
+ });
368
+ return { restored: original };
369
+ }
370
+ function revealSkill(target, revealPath) {
371
+ const show = typeof revealPath === "string" && revealPath !== "" ? revealPath : target;
372
+ try {
373
+ if (process.platform === "win32") {
374
+ spawn("explorer.exe", [`/select,${show}`], { detached: true, stdio: "ignore" }).unref();
375
+ return true;
376
+ }
377
+ if (process.platform === "darwin") {
378
+ spawn("open", ["-R", show], { detached: true, stdio: "ignore" }).unref();
379
+ return true;
380
+ }
381
+ spawn("xdg-open", [path3.dirname(show)], { detached: true, stdio: "ignore" }).unref();
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+
245
388
  // src/index.js
246
389
  var inject = ["webServer", "systemPrompt"];
247
390
  var SECTION_ORDER = 215;
248
391
  var SKILL_PICKER_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002";
249
392
  function userSkillsDir() {
250
- const home = process.env.DSH_HOME ?? path3.join(os2.homedir(), ".dsh");
251
- return path3.join(home, "skills");
393
+ const home = process.env.DSH_HOME ?? path4.join(os2.homedir(), ".dsh");
394
+ return path4.join(home, "skills");
252
395
  }
253
396
  function userAgentsSkillsDir() {
254
- const agentsHome = process.env.DSH_AGENTS_HOME ?? path3.join(os2.homedir(), ".agents");
255
- return path3.join(agentsHome, "skills");
397
+ const agentsHome = process.env.DSH_AGENTS_HOME ?? path4.join(os2.homedir(), ".agents");
398
+ return path4.join(agentsHome, "skills");
256
399
  }
257
400
  function frontmatterBlock(content) {
258
401
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -312,25 +455,35 @@ function isUserInvocableSkill(meta, content) {
312
455
  async function scanSkillsDirInto(map, dir) {
313
456
  let entries;
314
457
  try {
315
- entries = await readdir2(dir, { withFileTypes: true });
458
+ entries = await readdir3(dir, { withFileTypes: true });
316
459
  } catch {
317
460
  return;
318
461
  }
319
462
  for (const entry of entries) {
320
463
  if (!await isDirectoryEntry(dir, entry)) continue;
321
- const skillDir = path3.join(dir, entry.name);
464
+ const skillDir = path4.join(dir, entry.name);
322
465
  let content;
466
+ let entryFile = ENTRY;
467
+ let disabled = false;
323
468
  try {
324
- content = await readFile2(path3.join(skillDir, "SKILL.md"), "utf8");
469
+ content = await readFile3(path4.join(skillDir, ENTRY), "utf8");
325
470
  } catch {
326
- continue;
471
+ try {
472
+ content = await readFile3(path4.join(skillDir, DISABLED_ENTRY), "utf8");
473
+ entryFile = DISABLED_ENTRY;
474
+ disabled = true;
475
+ } catch {
476
+ continue;
477
+ }
327
478
  }
328
479
  const meta = parseFrontmatter(content);
329
480
  if (!isUserInvocableSkill(meta, content)) continue;
330
481
  map.set(meta.name ?? entry.name, {
331
482
  name: meta.name ?? entry.name,
332
483
  description: meta.description ?? "",
333
- path: skillDir
484
+ path: skillDir,
485
+ entry: entryFile,
486
+ disabled
334
487
  });
335
488
  }
336
489
  }
@@ -339,8 +492,8 @@ async function scanSkills(cwd) {
339
492
  await scanSkillsDirInto(map, userAgentsSkillsDir());
340
493
  await scanSkillsDirInto(map, userSkillsDir());
341
494
  if (typeof cwd === "string" && cwd !== "") {
342
- await scanSkillsDirInto(map, path3.join(cwd, ".agents", "skills"));
343
- await scanSkillsDirInto(map, path3.join(cwd, ".dsh", "skills"));
495
+ await scanSkillsDirInto(map, path4.join(cwd, ".agents", "skills"));
496
+ await scanSkillsDirInto(map, path4.join(cwd, ".dsh", "skills"));
344
497
  }
345
498
  return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
346
499
  }
@@ -362,7 +515,7 @@ function reportUiSkillPatches(report, io = console) {
362
515
  }
363
516
  }
364
517
  function sharedStateFile() {
365
- return path3.join(dshHome(), "dsh-skill-picker-state.json");
518
+ return path4.join(dshHome(), "dsh-skill-picker-state.json");
366
519
  }
367
520
  function normalizeSharedState(value) {
368
521
  const source = value !== null && typeof value === "object" ? value : {};
@@ -381,7 +534,7 @@ function normalizeSharedState(value) {
381
534
  }
382
535
  async function readSharedState() {
383
536
  try {
384
- return normalizeSharedState(JSON.parse(await readFile2(sharedStateFile(), "utf8")));
537
+ return normalizeSharedState(JSON.parse(await readFile3(sharedStateFile(), "utf8")));
385
538
  } catch {
386
539
  return null;
387
540
  }
@@ -389,11 +542,11 @@ async function readSharedState() {
389
542
  async function writeSharedState(state) {
390
543
  const normal = normalizeSharedState(state);
391
544
  const file = sharedStateFile();
392
- await mkdir(path3.dirname(file), { recursive: true });
545
+ await mkdir2(path4.dirname(file), { recursive: true });
393
546
  const tmp = `${file}.tmp`;
394
- await writeFile2(tmp, `${JSON.stringify(normal, null, 2)}
547
+ await writeFile3(tmp, `${JSON.stringify(normal, null, 2)}
395
548
  `, "utf8");
396
- await rename2(tmp, file);
549
+ await rename3(tmp, file);
397
550
  return normal;
398
551
  }
399
552
  function mergeSharedState(a, b) {
@@ -438,6 +591,75 @@ async function handleSharedState(req, res, url) {
438
591
  json(500, { ok: false, error: String(error?.message ?? error) });
439
592
  }
440
593
  }
594
+ function allSkillRoots(cwd) {
595
+ const roots = [userAgentsSkillsDir(), userSkillsDir()];
596
+ if (typeof cwd === "string" && cwd !== "") {
597
+ roots.push(path4.join(cwd, ".agents", "skills"), path4.join(cwd, ".dsh", "skills"));
598
+ }
599
+ return roots;
600
+ }
601
+ async function handleSkillOp(req, res, url) {
602
+ const json = (code, payload) => {
603
+ res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
604
+ res.end(JSON.stringify(payload));
605
+ };
606
+ if (req.method !== "POST") {
607
+ json(405, { ok: false, error: "POST only" });
608
+ return;
609
+ }
610
+ if (req.headers[CLIENT_HEADER] !== "1") {
611
+ json(403, { ok: false, error: "missing client header" });
612
+ return;
613
+ }
614
+ try {
615
+ const body = await readJsonBody(req);
616
+ const cwd = url.searchParams.get("cwd") ?? void 0;
617
+ const roots = allSkillRoots(cwd);
618
+ const action = String(body?.action ?? "");
619
+ if (action === "backups") {
620
+ json(200, { ok: true, backups: await listSkillBackups(roots) });
621
+ return;
622
+ }
623
+ if (action === "restore") {
624
+ json(200, { ok: true, ...await restoreSkillBackup(body?.id, roots) });
625
+ return;
626
+ }
627
+ const name = String(body?.name ?? "");
628
+ if (name === "") {
629
+ json(400, { ok: false, error: "name is required" });
630
+ return;
631
+ }
632
+ const known = (await scanSkills(cwd)).find((skill) => skill.name === name);
633
+ if (known === void 0) {
634
+ json(404, { ok: false, error: `no skill named "${name}"` });
635
+ return;
636
+ }
637
+ const target = resolveSkillPath(known.path, roots);
638
+ if (target === void 0) {
639
+ json(403, { ok: false, error: "that skill lives outside the skill roots" });
640
+ return;
641
+ }
642
+ if (action === "disable") {
643
+ json(200, { ok: true, ...await setSkillDisabled(target, true) });
644
+ return;
645
+ }
646
+ if (action === "enable") {
647
+ json(200, { ok: true, ...await setSkillDisabled(target, false) });
648
+ return;
649
+ }
650
+ if (action === "reveal") {
651
+ json(200, { ok: true, revealed: revealSkill(target) });
652
+ return;
653
+ }
654
+ if (action === "uninstall") {
655
+ json(200, { ok: true, ...await uninstallSkill(target) });
656
+ return;
657
+ }
658
+ json(400, { ok: false, error: `unknown action: ${action}` });
659
+ } catch (error) {
660
+ json(500, { ok: false, error: String(error?.message ?? error) });
661
+ }
662
+ }
441
663
  function apply(ctx) {
442
664
  ctx.effect(() => {
443
665
  const handler = async (req, res) => {
@@ -447,6 +669,10 @@ function apply(ctx) {
447
669
  await handleSharedState(req, res, url);
448
670
  return;
449
671
  }
672
+ if (url.pathname === "/dsh-skill-picker/skill") {
673
+ await handleSkillOp(req, res, url);
674
+ return;
675
+ }
450
676
  const cwd = url.searchParams.get("cwd") ?? void 0;
451
677
  const skills = await scanSkills(cwd);
452
678
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });