dsh-plugin-local-infer 0.1.2 → 0.1.4

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 (251) hide show
  1. package/README.md +32 -6
  2. package/cordis.patch.yml +7 -0
  3. package/docs/zh/README.md +24 -4
  4. package/dsh/client.js +4200 -4049
  5. package/package.json +33 -11
  6. package/packages/core/lib/adapter.d.ts +177 -11
  7. package/packages/core/lib/adapter.js +74 -1
  8. package/packages/core/lib/config.d.ts +1 -1
  9. package/packages/core/lib/config.js +1 -1
  10. package/packages/core/lib/debug.d.ts +1 -1
  11. package/packages/core/lib/debug.js +1 -1
  12. package/packages/core/lib/defaults.d.ts +1 -1
  13. package/packages/core/lib/defaults.js +1 -1
  14. package/packages/core/lib/deps.d.ts +48 -20
  15. package/packages/core/lib/deps.js +24 -8
  16. package/packages/core/lib/estimator.d.ts +1 -1
  17. package/packages/core/lib/estimator.js +1 -1
  18. package/packages/core/lib/guideline.d.ts +1 -1
  19. package/packages/core/lib/guideline.js +1 -1
  20. package/packages/core/lib/hardware.d.ts +1 -1
  21. package/packages/core/lib/hardware.js +1 -1
  22. package/packages/core/lib/hf-cache.d.ts +70 -0
  23. package/packages/core/lib/hf-cache.js +83 -0
  24. package/packages/core/lib/index.d.ts +16 -9
  25. package/packages/core/lib/index.js +10 -6
  26. package/packages/core/lib/infer-cache.d.ts +39 -0
  27. package/packages/core/lib/infer-cache.js +53 -0
  28. package/packages/core/lib/launch-command.d.ts +8 -4
  29. package/packages/core/lib/launch-command.js +13 -6
  30. package/packages/core/lib/launch-record.d.ts +1 -1
  31. package/packages/core/lib/launch-record.js +1 -1
  32. package/packages/core/lib/logs.d.ts +8 -1
  33. package/packages/core/lib/logs.js +12 -4
  34. package/packages/core/lib/models.d.ts +120 -14
  35. package/packages/core/lib/models.js +209 -17
  36. package/packages/core/lib/parameter.d.ts +31 -3
  37. package/packages/core/lib/parameter.js +1 -1
  38. package/packages/core/lib/progress.d.ts +54 -0
  39. package/packages/core/lib/progress.js +120 -0
  40. package/packages/core/lib/registrar.d.ts +25 -14
  41. package/packages/core/lib/registrar.js +30 -10
  42. package/packages/core/lib/reserve.d.ts +11 -3
  43. package/packages/core/lib/reserve.js +6 -4
  44. package/packages/core/lib/status.d.ts +12 -6
  45. package/packages/core/lib/status.js +21 -10
  46. package/packages/core/lib/sudo.d.ts +80 -0
  47. package/packages/core/lib/sudo.js +78 -0
  48. package/packages/core/package.json +1 -1
  49. package/packages/orchestrator/lib/config.d.ts +63 -0
  50. package/packages/orchestrator/lib/config.js +111 -0
  51. package/packages/orchestrator/lib/index.d.ts +51 -0
  52. package/packages/orchestrator/lib/index.js +73 -0
  53. package/packages/orchestrator/lib/route.d.ts +60 -0
  54. package/packages/orchestrator/lib/route.js +159 -0
  55. package/packages/orchestrator/locale/en.json +6 -0
  56. package/packages/orchestrator/package.json +39 -0
  57. package/packages/orchestrator/prompt.md +87 -0
  58. package/packages/providers/lib/acquire.d.ts +95 -15
  59. package/packages/providers/lib/acquire.js +239 -40
  60. package/packages/providers/lib/adapters.d.ts +155 -221
  61. package/packages/providers/lib/adapters.js +589 -935
  62. package/packages/providers/lib/debug.d.ts +1 -1
  63. package/packages/providers/lib/debug.js +1 -1
  64. package/packages/providers/lib/deps.d.ts +31 -212
  65. package/packages/providers/lib/deps.js +71 -205
  66. package/packages/providers/lib/engines/basalt/basalt.d.ts +130 -0
  67. package/packages/providers/lib/engines/basalt/basalt.js +304 -0
  68. package/packages/providers/lib/engines/basalt/basaltAdapter.d.ts +85 -0
  69. package/packages/providers/lib/engines/basalt/basaltAdapter.js +439 -0
  70. package/packages/providers/lib/engines/basalt/basaltLinux.d.ts +23 -0
  71. package/packages/providers/lib/engines/basalt/basaltLinux.js +47 -0
  72. package/packages/providers/lib/engines/basalt/basaltMacOS.d.ts +17 -0
  73. package/packages/providers/lib/engines/basalt/basaltMacOS.js +19 -0
  74. package/packages/providers/lib/engines/basalt/basaltWindows.d.ts +24 -0
  75. package/packages/providers/lib/engines/basalt/basaltWindows.js +49 -0
  76. package/packages/providers/lib/engines/engineOs.d.ts +24 -0
  77. package/packages/providers/lib/engines/engineOs.js +17 -0
  78. package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen/hyperqwen.d.ts} +2 -2
  79. package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen/hyperqwen.js} +14 -2
  80. package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.d.ts +38 -0
  81. package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.js +115 -0
  82. package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.d.ts +9 -0
  83. package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.js +9 -0
  84. package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.d.ts +9 -0
  85. package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.js +9 -0
  86. package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.d.ts +9 -0
  87. package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.js +9 -0
  88. package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.d.ts +10 -0
  89. package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.js +14 -0
  90. package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.d.ts +9 -0
  91. package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.js +9 -0
  92. package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.d.ts +9 -0
  93. package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.js +9 -0
  94. package/packages/providers/lib/{infernix.d.ts → engines/infernix/infernix.d.ts} +7 -4
  95. package/packages/providers/lib/{infernix.js → engines/infernix/infernix.js} +53 -8
  96. package/packages/providers/lib/engines/infernix/infernixAdapter.d.ts +52 -0
  97. package/packages/providers/lib/engines/infernix/infernixAdapter.js +235 -0
  98. package/packages/providers/lib/engines/infernix/infernixArch.d.ts +9 -0
  99. package/packages/providers/lib/engines/infernix/infernixArch.js +9 -0
  100. package/packages/providers/lib/engines/infernix/infernixDebian.d.ts +9 -0
  101. package/packages/providers/lib/engines/infernix/infernixDebian.js +9 -0
  102. package/packages/providers/lib/engines/infernix/infernixFedora.d.ts +9 -0
  103. package/packages/providers/lib/engines/infernix/infernixFedora.js +9 -0
  104. package/packages/providers/lib/engines/infernix/infernixLinux.d.ts +17 -0
  105. package/packages/providers/lib/engines/infernix/infernixLinux.js +35 -0
  106. package/packages/providers/lib/engines/infernix/infernixMacOS.d.ts +16 -0
  107. package/packages/providers/lib/engines/infernix/infernixMacOS.js +18 -0
  108. package/packages/providers/lib/engines/infernix/infernixWindows.d.ts +16 -0
  109. package/packages/providers/lib/engines/infernix/infernixWindows.js +34 -0
  110. package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp/llamacpp.d.ts} +2 -2
  111. package/packages/providers/lib/{llamacpp.js → engines/llamacpp/llamacpp.js} +12 -2
  112. package/packages/providers/lib/engines/llamacpp/llamacppAdapter.d.ts +50 -0
  113. package/packages/providers/lib/engines/llamacpp/llamacppAdapter.js +274 -0
  114. package/packages/providers/lib/engines/llamacpp/llamacppArch.d.ts +9 -0
  115. package/packages/providers/lib/engines/llamacpp/llamacppArch.js +9 -0
  116. package/packages/providers/lib/engines/llamacpp/llamacppDebian.d.ts +9 -0
  117. package/packages/providers/lib/engines/llamacpp/llamacppDebian.js +9 -0
  118. package/packages/providers/lib/engines/llamacpp/llamacppFedora.d.ts +9 -0
  119. package/packages/providers/lib/engines/llamacpp/llamacppFedora.js +9 -0
  120. package/packages/providers/lib/engines/llamacpp/llamacppLinux.d.ts +17 -0
  121. package/packages/providers/lib/engines/llamacpp/llamacppLinux.js +28 -0
  122. package/packages/providers/lib/engines/llamacpp/llamacppMacOS.d.ts +15 -0
  123. package/packages/providers/lib/engines/llamacpp/llamacppMacOS.js +17 -0
  124. package/packages/providers/lib/engines/llamacpp/llamacppWindows.d.ts +15 -0
  125. package/packages/providers/lib/engines/llamacpp/llamacppWindows.js +26 -0
  126. package/packages/providers/lib/{ninfer.d.ts → engines/ninfer/ninfer.d.ts} +2 -2
  127. package/packages/providers/lib/{ninfer.js → engines/ninfer/ninfer.js} +25 -3
  128. package/packages/providers/lib/engines/ninfer/ninferAdapter.d.ts +81 -0
  129. package/packages/providers/lib/engines/ninfer/ninferAdapter.js +392 -0
  130. package/packages/providers/lib/engines/ninfer/ninferArch.d.ts +9 -0
  131. package/packages/providers/lib/engines/ninfer/ninferArch.js +9 -0
  132. package/packages/providers/lib/engines/ninfer/ninferDebian.d.ts +9 -0
  133. package/packages/providers/lib/engines/ninfer/ninferDebian.js +9 -0
  134. package/packages/providers/lib/engines/ninfer/ninferFedora.d.ts +9 -0
  135. package/packages/providers/lib/engines/ninfer/ninferFedora.js +9 -0
  136. package/packages/providers/lib/engines/ninfer/ninferLinux.d.ts +17 -0
  137. package/packages/providers/lib/engines/ninfer/ninferLinux.js +37 -0
  138. package/packages/providers/lib/engines/ninfer/ninferMacOS.d.ts +15 -0
  139. package/packages/providers/lib/engines/ninfer/ninferMacOS.js +17 -0
  140. package/packages/providers/lib/engines/ninfer/ninferWindows.d.ts +29 -0
  141. package/packages/providers/lib/engines/ninfer/ninferWindows.js +59 -0
  142. package/packages/providers/lib/engines/strata/strata.d.ts +1 -0
  143. package/packages/providers/lib/engines/strata/strata.js +264 -0
  144. package/packages/providers/lib/engines/strata/strataAdapter.d.ts +97 -0
  145. package/packages/providers/lib/engines/strata/strataAdapter.js +463 -0
  146. package/packages/providers/lib/engines/strata/strataArch.d.ts +9 -0
  147. package/packages/providers/lib/engines/strata/strataArch.js +9 -0
  148. package/packages/providers/lib/engines/strata/strataDebian.d.ts +9 -0
  149. package/packages/providers/lib/engines/strata/strataDebian.js +9 -0
  150. package/packages/providers/lib/engines/strata/strataFedora.d.ts +9 -0
  151. package/packages/providers/lib/engines/strata/strataFedora.js +9 -0
  152. package/packages/providers/lib/engines/strata/strataLinux.d.ts +21 -0
  153. package/packages/providers/lib/engines/strata/strataLinux.js +30 -0
  154. package/packages/providers/lib/engines/strata/strataMacOS.d.ts +16 -0
  155. package/packages/providers/lib/engines/strata/strataMacOS.js +17 -0
  156. package/packages/providers/lib/engines/strata/strataWindows.d.ts +27 -0
  157. package/packages/providers/lib/engines/strata/strataWindows.js +37 -0
  158. package/packages/providers/lib/engines.d.ts +8 -5
  159. package/packages/providers/lib/engines.js +22 -11
  160. package/packages/providers/lib/guidelines.d.ts +5 -4
  161. package/packages/providers/lib/guidelines.js +18 -5
  162. package/packages/providers/lib/index.d.ts +33 -24
  163. package/packages/providers/lib/index.js +46 -24
  164. package/packages/providers/lib/model-defaults.d.ts +10 -5
  165. package/packages/providers/lib/model-defaults.js +47 -5
  166. package/packages/providers/lib/platform.d.ts +8 -5
  167. package/packages/providers/lib/platform.js +27 -7
  168. package/packages/providers/lib/presets.d.ts +6 -4
  169. package/packages/providers/lib/presets.js +53 -5
  170. package/packages/providers/lib/systemDeps.d.ts +313 -0
  171. package/packages/providers/lib/systemDeps.js +284 -0
  172. package/packages/providers/lib/vram.d.ts +34 -8
  173. package/packages/providers/lib/vram.js +81 -8
  174. package/packages/providers/package.json +2 -2
  175. package/packages/service/lib/index.d.ts +187 -23
  176. package/packages/service/lib/index.js +667 -90
  177. package/packages/service/lib/launch-target.d.ts +19 -0
  178. package/packages/service/lib/launch-target.js +28 -0
  179. package/packages/service/lib/run-logs.d.ts +72 -0
  180. package/packages/service/lib/run-logs.js +125 -0
  181. package/packages/service/lib/types.d.ts +166 -8
  182. package/packages/service/lib/types.js +1 -1
  183. package/packages/service/package.json +2 -2
  184. package/packages/tool/lib/index.d.ts +1 -1
  185. package/packages/tool/lib/index.js +1 -1
  186. package/packages/tool/package.json +2 -2
  187. package/packages/ui/lib/config.d.ts +89 -0
  188. package/packages/ui/lib/config.js +251 -0
  189. package/packages/ui/lib/deps.d.ts +4 -4
  190. package/packages/ui/lib/deps.js +1 -1
  191. package/packages/ui/lib/form.d.ts +1 -1
  192. package/packages/ui/lib/form.js +1 -1
  193. package/packages/ui/lib/index.d.ts +7 -1
  194. package/packages/ui/lib/index.js +12 -2
  195. package/packages/ui/lib/meter.d.ts +1 -1
  196. package/packages/ui/lib/meter.js +1 -1
  197. package/packages/ui/lib/ops/engine.d.ts +216 -0
  198. package/packages/ui/lib/ops/engine.js +697 -0
  199. package/packages/ui/lib/ops/get-steps.d.ts +98 -0
  200. package/packages/ui/lib/ops/get-steps.js +359 -0
  201. package/packages/ui/lib/ops/hf.d.ts +42 -0
  202. package/packages/ui/lib/ops/hf.js +150 -0
  203. package/packages/ui/lib/ops/logs.d.ts +78 -0
  204. package/packages/ui/lib/ops/logs.js +119 -0
  205. package/packages/ui/lib/ops/model.d.ts +71 -0
  206. package/packages/ui/lib/ops/model.js +123 -0
  207. package/packages/ui/lib/request.d.ts +106 -0
  208. package/packages/ui/lib/request.js +80 -0
  209. package/packages/ui/lib/route.d.ts +106 -422
  210. package/packages/ui/lib/route.js +136 -1845
  211. package/packages/ui/lib/routes/chat-template.d.ts +1 -0
  212. package/packages/ui/lib/routes/chat-template.js +1 -0
  213. package/packages/ui/lib/routes/deps.d.ts +1 -0
  214. package/packages/ui/lib/routes/deps.js +1 -0
  215. package/packages/ui/lib/routes/download.d.ts +1 -0
  216. package/packages/ui/lib/routes/download.js +1 -0
  217. package/packages/ui/lib/routes/get.d.ts +1 -0
  218. package/packages/ui/lib/routes/get.js +106 -0
  219. package/packages/ui/lib/routes/install.d.ts +16 -0
  220. package/packages/ui/lib/routes/install.js +36 -0
  221. package/packages/ui/lib/routes/launch.d.ts +24 -0
  222. package/packages/ui/lib/routes/launch.js +74 -0
  223. package/packages/ui/lib/routes/logs.d.ts +57 -0
  224. package/packages/ui/lib/routes/logs.js +117 -0
  225. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  226. package/packages/ui/lib/routes/provider-delete.js +37 -0
  227. package/packages/ui/lib/routes/register.d.ts +16 -0
  228. package/packages/ui/lib/routes/register.js +28 -0
  229. package/packages/ui/lib/routes/save.d.ts +18 -0
  230. package/packages/ui/lib/routes/save.js +159 -0
  231. package/packages/ui/lib/routes/stop.d.ts +19 -0
  232. package/packages/ui/lib/routes/stop.js +32 -0
  233. package/packages/ui/lib/routes/stream.d.ts +40 -0
  234. package/packages/ui/lib/routes/stream.js +68 -0
  235. package/packages/ui/lib/routes/summary.d.ts +15 -0
  236. package/packages/ui/lib/routes/summary.js +26 -0
  237. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  238. package/packages/ui/lib/routes/unregister.js +26 -0
  239. package/packages/ui/lib/viewmodels/deps.d.ts +1 -0
  240. package/packages/ui/lib/viewmodels/deps.js +1 -0
  241. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +1 -0
  242. package/packages/ui/lib/viewmodels/depsHelpers.js +94 -0
  243. package/packages/ui/lib/viewmodels/summary.d.ts +145 -0
  244. package/packages/ui/lib/viewmodels/summary.js +245 -0
  245. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  246. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  247. package/packages/ui/lib/warnings.d.ts +1 -1
  248. package/packages/ui/lib/warnings.js +1 -1
  249. package/packages/ui/package.json +4 -4
  250. package/packages/providers/lib/strata.d.ts +0 -15
  251. package/packages/providers/lib/strata.js +0 -14
@@ -17,7 +17,7 @@
17
17
  * mod-search bridge.
18
18
  * @module dsh-plugin-local-infer/service
19
19
  */
20
- import { exec, execFile, spawn } from 'node:child_process';
20
+ import { execFile, spawn } from 'node:child_process';
21
21
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
22
22
  import { availableParallelism, cpus, homedir, platform, release, totalmem } from 'node:os';
23
23
  import { dirname, join } from 'node:path';
@@ -25,7 +25,10 @@ import { Service } from '@deepseek-ai/cordis';
25
25
  // Relative, not the root package subpath: a self-referencing bare specifier
26
26
  // only resolves when the package sits inside a node_modules tree, so a
27
27
  // local-path or pnpm-linked install of this repo would fail the import.
28
- import { AGENT_DEFAULT_MODEL_NAMESPACE, agentDefaultOps, agentDefaultPointsAt, agentDefaultUnsetOps, buildModelEntry, detectHardware as detectHardwareFromProbes, detectPackageManager, evaluateGuidelines, estimateVram, hfChatTemplateCommand, hfDownloadCommand, isGpuBusy, LLM_PI_AI_NAMESPACE, matchLaunchSignature, modelGuidelines, reLinkRecord, computeProviderStatuses, modelRowsFrom, parseNvidiaSmiGpu, parseNvidiaSmiGpuUsage, presetFromFetch, providerRegistrationOps, providerRemovalOps, removeModelRow, renderInstallString, resolveDefaults as resolveDefaultsFromPresets, resolveOsReserve, runInstall, } from '../../core/lib/index.js';
28
+ import { AGENT_DEFAULT_MODEL_NAMESPACE, agentDefaultOps, agentDefaultPointsAt, agentDefaultUnsetOps, buildModelEntry, commandLogLine, detectHardware as detectHardwareFromProbes, detectPackageManager, evaluateGuidelines, estimateVram, guardSudoCommand, hfChatTemplateCommand, hfDatasetsCacheDirectory, hfDownloadCommand, hfHubCacheDirectory, isGpuBusy, LLM_PI_AI_NAMESPACE, matchLaunchSignature, modelGuidelines, reLinkRecord, computeProviderStatuses, modelRowsFrom, parseNvidiaSmiGpu, parseNvidiaSmiGpuUsage, presetFromFetch, providerRegistrationOps, providerRemovalOps, removeModelRow, renderInstallString, resolveDefaults as resolveDefaultsFromPresets, resolveOsReserve, runInstall, SUDO_PROBE_COMMAND, sudoFailureDetail, } from '../../core/lib/index.js';
29
+ import { RunLogStore, runLogKey } from "./run-logs.js";
30
+ import { findLaunchTarget } from "./launch-target.js";
31
+ export { RunLogStore, runLogKey, findLaunchTarget };
29
32
  /**
30
33
  * Local-inference failure. Extends Error with a stable `code`
31
34
  * (`LOCAL_INFER_CONFLICT`, `LOCAL_INFER_INVALID_ENGINE`,
@@ -46,6 +49,13 @@ export class LocalInferError extends Error {
46
49
  * "not available" instead.
47
50
  */
48
51
  const PROBE_TIMEOUT_MS = 5000;
52
+ /**
53
+ * The page-fetch deadline the modsearch CLI is told to cap itself with (its
54
+ * --deadline argument, milliseconds as text): the CLI kills its own fetch
55
+ * past this, so the spawned bridge process returns instead of sitting on an
56
+ * unreachable URL.
57
+ */
58
+ const MODSEARCH_DEADLINE_ARG = '60000';
49
59
  /**
50
60
  * The adopted-process poll interval. A re-adopted engine (a surviving process
51
61
  * with no ChildProcess after a DSH restart) is watched for death through a
@@ -77,8 +87,8 @@ export class LocalInfer extends Service {
77
87
  // confirm which model a live engine serves: a port the plugin launched reads
78
88
  // as that model, a port it did not launch (a foreign engine) has no entry and
79
89
  // reads as "engine running, model unknown". The map resets on a host
80
- // restart, so a fresh start tracks only the launches it made itself. See
81
- // PLAN.md for the PID-cache note: a full PID cache is a future refinement.
90
+ // restart, so a fresh start tracks only the launches it made itself. A full
91
+ // PID cache is a future refinement.
82
92
  launchedModels = new Map();
83
93
  // The live ports tracked per launched engine (launches and adopted re-links
84
94
  // both register). The death handler clears the engine's plugin-launched flag
@@ -94,11 +104,37 @@ export class LocalInfer extends Service {
94
104
  // handle. A summary re-link that adopts this DSH's own launch record must
95
105
  // not stack a poll watcher on top of the handle's close event.
96
106
  handledPids = new Set();
107
+ // The engine keys (engineId:port) whose launch this service is in flight
108
+ // for: the deploy has started and the health poll has not settled. While a
109
+ // key is here the provider-status detector keeps the provider's linked read
110
+ // (running, not unsure), so an engine that sizes its own model (Strata, no
111
+ // model tie) or a launch mid-poll never flashes the "Unable to link" banner.
112
+ // The key clears when the launch settles (a healthy poll, a failed deploy).
113
+ launchingEngines = new Set();
114
+ // The engine keys (engineId:port) whose stop this service is in flight for:
115
+ // the stop step signalled the engine and the process has not died yet. While
116
+ // a key is here the detector keeps the linked read, so a stop never reads as
117
+ // an unlinked provider for the moment the engine takes to shut down. The key
118
+ // clears when the process dies (the death observer) or the detector sees the
119
+ // process down (a stop of an engine it cannot watch).
120
+ stoppingEngines = new Set();
97
121
  // The route-registered hook for engine process deaths (a crash, a pkill, or
98
122
  // a poll-detected death of an adopted process). The observer persists the
99
123
  // provider entry's cleared state; a headless profile registers none and the
100
124
  // service still settles its own live state.
101
125
  deathObserver;
126
+ // The run-registered hooks for sudo commands the guard found waiting for a
127
+ // password on the CLI, one slot per run key (the runLogKey rule every run
128
+ // path shares). A guarded command announces through notifySudoNeeded to
129
+ // every registered observer (one CLI password wait blocks every plugin
130
+ // run), so concurrent runs keep their own note routing and a settle
131
+ // clears only its own slot. A headless profile registers none and the
132
+ // guard still announces internally and caps the wait.
133
+ sudoObservers = new Map();
134
+ // The shared per-run live log hub every provider process writes its output
135
+ // to, keyed by the runLogKey rule so the run paths and the streaming logs
136
+ // route predict one key per provider.
137
+ runLogs = new RunLogStore();
102
138
  constructor(ctx) {
103
139
  super(ctx, 'localInfer');
104
140
  }
@@ -352,17 +388,41 @@ export class LocalInfer extends Service {
352
388
  const port = signatures[index]?.port;
353
389
  return port !== undefined ? { ...provider, port } : provider;
354
390
  });
355
- return computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
391
+ const statuses = computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
392
+ // The in-flight launch and stop state machine: a provider the launch or
393
+ // stop step is in flight for keeps its linked read (running, not unsure)
394
+ // while the engine process is still up, so neither a stop (the record is
395
+ // cleared but the process is still shutting down) nor a launch (an engine
396
+ // that sizes its own model carries no model tie) flashes the "Unable to
397
+ // link" banner. A stopped provider whose process is down reads as stopped,
398
+ // and the stopping flag clears with it, so a stop of an engine the service
399
+ // cannot watch (no death event) never leaves a stale flag behind.
400
+ for (const provider of priced) {
401
+ const key = this.engineKey(provider.engine, provider.port);
402
+ const launching = this.launchingEngines.has(key);
403
+ const stopping = this.stoppingEngines.has(key);
404
+ if (!launching && !stopping)
405
+ continue;
406
+ if (statuses[provider.id] === 'unsure') {
407
+ statuses[provider.id] = 'running';
408
+ }
409
+ else if (stopping) {
410
+ this.stoppingEngines.delete(key);
411
+ }
412
+ }
413
+ return statuses;
356
414
  }
357
415
  /**
358
- * unsureBannerKillCommands returns the bash kill commands for the engine
416
+ * unsureBannerKillCommands returns the stop commands for the engine
359
417
  * processes the detector saw holding VRAM, for the "unsure" banner the card
360
418
  * shows when no provider is confirmed running but at least one reads as
361
419
  * unsure. A process is included when its command line carries an engine
362
420
  * binary marker and the card's VRAM usage is above 30% of the total (the
363
421
  * threshold below which a foreign engine is not considered to be holding
364
- * the card). The commands are one `kill <pid>` per matching process, in
365
- * pid order. An empty list means no matching process was seen, so the
422
+ * the card). The command form switches on the host OS: one
423
+ * `Stop-Process -Id <pid>` per matching process on Windows (the PowerShell
424
+ * cmdlet the banner's text area runs), one `kill <pid>` on Linux and macOS,
425
+ * in pid order. An empty list means no matching process was seen, so the
366
426
  * banner's text area renders empty.
367
427
  */
368
428
  async unsureBannerKillCommands(options) {
@@ -382,10 +442,13 @@ export class LocalInfer extends Service {
382
442
  gpuUsage.usedMiB / gpuUsage.totalMiB < 0.3) {
383
443
  return [];
384
444
  }
445
+ // The stop command form follows the host OS: Stop-Process on Windows,
446
+ // kill on Linux and macOS.
447
+ const isWindows = platform() === 'win32';
385
448
  const commands = [];
386
449
  for (const line of processes) {
387
450
  if (markers.some((marker) => line.command.includes(marker))) {
388
- commands.push(`kill ${line.pid}`);
451
+ commands.push(isWindows ? `Stop-Process -Id ${line.pid}` : `kill ${line.pid}`);
389
452
  }
390
453
  }
391
454
  return commands;
@@ -415,14 +478,27 @@ export class LocalInfer extends Service {
415
478
  * deps resolves the host deps an engine's deploy step needs for a build
416
479
  * mode. A registered adapter that declares systemDeps wins; the provider-
417
480
  * registered plan for the engine id is the fallback, and an engine with
418
- * neither plans an empty list (nothing to install, the gate passes).
481
+ * neither plans an empty list (nothing to install, the gate passes). The
482
+ * local list is per-OS: a Windows host reads the plan's windows entry (the
483
+ * winget toolchain the providers sourced from the Windows profile), every
484
+ * other host reads the linux entry, so the System dependencies section
485
+ * shows the right toolchain for the host.
419
486
  */
420
487
  async deps(engineId, buildMode, hw) {
421
488
  const adapter = this.engineRegistry.get(engineId);
422
489
  if (adapter !== undefined && typeof adapter.systemDeps === 'function') {
423
490
  return adapter.systemDeps(buildMode, hw);
424
491
  }
425
- return this.depPlanRegistry.get(engineId)?.[buildMode] ?? [];
492
+ const plan = this.depPlanRegistry.get(engineId);
493
+ if (plan === undefined)
494
+ return [];
495
+ if (buildMode === 'local') {
496
+ // A Windows host reads the windows entry; every other host (linux,
497
+ // macos) reads the linux entry, which the providers source from the
498
+ // shared Linux profile.
499
+ return plan.local?.[hw.os] ?? plan.local?.linux ?? [];
500
+ }
501
+ return plan[buildMode] ?? [];
426
502
  }
427
503
  /**
428
504
  * installString renders the copy/paste block for the resolved dep plan,
@@ -435,12 +511,29 @@ export class LocalInfer extends Service {
435
511
  }
436
512
  /**
437
513
  * runInstall is the Play/Run path: it runs the resolved dep plan's commands
438
- * through the shell runner and re-probes each dep. The runner captures the
439
- * per-command exit, so a failure names the command that broke.
514
+ * through the guarded shell runner and re-probes each dep. The runner
515
+ * captures the per-command exit, so a failure names the command that broke,
516
+ * and a declared-sudo command whose probe finds no cached credentials
517
+ * announces the CLI prompt through the sudo observer and fails at the
518
+ * sudo deadline. options.onLog is the install run's log sink: the command
519
+ * line reaches it as a `$ <command>` line before the guarded runner spawns
520
+ * the child (and before the sudo guard's probe announces), so the run text
521
+ * carries the command ahead of the per-dep results. options.runner injects
522
+ * the command runner so a test reads the command lines without a real shell.
440
523
  */
441
- async runInstall(engineId, buildMode, hw) {
524
+ async runInstall(engineId, buildMode, hw, options) {
442
525
  const deps = await this.deps(engineId, buildMode, hw);
443
- return runInstall(deps, hw, shellRunCommand);
526
+ // The guarded runner is built per call so every sudo announcement
527
+ // reaches the observers registered when this call runs.
528
+ const runner = options?.runner ?? guardedShellRunner((command) => this.notifySudoNeeded(command));
529
+ // The audit wrapper logs each command the core provisioner actually runs
530
+ // (a satisfied dep never runs, so it never logs) before delegating to the
531
+ // runner. Keeping the wrapper here leaves the core provisioner pure.
532
+ const loggedRunner = (command, runOptions) => {
533
+ options?.onLog?.(commandLogLine(command));
534
+ return runner(command, runOptions);
535
+ };
536
+ return runInstall(deps, hw, loggedRunner);
444
537
  }
445
538
  /**
446
539
  * launch is the lifecycle entry point: it resolves the adapter that rendered
@@ -465,15 +558,26 @@ export class LocalInfer extends Service {
465
558
  // whether the git sync (fetch, rebase, rebuild) runs at all.
466
559
  // exactOptionalPropertyTypes: only pass the option object when a key is
467
560
  // present.
468
- const hasDeployOptions = options?.allowRunDeps !== undefined || options?.autoRebuild !== undefined;
561
+ const hasDeployOptions = options?.allowRunDeps !== undefined ||
562
+ options?.autoRebuild !== undefined ||
563
+ options?.onLog !== undefined;
469
564
  const deployOptions = hasDeployOptions
470
565
  ? {
471
566
  ...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
472
567
  ...(options?.autoRebuild !== undefined ? { autoRebuild: options.autoRebuild } : {}),
568
+ ...(options?.onLog !== undefined ? { onLog: options.onLog } : {}),
473
569
  }
474
570
  : undefined;
571
+ // The in-flight launch flag: the provider-status detector keeps this
572
+ // provider's linked read while the deploy and health poll run, so an engine
573
+ // that sizes its own model (no model tie) never flashes "Unable to link".
574
+ const launchKey = this.engineKey(plan.engineId, plan.port);
575
+ this.launchingEngines.add(launchKey);
475
576
  const deployResult = await adapter.deploy(plan, platform, onProgress, deployOptions);
476
577
  if (!deployResult.ok) {
578
+ // A failed deploy settles the launch: clear the in-flight flag so the
579
+ // provider reads from the process state (stopped), not a stuck launch.
580
+ this.launchingEngines.delete(launchKey);
477
581
  // exactOptionalPropertyTypes: only write the detail key when it is present.
478
582
  const result = { plan, healthy: false };
479
583
  if (deployResult.detail !== undefined) {
@@ -517,10 +621,20 @@ export class LocalInfer extends Service {
517
621
  // "Launching..." while it waits for the engine to report ready.
518
622
  onProgress?.('launch');
519
623
  const health = await adapter.healthCheck(plan, options);
624
+ // The launch has settled (a healthy poll, a failed one, or a download that
625
+ // the card re-polls): clear the in-flight flag so the provider reads from
626
+ // the model tie (a healthy launch) or the process state (a failed one).
627
+ this.launchingEngines.delete(launchKey);
520
628
  const launchResult = { plan, healthy: health.healthy };
521
629
  if (health.detail !== undefined) {
522
630
  launchResult.detail = health.detail;
523
631
  }
632
+ // The start phase the poll observed (downloading, starting, ready, failed)
633
+ // rides the result so the card reads a long model download as in progress
634
+ // instead of a broken launch.
635
+ if (health.state !== undefined) {
636
+ launchResult.state = health.state;
637
+ }
524
638
  // The launch record the route persists for the provider config entry: the
525
639
  // engine's pid and the DSH process pid that launched it. The parent pid is
526
640
  // only written when the host process identifies as DSH, so a non-DSH host
@@ -611,6 +725,86 @@ export class LocalInfer extends Service {
611
725
  setEngineDeathObserver(observer) {
612
726
  this.deathObserver = observer;
613
727
  }
728
+ /**
729
+ * registerSudoObserver reserves `key`'s slot (the runLogKey rule every run
730
+ * path shares) for one run's hook for commands the sudo guard found
731
+ * waiting for a password on the launching CLI: the observer lands core's
732
+ * SUDO_CLI_NOTE line while the command waits. Registering a key that is
733
+ * taken replaces the old hook (a re-launch under the same key owns the
734
+ * note), and the disposer removes only its own hook, so a stale run's
735
+ * settle never clears a live one's routing. The run calls the disposer in
736
+ * a finally.
737
+ */
738
+ registerSudoObserver(key, observer) {
739
+ this.sudoObservers.set(key, observer);
740
+ return () => {
741
+ if (this.sudoObservers.get(key) === observer) {
742
+ this.sudoObservers.delete(key);
743
+ }
744
+ };
745
+ }
746
+ /**
747
+ * notifySudoNeeded announces one guarded command to every registered sudo
748
+ * observer (the shared CLI password wait can block any run, and the
749
+ * announce seam does not name the run that triggered it). A throwing
750
+ * observer is isolated per announce: the note is a log courtesy, and a
751
+ * broken route must not fail the install that is already racing the sudo
752
+ * deadline. Published on the service surface so a command runner outside
753
+ * this package (the providers' lifecycle host) reports through the same
754
+ * seam.
755
+ */
756
+ notifySudoNeeded(command) {
757
+ for (const observer of [...this.sudoObservers.values()]) {
758
+ try {
759
+ observer(command);
760
+ }
761
+ catch {
762
+ // The sudo deadline still settles a passwordless wait.
763
+ }
764
+ }
765
+ }
766
+ /**
767
+ * beginRun starts one provider process's live run-log run under the key
768
+ * runLogKey names: the buffer resets to the seed separator line, the
769
+ * running flag goes true, the seq restarts, and subscribers get the reset
770
+ * notice a mid-run attached reader needs to replace its display.
771
+ */
772
+ beginRun(key, seed) {
773
+ this.runLogs.begin(key, seed);
774
+ }
775
+ /**
776
+ * appendRunLog appends one chunk to the key's live log (tail-capped to
777
+ * LOG_TAIL_BYTES) and notifies subscribers. Appending with no open run
778
+ * opens one lazily without a seed, so a late note (an engine-death line)
779
+ * still lands.
780
+ */
781
+ appendRunLog(key, text) {
782
+ this.runLogs.append(key, text);
783
+ }
784
+ /**
785
+ * readRunLog answers the key's catch-up snapshot: the seq reached, the
786
+ * bounded tail text, and the running flag, the data a mid-run page
787
+ * refresh or a late-opened Logs pane renders from.
788
+ */
789
+ readRunLog(key) {
790
+ return this.runLogs.read(key);
791
+ }
792
+ /**
793
+ * subscribeRunLog registers a listener for the key's run-log events and
794
+ * returns the disposer (the route calls it when its stream closes).
795
+ * Listeners are independent per connection; none can block a writer.
796
+ */
797
+ subscribeRunLog(key, listener) {
798
+ return this.runLogs.subscribe(key, listener);
799
+ }
800
+ /**
801
+ * endRun settles the key's run: the running flag clears and subscribers
802
+ * get the end event. The text stays readable until the next beginRun, so
803
+ * a late Logs pane still shows the finished run.
804
+ */
805
+ endRun(key) {
806
+ this.runLogs.end(key);
807
+ }
614
808
  /**
615
809
  * watchLaunchedHandle ties a launched child's one-shot exit to the live
616
810
  * state: the host's close/error event (a crash, a pkill, or the stop kill)
@@ -671,6 +865,15 @@ export class LocalInfer extends Service {
671
865
  timer.unref();
672
866
  this.engineWatchers.set(port, { pid: record.pid, timer });
673
867
  }
868
+ /**
869
+ * engineKey is the in-flight state machine's key for one engine: the engine
870
+ * id plus its port when the plan names one, so two configs of one engine on
871
+ * different ports are distinct engines. A plan with no port keys on the
872
+ * engine id alone.
873
+ */
874
+ engineKey(engineId, port) {
875
+ return port !== undefined ? `${engineId}:${port}` : engineId;
876
+ }
674
877
  /**
675
878
  * stopEngineWatcher cancels one port's adopted-process poll, if it has one.
676
879
  */
@@ -693,6 +896,10 @@ export class LocalInfer extends Service {
693
896
  */
694
897
  observeEngineDeath(event) {
695
898
  if (event.port !== undefined) {
899
+ // The process is down: the in-flight stop (if one was in flight) has
900
+ // settled, so drop the flag and the deferred port-to-model tie the stop
901
+ // step kept for the provider's linked read.
902
+ this.stoppingEngines.delete(this.engineKey(event.engineId, event.port));
696
903
  const tracked = this.launchedModels.get(event.port);
697
904
  if (tracked !== undefined && tracked.engineId === event.engineId) {
698
905
  this.launchedModels.delete(event.port);
@@ -715,19 +922,39 @@ export class LocalInfer extends Service {
715
922
  /**
716
923
  * stop signals the engine a launch started: it resolves the adapter by the
717
924
  * plan's engineId and runs the adapter's stop step (kill the tracked process,
718
- * stop the named container, or compose down).
925
+ * stop the named container, or compose down). The optional `log` is the
926
+ * stop run's log sink: the adapter writes the `$ <command>` line of every
927
+ * command it runs (compose down, docker stop, a pid signal) through it
928
+ * before the child runs, so the stop's live run reads the command first.
719
929
  */
720
- async stop(plan) {
930
+ async stop(plan, log) {
721
931
  const adapter = this.engineRegistry.get(plan.engineId);
722
932
  if (adapter === undefined) {
723
933
  throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
724
934
  }
725
- await adapter.stop(plan);
726
- // Clear the port-to-model entry too, so the provider-status detector no
727
- // longer ties this port to the model the plugin launched it with, and
728
- // cancel any adopted-process watcher standing on the port.
935
+ // The in-flight stop flag: the provider-status detector keeps this
936
+ // provider's linked read while the engine shuts down, so a stop never
937
+ // flashes the "Unable to link" banner for the moment the process takes to
938
+ // die. The flag clears when the process dies (the death observer) or the
939
+ // detector sees the process down (a stop of an engine it cannot watch).
940
+ const stopKey = this.engineKey(plan.engineId, plan.port);
941
+ this.stoppingEngines.add(stopKey);
942
+ try {
943
+ await adapter.stop(plan, log);
944
+ }
945
+ catch (error) {
946
+ // A stop that failed (a kill the host could not deliver) settles the
947
+ // in-flight state: the flag clears, so the provider reads from the
948
+ // process state, not a stop stuck in flight.
949
+ this.stoppingEngines.delete(stopKey);
950
+ throw error;
951
+ }
952
+ // Clear the live-port ledger and the plugin-launched flag, and cancel any
953
+ // adopted-process watcher standing on the port. The port-to-model tie is
954
+ // deferred to the death observer, so the provider keeps its linked read
955
+ // (running, not unsure) for the moment the engine is still shutting down;
956
+ // the observer drops it the instant the process is down.
729
957
  if (plan.port !== undefined) {
730
- this.launchedModels.delete(plan.port);
731
958
  this.stopEngineWatcher(plan.port);
732
959
  const ports = this.launchedPorts.get(plan.engineId);
733
960
  if (ports !== undefined) {
@@ -836,7 +1063,10 @@ export class LocalInfer extends Service {
836
1063
  preset: undefined,
837
1064
  stored: false,
838
1065
  degraded: true,
839
- detail: `the modsearch fetch failed: ${detail}; the built-in presets apply`,
1066
+ // The URL rides the caller-visible detail: fetchPreset is not a
1067
+ // provider process (no run-log key to write a `> GET` line into), so
1068
+ // the audited rule is served by naming the queried URL here instead.
1069
+ detail: `the modsearch fetch failed for ${source.url}: ${detail}; the built-in presets apply`,
840
1070
  };
841
1071
  }
842
1072
  const preset = presetFromFetch(source, content, todayIso());
@@ -845,13 +1075,26 @@ export class LocalInfer extends Service {
845
1075
  return { preset, stored: true, degraded: false };
846
1076
  }
847
1077
  /**
848
- * defaultModelDirectory is the plugin's model download directory: under
849
- * the dsh home so a web user and a CLI user share one location, the way the
850
- * config file does. The directory is created when absent, so the field's
851
- * default is always a real path the deploy step can target.
1078
+ * defaultModelDirectory is the plugin's model download directory: the
1079
+ * Hugging Face hub cache the hf CLI and huggingface_hub share (HF_HUB_CACHE
1080
+ * when set, then $HF_HOME/hub, then ~/.cache/huggingface/hub), so a model
1081
+ * the plugin pulls lands where every other HF tooling looks. The directory
1082
+ * is created when absent, so the field's default is always a real path the
1083
+ * deploy step can target.
852
1084
  */
853
1085
  defaultModelDirectory() {
854
- const directory = join(homedir(), '.dsh', 'models');
1086
+ const directory = hfHubCacheDirectory();
1087
+ mkdirSync(directory, { recursive: true });
1088
+ return directory;
1089
+ }
1090
+ /**
1091
+ * defaultDatasetDirectory is the plugin's dataset download directory: the
1092
+ * Hugging Face datasets cache (HF_DATASETS_CACHE when set, then
1093
+ * $HF_HOME/datasets, then ~/.cache/huggingface/datasets). Created when
1094
+ * absent, for the same reason the model directory is.
1095
+ */
1096
+ defaultDatasetDirectory() {
1097
+ const directory = hfDatasetsCacheDirectory();
855
1098
  mkdirSync(directory, { recursive: true });
856
1099
  return directory;
857
1100
  }
@@ -864,7 +1107,10 @@ export class LocalInfer extends Service {
864
1107
  const injected = options?.cliPath;
865
1108
  if (injected !== undefined && injected.trim() !== '')
866
1109
  return injected;
867
- const which = await runCommand('which', ['hf']);
1110
+ // The presence probe is `which` on Linux/macOS and `where` on Windows, so
1111
+ // an installed hf CLI is found on every host.
1112
+ const probe = platform() === 'win32' ? 'where' : 'which';
1113
+ const which = await runCommand(probe, ['hf']);
868
1114
  if (which.ok && which.stdout.trim() !== '')
869
1115
  return which.stdout.trim();
870
1116
  return null;
@@ -873,7 +1119,10 @@ export class LocalInfer extends Service {
873
1119
  * downloadModel runs the hf CLI download for one default model into the
874
1120
  * model directory: it creates the target directory when absent (the
875
1121
  * recursive flag covers the parents too), renders the command through the
876
- * core builder (the single source the card shows), and runs it through the
1122
+ * core builder (the single source the card shows), logs the rendered
1123
+ * command's `$ <command>` line through options.onLog before the runner
1124
+ * spawns the child (so every caller's run reads command first, output
1125
+ * after, and the line is written once, here), and runs it through the
877
1126
  * injected runner when one is given, so a test captures the command without
878
1127
  * a live hf. A failure keeps the runner's detail, so the card names the
879
1128
  * broken step.
@@ -881,7 +1130,9 @@ export class LocalInfer extends Service {
881
1130
  async downloadModel(entry, directory, options) {
882
1131
  mkdirSync(directory, { recursive: true });
883
1132
  const runner = options?.runner ?? downloadRunner;
884
- const result = await runner(hfDownloadCommand(entry, directory));
1133
+ const command = hfDownloadCommand(entry, directory, options?.useHubCache === true);
1134
+ options?.onLog?.(commandLogLine(command));
1135
+ const result = await runner(command, options?.signal, options?.onChunk);
885
1136
  if (result.ok)
886
1137
  return { ok: true };
887
1138
  return { ok: false, detail: `model download failed: ${result.detail ?? 'unknown'}` };
@@ -890,13 +1141,16 @@ export class LocalInfer extends Service {
890
1141
  * downloadChatTemplate pulls the chat template file for one HF repo
891
1142
  * reference into the model directory, so a selected template (the Froggeric
892
1143
  * set, a custom repo) works out of the box. It shares the model download's
893
- * recursive directory creation, runner, and long timeout: a template is a
894
- * small file, but the hf CLI's cold start and auth are the same cost.
1144
+ * recursive directory creation, runner, log line before the spawn, and long
1145
+ * timeout: a template is a small file, but the hf CLI's cold start and auth
1146
+ * are the same cost.
895
1147
  */
896
1148
  async downloadChatTemplate(repo, directory, options) {
897
1149
  mkdirSync(directory, { recursive: true });
898
1150
  const runner = options?.runner ?? downloadRunner;
899
- const result = await runner(hfChatTemplateCommand(repo, directory));
1151
+ const command = hfChatTemplateCommand(repo, directory, options?.useHubCache === true);
1152
+ options?.onLog?.(commandLogLine(command));
1153
+ const result = await runner(command, options?.signal, options?.onChunk);
900
1154
  if (result.ok)
901
1155
  return { ok: true };
902
1156
  return { ok: false, detail: `chat template download failed: ${result.detail ?? 'unknown'}` };
@@ -946,6 +1200,71 @@ export class LocalInfer extends Service {
946
1200
  }
947
1201
  return entries;
948
1202
  }
1203
+ /**
1204
+ * listModelCache lists the hub cache's `models--*` directories, the data the
1205
+ * per-engine model dropdown's cache scan draws from when the model directory
1206
+ * is the plugin default (the hub cache). Each entry carries the repo id the
1207
+ * directory names (the `{namespace}/{name}` decoded from the
1208
+ * `models--{ns}--{name}` form) and the artifact file names its snapshots
1209
+ * hold (the union across every commit). A missing or unreadable cache reads
1210
+ * as an empty list, which the route renders as "no cached models".
1211
+ */
1212
+ listModelCache() {
1213
+ const hub = hfHubCacheDirectory();
1214
+ let dirents;
1215
+ try {
1216
+ dirents = readdirSync(hub, { withFileTypes: true });
1217
+ }
1218
+ catch {
1219
+ return [];
1220
+ }
1221
+ const entries = [];
1222
+ for (const dirent of dirents) {
1223
+ if (!dirent.isDirectory() || !dirent.name.startsWith('models--'))
1224
+ continue;
1225
+ // Decode models--{ns}--{name} back to {ns}/{name}: the name is the
1226
+ // segment after the last --, the namespace everything before it.
1227
+ const body = dirent.name.slice('models--'.length);
1228
+ const separator = body.lastIndexOf('--');
1229
+ if (separator <= 0)
1230
+ continue;
1231
+ const repoId = `${body.slice(0, separator)}/${body.slice(separator + 2)}`;
1232
+ entries.push({ repoId, files: this.snapshotFiles(hub, dirent.name) });
1233
+ }
1234
+ return entries;
1235
+ }
1236
+ /**
1237
+ * snapshotFiles unions the file names across every commit directory under a
1238
+ * `models--*` dir's `snapshots/`, the artifact set the per-engine cache scan
1239
+ * filters. A missing snapshots folder reads as an empty set.
1240
+ */
1241
+ snapshotFiles(hub, modelDir) {
1242
+ const snapshots = join(hub, modelDir, 'snapshots');
1243
+ let commits;
1244
+ try {
1245
+ commits = readdirSync(snapshots, { withFileTypes: true });
1246
+ }
1247
+ catch {
1248
+ return [];
1249
+ }
1250
+ const files = new Set();
1251
+ for (const commit of commits) {
1252
+ if (!commit.isDirectory())
1253
+ continue;
1254
+ let commitFiles;
1255
+ try {
1256
+ commitFiles = readdirSync(join(snapshots, commit.name), { withFileTypes: true });
1257
+ }
1258
+ catch {
1259
+ continue;
1260
+ }
1261
+ for (const file of commitFiles) {
1262
+ if (file.isFile())
1263
+ files.add(file.name);
1264
+ }
1265
+ }
1266
+ return [...files];
1267
+ }
949
1268
  /**
950
1269
  * specsForEngine reads the parameter surface of the registered adapter for
951
1270
  * an engine id, so the defaults resolver floors the result on that engine's
@@ -970,7 +1289,9 @@ export class LocalInfer extends Service {
970
1289
  const env = process.env.MODSEARCH_CLI;
971
1290
  if (env !== undefined && env.trim() !== '')
972
1291
  return env;
973
- const which = await runCommand('which', ['modsearch']);
1292
+ // The presence probe is `which` on Linux/macOS and `where` on Windows.
1293
+ const probe = platform() === 'win32' ? 'where' : 'which';
1294
+ const which = await runCommand(probe, ['modsearch']);
974
1295
  if (which.ok && which.stdout.trim() !== '')
975
1296
  return which.stdout.trim();
976
1297
  return null;
@@ -983,7 +1304,7 @@ export class LocalInfer extends Service {
983
1304
  */
984
1305
  fetchPage(url, cliPath) {
985
1306
  return new Promise((resolve, reject) => {
986
- const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline', '60000'], { stdio: ['ignore', 'pipe', 'pipe'] });
1307
+ const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline', MODSEARCH_DEADLINE_ARG], { stdio: ['ignore', 'pipe', 'pipe'] });
987
1308
  let stdout = '';
988
1309
  let stderr = '';
989
1310
  child.stdout.on('data', (chunk) => {
@@ -1239,36 +1560,132 @@ function runCommand(cmd, args) {
1239
1560
  */
1240
1561
  const INSTALL_OUTPUT_TAIL = 1024;
1241
1562
  /**
1242
- * shellRunCommand is the Play/Run runner: it runs one command line through
1243
- * the system shell and reports the exit. stdin is closed, so a sudo password
1244
- * prompt that cannot read a terminal fails fast instead of hanging the panel;
1245
- * the copy/paste path is the fallback the plan names for exactly this.
1563
+ * The high-water mark the streaming output trims to: the collected output
1564
+ * passes the mark only for a chatty command, and the trim keeps the last
1565
+ * kilobyte the failure detail shows, the same rule the providers' streaming
1566
+ * host applies.
1246
1567
  */
1247
- const shellRunCommand = (command) => new Promise(resolve => {
1248
- const child = spawn(command, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
1568
+ const INSTALL_OUTPUT_HIGH_WATER = INSTALL_OUTPUT_TAIL * 2;
1569
+ /**
1570
+ * tailOutput keeps a bounded tail of a streamed command's combined output:
1571
+ * append grows the buffer and trims it to the high-water mark, and tail
1572
+ * returns the trimmed last kilobyte a failure detail shows. The install and
1573
+ * download runners share it, so a chatty failure stays readable without
1574
+ * growing the host's memory.
1575
+ */
1576
+ function tailOutput() {
1249
1577
  let output = '';
1250
- const append = (chunk) => {
1251
- output += chunk.toString('utf8');
1252
- if (output.length > INSTALL_OUTPUT_TAIL * 2) {
1253
- output = output.slice(-INSTALL_OUTPUT_TAIL * 2);
1254
- }
1578
+ return {
1579
+ append: (text) => {
1580
+ output += text;
1581
+ if (output.length > INSTALL_OUTPUT_HIGH_WATER) {
1582
+ output = output.slice(-INSTALL_OUTPUT_HIGH_WATER);
1583
+ }
1584
+ },
1585
+ tail: () => output.trim().slice(-INSTALL_OUTPUT_TAIL),
1255
1586
  };
1256
- child.stdout.on('data', append);
1257
- child.stderr.on('data', append);
1258
- // Without a listener, a child that fails to start (a missing shell) emits
1259
- // an unlistened 'error' and throws it as an uncaughtException through the
1260
- // host. Read it as a failed command instead.
1261
- child.on('error', (error) => {
1262
- resolve({ ok: false, detail: error.message });
1587
+ }
1588
+ /**
1589
+ * sudoProbe answers whether sudo already holds cached (or NOPASSWD)
1590
+ * credentials: it runs SUDO_PROBE_COMMAND non-interactively through the
1591
+ * probe timeout and reads a clean exit as "no prompt is coming". A refused
1592
+ * -n, a missing sudo, and a hung probe all read as "a prompt is coming",
1593
+ * the safe side: the run announces and caps instead of dead-locking on a
1594
+ * TTY the card user cannot see.
1595
+ */
1596
+ export async function sudoProbe() {
1597
+ const [command, ...args] = SUDO_PROBE_COMMAND;
1598
+ if (command === undefined)
1599
+ return false;
1600
+ const { ok } = await runCommand(command, [...args]);
1601
+ return ok;
1602
+ }
1603
+ /**
1604
+ * sudoGuardFor is the service-side binding of core's guardSudoCommand: it
1605
+ * supplies sudoProbe, so this package's install runner and the providers'
1606
+ * lifecycle host drive one probe and one rule from one function. Calling
1607
+ * it is itself the sudo declaration (needsSudo is true); callers gate on
1608
+ * their own needsSudo fact first and never run a plain command through it.
1609
+ */
1610
+ export function sudoGuardFor(command, notify, kill, timeoutMs) {
1611
+ return guardSudoCommand({
1612
+ needsSudo: true,
1613
+ command,
1614
+ probe: sudoProbe,
1615
+ notify,
1616
+ kill,
1617
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1263
1618
  });
1264
- child.on('close', code => {
1265
- if (code === 0) {
1266
- resolve({ ok: true });
1267
- return;
1619
+ }
1620
+ /**
1621
+ * guardedShellRunner builds the Play/Run runner: it runs one command line
1622
+ * through the system shell and reports the exit. stdin stays closed. When
1623
+ * core hands the command the dep's sudo declaration and sudoProbe finds no
1624
+ * cached credentials, core's guard announces the CLI prompt through notify
1625
+ * and arms the password deadline; its fire signals the child through the
1626
+ * shared signalChild rule (SIGINT, then SIGTERM; the group signal reaches a
1627
+ * sudo survivor of a compound line) and the run settles with
1628
+ * sudoFailureDetail instead of the shell's bare signal exit. The guard adds
1629
+ * only probe, notify, and deadline to the existing spawn; the copy/paste
1630
+ * path stays the fallback for a passwordless host.
1631
+ */
1632
+ export function guardedShellRunner(notify) {
1633
+ return async (command, options) => {
1634
+ let child;
1635
+ const guard = options?.needsSudo === true
1636
+ ? await sudoGuardFor(command, notify, () => {
1637
+ if (child !== undefined)
1638
+ signalChild(child);
1639
+ })
1640
+ : undefined;
1641
+ try {
1642
+ const outcome = await new Promise((resolve) => {
1643
+ // The local keeps the spawn overload's non-null stream typing; the
1644
+ // outer child exists for the guard's kill. The shell spawns detached
1645
+ // so it leads its own process group and signalChild's negative-pid
1646
+ // signal reaches the whole line. libuv's detached spawn only changes
1647
+ // the process group (Node >= 22 ships libuv >= 1.48, which does not
1648
+ // create a session), so a sudo prompt still reaches the launching
1649
+ // terminal and the guard's whole point survives the detach.
1650
+ const started = spawn(command, {
1651
+ shell: true,
1652
+ detached: true,
1653
+ stdio: ['ignore', 'pipe', 'pipe'],
1654
+ });
1655
+ child = started;
1656
+ detachedChildren.add(started);
1657
+ const tail = tailOutput();
1658
+ const append = (chunk) => {
1659
+ tail.append(chunk.toString('utf8'));
1660
+ };
1661
+ started.stdout.on('data', append);
1662
+ started.stderr.on('data', append);
1663
+ // Without a listener, a child that fails to start (a missing shell) emits
1664
+ // an unlistened 'error' and throws it as an uncaughtException through the
1665
+ // host. Read it as a failed command instead.
1666
+ started.on('error', (error) => {
1667
+ resolve({ ok: false, detail: error.message });
1668
+ });
1669
+ started.on('close', code => {
1670
+ if (code === 0) {
1671
+ resolve({ ok: true });
1672
+ return;
1673
+ }
1674
+ resolve({ ok: false, detail: `exit ${code}: ${tail.tail()}` });
1675
+ });
1676
+ });
1677
+ if (guard !== undefined && guard.timedOut() && !outcome.ok) {
1678
+ // The password deadline killed the child: name the missed sudo
1679
+ // prompt instead of the shell's signal exit.
1680
+ return { ok: false, detail: sudoFailureDetail(command) };
1681
+ }
1682
+ return outcome;
1268
1683
  }
1269
- resolve({ ok: false, detail: `exit ${code}: ${output.trim().slice(-INSTALL_OUTPUT_TAIL)}` });
1270
- });
1271
- });
1684
+ finally {
1685
+ guard?.disarm();
1686
+ }
1687
+ };
1688
+ }
1272
1689
  /**
1273
1690
  * The download timeout. A model download moves tens of GiB, so the cap is
1274
1691
  * hours, not the probe's seconds; a hung download must not kill the panel,
@@ -1276,20 +1693,126 @@ const shellRunCommand = (command) => new Promise(resolve => {
1276
1693
  */
1277
1694
  const DOWNLOAD_TIMEOUT_MS = 7_200_000;
1278
1695
  /**
1279
- * downloadRunner is the model-download runner: it runs the rendered hf CLI
1280
- * command through the system shell and reports the exit. The output tail keeps
1281
- * the last kilobyte, the same rule the install runner uses, so a failure names
1282
- * the broken line.
1696
+ * The cancel grace period in ms: an aborted download gets SIGINT first, and a
1697
+ * child still alive after the grace period gets SIGTERM, so a stuck hf CLI
1698
+ * cannot keep the run open.
1283
1699
  */
1284
- const downloadRunner = (command) => new Promise(resolve => {
1285
- exec(command, { timeout: DOWNLOAD_TIMEOUT_MS }, (error, stdout, stderr) => {
1286
- if (error === null) {
1700
+ const DOWNLOAD_ABORT_GRACE_MS = 5_000;
1701
+ /**
1702
+ * The install shells the guarded runner spawned detached (process-group
1703
+ * leaders). signalProcess reads the set so the negative-pid group signal
1704
+ * only ever targets a child that actually leads its group; a child that
1705
+ * stays in the host's group (the download runner's shell) gets the direct
1706
+ * signal, so a stray negative pid can never fan out beyond the child.
1707
+ */
1708
+ const detachedChildren = new WeakSet();
1709
+ /**
1710
+ * signalProcess delivers one signal to one child: a process-group leader
1711
+ * gets it through its negative pid, so a compound command line
1712
+ * (`a && sudo b`) dies whole instead of leaving sudo as a group survivor;
1713
+ * every other child gets the direct signal. A group that is already gone
1714
+ * (ESRCH) or a platform that rejects negative pids falls back to the direct
1715
+ * signal, which reaches the shell itself.
1716
+ */
1717
+ function signalProcess(child, signal) {
1718
+ if (child.pid !== undefined && detachedChildren.has(child)) {
1719
+ try {
1720
+ process.kill(-child.pid, signal);
1721
+ return;
1722
+ }
1723
+ catch {
1724
+ // Group gone or unsupported: the direct signal below still reaches the
1725
+ // shell, and a child that already exited no-ops the way ChildProcess
1726
+ // .kill always does.
1727
+ }
1728
+ }
1729
+ child.kill(signal);
1730
+ }
1731
+ /**
1732
+ * signalChild applies the cancel rule: SIGINT first, SIGTERM after the grace
1733
+ * period if the child is still alive. The child already marked killed or
1734
+ * exited is left alone. The optional graceMs overrides the 5 s constant so a
1735
+ * test can exercise the escalation without waiting. The abort watch and the
1736
+ * sudo deadline kill share this one rule, the mirror of the providers'
1737
+ * lifecycle host copy (packages/providers/src/adapters.ts, signalChild):
1738
+ * each package owns its child-process wiring, so a change to the escalation
1739
+ * rule lands in two places by design.
1740
+ */
1741
+ function signalChild(child, graceMs) {
1742
+ if (child.killed || child.exitCode !== null)
1743
+ return;
1744
+ signalProcess(child, 'SIGINT');
1745
+ const escalate = setTimeout(() => {
1746
+ // A group signal never marks ChildProcess.kill, so the guard reads the
1747
+ // exit state: a child that has neither exited nor been signalled is
1748
+ // still alive and gets SIGTERM.
1749
+ if (child.exitCode === null && child.signalCode === null)
1750
+ signalProcess(child, 'SIGTERM');
1751
+ }, graceMs ?? DOWNLOAD_ABORT_GRACE_MS);
1752
+ escalate.unref();
1753
+ }
1754
+ /**
1755
+ * watchChildAbort kills the in-flight child when the signal aborts (the card's
1756
+ * cancel): SIGINT first, SIGTERM after the grace period if the child is still
1757
+ * alive, through the shared signalChild rule. A signal that is already
1758
+ * aborted kills immediately. A near-identical copy lives in the providers
1759
+ * package (packages/providers/src/adapters.ts, the lifecycle host's run and
1760
+ * runStream abort path): each package owns its own child-process wiring, and
1761
+ * the guard fix (the SIGTERM escalation) was applied to both copies, so a
1762
+ * change to the escalation rule lands in two places by design.
1763
+ */
1764
+ function watchChildAbort(child, signal, graceMs) {
1765
+ if (signal === undefined)
1766
+ return;
1767
+ const onAbort = () => {
1768
+ signalChild(child, graceMs);
1769
+ };
1770
+ if (signal.aborted) {
1771
+ onAbort();
1772
+ return;
1773
+ }
1774
+ signal.addEventListener('abort', onAbort, { once: true });
1775
+ }
1776
+ /**
1777
+ * downloadRunner is the model-download runner: it spawns the rendered hf CLI
1778
+ * command through the system shell and reports the exit. The spawned child's
1779
+ * stdout and stderr chunks stream to onChunk as they land (the hf CLI's tqdm
1780
+ * progress), while the output tail keeps the last kilobyte, the same rule the
1781
+ * install runner uses, so a failure names the broken line. An abort on the
1782
+ * optional signal interrupts the in-flight child (SIGINT, then SIGTERM), so a
1783
+ * canceled download stops instead of running to the timeout. The optional
1784
+ * graceMs overrides the 5 s SIGTERM escalation window for the abort path.
1785
+ */
1786
+ export const downloadRunner = (command, signal, onChunk, graceMs) => new Promise(resolve => {
1787
+ const child = spawn(command, {
1788
+ shell: true,
1789
+ timeout: DOWNLOAD_TIMEOUT_MS,
1790
+ stdio: ['ignore', 'pipe', 'pipe'],
1791
+ });
1792
+ const tail = tailOutput();
1793
+ const append = (chunk) => {
1794
+ const text = chunk.toString('utf8');
1795
+ tail.append(text);
1796
+ onChunk?.(text);
1797
+ };
1798
+ child.stdout.on('data', append);
1799
+ child.stderr.on('data', append);
1800
+ // Without a listener, a child that fails to start (a missing shell) emits
1801
+ // an unlistened 'error' and throws it as an uncaughtException through the
1802
+ // host. Read it as a failed command instead.
1803
+ child.on('error', (error) => {
1804
+ resolve({ ok: false, detail: error.message });
1805
+ });
1806
+ child.on('close', (code, killSignal) => {
1807
+ if (code === 0) {
1287
1808
  resolve({ ok: true });
1288
1809
  return;
1289
1810
  }
1290
- const detail = `${stderr ?? ''}${stdout ?? ''}`.trim().slice(-INSTALL_OUTPUT_TAIL);
1291
- resolve({ ok: false, detail: detail === '' ? String(error) : detail });
1811
+ const detail = tail.tail();
1812
+ const label = code !== null ? `exit ${code}` : `killed by ${killSignal ?? 'a signal'}`;
1813
+ resolve({ ok: false, detail: detail === '' ? label : `${label}: ${detail}` });
1292
1814
  });
1815
+ watchChildAbort(child, signal, graceMs);
1293
1816
  });
1294
1817
  /**
1295
1818
  * hasSafetensorsIn reports whether one directory holds a `.safetensors`
@@ -1331,15 +1854,21 @@ async function probeGpuUsage() {
1331
1854
  return result.ok ? parseNvidiaSmiGpuUsage(result.stdout) : null;
1332
1855
  }
1333
1856
  /**
1334
- * probeProcesses reads the running process table (`ps -eo pid,args`) into
1335
- * process lines the launch-signature matcher consumes. The env element of a
1336
- * line is read from the Linux /proc environ, and only for a pid whose command
1337
- * line carries one of the engine markers: a full table of env reads is slow,
1338
- * and the matcher's env-key element only fires on a marker-matched process
1339
- * anyway. A host without /proc (macOS) degrades to env-less lines, which the
1340
- * matcher reads as a neutral env element.
1857
+ * probeProcesses reads the running process table into process lines the
1858
+ * launch-signature matcher consumes. The path switches on the host OS:
1859
+ * Windows reads the table through PowerShell's Win32_Process CIM class (the
1860
+ * pid and the full command line), and Linux and macOS read it through
1861
+ * `ps -eo pid,args`. The env element of a line is read from the Linux /proc
1862
+ * environ, and only for a pid whose command line carries one of the engine
1863
+ * markers: a full table of env reads is slow, and the matcher's env-key
1864
+ * element only fires on a marker-matched process anyway. A host without /proc
1865
+ * (macOS, Windows) degrades to env-less lines, which the matcher reads as a
1866
+ * neutral env element.
1341
1867
  */
1342
1868
  async function probeProcesses(markers) {
1869
+ if (platform() === 'win32') {
1870
+ return probeWindowsProcesses();
1871
+ }
1343
1872
  const result = await runCommand('ps', ['-eo', 'pid,args']);
1344
1873
  if (!result.ok)
1345
1874
  return [];
@@ -1362,6 +1891,51 @@ async function probeProcesses(markers) {
1362
1891
  }
1363
1892
  return lines;
1364
1893
  }
1894
+ /**
1895
+ * parseWindowsProcessTable reads the `pid<TAB>commandLine` rows PowerShell's
1896
+ * Win32_Process probe emits into process lines the launch-signature matcher
1897
+ * consumes. Each row is split on its first tab: the pid is the left field, the
1898
+ * command line the right. A row with no tab (a system process with an empty
1899
+ * command line, trimmed to its pid alone) is skipped, and a non-numeric pid is
1900
+ * dropped. The env element stays undefined: Windows has no /proc environ to
1901
+ * read, and the matcher reads a missing env as neutral.
1902
+ */
1903
+ export function parseWindowsProcessTable(stdout) {
1904
+ const lines = [];
1905
+ for (const raw of stdout.split('\n')) {
1906
+ const trimmed = raw.trim();
1907
+ if (trimmed === '')
1908
+ continue;
1909
+ const tab = trimmed.indexOf('\t');
1910
+ if (tab <= 0)
1911
+ continue;
1912
+ const pid = Number(trimmed.slice(0, tab));
1913
+ const command = trimmed.slice(tab + 1).trim();
1914
+ if (!Number.isFinite(pid) || command === '')
1915
+ continue;
1916
+ lines.push({ pid, command });
1917
+ }
1918
+ return lines;
1919
+ }
1920
+ /**
1921
+ * probeWindowsProcesses reads the running process table through PowerShell's
1922
+ * Win32_Process CIM class (the pid and the full command line), the Windows
1923
+ * binding for probeProcesses. The probe emits one `pid<TAB>commandLine` row
1924
+ * per process, which parseWindowsProcessTable turns into lines. A host without
1925
+ * the CIM service (a hung WMI) or a missing powershell degrades to an empty
1926
+ * table, which the detector reads as "no foreign engine" rather than a crash.
1927
+ */
1928
+ async function probeWindowsProcesses() {
1929
+ const result = await runCommand('powershell', [
1930
+ '-NoProfile',
1931
+ '-NonInteractive',
1932
+ '-Command',
1933
+ 'Get-CimInstance Win32_Process | ForEach-Object { "{0}`t{1}" -f $_.ProcessId, $_.CommandLine }',
1934
+ ]);
1935
+ if (!result.ok)
1936
+ return [];
1937
+ return parseWindowsProcessTable(result.stdout);
1938
+ }
1365
1939
  /**
1366
1940
  * readProcEnviron reads one process's environment out of its /proc environ
1367
1941
  * file (NUL-separated KEY=VALUE entries). Any failure (no /proc, the process
@@ -1445,12 +2019,14 @@ async function probeDocker() {
1445
2019
  return result.ok;
1446
2020
  }
1447
2021
  /**
1448
- * probePackageManager checks which of the four package-manager binaries exist
1449
- * on the host. `which` is a read-only probe, so each answer is a plain
1450
- * presence flag the pure detectPackageManager mapping turns into the winner.
2022
+ * probePackageManager checks whether a package-manager binary exists on the
2023
+ * host. The probe is `which` on Linux and macOS and `where` on Windows (the
2024
+ * OS's own presence probe), so each answer is a plain presence flag the pure
2025
+ * detectPackageManager mapping turns into the winner.
1451
2026
  */
1452
- async function probePackageManager(binary) {
1453
- const result = await runCommand('which', [binary]);
2027
+ async function probePackageManager(binary, os) {
2028
+ const probe = os === 'windows' ? 'where' : 'which';
2029
+ const result = await runCommand(probe, [binary]);
1454
2030
  return result.ok;
1455
2031
  }
1456
2032
  /**
@@ -1467,13 +2043,14 @@ export async function runHardwareProbes() {
1467
2043
  darwin: 'macos',
1468
2044
  };
1469
2045
  const os = osMap[platform()] ?? 'linux';
1470
- const [gpu, dockerAvailable, apt, dnf, pacman, brew] = await Promise.all([
2046
+ const [gpu, dockerAvailable, apt, dnf, pacman, brew, winget] = await Promise.all([
1471
2047
  probeGpu(),
1472
2048
  probeDocker(),
1473
- probePackageManager('apt-get'),
1474
- probePackageManager('dnf'),
1475
- probePackageManager('pacman'),
1476
- probePackageManager('brew'),
2049
+ probePackageManager('apt-get', os),
2050
+ probePackageManager('dnf', os),
2051
+ probePackageManager('pacman', os),
2052
+ probePackageManager('brew', os),
2053
+ probePackageManager('winget', os),
1477
2054
  ]);
1478
2055
  return {
1479
2056
  os,
@@ -1482,8 +2059,8 @@ export async function runHardwareProbes() {
1482
2059
  wsl2: os === 'linux' && release().toLowerCase().includes('wsl'),
1483
2060
  hostRamGiB: totalmem() / 2 ** 30,
1484
2061
  gpu,
1485
- packageManager: detectPackageManager({ os, apt, dnf, pacman, brew }) ?? null,
2062
+ packageManager: detectPackageManager({ os, apt, dnf, pacman, brew, winget }) ?? null,
1486
2063
  };
1487
2064
  }
1488
2065
  export default LocalInfer;
1489
- //# sourceMappingURL=index.js.map
2066
+ //# sourceMappingURL=index.js.map