dsh-plugin-capabilities 0.1.1 → 0.1.3

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
@@ -796,6 +796,52 @@ function sendJson(response, status, body) {
796
796
  response.end(payload);
797
797
  }
798
798
 
799
+ // src/restart.ts
800
+ import { spawn } from "node:child_process";
801
+ import { openSync } from "node:fs";
802
+ import { tmpdir } from "node:os";
803
+ import { dirname, resolve } from "node:path";
804
+ function dshLaunch(argv = process.argv, execArgv = process.execArgv) {
805
+ const entry = argv[1];
806
+ if (entry !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
807
+ const abs = resolve(entry);
808
+ return { file: process.execPath, args: [...execArgv, abs, ...argv.slice(2)], cwd: dirname(abs), viaShell: false };
809
+ }
810
+ return { file: "dsh", args: [...argv.slice(2)], cwd: void 0, viaShell: process.platform === "win32" };
811
+ }
812
+ function scheduleRestart(launch) {
813
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
814
+ const logOut = `${tmpdir()}${tmpdir().endsWith("/") ? "" : "\\"}dsh-plugin-capabilities-restart-${stamp}.out.log`;
815
+ const logErr = logOut.replace(".out.log", ".err.log");
816
+ const child = spawn(launch.file, launch.args, {
817
+ cwd: launch.cwd,
818
+ stdio: ["ignore", openSync(logOut, "a"), openSync(logErr, "a")],
819
+ env: process.env,
820
+ shell: launch.viaShell,
821
+ windowsHide: true
822
+ });
823
+ child.unref();
824
+ setTimeout(() => process.kill(process.pid, "SIGTERM"), 500);
825
+ return { pid: process.pid, replacementPid: child.pid, logOut, logErr };
826
+ }
827
+ function trustedRestartRequest(request, socketAddress) {
828
+ const address = socketAddress ?? (request.socket.remoteAddress ?? "");
829
+ if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
830
+ if (request.headers.forwarded !== void 0 || request.headers["x-forwarded-for"] !== void 0 || request.headers["x-real-ip"] !== void 0) return false;
831
+ const origin = request.headers.origin;
832
+ const host = request.headers.host;
833
+ if (origin === void 0 || host === void 0) return false;
834
+ try {
835
+ const parsed = new URL(origin);
836
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host === host;
837
+ } catch {
838
+ return false;
839
+ }
840
+ }
841
+ function restartOwnedByShell(env = process.env) {
842
+ return env.DSH_DESKTOP === "1";
843
+ }
844
+
799
845
  // src/skills.ts
800
846
  import { existsSync as existsSync2, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
801
847
  import { homedir as homedir3 } from "node:os";
@@ -858,7 +904,10 @@ var SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,32}$/;
858
904
  function loadPatch(profileDirPath) {
859
905
  const path = join4(profileDirPath, "cordis.patch.yml");
860
906
  const text = existsSync3(path) ? readFileSync2(path, "utf8") : "[]";
861
- return parseDocument(text);
907
+ const doc = parseDocument(text);
908
+ const contents = doc.contents;
909
+ if (contents !== null && contents.flow === true && contents.items.length === 0) contents.flow = false;
910
+ return doc;
862
911
  }
863
912
  function savePatch(profileDirPath, doc) {
864
913
  mkdirSync2(profileDirPath, { recursive: true });
@@ -868,34 +917,87 @@ function toNode(value) {
868
917
  return new Document(value).contents;
869
918
  }
870
919
  function rowSeq(doc) {
871
- if (doc.contents === null) doc.contents = toNode([]);
920
+ if (doc.contents === null) {
921
+ doc.contents = toNode([]);
922
+ doc.contents.flow = false;
923
+ }
872
924
  return doc.contents;
873
925
  }
874
- function mcpRows(doc) {
875
- return (rowSeq(doc).items ?? []).filter((item) => item.get("name") === MCP_PLUGIN);
926
+ function isSeqNode(value) {
927
+ return typeof value === "object" && value !== null && Array.isArray(value.items);
928
+ }
929
+ function insertListOf(item) {
930
+ if (item.has("id")) return void 0;
931
+ const node = item.get("insert");
932
+ return isSeqNode(node) ? node : void 0;
933
+ }
934
+ function mcpRowItems(doc) {
935
+ const found = [];
936
+ for (const item of rowSeq(doc).items ?? []) {
937
+ if (item.get("name") === MCP_PLUGIN) found.push({ node: item });
938
+ const list = insertListOf(item);
939
+ for (const row of list?.items ?? []) {
940
+ if (row.get("name") === MCP_PLUGIN) found.push({ node: row, list });
941
+ }
942
+ }
943
+ return found;
944
+ }
945
+ function rowToMcp(doc, item) {
946
+ const configNode = item.get("config");
947
+ const plain = typeof configNode === "object" && configNode !== null && typeof configNode.toJS === "function" ? configNode.toJS(doc) : {};
948
+ return {
949
+ id: String(item.get("id") ?? ""),
950
+ serverName: String(plain.serverName ?? ""),
951
+ transport: plain.transport === "streamable-http" ? "streamable-http" : "stdio",
952
+ disabled: item.get("disabled") === true,
953
+ ...typeof plain.command === "string" && plain.command !== "" ? { command: plain.command } : {},
954
+ ...Array.isArray(plain.args) ? { args: plain.args.map(String) } : {},
955
+ ...isStringMap(plain.env) ? { env: plain.env } : {},
956
+ ...typeof plain.cwd === "string" && plain.cwd !== "" ? { cwd: plain.cwd } : {},
957
+ ...typeof plain.url === "string" && plain.url !== "" ? { url: plain.url } : {},
958
+ ...isStringMap(plain.headers) ? { headers: plain.headers } : {}
959
+ };
876
960
  }
877
961
  function isStringMap(value) {
878
962
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
879
963
  return Object.values(value).every((entry) => typeof entry === "string");
880
964
  }
965
+ function managedInsert(doc) {
966
+ const seq = rowSeq(doc);
967
+ const bare = [];
968
+ let target;
969
+ for (const item of seq.items ?? []) {
970
+ if (item.get("name") === MCP_PLUGIN) bare.push(item);
971
+ const list = insertListOf(item);
972
+ if (list !== void 0 && list.items.some((row) => row.get("name") === MCP_PLUGIN)) target ??= list;
973
+ }
974
+ if (target === void 0) {
975
+ const entry = toNode({ insert: [] });
976
+ seq.add(entry);
977
+ target = entry.get("insert");
978
+ target.flow = false;
979
+ }
980
+ for (const row of bare) {
981
+ seq.items.splice(seq.items.indexOf(row), 1);
982
+ target.add(row);
983
+ }
984
+ return target;
985
+ }
986
+ function takenIds(doc) {
987
+ const taken = /* @__PURE__ */ new Set();
988
+ for (const item of rowSeq(doc).items ?? []) {
989
+ const id = String(item.get("id") ?? "");
990
+ if (id !== "") taken.add(id);
991
+ for (const row of insertListOf(item)?.items ?? []) {
992
+ const rowId = String(row.get("id") ?? "");
993
+ if (rowId !== "") taken.add(rowId);
994
+ }
995
+ }
996
+ return taken;
997
+ }
881
998
  function listMcp(profileDirPath) {
882
999
  const doc = loadPatch(profileDirPath);
883
- return mcpRows(doc).map((item) => {
884
- const configNode = item.get("config");
885
- const plain = typeof configNode === "object" && configNode !== null && typeof configNode.toJS === "function" ? configNode.toJS(doc) : {};
886
- return {
887
- id: String(item.get("id") ?? ""),
888
- serverName: String(plain.serverName ?? ""),
889
- transport: plain.transport === "streamable-http" ? "streamable-http" : "stdio",
890
- disabled: item.get("disabled") === true,
891
- ...typeof plain.command === "string" && plain.command !== "" ? { command: plain.command } : {},
892
- ...Array.isArray(plain.args) ? { args: plain.args.map(String) } : {},
893
- ...isStringMap(plain.env) ? { env: plain.env } : {},
894
- ...typeof plain.cwd === "string" && plain.cwd !== "" ? { cwd: plain.cwd } : {},
895
- ...typeof plain.url === "string" && plain.url !== "" ? { url: plain.url } : {},
896
- ...isStringMap(plain.headers) ? { headers: plain.headers } : {}
897
- };
898
- });
1000
+ return mcpRowItems(doc).map(({ node }) => rowToMcp(doc, node));
899
1001
  }
900
1002
  function validateMcpInput(input) {
901
1003
  if (!SERVER_NAME_RE.test(input.serverName)) return "serverName must be 1-32 chars of A-Z a-z 0-9 _ -";
@@ -911,13 +1013,11 @@ function validateMcpInput(input) {
911
1013
  function upsertMcp(profileDirPath, input) {
912
1014
  const inputId = input.id ?? "";
913
1015
  const doc = loadPatch(profileDirPath);
914
- const seq = rowSeq(doc);
915
- const existing = inputId !== "" ? mcpRows(doc).find((item) => item.get("id") === inputId) : void 0;
1016
+ const list = managedInsert(doc);
1017
+ const existing = inputId !== "" ? mcpRowItems(doc).find(({ node: node2 }) => String(node2.get("id") ?? "") === inputId) : void 0;
916
1018
  let id = inputId !== "" ? inputId : `mcp-${input.serverName}`;
917
1019
  if (existing === void 0) {
918
- const taken = new Set(
919
- (seq.items ?? []).map((item) => String(item.get("id") ?? "")).filter((id2) => id2 !== "")
920
- );
1020
+ const taken = takenIds(doc);
921
1021
  let suffix = 2;
922
1022
  while (taken.has(id)) id = `mcp-${input.serverName}-${suffix++}`;
923
1023
  }
@@ -937,26 +1037,37 @@ function upsertMcp(profileDirPath, input) {
937
1037
  const row = { id, name: MCP_PLUGIN, config };
938
1038
  if (input.disabled === true) row.disabled = true;
939
1039
  const node = toNode(row);
940
- if (existing === void 0) seq.add(node);
941
- else seq.items[seq.items.indexOf(existing)] = node;
1040
+ if (existing === void 0) {
1041
+ list.add(node);
1042
+ } else if (existing.list !== void 0) {
1043
+ existing.list.items.splice(existing.list.items.indexOf(existing.node), 1, node);
1044
+ } else {
1045
+ rowSeq(doc).items.splice(rowSeq(doc).items.indexOf(existing.node), 1, node);
1046
+ }
942
1047
  savePatch(profileDirPath, doc);
943
1048
  return id;
944
1049
  }
945
1050
  function setMcpDisabled(profileDirPath, id, disabled) {
946
1051
  const doc = loadPatch(profileDirPath);
947
- const item = mcpRows(doc).find((row) => row.get("id") === id);
948
- if (item === void 0) return false;
949
- if (disabled) item.set("disabled", true);
950
- else item.delete("disabled");
1052
+ managedInsert(doc);
1053
+ const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
1054
+ if (hit === void 0) return false;
1055
+ if (disabled) hit.node.set("disabled", true);
1056
+ else hit.node.delete("disabled");
951
1057
  savePatch(profileDirPath, doc);
952
1058
  return true;
953
1059
  }
954
1060
  function removeMcp(profileDirPath, id) {
955
1061
  const doc = loadPatch(profileDirPath);
956
- const item = mcpRows(doc).find((row) => row.get("id") === id);
957
- if (item === void 0) return false;
1062
+ managedInsert(doc);
1063
+ const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
1064
+ if (hit === void 0 || hit.list === void 0) return false;
1065
+ hit.list.items.splice(hit.list.items.indexOf(hit.node), 1);
958
1066
  const seq = rowSeq(doc);
959
- seq.items.splice(seq.items.indexOf(item), 1);
1067
+ const owner = (seq.items ?? []).find((item) => insertListOf(item) === hit.list);
1068
+ if (owner !== void 0 && hit.list.items.length === 0 && owner.items.length === 1) {
1069
+ seq.items.splice(seq.items.indexOf(owner), 1);
1070
+ }
960
1071
  savePatch(profileDirPath, doc);
961
1072
  return true;
962
1073
  }
@@ -1217,6 +1328,27 @@ function mountCapabilitiesRoutes(host, config) {
1217
1328
  sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
1218
1329
  }
1219
1330
  }
1331
+ }),
1332
+ host.webServer.register({
1333
+ kind: "exact",
1334
+ path: "/dsh-plugin-capabilities/restart",
1335
+ handler: (request, response) => {
1336
+ if (request.method !== "POST") {
1337
+ response.writeHead(405, { allow: "POST" });
1338
+ response.end();
1339
+ return;
1340
+ }
1341
+ if (!trustedRestartRequest(request)) {
1342
+ sendJson(response, 403, { error: "untrusted origin" });
1343
+ return;
1344
+ }
1345
+ if (restartOwnedByShell()) {
1346
+ sendJson(response, 409, { error: "restart is owned by the desktop shell" });
1347
+ return;
1348
+ }
1349
+ const { pid, replacementPid, logOut } = scheduleRestart(dshLaunch());
1350
+ sendJson(response, 200, { ok: true, pid, replacementPid, logOut });
1351
+ }
1220
1352
  })
1221
1353
  ];
1222
1354
  return () => {