dsh-plugin-local-infer 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +10 -2
  2. package/docs/zh/README.md +10 -2
  3. package/dsh/client.js +669 -191
  4. package/package.json +6 -1
  5. package/packages/core/lib/adapter.d.ts +44 -2
  6. package/packages/core/lib/deps.d.ts +4 -3
  7. package/packages/core/lib/index.d.ts +1 -1
  8. package/packages/core/lib/registrar.d.ts +5 -5
  9. package/packages/core/lib/registrar.js +3 -3
  10. package/packages/core/package.json +1 -1
  11. package/packages/providers/lib/acquire.d.ts +5 -3
  12. package/packages/providers/lib/acquire.js +30 -9
  13. package/packages/providers/lib/adapters.d.ts +19 -209
  14. package/packages/providers/lib/adapters.js +13 -886
  15. package/packages/providers/lib/deps.d.ts +24 -2
  16. package/packages/providers/lib/deps.js +23 -4
  17. package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
  18. package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
  19. package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
  20. package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
  21. package/packages/providers/lib/engines/infernixAdapter.js +202 -0
  22. package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
  23. package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
  24. package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
  25. package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
  26. package/packages/providers/lib/engines/ninferAdapter.js +324 -0
  27. package/packages/providers/lib/engines/strata.d.ts +53 -0
  28. package/packages/providers/lib/engines/strata.js +197 -0
  29. package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
  30. package/packages/providers/lib/engines/strataAdapter.js +427 -0
  31. package/packages/providers/lib/engines.d.ts +5 -4
  32. package/packages/providers/lib/engines.js +10 -10
  33. package/packages/providers/lib/guidelines.js +1 -1
  34. package/packages/providers/lib/index.d.ts +14 -9
  35. package/packages/providers/lib/index.js +17 -7
  36. package/packages/providers/lib/model-defaults.d.ts +5 -4
  37. package/packages/providers/lib/model-defaults.js +22 -4
  38. package/packages/providers/lib/platform.js +7 -1
  39. package/packages/providers/package.json +1 -1
  40. package/packages/service/lib/index.d.ts +13 -4
  41. package/packages/service/lib/index.js +79 -5
  42. package/packages/service/lib/types.d.ts +5 -4
  43. package/packages/service/package.json +1 -1
  44. package/packages/tool/package.json +1 -1
  45. package/packages/ui/lib/config.d.ts +79 -0
  46. package/packages/ui/lib/config.js +235 -0
  47. package/packages/ui/lib/deps.d.ts +3 -3
  48. package/packages/ui/lib/ops/engine.d.ts +108 -0
  49. package/packages/ui/lib/ops/engine.js +493 -0
  50. package/packages/ui/lib/ops/hf.d.ts +27 -0
  51. package/packages/ui/lib/ops/hf.js +60 -0
  52. package/packages/ui/lib/ops/logs.d.ts +23 -0
  53. package/packages/ui/lib/ops/logs.js +58 -0
  54. package/packages/ui/lib/ops/model.d.ts +79 -0
  55. package/packages/ui/lib/ops/model.js +130 -0
  56. package/packages/ui/lib/request.d.ts +106 -0
  57. package/packages/ui/lib/request.js +80 -0
  58. package/packages/ui/lib/route.d.ts +88 -420
  59. package/packages/ui/lib/route.js +111 -1843
  60. package/packages/ui/lib/routes/chat-template.d.ts +14 -0
  61. package/packages/ui/lib/routes/chat-template.js +37 -0
  62. package/packages/ui/lib/routes/deps.d.ts +16 -0
  63. package/packages/ui/lib/routes/deps.js +27 -0
  64. package/packages/ui/lib/routes/download.d.ts +16 -0
  65. package/packages/ui/lib/routes/download.js +48 -0
  66. package/packages/ui/lib/routes/get.d.ts +18 -0
  67. package/packages/ui/lib/routes/get.js +57 -0
  68. package/packages/ui/lib/routes/install.d.ts +16 -0
  69. package/packages/ui/lib/routes/install.js +36 -0
  70. package/packages/ui/lib/routes/launch.d.ts +18 -0
  71. package/packages/ui/lib/routes/launch.js +50 -0
  72. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  73. package/packages/ui/lib/routes/provider-delete.js +37 -0
  74. package/packages/ui/lib/routes/register.d.ts +16 -0
  75. package/packages/ui/lib/routes/register.js +28 -0
  76. package/packages/ui/lib/routes/save.d.ts +18 -0
  77. package/packages/ui/lib/routes/save.js +139 -0
  78. package/packages/ui/lib/routes/stop.d.ts +16 -0
  79. package/packages/ui/lib/routes/stop.js +29 -0
  80. package/packages/ui/lib/routes/summary.d.ts +15 -0
  81. package/packages/ui/lib/routes/summary.js +26 -0
  82. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  83. package/packages/ui/lib/routes/unregister.js +26 -0
  84. package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
  85. package/packages/ui/lib/viewmodels/deps.js +89 -0
  86. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
  87. package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
  88. package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
  89. package/packages/ui/lib/viewmodels/summary.js +225 -0
  90. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  91. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  92. package/packages/ui/package.json +1 -1
  93. package/packages/providers/lib/strata.d.ts +0 -15
  94. package/packages/providers/lib/strata.js +0 -14
  95. /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
  96. /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
  97. /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
  98. /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
  99. /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
  100. /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The engine lifecycle operations: launchEngine renders the plan the
3
+ * registered adapter produces and runs the service's launch entry point
4
+ * (deploy, then the health poll); stopEngine stops the exact container or
5
+ * process the launch started; getEngine runs the Get step (git sync, dep
6
+ * plan, build, model artifacts). The launch record (the engine pid, start
7
+ * time, launch arguments, and the DSH process pid that launched it) is what
8
+ * a DSH restart re-links a surviving engine to its config:
9
+ * relinkStoredProviders re-adopts surviving engines on mount and per summary
10
+ * read, handleEngineDeath settles the stored state the moment an engine dies
11
+ * outside the stop path, and persistLaunchLogs stores the captured output.
12
+ * The platform-config registry sync keeps the service registry in step with
13
+ * the stored config.
14
+ * @module dsh-plugin-local-infer/ui/ops/engine
15
+ */
16
+ import type { Config, LaunchStep, LifecycleHost } from 'dsh-plugin-local-infer/core';
17
+ import type { EngineDeathEvent, LaunchResult, LocalInferService } from 'dsh-plugin-local-infer/service';
18
+ import type { StoredConfig, StoredProviderConfig } from '../config.ts';
19
+ import type { GetRequest, LaunchRequest } from '../request.ts';
20
+ /**
21
+ * resolveAdapter picks the registered engine adapter an engine id routes to. A
22
+ * card that names an engine no adapter serves gets a 404, not a blank panel.
23
+ */
24
+ export declare function resolveAdapter(localInfer: LocalInferService, engineId: string): import("dsh-plugin-local-infer/core").EngineAdapter;
25
+ /**
26
+ * launchEngine renders the plan the registered adapter produces for the config
27
+ * and the detected hardware, then runs the service's launch entry point
28
+ * (deploy, then the health poll). The detail the launch returns names the
29
+ * broken step when the engine does not come up. The launch line (the engine
30
+ * binary and its arguments) leads the run logs the card's Logs section and
31
+ * the log file carry, so a broken setup diagnoses against the exact child
32
+ * process the launch started. The launch step persists the
33
+ * launch record (the engine pid, start time, launch arguments, and the DSH
34
+ * process pid that launched it) on the request's provider entry, so a DSH
35
+ * restart can re-link a surviving engine to its config.
36
+ */
37
+ export declare function launchEngine(localInfer: LocalInferService, request: LaunchRequest, onProgress?: (step: LaunchStep) => void): Promise<LaunchResult>;
38
+ /**
39
+ * handleEngineDeath persists a provider entry's state when its engine process
40
+ * dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
41
+ * child seen through the service's exit watch, or the poll-detected exit of a
42
+ * process adopted after a DSH restart. The entry whose launch record names the
43
+ * dead pid (or the dead engine-and-port, when the report carries no pid match)
44
+ * has its record cleared, so a restart never re-links a dead pid, and the exit
45
+ * line lands in the card's last-run logs beside the engine's own output. An
46
+ * event that matches no entry carrying a record (a stop the route already
47
+ * cleared, an engine the plugin never tracked) is a no-op, which keeps an
48
+ * expected stop quiet.
49
+ */
50
+ export declare function handleEngineDeath(event: EngineDeathEvent): void;
51
+ /**
52
+ * persistLaunchLogs stores a launch's captured output as the last run and
53
+ * writes it to the configured log file. The log file is only written when the
54
+ * run produced output and the log location is set; the last-run logs are always
55
+ * persisted (an empty string clears a stale run) so the card's Logs section
56
+ * shows the previous run after a reload.
57
+ */
58
+ export declare function persistLaunchLogs(result: LaunchResult, engineId: string, config: Config): void;
59
+ /**
60
+ * stopEngine renders the same plan and runs the service's stop entry point, so
61
+ * the stop step targets the exact container or process the launch started. The
62
+ * stop then clears the target entry's launch record (the engine pid and the
63
+ * previous-pid list with it), so a recycled pid cannot re-link an engine the
64
+ * user just stopped.
65
+ */
66
+ export declare function stopEngine(localInfer: LocalInferService, request: LaunchRequest): Promise<void>;
67
+ /**
68
+ * relinkStoredProviders prices every stored provider launch record against the
69
+ * live process table through the service's re-link rule and returns the
70
+ * provider list the card should read: an adopted engine is re-marked plugin-
71
+ * launched and tied to its model by the service, and a liveness watcher starts
72
+ * on its port so a later pkill settles the stored state through the death
73
+ * observer without another summary read; a dead engine's record is cleared on
74
+ * disk here (the previous-pid list with it), so a recycled pid cannot re-link
75
+ * to a config whose engine is down. The route calls it on mount and the
76
+ * summary calls it per read, making re-attach independent of the card opening.
77
+ */
78
+ export declare function relinkStoredProviders(localInfer: LocalInferService, stored: StoredConfig): Promise<StoredProviderConfig[]>;
79
+ /**
80
+ * GetOutcome is the answer the Get step reports: ok with no detail, or a
81
+ * failure naming the broken step. The object literal union keeps the
82
+ * invariant the card reads: a detail only exists on a failure.
83
+ */
84
+ export type GetOutcome = {
85
+ ok: true;
86
+ } | {
87
+ ok: false;
88
+ detail?: string;
89
+ };
90
+ /**
91
+ * getEngine is the Get step: it syncs the engine checkout (clone the git
92
+ * repo, or the fork when the platform entry provides one; fetch and rebase
93
+ * a clean checkout; abort with a Logs alert when the checkout has local
94
+ * changes), runs the system dependency plan, and runs the engine's build
95
+ * steps. Every command and its output streams through log as it lands, so
96
+ * the card appends it to the Logs section live. The dependency install only
97
+ * runs while the Play/Run trust gate is on; with the gate off the deps are
98
+ * re-checked and a missing one aborts the step with the copy/paste note.
99
+ */
100
+ export declare function getEngine(localInfer: LocalInferService, request: GetRequest, log: (chunk: string) => void, host?: LifecycleHost): Promise<GetOutcome>;
101
+ /**
102
+ * syncPlatformConfigs re-syncs the service registry with the stored platform
103
+ * map: it registers (or re-registers) every engine that carries a config, and
104
+ * unregisters every engine that no longer does, so a launch always reads the
105
+ * platform config the user last saved.
106
+ */
107
+ export declare function syncPlatformConfigs(localInfer: LocalInferService, stored: StoredConfig): void;
108
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1,493 @@
1
+ /**
2
+ * The engine lifecycle operations: launchEngine renders the plan the
3
+ * registered adapter produces and runs the service's launch entry point
4
+ * (deploy, then the health poll); stopEngine stops the exact container or
5
+ * process the launch started; getEngine runs the Get step (git sync, dep
6
+ * plan, build, model artifacts). The launch record (the engine pid, start
7
+ * time, launch arguments, and the DSH process pid that launched it) is what
8
+ * a DSH restart re-links a surviving engine to its config:
9
+ * relinkStoredProviders re-adopts surviving engines on mount and per summary
10
+ * read, handleEngineDeath settles the stored state the moment an engine dies
11
+ * outside the stop path, and persistLaunchLogs stores the captured output.
12
+ * The platform-config registry sync keeps the service registry in step with
13
+ * the stored config.
14
+ * @module dsh-plugin-local-infer/ui/ops/engine
15
+ */
16
+ import { mkdirSync, writeFileSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { dirname, join } from 'node:path';
19
+ import { checkDeps, formatLaunchCommand, launchRecordFromPlan, resolveLogPath, } from '../../../core/lib/index.js';
20
+ import { buildEngineRepo, ENGINES, infernixGetDownloads, nodeLifecycleHost, syncEngineRepo, } from '../../../providers/lib/index.js';
21
+ import { readConfig, writeConfig } from "../config.js";
22
+ import { RouteError } from "../route.js";
23
+ import { resolveBuildMode } from "../viewmodels/deps.js";
24
+ import { resolveModelDirectory } from "./model.js";
25
+ /**
26
+ * resolveAdapter picks the registered engine adapter an engine id routes to. A
27
+ * card that names an engine no adapter serves gets a 404, not a blank panel.
28
+ */
29
+ export function resolveAdapter(localInfer, engineId) {
30
+ const adapter = localInfer.registeredEngines().find((engine) => engine.id === engineId);
31
+ if (adapter === undefined) {
32
+ throw new RouteError(404, `no engine adapter is registered for "${engineId}"`);
33
+ }
34
+ return adapter;
35
+ }
36
+ /**
37
+ * launchCommandNote renders the plan's child-process launch line for the
38
+ * card's Logs section: the core formatter's command under a [local-infer]
39
+ * launch prefix, newline-terminated. A plan that names no command (a bare
40
+ * docker plan with no argv) has no note, and the run logs stay as they are.
41
+ */
42
+ function launchCommandNote(plan) {
43
+ const command = formatLaunchCommand(plan);
44
+ if (command === undefined)
45
+ return undefined;
46
+ return `[local-infer] launch: ${command}\n`;
47
+ }
48
+ /**
49
+ * applyComposeArgs seeds a compose plan's compose arguments from the stored
50
+ * platform config (the card's docker-tag field for a compose engine, e.g.
51
+ * `--profile single`), so the up line the deploy runs and the down line the
52
+ * stop runs both select the profile the user named. A compose plan with no
53
+ * stored docker-tag keeps the engine's default profile; a non-compose plan is
54
+ * untouched.
55
+ */
56
+ function applyComposeArgs(plan, localInfer, engineId) {
57
+ if (plan.kind !== 'compose')
58
+ return;
59
+ const platform = localInfer.enginePlatformConfig(engineId);
60
+ if (platform?.dockerTag !== undefined) {
61
+ plan.composeArgs = platform.dockerTag;
62
+ }
63
+ }
64
+ /**
65
+ * persistStoredLogs overwrites the stored last-run logs (the card's Logs
66
+ * section shows them after a reload) and leaves every other stored field as
67
+ * the launch reads it.
68
+ */
69
+ function persistStoredLogs(logs) {
70
+ const stored = readConfig();
71
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
72
+ }
73
+ /**
74
+ * launchEngine renders the plan the registered adapter produces for the config
75
+ * and the detected hardware, then runs the service's launch entry point
76
+ * (deploy, then the health poll). The detail the launch returns names the
77
+ * broken step when the engine does not come up. The launch line (the engine
78
+ * binary and its arguments) leads the run logs the card's Logs section and
79
+ * the log file carry, so a broken setup diagnoses against the exact child
80
+ * process the launch started. The launch step persists the
81
+ * launch record (the engine pid, start time, launch arguments, and the DSH
82
+ * process pid that launched it) on the request's provider entry, so a DSH
83
+ * restart can re-link a surviving engine to its config.
84
+ */
85
+ export async function launchEngine(localInfer, request, onProgress) {
86
+ const stored = readConfig();
87
+ const engineId = request.engine ?? stored.engine;
88
+ const config = request.config ?? stored.config;
89
+ const adapter = resolveAdapter(localInfer, engineId);
90
+ const hardware = await localInfer.detectHardware();
91
+ // Register the request's platform override (the card's unsaved Engine
92
+ // Directory / Engine Fork edits) so the service's launch resolves it against
93
+ // the adapter's deploy step. An absent platform keeps the stored entry,
94
+ // which the save route and route registration already synced.
95
+ if (request.platform !== undefined) {
96
+ setPlatformConfig(localInfer, engineId, request.platform);
97
+ }
98
+ const plan = adapter.renderLaunch(config, hardware);
99
+ applyComposeArgs(plan, localInfer, engineId);
100
+ const commandNote = launchCommandNote(plan);
101
+ if (commandNote !== undefined) {
102
+ // Persist before the deploy starts: a slow or hung launch keeps the
103
+ // command readable in the Logs section across a card reload, where the
104
+ // engine output has not landed yet.
105
+ persistStoredLogs(commandNote);
106
+ }
107
+ // The prereqs phase: the system-dependency install the local build needs. It
108
+ // runs only while the Play/Run trust gate is on, the same rule the Get step
109
+ // applies; a failed install aborts the launch before the deploy starts. The
110
+ // card's button reads "Prereqs..." for this phase, so the phase is reported
111
+ // only while an install actually runs (the gate on), not when the deps are
112
+ // already satisfied and the phase is skipped.
113
+ if (stored.allowRunDeps === true) {
114
+ onProgress?.('prereqs');
115
+ // Price the dep plan for the build mode the config names (docker, local,
116
+ // or the Strata binary install), not just the plan kind, so a binary
117
+ // launch installs its own (thinner) dep list.
118
+ const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
119
+ const buildMode = resolveBuildMode(engine, config);
120
+ const install = await localInfer.runInstall(engineId, buildMode, hardware);
121
+ if (!install.ok) {
122
+ const result = {
123
+ plan,
124
+ healthy: false,
125
+ detail: 'a system dependency install failed',
126
+ };
127
+ persistLaunchLogs(result, engineId, config);
128
+ persistLaunchRecord(result, engineId, config, request.providerId);
129
+ return result;
130
+ }
131
+ }
132
+ // The trust gate and the auto-rebuild flag ride into the deploy step, which
133
+ // reads them to decide whether a rebuild's missing-binary symlink into
134
+ // /usr/local/bin runs (on) or is written to the card's Logs section (off),
135
+ // and whether the git sync (fetch, rebase, rebuild) runs at all.
136
+ const result = await localInfer.launch(plan, {
137
+ ...(onProgress !== undefined ? { onProgress } : {}),
138
+ allowRunDeps: stored.allowRunDeps,
139
+ ...(request.autoRebuild !== undefined ? { autoRebuild: request.autoRebuild } : {}),
140
+ });
141
+ // Order the run logs: the build output (the rebuild decision, the build
142
+ // command, and its output) leads, the launch command follows it (after a
143
+ // successful build, or a direct launch with no build), and the engine's
144
+ // runtime output comes last. A deploy that failed before the engine printed
145
+ // anything still reports the command it tried to run.
146
+ const buildLogs = result.buildLogs ?? '';
147
+ const engineLogs = result.logs ?? '';
148
+ result.logs = buildLogs + (commandNote ?? '') + engineLogs;
149
+ if (result.buildLogs !== undefined) {
150
+ delete result.buildLogs;
151
+ }
152
+ persistLaunchLogs(result, engineId, config);
153
+ persistLaunchRecord(result, engineId, config, request.providerId);
154
+ return result;
155
+ }
156
+ /**
157
+ * findLaunchTarget resolves the stored provider entry a launch or stop
158
+ * targets: the entry the request names by id, or (an id the card does not
159
+ * send, a launch of the active config) the stored entry with the same engine
160
+ * and model. A launch with no resolvable entry persists no record, which
161
+ * reads as "no provider config to re-link" after a restart.
162
+ */
163
+ function findLaunchTarget(stored, engineId, providerId, config) {
164
+ if (providerId !== undefined) {
165
+ const byId = stored.providers.find((entry) => entry.id === providerId);
166
+ if (byId !== undefined)
167
+ return byId;
168
+ }
169
+ const model = typeof config.model === 'string' ? config.model : undefined;
170
+ if (model === undefined)
171
+ return undefined;
172
+ return stored.providers.find((entry) => entry.engine === engineId &&
173
+ (typeof entry.config.model === 'string' ? entry.config.model : undefined) === model);
174
+ }
175
+ /**
176
+ * persistLaunchRecord stores a launch's record on the target provider entry:
177
+ * the engine pid the deploy step started, the start time, the plan's argv,
178
+ * the DSH process pid the launch ran under (only when it identified as DSH),
179
+ * and the previous-pid list the re-link rule prices (the entry's own list
180
+ * plus the current DSH pid, capped by the core builder). A launch whose
181
+ * deploy started no tracked process (a compose up) persists nothing, and a
182
+ * launch with no resolvable entry persists nothing.
183
+ */
184
+ function persistLaunchRecord(result, engineId, config, providerId) {
185
+ if (result.pid === undefined)
186
+ return;
187
+ const stored = readConfig();
188
+ const target = findLaunchTarget(stored, engineId, providerId, config);
189
+ if (target === undefined)
190
+ return;
191
+ const record = launchRecordFromPlan(result.plan, result.pid, result.parentPid, target.launch?.previousPids ?? []);
192
+ const providers = stored.providers.map((entry) => entry.id === target.id ? { ...entry, launch: record } : entry);
193
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
194
+ }
195
+ /**
196
+ * clearLaunchRecord removes the target provider entry's launch record (the
197
+ * engine pid and the previous-pid list with it), so a stopped engine's pid
198
+ * cannot be re-linked to its config by a recycled pid. A stop with no
199
+ * resolvable entry, or an entry that carries no record, is a no-op.
200
+ */
201
+ function clearLaunchRecord(engineId, config, providerId) {
202
+ const stored = readConfig();
203
+ const target = findLaunchTarget(stored, engineId, providerId, config);
204
+ if (target === undefined || target.launch === undefined)
205
+ return;
206
+ const providers = stored.providers.map((entry) => {
207
+ if (entry.id !== target.id)
208
+ return entry;
209
+ // Rebuild the entry without the launch key, so the record (including the
210
+ // previous-pid list) is gone from the file, not just ignored.
211
+ return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
212
+ });
213
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
214
+ }
215
+ /**
216
+ * handleEngineDeath persists a provider entry's state when its engine process
217
+ * dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
218
+ * child seen through the service's exit watch, or the poll-detected exit of a
219
+ * process adopted after a DSH restart. The entry whose launch record names the
220
+ * dead pid (or the dead engine-and-port, when the report carries no pid match)
221
+ * has its record cleared, so a restart never re-links a dead pid, and the exit
222
+ * line lands in the card's last-run logs beside the engine's own output. An
223
+ * event that matches no entry carrying a record (a stop the route already
224
+ * cleared, an engine the plugin never tracked) is a no-op, which keeps an
225
+ * expected stop quiet.
226
+ */
227
+ export function handleEngineDeath(event) {
228
+ const stored = readConfig();
229
+ let matched = false;
230
+ const providers = stored.providers.map((entry) => {
231
+ const record = entry.launch;
232
+ if (record === undefined)
233
+ return entry;
234
+ const byPid = event.pid !== undefined && record.pid === event.pid;
235
+ const byEnginePort = event.port !== undefined && record.engineId === entry.engine && record.port === event.port;
236
+ if (!byPid && !byEnginePort)
237
+ return entry;
238
+ matched = true;
239
+ // Rebuild the entry without the launch key, so the record (including the
240
+ // previous-pid list) is gone from the file, not just ignored.
241
+ return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
242
+ });
243
+ if (!matched)
244
+ return;
245
+ const signalNote = event.signal !== null && event.signal !== undefined
246
+ ? ` on signal ${event.signal}`
247
+ : ` with exit code ${event.code}`;
248
+ const note = `[local-infer] engine ${event.engineId}` +
249
+ `${event.port !== undefined ? ` (port ${event.port})` : ''}` +
250
+ ` pid ${event.pid ?? 'unknown'} exited${signalNote}` +
251
+ `${event.detail !== undefined ? `: ${event.detail}` : ''}\n`;
252
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath,
253
+ // Keep the last-run tail bounded, the same rule the launch logs cap uses.
254
+ (stored.lastLogs + note).slice(-65536));
255
+ }
256
+ /**
257
+ * persistLaunchLogs stores a launch's captured output as the last run and
258
+ * writes it to the configured log file. The log file is only written when the
259
+ * run produced output and the log location is set; the last-run logs are always
260
+ * persisted (an empty string clears a stale run) so the card's Logs section
261
+ * shows the previous run after a reload.
262
+ */
263
+ export function persistLaunchLogs(result, engineId, config) {
264
+ const stored = readConfig();
265
+ // The "Launching..." separator leads the persisted run, so the Logs
266
+ // section and the log file read the launch as a fresh process section, the
267
+ // same line the card writes when a Launch button is clicked.
268
+ const logs = result.logs === undefined || result.logs === '' ? '' : `--- Launching...\n${result.logs}`;
269
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
270
+ if (logs === '') {
271
+ return;
272
+ }
273
+ const modelId = typeof config.model === 'string' ? config.model : undefined;
274
+ const logPath = resolveLogPath(stored.logPath, engineId, modelId, new Date(), homedir());
275
+ try {
276
+ mkdirSync(dirname(logPath), { recursive: true });
277
+ writeFileSync(logPath, logs);
278
+ }
279
+ catch {
280
+ // A log file that cannot be written (a read-only dir, a bad path) must not
281
+ // fail the launch; the in-memory last-run logs still reach the card.
282
+ }
283
+ }
284
+ /**
285
+ * stopEngine renders the same plan and runs the service's stop entry point, so
286
+ * the stop step targets the exact container or process the launch started. The
287
+ * stop then clears the target entry's launch record (the engine pid and the
288
+ * previous-pid list with it), so a recycled pid cannot re-link an engine the
289
+ * user just stopped.
290
+ */
291
+ export async function stopEngine(localInfer, request) {
292
+ const stored = readConfig();
293
+ const engineId = request.engine ?? stored.engine;
294
+ const config = request.config ?? stored.config;
295
+ const adapter = resolveAdapter(localInfer, engineId);
296
+ const hardware = await localInfer.detectHardware();
297
+ const plan = adapter.renderLaunch(config, hardware);
298
+ applyComposeArgs(plan, localInfer, engineId);
299
+ await localInfer.stop(plan);
300
+ clearLaunchRecord(engineId, config, request.providerId);
301
+ }
302
+ /**
303
+ * relinkStoredProviders prices every stored provider launch record against the
304
+ * live process table through the service's re-link rule and returns the
305
+ * provider list the card should read: an adopted engine is re-marked plugin-
306
+ * launched and tied to its model by the service, and a liveness watcher starts
307
+ * on its port so a later pkill settles the stored state through the death
308
+ * observer without another summary read; a dead engine's record is cleared on
309
+ * disk here (the previous-pid list with it), so a recycled pid cannot re-link
310
+ * to a config whose engine is down. The route calls it on mount and the
311
+ * summary calls it per read, making re-attach independent of the card opening.
312
+ */
313
+ export async function relinkStoredProviders(localInfer, stored) {
314
+ const recorded = stored.providers.filter((provider) => provider.launch !== undefined);
315
+ if (recorded.length === 0)
316
+ return stored.providers;
317
+ const outcomes = await localInfer.reLinkProviders(recorded.map((provider) => ({ id: provider.id, record: provider.launch })));
318
+ if (!recorded.some((provider) => outcomes[provider.id] === 'dead')) {
319
+ return stored.providers;
320
+ }
321
+ const providers = stored.providers.map((provider) => {
322
+ if (provider.launch === undefined || outcomes[provider.id] !== 'dead') {
323
+ return provider;
324
+ }
325
+ // Rebuild the dead entry without the launch key, so the record (including
326
+ // the previous-pid list) is gone from the file, not just ignored.
327
+ return {
328
+ id: provider.id,
329
+ engine: provider.engine,
330
+ config: provider.config,
331
+ platform: provider.platform,
332
+ };
333
+ });
334
+ writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
335
+ return providers;
336
+ }
337
+ /**
338
+ * resolveEngineDirectory picks the checkout the Get step syncs: the Engine
339
+ * Directory override when it names a path, else a per-engine folder inside
340
+ * the model directory (the default model directory, or the config's
341
+ * override), so a Get with no override still has a real place to live.
342
+ */
343
+ function resolveEngineDirectory(localInfer, config, engineId, platform) {
344
+ const override = platform?.engineDirectory;
345
+ if (override !== undefined && override.trim() !== '') {
346
+ return override;
347
+ }
348
+ const rawModelDir = config.modelDirectory;
349
+ const modelDir = typeof rawModelDir === 'string' && rawModelDir.trim() !== ''
350
+ ? rawModelDir
351
+ : localInfer.defaultModelDirectory();
352
+ return join(modelDir, engineId);
353
+ }
354
+ /**
355
+ * getEngine is the Get step: it syncs the engine checkout (clone the git
356
+ * repo, or the fork when the platform entry provides one; fetch and rebase
357
+ * a clean checkout; abort with a Logs alert when the checkout has local
358
+ * changes), runs the system dependency plan, and runs the engine's build
359
+ * steps. Every command and its output streams through log as it lands, so
360
+ * the card appends it to the Logs section live. The dependency install only
361
+ * runs while the Play/Run trust gate is on; with the gate off the deps are
362
+ * re-checked and a missing one aborts the step with the copy/paste note.
363
+ */
364
+ export async function getEngine(localInfer, request, log, host = nodeLifecycleHost) {
365
+ const stored = readConfig();
366
+ const engineId = request.engine ?? stored.engine;
367
+ const config = request.config ?? stored.config;
368
+ // ENGINES is a known non-empty constant, so the fallback is always defined.
369
+ const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
370
+ // Register the request's platform override the same way launchEngine does,
371
+ // so the next launch reads the Engine Directory / Engine Fork the Get used.
372
+ if (request.platform !== undefined) {
373
+ setPlatformConfig(localInfer, engineId, request.platform);
374
+ }
375
+ const platform = request.platform ?? stored.platform[engineId];
376
+ const directory = resolveEngineDirectory(localInfer, config, engineId, platform);
377
+ const fork = platform?.engineFork;
378
+ log(`get ${engineId} in ${directory}\n`);
379
+ const sync = await syncEngineRepo(host, { engineId, directory, ...(fork !== undefined && fork.trim() !== '' ? { fork } : {}) }, log);
380
+ if (!sync.ok) {
381
+ // A dirty checkout already alerted in the sync step; any other failure
382
+ // names its step here.
383
+ if (sync.aborted !== true && sync.detail !== undefined) {
384
+ log(`!! ${sync.detail}\n`);
385
+ }
386
+ return { ok: false, ...(sync.detail !== undefined ? { detail: sync.detail } : {}) };
387
+ }
388
+ const hardware = await localInfer.detectHardware();
389
+ const buildMode = resolveBuildMode(engine, config);
390
+ log(`system dependencies (${buildMode})\n`);
391
+ if (stored.allowRunDeps === true) {
392
+ const result = await localInfer.runInstall(engineId, buildMode, hardware);
393
+ for (const entry of result.results) {
394
+ log(` ${entry.id}: ${entry.status}${entry.detail !== undefined ? ` (${entry.detail})` : ''}\n`);
395
+ }
396
+ if (!result.ok) {
397
+ log('!! a system dependency install failed; copy the install string from the panel\n');
398
+ return { ok: false, detail: 'a system dependency install failed' };
399
+ }
400
+ }
401
+ else {
402
+ const deps = await localInfer.deps(engineId, buildMode, hardware);
403
+ const checks = await checkDeps(deps, hardware);
404
+ let missing = false;
405
+ for (const check of checks) {
406
+ log(` ${check.dep.id}: ${check.status}\n`);
407
+ if (check.status !== 'satisfied')
408
+ missing = true;
409
+ }
410
+ if (missing) {
411
+ log('!! a system dependency is missing and one-click install is off; copy the install string from the panel\n');
412
+ return { ok: false, detail: 'a system dependency is missing' };
413
+ }
414
+ }
415
+ // The user's local build command (the card's Build commands field) overrides
416
+ // the engine's default build steps; absent runs the engine's acquisition
417
+ // steps.
418
+ const build = await buildEngineRepo(host, engineId, directory, log, platform?.buildCommands);
419
+ if (!build.ok) {
420
+ if (build.detail !== undefined) {
421
+ log(`!! ${build.detail}\n`);
422
+ }
423
+ return { ok: false, ...(build.detail !== undefined ? { detail: build.detail } : {}) };
424
+ }
425
+ // The model artifacts a Get leaves ready to launch: the Infernix DFlash2
426
+ // drafter (when a preset is selected) and the PLE n-gram volume (when the
427
+ // model key names one), pulled through the hf CLI into the model directory.
428
+ // A download failure aborts the Get, so a launch never reads a missing
429
+ // drafter or table.
430
+ if (engineId === 'infernix') {
431
+ const modelDir = resolveModelDirectory(localInfer, config);
432
+ for (const command of infernixGetDownloads(config, modelDir)) {
433
+ log(`$ ${command}\n`);
434
+ const outcome = host.runStream !== undefined
435
+ ? await host.runStream(command, (chunk) => log(chunk))
436
+ : await host.run(command);
437
+ if (!outcome.ok) {
438
+ log(`!! ${command} failed: ${outcome.detail ?? 'unknown'}\n`);
439
+ return { ok: false, detail: 'a model artifact download failed' };
440
+ }
441
+ }
442
+ }
443
+ log('get complete\n');
444
+ return { ok: true };
445
+ }
446
+ /**
447
+ * The route's view of which engine platform configs are registered with the
448
+ * service, keyed by engine id. The service registry is the single source the
449
+ * launch step reads, so the route keeps it in sync with the stored config:
450
+ * on route registration, after every save, and when a launch request carries
451
+ * a platform override. A disposer is tracked per engine, so an engine whose
452
+ * stored config is cleared is unregistered rather than left stale.
453
+ */
454
+ const platformDisposers = new Map();
455
+ /**
456
+ * setPlatformConfig registers one engine's platform config with the service,
457
+ * replacing any stored entry. A config with no fields is read as "no platform
458
+ * config", which unregisters the engine so the deploy step falls back to the
459
+ * adapter's default source and cache directory.
460
+ */
461
+ function setPlatformConfig(localInfer, engineId, config) {
462
+ platformDisposers.get(engineId)?.();
463
+ platformDisposers.delete(engineId);
464
+ if (config === undefined) {
465
+ return;
466
+ }
467
+ if (config.engineDirectory === undefined &&
468
+ config.engineFork === undefined &&
469
+ config.buildCommands === undefined &&
470
+ config.dockerTag === undefined) {
471
+ return;
472
+ }
473
+ const platform = { engineId, ...config };
474
+ platformDisposers.set(engineId, localInfer.registerEnginePlatformConfig(platform));
475
+ }
476
+ /**
477
+ * syncPlatformConfigs re-syncs the service registry with the stored platform
478
+ * map: it registers (or re-registers) every engine that carries a config, and
479
+ * unregisters every engine that no longer does, so a launch always reads the
480
+ * platform config the user last saved.
481
+ */
482
+ export function syncPlatformConfigs(localInfer, stored) {
483
+ const wanted = stored.platform;
484
+ for (const engineId of [...platformDisposers.keys()]) {
485
+ if (wanted[engineId] === undefined) {
486
+ setPlatformConfig(localInfer, engineId, undefined);
487
+ }
488
+ }
489
+ for (const [engineId, config] of Object.entries(wanted)) {
490
+ setPlatformConfig(localInfer, engineId, config);
491
+ }
492
+ }
493
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The hf CLI download operations: downloadFor runs the download for a default
3
+ * model, and chatTemplateDownloadFor pulls a selected chat template into the
4
+ * model directory. Both resolve the directory against the request, the
5
+ * stored config, and the plugin's default model directory in that order.
6
+ * @module dsh-plugin-local-infer/ui/ops/hf
7
+ */
8
+ import type { DownloadResult, LocalInferService } from 'dsh-plugin-local-infer/service';
9
+ import type { ChatTemplateDownloadRequest, DownloadRequest } from '../request.ts';
10
+ /**
11
+ * downloadFor runs the hf CLI download for the request's default model: it
12
+ * resolves the entry through the providers' default list (a 404 when no
13
+ * entry names the pair), resolves the directory against the request, the
14
+ * stored config, and the plugin's default model directory in that order, and
15
+ * runs the download through the service.
16
+ */
17
+ export declare function downloadFor(localInfer: LocalInferService, request: DownloadRequest): Promise<DownloadResult>;
18
+ /**
19
+ * chatTemplateDownloadFor runs the hf CLI download for the request's chat
20
+ * template: it resolves the template repo reference from the request or the
21
+ * stored config's chatTemplate (a 400 when neither names an HF repo
22
+ * reference), resolves the directory against the request, the stored config,
23
+ * and the plugin's default model directory in that order, and runs the
24
+ * download through the service.
25
+ */
26
+ export declare function chatTemplateDownloadFor(localInfer: LocalInferService, request: ChatTemplateDownloadRequest): Promise<DownloadResult>;
27
+ //# sourceMappingURL=hf.d.ts.map