@artenx/dshb 0.1.8 → 0.1.10

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.
Files changed (47) hide show
  1. package/lib/client.js +8 -8
  2. package/lib/client.js.map +1 -1
  3. package/lib/core/index.js +5 -1
  4. package/lib/core/index.js.map +1 -1
  5. package/lib/core/routes.d.ts +8 -1
  6. package/lib/core/routes.js +65 -3
  7. package/lib/core/routes.js.map +1 -1
  8. package/lib/core/workspace-bindings.js +6 -4
  9. package/lib/core/workspace-bindings.js.map +1 -1
  10. package/lib/exec-docker/daemon-runner.js +2 -2
  11. package/lib/exec-docker/docker-backend.d.ts +8 -0
  12. package/lib/exec-docker/docker-client.d.ts +9 -0
  13. package/lib/exec-docker/docker-client.js +10 -2
  14. package/lib/exec-docker/docker-client.js.map +1 -1
  15. package/lib/exec-docker/docker-routes.js +1 -1
  16. package/lib/exec-docker/fs-provider.d.ts +3 -12
  17. package/lib/exec-docker/fs-provider.js +19 -2
  18. package/lib/exec-docker/fs-provider.js.map +1 -1
  19. package/lib/exec-docker/index.js +1 -1
  20. package/lib/exec-docker/process-provider.d.ts +3 -1
  21. package/lib/exec-docker/process-provider.js +29 -0
  22. package/lib/exec-docker/process-provider.js.map +1 -1
  23. package/lib/exec-docker/remote-docker-cli.d.ts +8 -0
  24. package/lib/exec-docker/remote-docker-cli.js +27 -1
  25. package/lib/exec-docker/remote-docker-cli.js.map +1 -1
  26. package/lib/exec-docker/world.d.ts +1 -1
  27. package/lib/exec-ssh/executor.d.ts +7 -0
  28. package/lib/exec-ssh/executor.js +18 -0
  29. package/lib/exec-ssh/executor.js.map +1 -1
  30. package/lib/exec-ssh/fs-provider.d.ts +3 -12
  31. package/lib/exec-ssh/fs-provider.js +20 -3
  32. package/lib/exec-ssh/fs-provider.js.map +1 -1
  33. package/lib/exec-ssh/process-provider.d.ts +3 -1
  34. package/lib/exec-ssh/process-provider.js +29 -0
  35. package/lib/exec-ssh/process-provider.js.map +1 -1
  36. package/lib/exec-ssh/world.d.ts +1 -1
  37. package/lib/router/decorate-fs.js +16 -2
  38. package/lib/router/decorate-fs.js.map +1 -1
  39. package/lib/router/decorate-shell.d.ts +1 -1
  40. package/lib/router/index.d.ts +1 -1
  41. package/lib/router/index.js +20 -0
  42. package/lib/router/index.js.map +1 -1
  43. package/lib/router/resolve.d.ts +1 -1
  44. package/lib/router/router-fs.d.ts +1 -1
  45. package/lib/router/types.d.ts +1 -1
  46. package/lib/{types-C0TtEO7z.d.ts → types-DPP05ZsA.d.ts} +4 -2
  47. package/package.json +1 -1
package/lib/core/index.js CHANGED
@@ -16,12 +16,16 @@ function apply(ctx) {
16
16
  const knownHosts = new KnownHostsStore();
17
17
  const registry = new NodeRegistry(ctx);
18
18
  const bindings = new WorkspaceBindingsStore();
19
+ let workspaceRegistry;
20
+ if (typeof ctx.inject === "function") ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
21
+ workspaceRegistry = workspaceCtx.workspaceRegistry;
22
+ });
19
23
  ctx.provide("nodeRegistry", registry);
20
24
  ctx.provide("knownHosts", knownHosts);
21
25
  ctx.provide("workspaceBindings", bindings);
22
26
  ctx.provide("dshbAudit", sharedAuditLogger());
23
27
  sharedWorldResolver().setBindings(bindings);
24
- registerNodeRoutes(ctx, registry);
28
+ registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry);
25
29
  registerSshConfigRoutes(ctx, knownHosts);
26
30
  registerWorkspaceRoutes(ctx, registry, bindings);
27
31
  registerRemoteDownloadRoutes(ctx, registry, bindings);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,aAAa,aAAa;AAEjD,SAAgB,MAAM,KAAoB;CACxC,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,WAAW,IAAI,aAAa,GAAG;CACrC,MAAM,WAAW,IAAI,uBAAuB;CAC5C,IAAI,QAAQ,gBAAgB,QAAQ;CACpC,IAAI,QAAQ,cAAc,UAAU;CACpC,IAAI,QAAQ,qBAAqB,QAAQ;CACzC,IAAI,QAAQ,aAAa,kBAAkB,CAAC;CAC5C,oBAAoB,CAAC,CAAC,YAAY,QAAQ;CAC1C,mBAAwB,KAAK,QAAQ;CACrC,wBAA6B,KAAK,UAAU;CAC5C,wBAA6B,KAAK,UAAU,QAAQ;CACpD,6BAAkC,KAAK,UAAU,QAAQ;CACzD,mCAAwC,KAAK,gCAAgC,CAAC;CAC9E,qCAAqC,GAAG;CACxC,aAAa,KAAK,UAAU,QAAQ;CACpC,eAAe,KAAK,QAAQ;CAC5B,uBAAuB,KAAK,QAAQ;AACtC;AAEA,MAAM,eAAe;AAErB,SAAS,eAAe,KAAc,UAA8B;CAClE,MAAM,OAAO,YAA2B;EACtC,MAAM,YAAY,IAAI,IAAI,aAAa;EACvC,MAAM,eAAe,IAAI,IAAI,kBAAkB;EAC/C,KAAK,MAAM,QAAQ,SAAS,KAAK,GAC/B,IAAI;GACF,MAAM,SAAS,UAAU,KAAK,IAAI,WAAW,KAAA,GAAW,YAAY;EACtE,QAAQ,CAER;CAEJ;CACA,MAAM,QAAQ,kBAAkB,KAAK,KAAK,GAAG,YAAY;CACzD,IAAI,mBAAmB,cAAc,KAAK,CAAC;CAC3C,mBAAmB,KAAK,KAAK,CAAC;AAChC;AAEA,SAAS,uBAAuB,KAAc,UAA8B;CAC1E,mBAAmB;EACjB,CAAM,YAAY;GAChB,MAAM,cAAc,IAAI,IAAI,uBAAuB;GACnD,IAAI,CAAC,aAAa;GAClB,KAAK,MAAM,QAAQ,SAAS,KAAK,GAAG;IAClC,IAAI,KAAK,SAAS,kBAAkB,KAAK,SAAS,iBAAiB;IACnE,IAAI,CAAC,KAAK,QAAQ,OAAO;IACzB,IAAI,KAAK,QAAQ,aAAa;IAC9B,IAAI;KACF,MAAM,YAAY,UAAU,KAAK,EAAE;IACrC,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH;AAMA,SAAS,aAAa,KAAc,UAAwB,UAAwC;CAClG,mBAAmB;EACjB,CAAM,YAAY;GAChB,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;IAC/B,MAAM,OAAO,SAAS,IAAI,EAAE,MAAM;IAClC,IAAI,CAAC,QAAQ,KAAK,SAAS,cAAc;IACzC,IAAI;KAEF,MAAM,SADW,KAAK,SAAS,kBAAkB,KAAK,SAAS,kBAE1D,IAAI,IAAI,yBAAyB,IACjC,IAAI,IAAI,YAAY;KACzB,IAAI,CAAC,QAAQ;KACb,MAAM,QAAQ,MAAM,OAAO,OAAO,EAAE,MAAM;KAC1C,IAAI,OAAO,MAAM,MAAM,UAAU,EAAE,UAAU;IAC/C,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n let workspaceRegistry: { list(): Array<{ path: string; title: string }> } | undefined\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (workspaceCtx: Context) => {\n workspaceRegistry = (workspaceCtx as unknown as { workspaceRegistry?: typeof workspaceRegistry }).workspaceRegistry\n })\n }\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,aAAa,aAAa;AAEjD,SAAgB,MAAM,KAAoB;CACxC,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,WAAW,IAAI,aAAa,GAAG;CACrC,MAAM,WAAW,IAAI,uBAAuB;CAC5C,IAAI;CACJ,IAAI,OAAO,IAAI,WAAW,YACxB,IAAI,OAAO,CAAC,mBAAmB,IAAI,iBAA0B;EAC3D,oBAAqB,aAA6E;CACpG,CAAC;CAEH,IAAI,QAAQ,gBAAgB,QAAQ;CACpC,IAAI,QAAQ,cAAc,UAAU;CACpC,IAAI,QAAQ,qBAAqB,QAAQ;CACzC,IAAI,QAAQ,aAAa,kBAAkB,CAAC;CAC5C,oBAAoB,CAAC,CAAC,YAAY,QAAQ;CAC1C,mBAAwB,KAAK,UAAU,gBAAgB,iBAAiB;CACxE,wBAA6B,KAAK,UAAU;CAC5C,wBAA6B,KAAK,UAAU,QAAQ;CACpD,6BAAkC,KAAK,UAAU,QAAQ;CACzD,mCAAwC,KAAK,gCAAgC,CAAC;CAC9E,qCAAqC,GAAG;CACxC,aAAa,KAAK,UAAU,QAAQ;CACpC,eAAe,KAAK,QAAQ;CAC5B,uBAAuB,KAAK,QAAQ;AACtC;AAEA,MAAM,eAAe;AAErB,SAAS,eAAe,KAAc,UAA8B;CAClE,MAAM,OAAO,YAA2B;EACtC,MAAM,YAAY,IAAI,IAAI,aAAa;EACvC,MAAM,eAAe,IAAI,IAAI,kBAAkB;EAC/C,KAAK,MAAM,QAAQ,SAAS,KAAK,GAC/B,IAAI;GACF,MAAM,SAAS,UAAU,KAAK,IAAI,WAAW,KAAA,GAAW,YAAY;EACtE,QAAQ,CAER;CAEJ;CACA,MAAM,QAAQ,kBAAkB,KAAK,KAAK,GAAG,YAAY;CACzD,IAAI,mBAAmB,cAAc,KAAK,CAAC;CAC3C,mBAAmB,KAAK,KAAK,CAAC;AAChC;AAEA,SAAS,uBAAuB,KAAc,UAA8B;CAC1E,mBAAmB;EACjB,CAAM,YAAY;GAChB,MAAM,cAAc,IAAI,IAAI,uBAAuB;GACnD,IAAI,CAAC,aAAa;GAClB,KAAK,MAAM,QAAQ,SAAS,KAAK,GAAG;IAClC,IAAI,KAAK,SAAS,kBAAkB,KAAK,SAAS,iBAAiB;IACnE,IAAI,CAAC,KAAK,QAAQ,OAAO;IACzB,IAAI,KAAK,QAAQ,aAAa;IAC9B,IAAI;KACF,MAAM,YAAY,UAAU,KAAK,EAAE;IACrC,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH;AAMA,SAAS,aAAa,KAAc,UAAwB,UAAwC;CAClG,mBAAmB;EACjB,CAAM,YAAY;GAChB,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;IAC/B,MAAM,OAAO,SAAS,IAAI,EAAE,MAAM;IAClC,IAAI,CAAC,QAAQ,KAAK,SAAS,cAAc;IACzC,IAAI;KAEF,MAAM,SADW,KAAK,SAAS,kBAAkB,KAAK,SAAS,kBAE1D,IAAI,IAAI,yBAAyB,IACjC,IAAI,IAAI,YAAY;KACzB,IAAI,CAAC,QAAQ;KACb,MAAM,QAAQ,MAAM,OAAO,OAAO,EAAE,MAAM;KAC1C,IAAI,OAAO,MAAM,MAAM,UAAU,EAAE,UAAU;IAC/C,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH"}
@@ -1,8 +1,15 @@
1
1
  import { KnownHostsStore } from "./known-hosts.js";
2
2
  import { NodeRegistry } from "./node-registry.js";
3
+ import { WorkspaceBindingsStore } from "./workspace-bindings.js";
3
4
  import { Context } from "@deepseek-ai/cordis";
4
5
  //#region src/core/routes.d.ts
5
- declare function registerNodeRoutes(ctx: Context, registry: NodeRegistry): Promise<void>;
6
+ interface WorkspaceRegistryLike {
7
+ list(): Array<{
8
+ path: string;
9
+ title: string;
10
+ }>;
11
+ }
12
+ declare function registerNodeRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore, getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined): Promise<void>;
6
13
  declare function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void>;
7
14
  //#endregion
8
15
  export { registerNodeRoutes, registerSshConfigRoutes };
@@ -1,3 +1,4 @@
1
+ import { resolveHostRunner } from "../exec-docker/host-runner.js";
1
2
  import { readSshConfig, resolveSshConfigEntry } from "./ssh-config.js";
2
3
  import { testNode } from "./test.js";
3
4
  import { mkdirSync, readdirSync } from "node:fs";
@@ -58,7 +59,7 @@ function maybeProvision(ctx, node) {
58
59
  provisioner.provision(node.id).catch(() => {});
59
60
  return true;
60
61
  }
61
- async function registerNodeRoutes(ctx, registry) {
62
+ async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry) {
62
63
  const webServer = ctx.webServer;
63
64
  ctx.effect(() => webServer.register({
64
65
  kind: "prefix",
@@ -291,8 +292,69 @@ async function registerNodeRoutes(ctx, registry) {
291
292
  return;
292
293
  }
293
294
  if (req.method === "DELETE" && segs[4] === void 0) {
294
- await registry.remove(resourceId);
295
- sendJson(res, 200, { ok: true });
295
+ const node = registry.get(resourceId);
296
+ if (!node) {
297
+ sendJson(res, 404, {
298
+ ok: false,
299
+ error: "node not found"
300
+ });
301
+ return;
302
+ }
303
+ const attached = bindings.list().filter((binding) => binding.nodeId === resourceId);
304
+ let workspaceRegistry;
305
+ try {
306
+ workspaceRegistry = getWorkspaceRegistry?.();
307
+ } catch {}
308
+ let active = attached;
309
+ if (workspaceRegistry?.list) try {
310
+ const workspaces = workspaceRegistry.list();
311
+ const byPath = new Map(workspaces.map((workspace) => [workspace.path, workspace.title]));
312
+ active = attached.filter((binding) => byPath.has(binding.mirrorPath));
313
+ if (active.length > 0) {
314
+ const titles = active.map((binding) => byPath.get(binding.mirrorPath) ?? binding.mirrorPath);
315
+ sendJson(res, 409, {
316
+ ok: false,
317
+ code: "node/in-use",
318
+ error: `节点仍被工作区使用:${titles.join("、")}。请先移除或迁移这些工作区。`,
319
+ workspaces: titles
320
+ });
321
+ return;
322
+ }
323
+ } catch {
324
+ active = attached;
325
+ }
326
+ if (active.length > 0) {
327
+ sendJson(res, 409, {
328
+ ok: false,
329
+ code: "node/in-use",
330
+ error: `节点仍绑定 ${active.length} 个工作区,请先移除或迁移工作区。`
331
+ });
332
+ return;
333
+ }
334
+ try {
335
+ if ((node.type === "local-docker" || node.type === "remote-docker") && node.docker?.containerId) {
336
+ const runner = await resolveHostRunner(ctx, node);
337
+ if (!runner) throw new Error("Docker 节点执行环境不可用,节点未删除");
338
+ const result = await runner.run([
339
+ "docker",
340
+ "rm",
341
+ "-f",
342
+ node.docker.containerId
343
+ ]);
344
+ if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || "删除容器失败,节点未删除");
345
+ }
346
+ await registry.remove(resourceId);
347
+ const sshWorlds = ctx.get("dshbSshWorlds");
348
+ const dockerWorlds = ctx.get("dshbDockerWorlds");
349
+ sshWorlds?.remove?.(resourceId);
350
+ dockerWorlds?.remove?.(resourceId);
351
+ sendJson(res, 200, { ok: true });
352
+ } catch (err) {
353
+ sendJson(res, 502, {
354
+ ok: false,
355
+ error: err instanceof Error ? err.message : String(err)
356
+ });
357
+ }
296
358
  return;
297
359
  }
298
360
  if (req.method === "POST" && resourceId === "test-unsaved") {
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../src/core/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mkdirSync, readdirSync } from 'node:fs'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { NodeRegistry, type NodeProfile, type NodeType } from './node-registry.js'\nimport { readSshConfig, resolveSshConfigEntry } from './ssh-config.js'\nimport type { KnownHostsStore } from './known-hosts.js'\nimport { testNode, type SshHandshakeTester, type DockerNodeTester } from './test.js'\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{\n ensureDir(path: string): Promise<void>\n fs: { listDir(path: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean }>> }\n } | undefined>\n}\n\nfunction resolveWorlds(ctx: Context, nodeType: string): WorldsLike | undefined {\n if (nodeType === 'local-docker' || nodeType === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n }\n return ctx.get('dshbWorlds') as WorldsLike | undefined\n}\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n return path.split('/').filter(Boolean)\n}\n\nconst VALID_TYPES: NodeType[] = ['local-host', 'local-docker', 'remote-ssh', 'remote-docker']\n\ninterface DockerProvisionerLike {\n provision(nodeId: string): Promise<unknown>\n status(nodeId: string): { state: string; error?: string; containerId?: string; updatedAt: string } | undefined\n}\n\nfunction maybeProvision(ctx: Context, node: NodeProfile): boolean {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') return false\n if (!node.docker?.image) return false\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n if (!provisioner) return false\n void provisioner.provision(node.id).catch(() => {})\n return true\n}\n\nexport async function registerNodeRoutes(ctx: Context, registry: NodeRegistry): Promise<void> {\n const webServer = ctx.webServer\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/nodes',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const resourceId = segs[3]\n\n if (req.method === 'GET' && segs[4] === 'browse' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? (node.type === 'local-host' ? process.env.HOME ?? '/' : '/')\n try {\n if (node.type === 'local-host') {\n const entries = readdirSync(path, { withFileTypes: true }).map((e) => ({\n name: e.name,\n path: `${path.replace(/\\/$/, '')}/${e.name}`,\n isDirectory: e.isDirectory(),\n isFile: e.isFile(),\n }))\n sendJson(res, 200, { ok: true, path, entries })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n const entries = await world.fs.listDir(path)\n sendJson(res, 200, { ok: true, path, entries })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'mkdir' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const body = await readBody(req)\n const path = body && typeof body.path === 'string' ? body.path : undefined\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n try {\n if (node.type === 'local-host') {\n mkdirSync(path, { recursive: true })\n sendJson(res, 200, { ok: true, path })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(path)\n sendJson(res, 200, { ok: true, path })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'GET' && resourceId === undefined) {\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n const nodes = registry.list()\n const withFlags = await Promise.all(\n nodes.map(async (n) => ({\n ...n,\n hasSecret: await registry.hasSecret(n.id),\n status: registry.status(n.id),\n provision: provisioner?.status(n.id),\n })),\n )\n sendJson(res, 200, { ok: true, nodes: withFlags })\n return\n }\n\n if (req.method === 'POST' && resourceId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n const type = String(body.type ?? '') as NodeType\n if (!VALID_TYPES.includes(type)) {\n sendJson(res, 400, { ok: false, error: 'type 必须是 local-host/local-docker/remote-ssh/remote-docker' })\n return\n }\n try {\n const node: NodeProfile = await registry.create({\n name: String(body.name ?? ''),\n type,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 201, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 409, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (resourceId === undefined) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (req.method === 'GET' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id), status: registry.status(resourceId) } })\n return\n }\n\n if (req.method === 'PATCH' && segs[4] === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n try {\n const node = await registry.update(resourceId, {\n name: body.name as string | undefined,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 404, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'DELETE' && segs[4] === undefined) {\n await registry.remove(resourceId)\n sendJson(res, 200, { ok: true })\n return\n }\n\n if (req.method === 'POST' && resourceId === 'test-unsaved') {\n const body = await readBody(req)\n if (!body || !body.ssh) {\n sendJson(res, 400, { ok: false, error: '需要 ssh 配置' })\n return\n }\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n if (!sshTester) {\n sendJson(res, 503, { ok: false, error: 'SSH 执行通道不可用' })\n return\n }\n const ssh = body.ssh as { host: string; port: number; username: string; auth: never; jump?: never }\n const secrets = (body.secrets ?? {}) as { password?: string; privateKey?: string; passphrase?: string }\n const t = await sshTester.test(\n { host: ssh.host, port: Number(ssh.port) || 22, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n sendJson(res, 200, { ok: true, report: { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) } })\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'test') {\n const body = await readBody(req)\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n const overrides = body && (body.ssh || body.secrets)\n ? { ssh: body.ssh as never, secrets: body.secrets as never }\n : undefined\n const report = await testNode(registry, resourceId, sshTester, overrides, dockerTester)\n sendJson(res, 200, { ok: true, report })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\nexport async function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void> {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/api/dshb/ssh-config',\n handler: (_req, res) => {\n const entries = readSshConfig().map(resolveSshConfigEntry)\n sendJson(res, 200, { ok: true, entries })\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/known-hosts',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'GET' && action === undefined) {\n sendJson(res, 200, { ok: true, hosts: knownHosts.list() })\n return\n }\n\n if (req.method === 'POST' && action === 'forget') {\n const body = await readBody(req)\n if (!body || typeof body.host !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 host' })\n return\n }\n const port = Number(body.port ?? 22)\n knownHosts.forget(String(body.host), port)\n sendJson(res, 200, { ok: true })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":";;;;AAgBA,SAAS,cAAc,KAAc,UAA0C;CAC7E,IAAI,aAAa,kBAAkB,aAAa,iBAC9C,OAAO,IAAI,IAAI,yBAAyB;CAE1C,OAAO,IAAI,IAAI,YAAY;AAC7B;AAEA,MAAM,WAAW;AAEjB,SAAS,SAAS,KAAqB,QAAgB,MAAe,UAAkC,CAAC,GAAS;CAChH,IAAI,UAAU,QAAQ;EAAE,gBAAgB;EAAoB,GAAG;CAAQ,CAAC;CACxE,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,SAAS,KAA+D;CAC/E,OAAO,IAAI,SAAS,YAAY;EAC9B,KAAK,IAAI,QAAQ,mBAAmB,QAAQ,oBAAoB;GAC9D,QAAQ,IAAI;GACZ;EACF;EACA,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ,MAAM;GACd,IAAI,OAAO,UAAU;IACnB,QAAQ,IAAI;IACZ,IAAI,QAAQ;IACZ;GACF;GACA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,IAAI,GAAG,aAAa;GAClB,IAAI;IACF,QAAQ,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,CAA4B;GACvF,QAAQ;IACN,QAAQ,IAAI;GACd;EACF,CAAC;EACD,IAAI,GAAG,eAAe,QAAQ,IAAI,CAAC;CACrC,CAAC;AACH;AAEA,SAAS,aAAa,KAAgC;CAEpD,OADa,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SACrC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AACvC;AAEA,MAAM,cAA0B;CAAC;CAAc;CAAgB;CAAc;AAAe;AAO5F,SAAS,eAAe,KAAc,MAA4B;CAChE,IAAI,KAAK,SAAS,kBAAkB,KAAK,SAAS,iBAAiB,OAAO;CAC1E,IAAI,CAAC,KAAK,QAAQ,OAAO,OAAO;CAChC,MAAM,cAAc,IAAI,IAAI,uBAAuB;CACnD,IAAI,CAAC,aAAa,OAAO;CACzB,YAAiB,UAAU,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;CAClD,OAAO;AACT;AAEA,eAAsB,mBAAmB,KAAc,UAAuC;CAC5F,MAAM,YAAY,IAAI;CACtB,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,MAAM,OAAO,aAAa,GAAG;GAC7B,MAAM,aAAa,KAAK;GAExB,IAAI,IAAI,WAAW,SAAS,KAAK,OAAO,YAAY,YAAY;IAC9D,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,MAAM,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,aAAa,IAAI,MAAM,MAAM,KAAK,SAAS,eAAe,QAAQ,IAAI,QAAQ,MAAM;IACrI,IAAI;KACF,IAAI,KAAK,SAAS,cAAc;MAO9B,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM;OAAM,SANrB,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;QACrE,MAAM,EAAE;QACR,MAAM,GAAG,KAAK,QAAQ,OAAO,EAAE,EAAE,GAAG,EAAE;QACtC,aAAa,EAAE,YAAY;QAC3B,QAAQ,EAAE,OAAO;OACnB,EAC2C;MAAE,CAAC;MAC9C;KACF;KACA,MAAM,SAAS,cAAc,KAAK,KAAK,IAAI;KAC3C,IAAI,CAAC,QAAQ;MACX,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,QAAQ,MAAM,OAAO,OAAO,UAAU;KAC5C,IAAI,CAAC,OAAO;MACV,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KAEA,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM;MAAM,SAAA,MADf,MAAM,GAAG,QAAQ,IAAI;KACE,CAAC;IAChD,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,KAAK,OAAO,WAAW,YAAY;IAC9D,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,OAAO,QAAQ,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,KAAA;IACjE,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAU,CAAC;KAClD;IACF;IACA,IAAI;KACF,IAAI,KAAK,SAAS,cAAc;MAC9B,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;MACnC,SAAS,KAAK,KAAK;OAAE,IAAI;OAAM;MAAK,CAAC;MACrC;KACF;KACA,MAAM,SAAS,cAAc,KAAK,KAAK,IAAI;KAC3C,IAAI,CAAC,QAAQ;MACX,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,QAAQ,MAAM,OAAO,OAAO,UAAU;KAC5C,IAAI,CAAC,OAAO;MACV,SAAS,KAAK,KAAK;OAAE,IAAI;OAAO,OAAO;MAAY,CAAC;MACpD;KACF;KACA,MAAM,MAAM,UAAU,IAAI;KAC1B,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM;KAAK,CAAC;IACvC,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,SAAS,eAAe,KAAA,GAAW;IACpD,MAAM,cAAc,IAAI,IAAI,uBAAuB;IACnD,MAAM,QAAQ,SAAS,KAAK;IAS5B,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,OAAO,MARd,QAAQ,IAC9B,MAAM,IAAI,OAAO,OAAO;MACtB,GAAG;MACH,WAAW,MAAM,SAAS,UAAU,EAAE,EAAE;MACxC,QAAQ,SAAS,OAAO,EAAE,EAAE;MAC5B,WAAW,aAAa,OAAO,EAAE,EAAE;KACrC,EAAE,CACJ;IACgD,CAAC;IACjD;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,eAAe,KAAA,GAAW;IACrD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;IACnC,IAAI,CAAC,YAAY,SAAS,IAAI,GAAG;KAC/B,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4D,CAAC;KACpG;IACF;IACA,IAAI;KACF,MAAM,OAAoB,MAAM,SAAS,OAAO;MAC9C,MAAM,OAAO,KAAK,QAAQ,EAAE;MAC5B;MACA,KAAK,KAAK;MACV,QAAQ,KAAK;MACb,SAAS,KAAK;KAChB,CAAC;KACD,MAAM,YAAY,eAAe,KAAK,IAAI;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM;OAAE,GAAG;OAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAE;MAAG,cAAc;KAAU,CAAC;IAC3H,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,eAAe,KAAA,GAAW;IAC5B,SAAS,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IAC7D;GACF;GAEA,IAAI,IAAI,WAAW,SAAS,KAAK,OAAO,KAAA,GAAW;IACjD,MAAM,OAAO,SAAS,IAAI,UAAU;IACpC,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAiB,CAAC;KACzD;IACF;IACA,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,MAAM;MAAE,GAAG;MAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAG,QAAQ,SAAS,OAAO,UAAU;KAAE;IAAE,CAAC;IACrI;GACF;GAEA,IAAI,IAAI,WAAW,WAAW,KAAK,OAAO,KAAA,GAAW;IACnD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,MAAM;KACT,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,IAAI;KACF,MAAM,OAAO,MAAM,SAAS,OAAO,YAAY;MAC7C,MAAM,KAAK;MACX,KAAK,KAAK;MACV,QAAQ,KAAK;MACb,SAAS,KAAK;KAChB,CAAC;KACD,MAAM,YAAY,eAAe,KAAK,IAAI;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,MAAM;OAAE,GAAG;OAAM,WAAW,MAAM,SAAS,UAAU,KAAK,EAAE;MAAE;MAAG,cAAc;KAAU,CAAC;IAC3H,SAAS,KAAK;KACZ,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;KAAE,CAAC;IAC3F;IACA;GACF;GAEA,IAAI,IAAI,WAAW,YAAY,KAAK,OAAO,KAAA,GAAW;IACpD,MAAM,SAAS,OAAO,UAAU;IAChC,SAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IAC/B;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,eAAe,gBAAgB;IAC1D,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK;KACtB,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAY,CAAC;KACpD;IACF;IACA,MAAM,YAAY,IAAI,IAAI,aAAa;IACvC,IAAI,CAAC,WAAW;KACd,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAc,CAAC;KACtD;IACF;IACA,MAAM,MAAM,KAAK;IACjB,MAAM,UAAW,KAAK,WAAW,CAAC;IAClC,MAAM,IAAI,MAAM,UAAU,KACxB;KAAE,MAAM,IAAI;KAAM,MAAM,OAAO,IAAI,IAAI,KAAK;KAAI,UAAU,IAAI;KAAU,MAAM,IAAI;KAAM,MAAM,IAAI;IAAK,GACvG,OACF;IACA,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,QAAQ;MAAE,IAAI,EAAE;MAAI,WAAW,EAAE;MAAI,OAAO,EAAE;MAAO,GAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,IAAI,CAAC;KAAG;IAAE,CAAC;IACvI;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,KAAK,OAAO,QAAQ;IAC/C,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,MAAM,YAAY,IAAI,IAAI,aAAa;IACvC,MAAM,eAAe,IAAI,IAAI,kBAAkB;IAC/C,MAAM,YAAY,SAAS,KAAK,OAAO,KAAK,WACxC;KAAE,KAAK,KAAK;KAAc,SAAS,KAAK;IAAiB,IACzD,KAAA;IAEJ,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,QAAA,MADV,SAAS,UAAU,YAAY,WAAW,WAAW,YAAY;IAChD,CAAC;IACvC;GACF;GAEA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF;AAEA,eAAsB,wBAAwB,KAAc,YAA4C;CACtG,MAAM,YAAY,IAAI;CAEtB,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,UAAU,MAAM,QAAQ;GAEtB,SAAS,KAAK,KAAK;IAAE,IAAI;IAAM,SADf,cAAc,CAAC,CAAC,IAAI,qBACC;GAAE,CAAC;EAC1C;CACF,CAAC,CACH;CAEA,IAAI,aACF,UAAU,SAAS;EACjB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAE3B,MAAM,SADO,aAAa,GACR,CAAC,CAAC;GAEpB,IAAI,IAAI,WAAW,SAAS,WAAW,KAAA,GAAW;IAChD,SAAS,KAAK,KAAK;KAAE,IAAI;KAAM,OAAO,WAAW,KAAK;IAAE,CAAC;IACzD;GACF;GAEA,IAAI,IAAI,WAAW,UAAU,WAAW,UAAU;IAChD,MAAM,OAAO,MAAM,SAAS,GAAG;IAC/B,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,UAAU;KAC1C,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAU,CAAC;KAClD;IACF;IACA,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;IACnC,WAAW,OAAO,OAAO,KAAK,IAAI,GAAG,IAAI;IACzC,SAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IAC/B;GACF;GAEA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../src/core/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mkdirSync, readdirSync } from 'node:fs'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { resolveHostRunner } from '../exec-docker/host-runner.js'\nimport { NodeRegistry, type NodeProfile, type NodeType } from './node-registry.js'\nimport { readSshConfig, resolveSshConfigEntry } from './ssh-config.js'\nimport type { KnownHostsStore } from './known-hosts.js'\nimport { testNode, type SshHandshakeTester, type DockerNodeTester } from './test.js'\nimport type { WorkspaceBindingsStore } from './workspace-bindings.js'\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{\n ensureDir(path: string): Promise<void>\n fs: { listDir(path: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean }>> }\n } | undefined>\n}\n\nfunction resolveWorlds(ctx: Context, nodeType: string): WorldsLike | undefined {\n if (nodeType === 'local-docker' || nodeType === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n }\n return ctx.get('dshbWorlds') as WorldsLike | undefined\n}\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n return path.split('/').filter(Boolean)\n}\n\nconst VALID_TYPES: NodeType[] = ['local-host', 'local-docker', 'remote-ssh', 'remote-docker']\n\ninterface DockerProvisionerLike {\n provision(nodeId: string): Promise<unknown>\n status(nodeId: string): { state: string; error?: string; containerId?: string; updatedAt: string } | undefined\n}\n\ninterface WorkspaceRegistryLike {\n list(): Array<{ path: string; title: string }>\n}\n\nfunction maybeProvision(ctx: Context, node: NodeProfile): boolean {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') return false\n if (!node.docker?.image) return false\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n if (!provisioner) return false\n void provisioner.provision(node.id).catch(() => {})\n return true\n}\n\nexport async function registerNodeRoutes(\n ctx: Context,\n registry: NodeRegistry,\n bindings: WorkspaceBindingsStore,\n getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined,\n): Promise<void> {\n const webServer = ctx.webServer\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/nodes',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const resourceId = segs[3]\n\n if (req.method === 'GET' && segs[4] === 'browse' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? (node.type === 'local-host' ? process.env.HOME ?? '/' : '/')\n try {\n if (node.type === 'local-host') {\n const entries = readdirSync(path, { withFileTypes: true }).map((e) => ({\n name: e.name,\n path: `${path.replace(/\\/$/, '')}/${e.name}`,\n isDirectory: e.isDirectory(),\n isFile: e.isFile(),\n }))\n sendJson(res, 200, { ok: true, path, entries })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n const entries = await world.fs.listDir(path)\n sendJson(res, 200, { ok: true, path, entries })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'mkdir' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const body = await readBody(req)\n const path = body && typeof body.path === 'string' ? body.path : undefined\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n try {\n if (node.type === 'local-host') {\n mkdirSync(path, { recursive: true })\n sendJson(res, 200, { ok: true, path })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(path)\n sendJson(res, 200, { ok: true, path })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'GET' && resourceId === undefined) {\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n const nodes = registry.list()\n const withFlags = await Promise.all(\n nodes.map(async (n) => ({\n ...n,\n hasSecret: await registry.hasSecret(n.id),\n status: registry.status(n.id),\n provision: provisioner?.status(n.id),\n })),\n )\n sendJson(res, 200, { ok: true, nodes: withFlags })\n return\n }\n\n if (req.method === 'POST' && resourceId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n const type = String(body.type ?? '') as NodeType\n if (!VALID_TYPES.includes(type)) {\n sendJson(res, 400, { ok: false, error: 'type 必须是 local-host/local-docker/remote-ssh/remote-docker' })\n return\n }\n try {\n const node: NodeProfile = await registry.create({\n name: String(body.name ?? ''),\n type,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 201, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 409, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (resourceId === undefined) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (req.method === 'GET' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id), status: registry.status(resourceId) } })\n return\n }\n\n if (req.method === 'PATCH' && segs[4] === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n try {\n const node = await registry.update(resourceId, {\n name: body.name as string | undefined,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 404, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'DELETE' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n\n const attached = bindings.list().filter((binding) => binding.nodeId === resourceId)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n try {\n workspaceRegistry = getWorkspaceRegistry?.()\n } catch { /* protect deletion when the workspace registry is unavailable */ }\n let active = attached\n if (workspaceRegistry?.list) {\n try {\n const workspaces = workspaceRegistry.list()\n const byPath = new Map(workspaces.map((workspace) => [workspace.path, workspace.title]))\n active = attached.filter((binding) => byPath.has(binding.mirrorPath))\n if (active.length > 0) {\n const titles = active.map((binding) => byPath.get(binding.mirrorPath) ?? binding.mirrorPath)\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍被工作区使用:${titles.join('、')}。请先移除或迁移这些工作区。`,\n workspaces: titles,\n })\n return\n }\n } catch {\n active = attached\n }\n }\n if (active.length > 0) {\n sendJson(res, 409, {\n ok: false,\n code: 'node/in-use',\n error: `节点仍绑定 ${active.length} 个工作区,请先移除或迁移工作区。`,\n })\n return\n }\n\n try {\n if ((node.type === 'local-docker' || node.type === 'remote-docker') && node.docker?.containerId) {\n const runner = await resolveHostRunner(ctx, node)\n if (!runner) throw new Error('Docker 节点执行环境不可用,节点未删除')\n const result = await runner.run(['docker', 'rm', '-f', node.docker.containerId])\n if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || '删除容器失败,节点未删除')\n }\n await registry.remove(resourceId)\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(resourceId)\n dockerWorlds?.remove?.(resourceId)\n sendJson(res, 200, { ok: true })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && resourceId === 'test-unsaved') {\n const body = await readBody(req)\n if (!body || !body.ssh) {\n sendJson(res, 400, { ok: false, error: '需要 ssh 配置' })\n return\n }\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n if (!sshTester) {\n sendJson(res, 503, { ok: false, error: 'SSH 执行通道不可用' })\n return\n }\n const ssh = body.ssh as { host: string; port: number; username: string; auth: never; jump?: never }\n const secrets = (body.secrets ?? {}) as { password?: string; privateKey?: string; passphrase?: string }\n const t = await sshTester.test(\n { host: ssh.host, port: Number(ssh.port) || 22, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n sendJson(res, 200, { ok: true, report: { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) } })\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'test') {\n const body = await readBody(req)\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n const overrides = body && (body.ssh || body.secrets)\n ? { ssh: body.ssh as never, secrets: body.secrets as never }\n : undefined\n const report = await testNode(registry, resourceId, sshTester, overrides, dockerTester)\n sendJson(res, 200, { ok: true, report })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\nexport async function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void> {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/api/dshb/ssh-config',\n handler: (_req, res) => {\n const entries = readSshConfig().map(resolveSshConfigEntry)\n sendJson(res, 200, { ok: true, entries })\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/known-hosts',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'GET' && action === undefined) {\n sendJson(res, 200, { ok: true, hosts: knownHosts.list() })\n return\n }\n\n if (req.method === 'POST' && action === 'forget') {\n const body = await readBody(req)\n if (!body || typeof body.host !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 host' })\n return\n }\n const port = Number(body.port ?? 22)\n knownHosts.forget(String(body.host), port)\n sendJson(res, 200, { ok: true })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ function dshHome() {
6
6
  return process.env.DSH_HOME ?? join(homedir(), ".dsh");
7
7
  }
8
8
  function normalize(p) {
9
- return p.replace(/\/+$/, "") || "/";
9
+ return p.replace(/\/{2,}/g, "/").replace(/\/+$/, "") || "/";
10
10
  }
11
11
  var WorkspaceBindingsStore = class {
12
12
  bindings = [];
@@ -20,7 +20,8 @@ var WorkspaceBindingsStore = class {
20
20
  try {
21
21
  this.bindings = JSON.parse(readFileSync(this.file(), "utf8")).map((b) => ({
22
22
  ...b,
23
- mirrorPath: normalize(b.mirrorPath)
23
+ mirrorPath: normalize(b.mirrorPath),
24
+ remotePath: normalize(b.remotePath)
24
25
  }));
25
26
  } catch {
26
27
  this.bindings = [];
@@ -43,7 +44,8 @@ var WorkspaceBindingsStore = class {
43
44
  this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== mirrorPath);
44
45
  this.bindings.push({
45
46
  ...binding,
46
- mirrorPath
47
+ mirrorPath,
48
+ remotePath: normalize(binding.remotePath)
47
49
  });
48
50
  this.save();
49
51
  }
@@ -73,7 +75,7 @@ var WorkspaceBindingsStore = class {
73
75
  const suffix = target === root ? "" : target.slice(root.length);
74
76
  return {
75
77
  nodeId: best.nodeId,
76
- remotePath: `${remoteRoot}${suffix}`
78
+ remotePath: normalize(`${remoteRoot}${suffix}`)
77
79
  };
78
80
  }
79
81
  mirrorRoot(nodeId, slug) {
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-bindings.js","names":[],"sources":["../../src/core/workspace-bindings.ts"],"sourcesContent":["import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface WorkspaceBinding {\n mirrorPath: string\n nodeId: string\n remotePath: string\n workspaceId?: string\n}\n\nexport interface BindingHit {\n nodeId: string\n remotePath: string\n}\n\nfunction dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction normalize(p: string): string {\n return p.replace(/\\/+$/, '') || '/'\n}\n\nexport class WorkspaceBindingsStore {\n private bindings: WorkspaceBinding[] = []\n private loaded = false\n\n private file(): string {\n return join(dshHome(), 'dshb', 'workspace-bindings.json')\n }\n\n private load(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n this.bindings = (JSON.parse(readFileSync(this.file(), 'utf8')) as WorkspaceBinding[]).map((b) => ({\n ...b,\n mirrorPath: normalize(b.mirrorPath),\n }))\n } catch {\n this.bindings = []\n }\n }\n\n private save(): void {\n const file = this.file()\n mkdirSync(join(file, '..'), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(this.bindings, null, 2), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n }\n\n add(binding: Omit<WorkspaceBinding, 'mirrorPath'> & { mirrorPath: string }): void {\n this.load()\n const mirrorPath = normalize(binding.mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== mirrorPath)\n this.bindings.push({ ...binding, mirrorPath })\n this.save()\n }\n\n remove(mirrorPath: string): void {\n this.load()\n const target = normalize(mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== target)\n this.save()\n }\n\n list(): WorkspaceBinding[] {\n this.load()\n return this.bindings.map((b) => ({ ...b }))\n }\n\n resolve(path: string): BindingHit | undefined {\n this.load()\n const target = normalize(path)\n let best: WorkspaceBinding | undefined\n for (const b of this.bindings) {\n const root = normalize(b.mirrorPath)\n if (target === root || target.startsWith(`${root}/`)) {\n if (!best || root.length > normalize(best.mirrorPath).length) best = b\n }\n }\n if (!best) return undefined\n const root = normalize(best.mirrorPath)\n const remoteRoot = normalize(best.remotePath)\n const suffix = target === root ? '' : target.slice(root.length)\n return { nodeId: best.nodeId, remotePath: `${remoteRoot}${suffix}` }\n }\n\n mirrorRoot(nodeId: string, slug: string): string {\n return join(dshHome(), 'dshb', 'mirrors', nodeId, slug)\n }\n}\n"],"mappings":";;;;AAgBA,SAAS,UAAkB;CACzB,OAAO,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,MAAM;AACvD;AAEA,SAAS,UAAU,GAAmB;CACpC,OAAO,EAAE,QAAQ,QAAQ,EAAE,KAAK;AAClC;AAEA,IAAa,yBAAb,MAAoC;CAClC,WAAuC,CAAC;CACxC,SAAiB;CAEjB,OAAuB;EACrB,OAAO,KAAK,QAAQ,GAAG,QAAQ,yBAAyB;CAC1D;CAEA,OAAqB;EACnB,IAAI,KAAK,QAAQ;EACjB,KAAK,SAAS;EACd,IAAI;GACF,KAAK,WAAY,KAAK,MAAM,aAAa,KAAK,KAAK,GAAG,MAAM,CAAC,CAAC,CAAwB,KAAK,OAAO;IAChG,GAAG;IACH,YAAY,UAAU,EAAE,UAAU;GACpC,EAAE;EACJ,QAAQ;GACN,KAAK,WAAW,CAAC;EACnB;CACF;CAEA,OAAqB;EACnB,MAAM,OAAO,KAAK,KAAK;EACvB,UAAU,KAAK,MAAM,IAAI,GAAG;GAAE,WAAW;GAAM,MAAM;EAAM,CAAC;EAC5D,MAAM,MAAM,GAAG,KAAK,GAAG,QAAQ,IAAI;EACnC,cAAc,KAAK,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;EAC1E,WAAW,KAAK,IAAI;EACpB,UAAU,MAAM,GAAK;CACvB;CAEA,IAAI,SAA8E;EAChF,KAAK,KAAK;EACV,MAAM,aAAa,UAAU,QAAQ,UAAU;EAC/C,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,UAAU;EAClF,KAAK,SAAS,KAAK;GAAE,GAAG;GAAS;EAAW,CAAC;EAC7C,KAAK,KAAK;CACZ;CAEA,OAAO,YAA0B;EAC/B,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,UAAU;EACnC,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,MAAM;EAC9E,KAAK,KAAK;CACZ;CAEA,OAA2B;EACzB,KAAK,KAAK;EACV,OAAO,KAAK,SAAS,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE;CAC5C;CAEA,QAAQ,MAAsC;EAC5C,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,IAAI;EAC7B,IAAI;EACJ,KAAK,MAAM,KAAK,KAAK,UAAU;GAC7B,MAAM,OAAO,UAAU,EAAE,UAAU;GACnC,IAAI,WAAW,QAAQ,OAAO,WAAW,GAAG,KAAK,EAAE,GAC7C;QAAA,CAAC,QAAQ,KAAK,SAAS,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ,OAAO;GAAA;EAEzE;EACA,IAAI,CAAC,MAAM,OAAO,KAAA;EAClB,MAAM,OAAO,UAAU,KAAK,UAAU;EACtC,MAAM,aAAa,UAAU,KAAK,UAAU;EAC5C,MAAM,SAAS,WAAW,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM;EAC9D,OAAO;GAAE,QAAQ,KAAK;GAAQ,YAAY,GAAG,aAAa;EAAS;CACrE;CAEA,WAAW,QAAgB,MAAsB;EAC/C,OAAO,KAAK,QAAQ,GAAG,QAAQ,WAAW,QAAQ,IAAI;CACxD;AACF"}
1
+ {"version":3,"file":"workspace-bindings.js","names":[],"sources":["../../src/core/workspace-bindings.ts"],"sourcesContent":["import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface WorkspaceBinding {\n mirrorPath: string\n nodeId: string\n remotePath: string\n workspaceId?: string\n}\n\nexport interface BindingHit {\n nodeId: string\n remotePath: string\n}\n\nfunction dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction normalize(p: string): string {\n return p.replace(/\\/{2,}/g, '/').replace(/\\/+$/, '') || '/'\n}\n\nexport class WorkspaceBindingsStore {\n private bindings: WorkspaceBinding[] = []\n private loaded = false\n\n private file(): string {\n return join(dshHome(), 'dshb', 'workspace-bindings.json')\n }\n\n private load(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n this.bindings = (JSON.parse(readFileSync(this.file(), 'utf8')) as WorkspaceBinding[]).map((b) => ({\n ...b,\n mirrorPath: normalize(b.mirrorPath),\n remotePath: normalize(b.remotePath),\n }))\n } catch {\n this.bindings = []\n }\n }\n\n private save(): void {\n const file = this.file()\n mkdirSync(join(file, '..'), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(this.bindings, null, 2), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n }\n\n add(binding: Omit<WorkspaceBinding, 'mirrorPath'> & { mirrorPath: string }): void {\n this.load()\n const mirrorPath = normalize(binding.mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== mirrorPath)\n this.bindings.push({ ...binding, mirrorPath, remotePath: normalize(binding.remotePath) })\n this.save()\n }\n\n remove(mirrorPath: string): void {\n this.load()\n const target = normalize(mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== target)\n this.save()\n }\n\n list(): WorkspaceBinding[] {\n this.load()\n return this.bindings.map((b) => ({ ...b }))\n }\n\n resolve(path: string): BindingHit | undefined {\n this.load()\n const target = normalize(path)\n let best: WorkspaceBinding | undefined\n for (const b of this.bindings) {\n const root = normalize(b.mirrorPath)\n if (target === root || target.startsWith(`${root}/`)) {\n if (!best || root.length > normalize(best.mirrorPath).length) best = b\n }\n }\n if (!best) return undefined\n const root = normalize(best.mirrorPath)\n const remoteRoot = normalize(best.remotePath)\n const suffix = target === root ? '' : target.slice(root.length)\n return { nodeId: best.nodeId, remotePath: normalize(`${remoteRoot}${suffix}`) }\n }\n\n mirrorRoot(nodeId: string, slug: string): string {\n return join(dshHome(), 'dshb', 'mirrors', nodeId, slug)\n }\n}\n"],"mappings":";;;;AAgBA,SAAS,UAAkB;CACzB,OAAO,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,MAAM;AACvD;AAEA,SAAS,UAAU,GAAmB;CACpC,OAAO,EAAE,QAAQ,WAAW,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAAE,KAAK;AAC1D;AAEA,IAAa,yBAAb,MAAoC;CAClC,WAAuC,CAAC;CACxC,SAAiB;CAEjB,OAAuB;EACrB,OAAO,KAAK,QAAQ,GAAG,QAAQ,yBAAyB;CAC1D;CAEA,OAAqB;EACnB,IAAI,KAAK,QAAQ;EACjB,KAAK,SAAS;EACd,IAAI;GACF,KAAK,WAAY,KAAK,MAAM,aAAa,KAAK,KAAK,GAAG,MAAM,CAAC,CAAC,CAAwB,KAAK,OAAO;IAChG,GAAG;IACH,YAAY,UAAU,EAAE,UAAU;IAClC,YAAY,UAAU,EAAE,UAAU;GACpC,EAAE;EACJ,QAAQ;GACN,KAAK,WAAW,CAAC;EACnB;CACF;CAEA,OAAqB;EACnB,MAAM,OAAO,KAAK,KAAK;EACvB,UAAU,KAAK,MAAM,IAAI,GAAG;GAAE,WAAW;GAAM,MAAM;EAAM,CAAC;EAC5D,MAAM,MAAM,GAAG,KAAK,GAAG,QAAQ,IAAI;EACnC,cAAc,KAAK,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;EAC1E,WAAW,KAAK,IAAI;EACpB,UAAU,MAAM,GAAK;CACvB;CAEA,IAAI,SAA8E;EAChF,KAAK,KAAK;EACV,MAAM,aAAa,UAAU,QAAQ,UAAU;EAC/C,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,UAAU;EAClF,KAAK,SAAS,KAAK;GAAE,GAAG;GAAS;GAAY,YAAY,UAAU,QAAQ,UAAU;EAAE,CAAC;EACxF,KAAK,KAAK;CACZ;CAEA,OAAO,YAA0B;EAC/B,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,UAAU;EACnC,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,MAAM;EAC9E,KAAK,KAAK;CACZ;CAEA,OAA2B;EACzB,KAAK,KAAK;EACV,OAAO,KAAK,SAAS,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE;CAC5C;CAEA,QAAQ,MAAsC;EAC5C,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,IAAI;EAC7B,IAAI;EACJ,KAAK,MAAM,KAAK,KAAK,UAAU;GAC7B,MAAM,OAAO,UAAU,EAAE,UAAU;GACnC,IAAI,WAAW,QAAQ,OAAO,WAAW,GAAG,KAAK,EAAE,GAC7C;QAAA,CAAC,QAAQ,KAAK,SAAS,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ,OAAO;GAAA;EAEzE;EACA,IAAI,CAAC,MAAM,OAAO,KAAA;EAClB,MAAM,OAAO,UAAU,KAAK,UAAU;EACtC,MAAM,aAAa,UAAU,KAAK,UAAU;EAC5C,MAAM,SAAS,WAAW,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM;EAC9D,OAAO;GAAE,QAAQ,KAAK;GAAQ,YAAY,UAAU,GAAG,aAAa,QAAQ;EAAE;CAChF;CAEA,WAAW,QAAgB,MAAsB;EAC/C,OAAO,KAAK,QAAQ,GAAG,QAAQ,WAAW,QAAQ,IAAI;CACxD;AACF"}
@@ -1,7 +1,7 @@
1
- import { request } from "node:http";
2
- import { request as request$1 } from "node:https";
3
1
  import { execFile } from "node:child_process";
4
2
  import { promisify } from "node:util";
3
+ import { request } from "node:http";
4
+ import { request as request$1 } from "node:https";
5
5
  //#region src/exec-docker/daemon-runner.ts
6
6
  const execFileAsync = promisify(execFile);
7
7
  function parseDaemonHost(env = process.env) {
@@ -25,6 +25,14 @@ interface DockerBackend {
25
25
  mtime: number;
26
26
  isDirectory: boolean;
27
27
  isFile: boolean;
28
+ isSymlink?: boolean;
29
+ } | undefined>;
30
+ lstat(path: string): Promise<{
31
+ size: number;
32
+ mtime: number;
33
+ isDirectory: boolean;
34
+ isFile: boolean;
35
+ isSymlink?: boolean;
28
36
  } | undefined>;
29
37
  mkdir(path: string): Promise<void>;
30
38
  remove(path: string): Promise<void>;
@@ -30,7 +30,16 @@ declare class DockerClient implements DockerBackend {
30
30
  mtime: number;
31
31
  isDirectory: boolean;
32
32
  isFile: boolean;
33
+ isSymlink: boolean;
34
+ } | undefined>;
35
+ lstat(path: string): Promise<{
36
+ size: number;
37
+ mtime: number;
38
+ isDirectory: boolean;
39
+ isFile: boolean;
40
+ isSymlink: boolean;
33
41
  } | undefined>;
42
+ private readStat;
34
43
  mkdir(path: string): Promise<void>;
35
44
  remove(path: string): Promise<void>;
36
45
  rename(src: string, dest: string): Promise<void>;
@@ -215,7 +215,14 @@ var DockerClient = class {
215
215
  }).filter((e) => e !== null);
216
216
  }
217
217
  async stat(path) {
218
- const line = (await this.execShell(`stat -c '%s %Y %F' ${shellQuote(path)} 2>/dev/null`, "/")).stdout.trim();
218
+ return this.readStat(path, true);
219
+ }
220
+ async lstat(path) {
221
+ return this.readStat(path, false);
222
+ }
223
+ async readStat(path, followSymlink) {
224
+ const follow = followSymlink ? "-L " : "";
225
+ const line = (await this.execShell(`stat ${follow}-c '%s %Y %F' ${shellQuote(path)} 2>/dev/null`, "/")).stdout.trim();
219
226
  if (!line) return void 0;
220
227
  const parts = line.split(/\s+/);
221
228
  if (parts.length < 3) return void 0;
@@ -226,7 +233,8 @@ var DockerClient = class {
226
233
  size,
227
234
  mtime,
228
235
  isDirectory: ftype.includes("directory"),
229
- isFile: ftype.includes("regular file") || ftype.includes("regular empty file")
236
+ isFile: ftype.includes("regular file") || ftype.includes("regular empty file"),
237
+ isSymlink: ftype.includes("symbolic link")
230
238
  };
231
239
  }
232
240
  async mkdir(path) {
@@ -1 +1 @@
1
- {"version":3,"file":"docker-client.js","names":["httpRequest","httpsRequest"],"sources":["../../src/exec-docker/docker-client.ts"],"sourcesContent":["import type { IncomingMessage, ClientRequest } from 'node:http'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport type { DockerBackend } from './docker-backend.js'\nimport { parseDaemonHost } from './daemon-runner.js'\n\nconst DAEMON = parseDaemonHost()\n\ninterface ExecResult {\n code: number\n stdout: string\n stderr: string\n}\n\ninterface Demuxed {\n stdout: Buffer[]\n stderr: Buffer[]\n}\n\nfunction requestRaw(\n opts: { method: string; path: string; headers: Record<string, string> },\n onResponse: (res: IncomingMessage) => void,\n): ClientRequest {\n const common = { ...opts }\n if (DAEMON.socketPath) return httpRequest({ ...common, socketPath: DAEMON.socketPath } as never, onResponse)\n const mod = DAEMON.tls ? httpsRequest : httpRequest\n return mod({ ...common, hostname: DAEMON.host, port: DAEMON.port } as never, onResponse)\n}\n\nfunction dockerRequest(method: string, path: string, body?: unknown): Promise<{ statusCode: number; headers: Record<string, string>; body: Buffer }> {\n const headers = body ? { 'content-type': 'application/json' } : {}\n if (DAEMON.socketPath) {\n return new Promise((resolve, reject) => {\n const req = httpRequest({ method, path, socketPath: DAEMON.socketPath as string, headers }, (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n resolve({\n statusCode: res.statusCode ?? 0,\n headers: res.headers as Record<string, string>,\n body: Buffer.concat(chunks),\n })\n })\n res.on('error', reject)\n })\n req.on('error', reject)\n if (body) req.write(JSON.stringify(body))\n req.end()\n })\n }\n const mod = DAEMON.tls ? httpsRequest : httpRequest\n return new Promise((resolve, reject) => {\n const req = mod({ method, path, hostname: DAEMON.host, port: DAEMON.port, headers }, (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n resolve({\n statusCode: res.statusCode ?? 0,\n headers: res.headers as Record<string, string>,\n body: Buffer.concat(chunks),\n })\n })\n res.on('error', reject)\n })\n req.on('error', reject)\n if (body) req.write(JSON.stringify(body))\n req.end()\n })\n}\n\nfunction demux(data: Buffer): Demuxed {\n const stdout: Buffer[] = []\n const stderr: Buffer[] = []\n let pos = 0\n while (pos + 8 <= data.length) {\n const streamType = data[pos]\n const length = data.readUInt32BE(pos + 4)\n pos += 8\n if (pos + length > data.length) break\n const payload = data.subarray(pos, pos + length)\n pos += length\n if (streamType === 1) stdout.push(payload)\n else if (streamType === 2) stderr.push(payload)\n }\n return { stdout, stderr }\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`\n}\n\nexport class DockerClient implements DockerBackend {\n private readonly containerId: string\n\n constructor(containerId: string) {\n this.containerId = containerId\n }\n\n async ping(): Promise<boolean> {\n try {\n const res = await dockerRequest('GET', '/_ping')\n return res.statusCode === 200\n } catch {\n return false\n }\n }\n\n async inspect(): Promise<{ running: boolean; name: string } | undefined> {\n const res = await dockerRequest('GET', `/containers/${this.containerId}/json`)\n if (res.statusCode !== 200) return undefined\n const data = JSON.parse(res.body.toString('utf8'))\n return { running: data.State?.Running ?? false, name: data.Name ?? this.containerId }\n }\n\n async exec(argv: string[], cwd: string, env: Record<string, string> = {}, stdinData?: Buffer): Promise<ExecResult> {\n const createRes = await dockerRequest('POST', `/containers/${this.containerId}/exec`, {\n AttachStdout: true,\n AttachStderr: true,\n AttachStdin: stdinData !== undefined,\n Tty: false,\n WorkingDir: cwd,\n Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),\n Cmd: argv,\n })\n if (createRes.statusCode !== 201) {\n const msg = createRes.body.toString('utf8')\n return { code: 126, stdout: '', stderr: `docker exec create failed: ${msg}` }\n }\n const execId = JSON.parse(createRes.body.toString('utf8')).Id\n if (!execId) return { code: 126, stdout: '', stderr: 'docker exec: no Id returned' }\n\n return new Promise<ExecResult>((resolve, reject) => {\n const req = requestRaw(\n {\n method: 'POST',\n path: `/exec/${execId}/start`,\n headers: { 'content-type': 'application/json', connection: 'upgrade' },\n },\n (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n const raw = Buffer.concat(chunks)\n const demuxed = demux(raw)\n const inspectReq = requestRaw(\n { method: 'GET', path: `/exec/${execId}/json`, headers: {} },\n (inspectRes) => {\n const ichunks: Buffer[] = []\n inspectRes.on('data', (c: Buffer) => ichunks.push(c))\n inspectRes.on('end', () => {\n let code = 0\n try {\n const info = JSON.parse(Buffer.concat(ichunks).toString('utf8'))\n code = info.ExitCode ?? 0\n } catch {}\n resolve({\n code,\n stdout: Buffer.concat(demuxed.stdout).toString('utf8'),\n stderr: Buffer.concat(demuxed.stderr).toString('utf8'),\n })\n })\n inspectRes.on('error', () => resolve({ code: 0, stdout: Buffer.concat(demuxed.stdout).toString('utf8'), stderr: Buffer.concat(demuxed.stderr).toString('utf8') }))\n },\n )\n inspectReq.on('error', () => resolve({ code: 0, stdout: Buffer.concat(demuxed.stdout).toString('utf8'), stderr: Buffer.concat(demuxed.stderr).toString('utf8') }))\n inspectReq.end()\n })\n res.on('error', reject)\n },\n )\n req.on('error', reject)\n req.write(JSON.stringify({ Detach: false, Tty: false }))\n if (stdinData) req.write(stdinData)\n req.end()\n })\n }\n\n async execShell(command: string, cwd: string): Promise<ExecResult> {\n return this.exec(['bash', '-c', command], cwd)\n }\n\n async readFile(path: string): Promise<Buffer> {\n const result = await this.exec(['cat', path], '/')\n return Buffer.from(result.stdout, 'utf8')\n }\n\n async writeFile(path: string, content: Buffer | string): Promise<void> {\n const data = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8')\n const b64 = data.toString('base64')\n await this.execShell(`printf '%s' '${b64}' | base64 -d > ${shellQuote(path)}`, '/')\n }\n\n async listDir(path: string): Promise<Array<{ name: string; isDir: boolean; isFile: boolean; isSymlink: boolean; size: number; mtime: number }>> {\n const result = await this.execShell(`ls -1pa --time-style=+%s ${shellQuote(path)} 2>/dev/null`, '/')\n return result.stdout\n .split('\\n')\n .filter((line) => line.trim())\n .map((line) => {\n const name = line.replace(/[/@*]$/, '').trim()\n if (!name) return null\n const isDir = line.endsWith('/')\n const isSymlink = line.endsWith('@')\n return {\n name,\n isDir,\n isFile: !isDir && !isSymlink,\n isSymlink,\n size: 0,\n mtime: 0,\n }\n })\n .filter((e): e is NonNullable<typeof e> => e !== null)\n }\n\n async stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean } | undefined> {\n const result = await this.execShell(`stat -c '%s %Y %F' ${shellQuote(path)} 2>/dev/null`, '/')\n const line = result.stdout.trim()\n if (!line) return undefined\n const parts = line.split(/\\s+/)\n if (parts.length < 3) return undefined\n const size = Number(parts[0])\n const mtime = Number(parts[1]) * 1000\n const ftype = parts.slice(2).join(' ')\n return {\n size,\n mtime,\n isDirectory: ftype.includes('directory'),\n isFile: ftype.includes('regular file') || ftype.includes('regular empty file'),\n }\n }\n\n async mkdir(path: string): Promise<void> {\n await this.exec(['mkdir', '-p', path], '/')\n }\n\n async remove(path: string): Promise<void> {\n await this.exec(['rm', '-f', path], '/')\n }\n\n async rename(src: string, dest: string): Promise<void> {\n await this.exec(['mv', src, dest], '/')\n }\n\n async pty(argv: string[], cwd: string, cols: number, rows: number, env: Record<string, string> = {}, terminalType = 'xterm-256color'): Promise<{ stream: NodeJS.ReadWriteStream; resize: (c: number, r: number) => void; kill: () => void }> {\n const environment = Object.entries(env).map(([key, value]) => `${key}=${value}`)\n if (!environment.some((entry) => entry.startsWith('TERM='))) environment.push(`TERM=${terminalType}`)\n const createRes = await dockerRequest('POST', `/containers/${this.containerId}/exec`, {\n AttachStdout: true,\n AttachStderr: true,\n AttachStdin: true,\n Tty: true,\n WorkingDir: cwd,\n Env: environment,\n Cmd: argv,\n })\n if (createRes.statusCode !== 201) throw new Error('docker exec create failed')\n const execId = JSON.parse(createRes.body.toString('utf8')).Id\n\n return new Promise((resolve, reject) => {\n const req = requestRaw(\n {\n method: 'POST',\n path: `/exec/${execId}/start`,\n headers: { 'content-type': 'application/json', connection: 'upgrade' },\n },\n (res) => {\n resolve({\n stream: res as unknown as NodeJS.ReadWriteStream,\n resize: (c: number, r: number) => {\n const r2 = requestRaw({ method: 'POST', path: `/exec/${execId}/resize?h=${r}&w=${c}`, headers: {} }, () => {})\n r2.end()\n },\n kill: () => {\n res.destroy()\n },\n })\n },\n )\n req.on('error', reject)\n req.write(JSON.stringify({ Detach: false, Tty: true }))\n req.end()\n })\n }\n\n async ensureRg(): Promise<void> {\n const result = await this.execShell('command -v rg || echo __missing__', '/')\n if (result.stdout.includes('__missing__')) {\n await this.execShell('apk add --no-cache ripgrep 2>/dev/null || apt-get install -y ripgrep 2>/dev/null || true', '/')\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"docker-client.js","names":["httpRequest","httpsRequest"],"sources":["../../src/exec-docker/docker-client.ts"],"sourcesContent":["import type { IncomingMessage, ClientRequest } from 'node:http'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport type { DockerBackend } from './docker-backend.js'\nimport { parseDaemonHost } from './daemon-runner.js'\n\nconst DAEMON = parseDaemonHost()\n\ninterface ExecResult {\n code: number\n stdout: string\n stderr: string\n}\n\ninterface Demuxed {\n stdout: Buffer[]\n stderr: Buffer[]\n}\n\nfunction requestRaw(\n opts: { method: string; path: string; headers: Record<string, string> },\n onResponse: (res: IncomingMessage) => void,\n): ClientRequest {\n const common = { ...opts }\n if (DAEMON.socketPath) return httpRequest({ ...common, socketPath: DAEMON.socketPath } as never, onResponse)\n const mod = DAEMON.tls ? httpsRequest : httpRequest\n return mod({ ...common, hostname: DAEMON.host, port: DAEMON.port } as never, onResponse)\n}\n\nfunction dockerRequest(method: string, path: string, body?: unknown): Promise<{ statusCode: number; headers: Record<string, string>; body: Buffer }> {\n const headers = body ? { 'content-type': 'application/json' } : {}\n if (DAEMON.socketPath) {\n return new Promise((resolve, reject) => {\n const req = httpRequest({ method, path, socketPath: DAEMON.socketPath as string, headers }, (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n resolve({\n statusCode: res.statusCode ?? 0,\n headers: res.headers as Record<string, string>,\n body: Buffer.concat(chunks),\n })\n })\n res.on('error', reject)\n })\n req.on('error', reject)\n if (body) req.write(JSON.stringify(body))\n req.end()\n })\n }\n const mod = DAEMON.tls ? httpsRequest : httpRequest\n return new Promise((resolve, reject) => {\n const req = mod({ method, path, hostname: DAEMON.host, port: DAEMON.port, headers }, (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n resolve({\n statusCode: res.statusCode ?? 0,\n headers: res.headers as Record<string, string>,\n body: Buffer.concat(chunks),\n })\n })\n res.on('error', reject)\n })\n req.on('error', reject)\n if (body) req.write(JSON.stringify(body))\n req.end()\n })\n}\n\nfunction demux(data: Buffer): Demuxed {\n const stdout: Buffer[] = []\n const stderr: Buffer[] = []\n let pos = 0\n while (pos + 8 <= data.length) {\n const streamType = data[pos]\n const length = data.readUInt32BE(pos + 4)\n pos += 8\n if (pos + length > data.length) break\n const payload = data.subarray(pos, pos + length)\n pos += length\n if (streamType === 1) stdout.push(payload)\n else if (streamType === 2) stderr.push(payload)\n }\n return { stdout, stderr }\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`\n}\n\nexport class DockerClient implements DockerBackend {\n private readonly containerId: string\n\n constructor(containerId: string) {\n this.containerId = containerId\n }\n\n async ping(): Promise<boolean> {\n try {\n const res = await dockerRequest('GET', '/_ping')\n return res.statusCode === 200\n } catch {\n return false\n }\n }\n\n async inspect(): Promise<{ running: boolean; name: string } | undefined> {\n const res = await dockerRequest('GET', `/containers/${this.containerId}/json`)\n if (res.statusCode !== 200) return undefined\n const data = JSON.parse(res.body.toString('utf8'))\n return { running: data.State?.Running ?? false, name: data.Name ?? this.containerId }\n }\n\n async exec(argv: string[], cwd: string, env: Record<string, string> = {}, stdinData?: Buffer): Promise<ExecResult> {\n const createRes = await dockerRequest('POST', `/containers/${this.containerId}/exec`, {\n AttachStdout: true,\n AttachStderr: true,\n AttachStdin: stdinData !== undefined,\n Tty: false,\n WorkingDir: cwd,\n Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),\n Cmd: argv,\n })\n if (createRes.statusCode !== 201) {\n const msg = createRes.body.toString('utf8')\n return { code: 126, stdout: '', stderr: `docker exec create failed: ${msg}` }\n }\n const execId = JSON.parse(createRes.body.toString('utf8')).Id\n if (!execId) return { code: 126, stdout: '', stderr: 'docker exec: no Id returned' }\n\n return new Promise<ExecResult>((resolve, reject) => {\n const req = requestRaw(\n {\n method: 'POST',\n path: `/exec/${execId}/start`,\n headers: { 'content-type': 'application/json', connection: 'upgrade' },\n },\n (res) => {\n const chunks: Buffer[] = []\n res.on('data', (c: Buffer) => chunks.push(c))\n res.on('end', () => {\n const raw = Buffer.concat(chunks)\n const demuxed = demux(raw)\n const inspectReq = requestRaw(\n { method: 'GET', path: `/exec/${execId}/json`, headers: {} },\n (inspectRes) => {\n const ichunks: Buffer[] = []\n inspectRes.on('data', (c: Buffer) => ichunks.push(c))\n inspectRes.on('end', () => {\n let code = 0\n try {\n const info = JSON.parse(Buffer.concat(ichunks).toString('utf8'))\n code = info.ExitCode ?? 0\n } catch {}\n resolve({\n code,\n stdout: Buffer.concat(demuxed.stdout).toString('utf8'),\n stderr: Buffer.concat(demuxed.stderr).toString('utf8'),\n })\n })\n inspectRes.on('error', () => resolve({ code: 0, stdout: Buffer.concat(demuxed.stdout).toString('utf8'), stderr: Buffer.concat(demuxed.stderr).toString('utf8') }))\n },\n )\n inspectReq.on('error', () => resolve({ code: 0, stdout: Buffer.concat(demuxed.stdout).toString('utf8'), stderr: Buffer.concat(demuxed.stderr).toString('utf8') }))\n inspectReq.end()\n })\n res.on('error', reject)\n },\n )\n req.on('error', reject)\n req.write(JSON.stringify({ Detach: false, Tty: false }))\n if (stdinData) req.write(stdinData)\n req.end()\n })\n }\n\n async execShell(command: string, cwd: string): Promise<ExecResult> {\n return this.exec(['bash', '-c', command], cwd)\n }\n\n async readFile(path: string): Promise<Buffer> {\n const result = await this.exec(['cat', path], '/')\n return Buffer.from(result.stdout, 'utf8')\n }\n\n async writeFile(path: string, content: Buffer | string): Promise<void> {\n const data = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8')\n const b64 = data.toString('base64')\n await this.execShell(`printf '%s' '${b64}' | base64 -d > ${shellQuote(path)}`, '/')\n }\n\n async listDir(path: string): Promise<Array<{ name: string; isDir: boolean; isFile: boolean; isSymlink: boolean; size: number; mtime: number }>> {\n const result = await this.execShell(`ls -1pa --time-style=+%s ${shellQuote(path)} 2>/dev/null`, '/')\n return result.stdout\n .split('\\n')\n .filter((line) => line.trim())\n .map((line) => {\n const name = line.replace(/[/@*]$/, '').trim()\n if (!name) return null\n const isDir = line.endsWith('/')\n const isSymlink = line.endsWith('@')\n return {\n name,\n isDir,\n isFile: !isDir && !isSymlink,\n isSymlink,\n size: 0,\n mtime: 0,\n }\n })\n .filter((e): e is NonNullable<typeof e> => e !== null)\n }\n\n async stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean; isSymlink: boolean } | undefined> {\n return this.readStat(path, true)\n }\n\n async lstat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean; isSymlink: boolean } | undefined> {\n return this.readStat(path, false)\n }\n\n private async readStat(path: string, followSymlink: boolean): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean; isSymlink: boolean } | undefined> {\n const follow = followSymlink ? '-L ' : ''\n const result = await this.execShell(`stat ${follow}-c '%s %Y %F' ${shellQuote(path)} 2>/dev/null`, '/')\n const line = result.stdout.trim()\n if (!line) return undefined\n const parts = line.split(/\\s+/)\n if (parts.length < 3) return undefined\n const size = Number(parts[0])\n const mtime = Number(parts[1]) * 1000\n const ftype = parts.slice(2).join(' ')\n return {\n size,\n mtime,\n isDirectory: ftype.includes('directory'),\n isFile: ftype.includes('regular file') || ftype.includes('regular empty file'),\n isSymlink: ftype.includes('symbolic link'),\n }\n }\n\n async mkdir(path: string): Promise<void> {\n await this.exec(['mkdir', '-p', path], '/')\n }\n\n async remove(path: string): Promise<void> {\n await this.exec(['rm', '-f', path], '/')\n }\n\n async rename(src: string, dest: string): Promise<void> {\n await this.exec(['mv', src, dest], '/')\n }\n\n async pty(argv: string[], cwd: string, cols: number, rows: number, env: Record<string, string> = {}, terminalType = 'xterm-256color'): Promise<{ stream: NodeJS.ReadWriteStream; resize: (c: number, r: number) => void; kill: () => void }> {\n const environment = Object.entries(env).map(([key, value]) => `${key}=${value}`)\n if (!environment.some((entry) => entry.startsWith('TERM='))) environment.push(`TERM=${terminalType}`)\n const createRes = await dockerRequest('POST', `/containers/${this.containerId}/exec`, {\n AttachStdout: true,\n AttachStderr: true,\n AttachStdin: true,\n Tty: true,\n WorkingDir: cwd,\n Env: environment,\n Cmd: argv,\n })\n if (createRes.statusCode !== 201) throw new Error('docker exec create failed')\n const execId = JSON.parse(createRes.body.toString('utf8')).Id\n\n return new Promise((resolve, reject) => {\n const req = requestRaw(\n {\n method: 'POST',\n path: `/exec/${execId}/start`,\n headers: { 'content-type': 'application/json', connection: 'upgrade' },\n },\n (res) => {\n resolve({\n stream: res as unknown as NodeJS.ReadWriteStream,\n resize: (c: number, r: number) => {\n const r2 = requestRaw({ method: 'POST', path: `/exec/${execId}/resize?h=${r}&w=${c}`, headers: {} }, () => {})\n r2.end()\n },\n kill: () => {\n res.destroy()\n },\n })\n },\n )\n req.on('error', reject)\n req.write(JSON.stringify({ Detach: false, Tty: true }))\n req.end()\n })\n }\n\n async ensureRg(): Promise<void> {\n const result = await this.execShell('command -v rg || echo __missing__', '/')\n if (result.stdout.includes('__missing__')) {\n await this.execShell('apk add --no-cache ripgrep 2>/dev/null || apt-get install -y ripgrep 2>/dev/null || true', '/')\n }\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { listContainers, provisionContainer } from "./provision.js";
2
1
  import { resolveHostRunner } from "./host-runner.js";
2
+ import { listContainers, provisionContainer } from "./provision.js";
3
3
  //#region src/exec-docker/docker-routes.ts
4
4
  function sendJson(res, status, body) {
5
5
  res.writeHead(status, { "content-type": "application/json" });
@@ -1,4 +1,5 @@
1
1
  import { DockerBackend } from "./docker-backend.js";
2
+ import { FsInfo, FsPathInfo } from "@deepseek-ai/dsh-fs";
2
3
  //#region src/exec-docker/fs-provider.d.ts
3
4
  declare class DockerFileSystem {
4
5
  private readonly client;
@@ -7,20 +8,10 @@ declare class DockerFileSystem {
7
8
  processPath(target: string): string;
8
9
  fileUrl(target: string): string;
9
10
  contains(parent: string, child: string): boolean;
10
- stat(target: string, _signal?: AbortSignal): Promise<{
11
- size: number;
12
- mtime: number;
13
- isDirectory: boolean;
14
- isFile: boolean;
15
- } | undefined>;
11
+ stat(target: string, _signal?: AbortSignal): Promise<FsInfo | undefined>;
16
12
  lstat(path: string, _opts?: {
17
13
  cwd?: string;
18
- }, _signal?: AbortSignal): Promise<{
19
- size: number;
20
- mtime: number;
21
- isDirectory: boolean;
22
- isFile: boolean;
23
- } | undefined>;
14
+ }, _signal?: AbortSignal): Promise<FsPathInfo | undefined>;
24
15
  readText(target: string, _signal?: AbortSignal): Promise<string>;
25
16
  streamText(target: string, _signal?: AbortSignal): Promise<AsyncIterable<string>>;
26
17
  readBytes(target: string, _signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
@@ -1,4 +1,9 @@
1
+ import { FsVersion } from "@deepseek-ai/dsh-fs";
1
2
  //#region src/exec-docker/fs-provider.ts
3
+ function versionOf(info) {
4
+ const type = info.isDirectory ? "directory" : info.isFile ? "file" : info.isSymlink ? "symlink" : "other";
5
+ return FsVersion(`${type}:${info.size}:${info.mtime}`);
6
+ }
2
7
  var DockerFileSystem = class {
3
8
  client;
4
9
  constructor(client) {
@@ -17,10 +22,22 @@ var DockerFileSystem = class {
17
22
  return child === parent || child.startsWith(`${parent}/`);
18
23
  }
19
24
  async stat(target, _signal) {
20
- return this.client.stat(target);
25
+ const info = await this.client.stat(target);
26
+ if (!info) return void 0;
27
+ return {
28
+ version: versionOf(info),
29
+ type: info.isDirectory ? "directory" : info.isFile ? "file" : "other",
30
+ size: info.size
31
+ };
21
32
  }
22
33
  async lstat(path, _opts, _signal) {
23
- return this.client.stat(path);
34
+ const info = await this.client.lstat(path);
35
+ if (!info) return void 0;
36
+ return {
37
+ version: versionOf(info),
38
+ type: info.isSymlink ? "symlink" : info.isDirectory ? "directory" : info.isFile ? "file" : "other",
39
+ size: info.size
40
+ };
24
41
  }
25
42
  async readText(target, _signal) {
26
43
  return (await this.client.readFile(target)).toString("utf8");
@@ -1 +1 @@
1
- {"version":3,"file":"fs-provider.js","names":[],"sources":["../../src/exec-docker/fs-provider.ts"],"sourcesContent":["import type { DockerBackend } from './docker-backend.js'\n\nexport class DockerFileSystem {\n constructor(private readonly client: DockerBackend) {}\n\n async resolve(path: string): Promise<string> {\n return path\n }\n\n processPath(target: string): string {\n return target\n }\n\n fileUrl(target: string): string {\n return `file://${target}`\n }\n\n contains(parent: string, child: string): boolean {\n return child === parent || child.startsWith(`${parent}/`)\n }\n\n async stat(target: string, _signal?: AbortSignal): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean } | undefined> {\n return this.client.stat(target)\n }\n\n async lstat(path: string, _opts?: { cwd?: string }, _signal?: AbortSignal): Promise<{ size: number; mtime: number; isDirectory: boolean; isFile: boolean } | undefined> {\n return this.client.stat(path)\n }\n\n async readText(target: string, _signal?: AbortSignal): Promise<string> {\n const data = await this.client.readFile(target)\n return data.toString('utf8')\n }\n\n async streamText(target: string, _signal?: AbortSignal): Promise<AsyncIterable<string>> {\n const data = await this.client.readFile(target)\n const text = data.toString('utf8')\n return (async function* () {\n yield text\n })()\n }\n\n async readBytes(target: string, _signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array> {\n const data = await this.client.readFile(target)\n const slice = data.subarray(0, maxBytes)\n return new Uint8Array(slice.buffer, slice.byteOffset, slice.byteLength)\n }\n\n async listDir(target: string, _signal?: AbortSignal): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; size: number; mtime: number }>> {\n const entries = await this.client.listDir(target)\n return entries.map((e) => ({\n name: e.name,\n path: `${target.replace(/\\/+$/, '')}/${e.name}`,\n isDirectory: e.isDir,\n isFile: e.isFile,\n isSymlink: e.isSymlink,\n size: e.size,\n mtime: e.mtime,\n }))\n }\n\n async writeText(target: string, content: string, _expected?: unknown, _signal?: AbortSignal, _policy?: unknown): Promise<{ operation: 'create' | 'update'; version: string; before: string | null; after: string }> {\n const normalizeLf = (s: string) => s.replace(/\\r\\n/g, '\\n')\n const beforeInfo = await this.client.stat(target)\n let before: string | null = null\n if (beforeInfo?.isFile) {\n try {\n before = normalizeLf((await this.client.readFile(target)).toString('utf8'))\n } catch {\n before = null\n }\n }\n const after = normalizeLf(content)\n await this.client.writeFile(target, content)\n const afterInfo = await this.client.stat(target)\n const version = `${afterInfo?.size ?? content.length}:${afterInfo?.mtime ?? Date.now()}`\n return { operation: before === null ? 'create' : 'update', version, before, after }\n }\n\n async editText(target: string, edit: unknown, _expected?: unknown, _signal?: AbortSignal, _policy?: unknown): Promise<{ version: string; before: string; after: string }> {\n const normalizeLf = (s: string) => s.replace(/\\r\\n/g, '\\n')\n const before = normalizeLf((await this.client.readFile(target)).toString('utf8'))\n const req = edit as { oldString: string; newString: string; replaceAll: boolean }\n const oldString = normalizeLf(String(req.oldString ?? ''))\n if (oldString.length === 0) throw new Error('edit oldString must be non-empty')\n const newString = normalizeLf(String(req.newString ?? ''))\n const matches = before.split(oldString).length - 1\n if (matches === 0) throw new Error('edit oldString not found in file')\n if (!req.replaceAll && matches !== 1) throw new Error(`edit oldString matched ${matches} times; expected exactly one`)\n const after = req.replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString)\n await this.client.writeFile(target, after)\n const afterInfo = await this.client.stat(target)\n const version = `${afterInfo?.size ?? after.length}:${afterInfo?.mtime ?? Date.now()}`\n return { version, before, after }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fs-provider.js","names":[],"sources":["../../src/exec-docker/fs-provider.ts"],"sourcesContent":["import type { DockerBackend } from './docker-backend.js'\nimport { FsVersion, type FsInfo, type FsPathInfo } from '@deepseek-ai/dsh-fs'\n\nfunction versionOf(info: { size: number; mtime: number; isDirectory: boolean; isFile: boolean; isSymlink?: boolean }): FsInfo['version'] {\n const type = info.isDirectory ? 'directory' : info.isFile ? 'file' : info.isSymlink ? 'symlink' : 'other'\n return FsVersion(`${type}:${info.size}:${info.mtime}`)\n}\n\nexport class DockerFileSystem {\n constructor(private readonly client: DockerBackend) {}\n\n async resolve(path: string): Promise<string> {\n return path\n }\n\n processPath(target: string): string {\n return target\n }\n\n fileUrl(target: string): string {\n return `file://${target}`\n }\n\n contains(parent: string, child: string): boolean {\n return child === parent || child.startsWith(`${parent}/`)\n }\n\n async stat(target: string, _signal?: AbortSignal): Promise<FsInfo | undefined> {\n const info = await this.client.stat(target)\n if (!info) return undefined\n return {\n version: versionOf(info),\n type: info.isDirectory ? 'directory' : info.isFile ? 'file' : 'other',\n size: info.size,\n }\n }\n\n async lstat(path: string, _opts?: { cwd?: string }, _signal?: AbortSignal): Promise<FsPathInfo | undefined> {\n const info = await this.client.lstat(path)\n if (!info) return undefined\n return {\n version: versionOf(info),\n type: info.isSymlink ? 'symlink' : info.isDirectory ? 'directory' : info.isFile ? 'file' : 'other',\n size: info.size,\n }\n }\n\n async readText(target: string, _signal?: AbortSignal): Promise<string> {\n const data = await this.client.readFile(target)\n return data.toString('utf8')\n }\n\n async streamText(target: string, _signal?: AbortSignal): Promise<AsyncIterable<string>> {\n const data = await this.client.readFile(target)\n const text = data.toString('utf8')\n return (async function* () {\n yield text\n })()\n }\n\n async readBytes(target: string, _signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array> {\n const data = await this.client.readFile(target)\n const slice = data.subarray(0, maxBytes)\n return new Uint8Array(slice.buffer, slice.byteOffset, slice.byteLength)\n }\n\n async listDir(target: string, _signal?: AbortSignal): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; size: number; mtime: number }>> {\n const entries = await this.client.listDir(target)\n return entries.map((e) => ({\n name: e.name,\n path: `${target.replace(/\\/+$/, '')}/${e.name}`,\n isDirectory: e.isDir,\n isFile: e.isFile,\n isSymlink: e.isSymlink,\n size: e.size,\n mtime: e.mtime,\n }))\n }\n\n async writeText(target: string, content: string, _expected?: unknown, _signal?: AbortSignal, _policy?: unknown): Promise<{ operation: 'create' | 'update'; version: string; before: string | null; after: string }> {\n const normalizeLf = (s: string) => s.replace(/\\r\\n/g, '\\n')\n const beforeInfo = await this.client.stat(target)\n let before: string | null = null\n if (beforeInfo?.isFile) {\n try {\n before = normalizeLf((await this.client.readFile(target)).toString('utf8'))\n } catch {\n before = null\n }\n }\n const after = normalizeLf(content)\n await this.client.writeFile(target, content)\n const afterInfo = await this.client.stat(target)\n const version = `${afterInfo?.size ?? content.length}:${afterInfo?.mtime ?? Date.now()}`\n return { operation: before === null ? 'create' : 'update', version, before, after }\n }\n\n async editText(target: string, edit: unknown, _expected?: unknown, _signal?: AbortSignal, _policy?: unknown): Promise<{ version: string; before: string; after: string }> {\n const normalizeLf = (s: string) => s.replace(/\\r\\n/g, '\\n')\n const before = normalizeLf((await this.client.readFile(target)).toString('utf8'))\n const req = edit as { oldString: string; newString: string; replaceAll: boolean }\n const oldString = normalizeLf(String(req.oldString ?? ''))\n if (oldString.length === 0) throw new Error('edit oldString must be non-empty')\n const newString = normalizeLf(String(req.newString ?? ''))\n const matches = before.split(oldString).length - 1\n if (matches === 0) throw new Error('edit oldString not found in file')\n if (!req.replaceAll && matches !== 1) throw new Error(`edit oldString matched ${matches} times; expected exactly one`)\n const after = req.replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString)\n await this.client.writeFile(target, after)\n const afterInfo = await this.client.stat(target)\n const version = `${afterInfo?.size ?? after.length}:${afterInfo?.mtime ?? Date.now()}`\n return { version, before, after }\n }\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { sharedWorldResolver } from "../router/resolve.js";
2
+ import { resolveHostRunner } from "./host-runner.js";
2
3
  import { DockerClient } from "./docker-client.js";
3
4
  import { RemoteDockerCli } from "./remote-docker-cli.js";
4
5
  import { DockerFileSystem } from "./fs-provider.js";
5
6
  import { DockerShellExecutor, DockerSubprocessRuntime } from "./process-provider.js";
6
7
  import { DockerExecutionWorld, DockerWorldRegistry } from "./world.js";
7
8
  import { listContainers, provisionContainer, removeContainer } from "./provision.js";
8
- import { resolveHostRunner } from "./host-runner.js";
9
9
  import { registerDockerRoutes } from "./docker-routes.js";
10
10
  //#region src/exec-docker/index.ts
11
11
  const name = "dshb-exec-docker";