dsh-plugin-local-infer 0.1.3 → 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.
- package/README.md +22 -4
- package/cordis.patch.yml +7 -0
- package/docs/zh/README.md +14 -2
- package/dsh/client.js +4195 -4522
- package/package.json +28 -11
- package/packages/core/lib/adapter.d.ts +133 -9
- package/packages/core/lib/adapter.js +74 -1
- package/packages/core/lib/config.d.ts +1 -1
- package/packages/core/lib/config.js +1 -1
- package/packages/core/lib/debug.d.ts +1 -1
- package/packages/core/lib/debug.js +1 -1
- package/packages/core/lib/defaults.d.ts +1 -1
- package/packages/core/lib/defaults.js +1 -1
- package/packages/core/lib/deps.d.ts +44 -17
- package/packages/core/lib/deps.js +24 -8
- package/packages/core/lib/estimator.d.ts +1 -1
- package/packages/core/lib/estimator.js +1 -1
- package/packages/core/lib/guideline.d.ts +1 -1
- package/packages/core/lib/guideline.js +1 -1
- package/packages/core/lib/hardware.d.ts +1 -1
- package/packages/core/lib/hardware.js +1 -1
- package/packages/core/lib/hf-cache.d.ts +70 -0
- package/packages/core/lib/hf-cache.js +83 -0
- package/packages/core/lib/index.d.ts +16 -9
- package/packages/core/lib/index.js +10 -6
- package/packages/core/lib/infer-cache.d.ts +39 -0
- package/packages/core/lib/infer-cache.js +53 -0
- package/packages/core/lib/launch-command.d.ts +8 -4
- package/packages/core/lib/launch-command.js +13 -6
- package/packages/core/lib/launch-record.d.ts +1 -1
- package/packages/core/lib/launch-record.js +1 -1
- package/packages/core/lib/logs.d.ts +8 -1
- package/packages/core/lib/logs.js +12 -4
- package/packages/core/lib/models.d.ts +120 -14
- package/packages/core/lib/models.js +209 -17
- package/packages/core/lib/parameter.d.ts +31 -3
- package/packages/core/lib/parameter.js +1 -1
- package/packages/core/lib/progress.d.ts +54 -0
- package/packages/core/lib/progress.js +120 -0
- package/packages/core/lib/registrar.d.ts +20 -9
- package/packages/core/lib/registrar.js +27 -7
- package/packages/core/lib/reserve.d.ts +11 -3
- package/packages/core/lib/reserve.js +6 -4
- package/packages/core/lib/status.d.ts +12 -6
- package/packages/core/lib/status.js +21 -10
- package/packages/core/lib/sudo.d.ts +80 -0
- package/packages/core/lib/sudo.js +78 -0
- package/packages/core/package.json +1 -1
- package/packages/orchestrator/lib/config.d.ts +63 -0
- package/packages/orchestrator/lib/config.js +111 -0
- package/packages/orchestrator/lib/index.d.ts +51 -0
- package/packages/orchestrator/lib/index.js +73 -0
- package/packages/orchestrator/lib/route.d.ts +60 -0
- package/packages/orchestrator/lib/route.js +159 -0
- package/packages/orchestrator/locale/en.json +6 -0
- package/packages/orchestrator/package.json +39 -0
- package/packages/orchestrator/prompt.md +87 -0
- package/packages/providers/lib/acquire.d.ts +92 -14
- package/packages/providers/lib/acquire.js +215 -37
- package/packages/providers/lib/adapters.d.ts +136 -12
- package/packages/providers/lib/adapters.js +597 -70
- package/packages/providers/lib/debug.d.ts +1 -1
- package/packages/providers/lib/debug.js +1 -1
- package/packages/providers/lib/deps.d.ts +27 -230
- package/packages/providers/lib/deps.js +61 -214
- package/packages/providers/lib/engines/basalt/basalt.d.ts +130 -0
- package/packages/providers/lib/engines/basalt/basalt.js +304 -0
- package/packages/providers/lib/engines/basalt/basaltAdapter.d.ts +85 -0
- package/packages/providers/lib/engines/basalt/basaltAdapter.js +439 -0
- package/packages/providers/lib/engines/basalt/basaltLinux.d.ts +23 -0
- package/packages/providers/lib/engines/basalt/basaltLinux.js +47 -0
- package/packages/providers/lib/engines/basalt/basaltMacOS.d.ts +17 -0
- package/packages/providers/lib/engines/basalt/basaltMacOS.js +19 -0
- package/packages/providers/lib/engines/basalt/basaltWindows.d.ts +24 -0
- package/packages/providers/lib/engines/basalt/basaltWindows.js +49 -0
- package/packages/providers/lib/engines/engineOs.d.ts +24 -0
- package/packages/providers/lib/engines/engineOs.js +17 -0
- package/packages/providers/lib/engines/{hyperqwen.d.ts → hyperqwen/hyperqwen.d.ts} +2 -2
- package/packages/providers/lib/engines/{hyperqwen.js → hyperqwen/hyperqwen.js} +14 -2
- package/packages/providers/lib/engines/{hyperqwenAdapter.d.ts → hyperqwen/hyperqwenAdapter.d.ts} +6 -6
- package/packages/providers/lib/engines/{hyperqwenAdapter.js → hyperqwen/hyperqwenAdapter.js} +24 -14
- package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.d.ts +10 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.js +14 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.js +9 -0
- package/packages/providers/lib/engines/{infernix.d.ts → infernix/infernix.d.ts} +7 -4
- package/packages/providers/lib/engines/{infernix.js → infernix/infernix.js} +52 -7
- package/packages/providers/lib/engines/{infernixAdapter.d.ts → infernix/infernixAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{infernixAdapter.js → infernix/infernixAdapter.js} +75 -42
- package/packages/providers/lib/engines/infernix/infernixArch.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixArch.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixDebian.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixDebian.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixFedora.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixFedora.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixLinux.d.ts +17 -0
- package/packages/providers/lib/engines/infernix/infernixLinux.js +35 -0
- package/packages/providers/lib/engines/infernix/infernixMacOS.d.ts +16 -0
- package/packages/providers/lib/engines/infernix/infernixMacOS.js +18 -0
- package/packages/providers/lib/engines/infernix/infernixWindows.d.ts +16 -0
- package/packages/providers/lib/engines/infernix/infernixWindows.js +34 -0
- package/packages/providers/lib/engines/{llamacpp.d.ts → llamacpp/llamacpp.d.ts} +2 -2
- package/packages/providers/lib/engines/{llamacpp.js → llamacpp/llamacpp.js} +12 -2
- package/packages/providers/lib/engines/{llamacppAdapter.d.ts → llamacpp/llamacppAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{llamacppAdapter.js → llamacpp/llamacppAdapter.js} +87 -45
- package/packages/providers/lib/engines/llamacpp/llamacppArch.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppArch.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppDebian.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppDebian.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppFedora.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppFedora.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppLinux.d.ts +17 -0
- package/packages/providers/lib/engines/llamacpp/llamacppLinux.js +28 -0
- package/packages/providers/lib/engines/llamacpp/llamacppMacOS.d.ts +15 -0
- package/packages/providers/lib/engines/llamacpp/llamacppMacOS.js +17 -0
- package/packages/providers/lib/engines/llamacpp/llamacppWindows.d.ts +15 -0
- package/packages/providers/lib/engines/llamacpp/llamacppWindows.js +26 -0
- package/packages/providers/lib/engines/{ninfer.d.ts → ninfer/ninfer.d.ts} +2 -2
- package/packages/providers/lib/engines/{ninfer.js → ninfer/ninfer.js} +24 -2
- package/packages/providers/lib/engines/{ninferAdapter.d.ts → ninfer/ninferAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{ninferAdapter.js → ninfer/ninferAdapter.js} +123 -55
- package/packages/providers/lib/engines/ninfer/ninferArch.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferArch.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferDebian.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferDebian.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferFedora.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferFedora.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferLinux.d.ts +17 -0
- package/packages/providers/lib/engines/ninfer/ninferLinux.js +37 -0
- package/packages/providers/lib/engines/ninfer/ninferMacOS.d.ts +15 -0
- package/packages/providers/lib/engines/ninfer/ninferMacOS.js +17 -0
- package/packages/providers/lib/engines/ninfer/ninferWindows.d.ts +29 -0
- package/packages/providers/lib/engines/ninfer/ninferWindows.js +59 -0
- package/packages/providers/lib/engines/strata/strata.d.ts +1 -0
- package/packages/providers/lib/engines/{strata.js → strata/strata.js} +70 -3
- package/packages/providers/lib/engines/{strataAdapter.d.ts → strata/strataAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{strataAdapter.js → strata/strataAdapter.js} +147 -111
- package/packages/providers/lib/engines/strata/strataArch.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataArch.js +9 -0
- package/packages/providers/lib/engines/strata/strataDebian.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataDebian.js +9 -0
- package/packages/providers/lib/engines/strata/strataFedora.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataFedora.js +9 -0
- package/packages/providers/lib/engines/strata/strataLinux.d.ts +21 -0
- package/packages/providers/lib/engines/strata/strataLinux.js +30 -0
- package/packages/providers/lib/engines/strata/strataMacOS.d.ts +16 -0
- package/packages/providers/lib/engines/strata/strataMacOS.js +17 -0
- package/packages/providers/lib/engines/strata/strataWindows.d.ts +27 -0
- package/packages/providers/lib/engines/strata/strataWindows.js +37 -0
- package/packages/providers/lib/engines.d.ts +7 -5
- package/packages/providers/lib/engines.js +21 -10
- package/packages/providers/lib/guidelines.d.ts +5 -4
- package/packages/providers/lib/guidelines.js +18 -5
- package/packages/providers/lib/index.d.ts +33 -29
- package/packages/providers/lib/index.js +45 -33
- package/packages/providers/lib/model-defaults.d.ts +5 -1
- package/packages/providers/lib/model-defaults.js +25 -1
- package/packages/providers/lib/platform.d.ts +8 -5
- package/packages/providers/lib/platform.js +23 -9
- package/packages/providers/lib/presets.d.ts +6 -4
- package/packages/providers/lib/presets.js +53 -5
- package/packages/providers/lib/systemDeps.d.ts +313 -0
- package/packages/providers/lib/systemDeps.js +284 -0
- package/packages/providers/lib/vram.d.ts +34 -8
- package/packages/providers/lib/vram.js +81 -8
- package/packages/providers/package.json +2 -2
- package/packages/service/lib/index.d.ts +176 -21
- package/packages/service/lib/index.js +589 -86
- package/packages/service/lib/launch-target.d.ts +19 -0
- package/packages/service/lib/launch-target.js +28 -0
- package/packages/service/lib/run-logs.d.ts +72 -0
- package/packages/service/lib/run-logs.js +125 -0
- package/packages/service/lib/types.d.ts +163 -6
- package/packages/service/lib/types.js +1 -1
- package/packages/service/package.json +2 -2
- package/packages/tool/lib/index.d.ts +1 -1
- package/packages/tool/lib/index.js +1 -1
- package/packages/tool/package.json +2 -2
- package/packages/ui/lib/config.d.ts +12 -2
- package/packages/ui/lib/config.js +18 -2
- package/packages/ui/lib/deps.d.ts +1 -1
- package/packages/ui/lib/deps.js +1 -1
- package/packages/ui/lib/form.d.ts +1 -1
- package/packages/ui/lib/form.js +1 -1
- package/packages/ui/lib/index.d.ts +7 -1
- package/packages/ui/lib/index.js +12 -2
- package/packages/ui/lib/meter.d.ts +1 -1
- package/packages/ui/lib/meter.js +1 -1
- package/packages/ui/lib/ops/engine.d.ts +142 -34
- package/packages/ui/lib/ops/engine.js +469 -265
- package/packages/ui/lib/ops/get-steps.d.ts +98 -0
- package/packages/ui/lib/ops/get-steps.js +359 -0
- package/packages/ui/lib/ops/hf.d.ts +26 -11
- package/packages/ui/lib/ops/hf.js +115 -25
- package/packages/ui/lib/ops/logs.d.ts +70 -15
- package/packages/ui/lib/ops/logs.js +96 -35
- package/packages/ui/lib/ops/model.d.ts +2 -10
- package/packages/ui/lib/ops/model.js +9 -16
- package/packages/ui/lib/request.d.ts +1 -1
- package/packages/ui/lib/request.js +1 -1
- package/packages/ui/lib/route.d.ts +22 -6
- package/packages/ui/lib/route.js +32 -9
- package/packages/ui/lib/routes/chat-template.d.ts +1 -14
- package/packages/ui/lib/routes/chat-template.js +1 -37
- package/packages/ui/lib/routes/deps.d.ts +1 -16
- package/packages/ui/lib/routes/deps.js +1 -27
- package/packages/ui/lib/routes/download.d.ts +1 -16
- package/packages/ui/lib/routes/download.js +1 -48
- package/packages/ui/lib/routes/get.d.ts +1 -18
- package/packages/ui/lib/routes/get.js +66 -17
- package/packages/ui/lib/routes/install.d.ts +1 -1
- package/packages/ui/lib/routes/install.js +1 -1
- package/packages/ui/lib/routes/launch.d.ts +13 -7
- package/packages/ui/lib/routes/launch.js +33 -9
- package/packages/ui/lib/routes/logs.d.ts +57 -0
- package/packages/ui/lib/routes/logs.js +117 -0
- package/packages/ui/lib/routes/provider-delete.d.ts +1 -1
- package/packages/ui/lib/routes/provider-delete.js +1 -1
- package/packages/ui/lib/routes/register.d.ts +1 -1
- package/packages/ui/lib/routes/register.js +1 -1
- package/packages/ui/lib/routes/save.d.ts +1 -1
- package/packages/ui/lib/routes/save.js +22 -2
- package/packages/ui/lib/routes/stop.d.ts +7 -4
- package/packages/ui/lib/routes/stop.js +7 -4
- package/packages/ui/lib/routes/stream.d.ts +40 -0
- package/packages/ui/lib/routes/stream.js +68 -0
- package/packages/ui/lib/routes/summary.d.ts +1 -1
- package/packages/ui/lib/routes/summary.js +1 -1
- package/packages/ui/lib/routes/unregister.d.ts +1 -1
- package/packages/ui/lib/routes/unregister.js +1 -1
- package/packages/ui/lib/viewmodels/deps.d.ts +1 -31
- package/packages/ui/lib/viewmodels/deps.js +1 -89
- package/packages/ui/lib/viewmodels/depsHelpers.d.ts +1 -28
- package/packages/ui/lib/viewmodels/depsHelpers.js +72 -11
- package/packages/ui/lib/viewmodels/summary.d.ts +7 -2
- package/packages/ui/lib/viewmodels/summary.js +55 -35
- package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +1 -1
- package/packages/ui/lib/viewmodels/summaryHelpers.js +1 -1
- package/packages/ui/lib/warnings.d.ts +1 -1
- package/packages/ui/lib/warnings.js +1 -1
- package/packages/ui/package.json +4 -4
- package/packages/providers/lib/engines/strata.d.ts +0 -53
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* mod-search bridge.
|
|
18
18
|
* @module dsh-plugin-local-infer/service
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
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.
|
|
81
|
-
//
|
|
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
|
|
@@ -113,6 +123,18 @@ export class LocalInfer extends Service {
|
|
|
113
123
|
// provider entry's cleared state; a headless profile registers none and the
|
|
114
124
|
// service still settles its own live state.
|
|
115
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();
|
|
116
138
|
constructor(ctx) {
|
|
117
139
|
super(ctx, 'localInfer');
|
|
118
140
|
}
|
|
@@ -391,14 +413,16 @@ export class LocalInfer extends Service {
|
|
|
391
413
|
return statuses;
|
|
392
414
|
}
|
|
393
415
|
/**
|
|
394
|
-
* unsureBannerKillCommands returns the
|
|
416
|
+
* unsureBannerKillCommands returns the stop commands for the engine
|
|
395
417
|
* processes the detector saw holding VRAM, for the "unsure" banner the card
|
|
396
418
|
* shows when no provider is confirmed running but at least one reads as
|
|
397
419
|
* unsure. A process is included when its command line carries an engine
|
|
398
420
|
* binary marker and the card's VRAM usage is above 30% of the total (the
|
|
399
421
|
* threshold below which a foreign engine is not considered to be holding
|
|
400
|
-
* the card). The
|
|
401
|
-
*
|
|
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
|
|
402
426
|
* banner's text area renders empty.
|
|
403
427
|
*/
|
|
404
428
|
async unsureBannerKillCommands(options) {
|
|
@@ -418,10 +442,13 @@ export class LocalInfer extends Service {
|
|
|
418
442
|
gpuUsage.usedMiB / gpuUsage.totalMiB < 0.3) {
|
|
419
443
|
return [];
|
|
420
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';
|
|
421
448
|
const commands = [];
|
|
422
449
|
for (const line of processes) {
|
|
423
450
|
if (markers.some((marker) => line.command.includes(marker))) {
|
|
424
|
-
commands.push(`kill ${line.pid}`);
|
|
451
|
+
commands.push(isWindows ? `Stop-Process -Id ${line.pid}` : `kill ${line.pid}`);
|
|
425
452
|
}
|
|
426
453
|
}
|
|
427
454
|
return commands;
|
|
@@ -451,14 +478,27 @@ export class LocalInfer extends Service {
|
|
|
451
478
|
* deps resolves the host deps an engine's deploy step needs for a build
|
|
452
479
|
* mode. A registered adapter that declares systemDeps wins; the provider-
|
|
453
480
|
* registered plan for the engine id is the fallback, and an engine with
|
|
454
|
-
* 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.
|
|
455
486
|
*/
|
|
456
487
|
async deps(engineId, buildMode, hw) {
|
|
457
488
|
const adapter = this.engineRegistry.get(engineId);
|
|
458
489
|
if (adapter !== undefined && typeof adapter.systemDeps === 'function') {
|
|
459
490
|
return adapter.systemDeps(buildMode, hw);
|
|
460
491
|
}
|
|
461
|
-
|
|
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] ?? [];
|
|
462
502
|
}
|
|
463
503
|
/**
|
|
464
504
|
* installString renders the copy/paste block for the resolved dep plan,
|
|
@@ -471,12 +511,29 @@ export class LocalInfer extends Service {
|
|
|
471
511
|
}
|
|
472
512
|
/**
|
|
473
513
|
* runInstall is the Play/Run path: it runs the resolved dep plan's commands
|
|
474
|
-
* through the shell runner and re-probes each dep. The runner
|
|
475
|
-
* 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.
|
|
476
523
|
*/
|
|
477
|
-
async runInstall(engineId, buildMode, hw) {
|
|
524
|
+
async runInstall(engineId, buildMode, hw, options) {
|
|
478
525
|
const deps = await this.deps(engineId, buildMode, hw);
|
|
479
|
-
|
|
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);
|
|
480
537
|
}
|
|
481
538
|
/**
|
|
482
539
|
* launch is the lifecycle entry point: it resolves the adapter that rendered
|
|
@@ -501,11 +558,14 @@ export class LocalInfer extends Service {
|
|
|
501
558
|
// whether the git sync (fetch, rebase, rebuild) runs at all.
|
|
502
559
|
// exactOptionalPropertyTypes: only pass the option object when a key is
|
|
503
560
|
// present.
|
|
504
|
-
const hasDeployOptions = options?.allowRunDeps !== undefined ||
|
|
561
|
+
const hasDeployOptions = options?.allowRunDeps !== undefined ||
|
|
562
|
+
options?.autoRebuild !== undefined ||
|
|
563
|
+
options?.onLog !== undefined;
|
|
505
564
|
const deployOptions = hasDeployOptions
|
|
506
565
|
? {
|
|
507
566
|
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
508
567
|
...(options?.autoRebuild !== undefined ? { autoRebuild: options.autoRebuild } : {}),
|
|
568
|
+
...(options?.onLog !== undefined ? { onLog: options.onLog } : {}),
|
|
509
569
|
}
|
|
510
570
|
: undefined;
|
|
511
571
|
// The in-flight launch flag: the provider-status detector keeps this
|
|
@@ -665,6 +725,86 @@ export class LocalInfer extends Service {
|
|
|
665
725
|
setEngineDeathObserver(observer) {
|
|
666
726
|
this.deathObserver = observer;
|
|
667
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
|
+
}
|
|
668
808
|
/**
|
|
669
809
|
* watchLaunchedHandle ties a launched child's one-shot exit to the live
|
|
670
810
|
* state: the host's close/error event (a crash, a pkill, or the stop kill)
|
|
@@ -782,9 +922,12 @@ export class LocalInfer extends Service {
|
|
|
782
922
|
/**
|
|
783
923
|
* stop signals the engine a launch started: it resolves the adapter by the
|
|
784
924
|
* plan's engineId and runs the adapter's stop step (kill the tracked process,
|
|
785
|
-
* 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.
|
|
786
929
|
*/
|
|
787
|
-
async stop(plan) {
|
|
930
|
+
async stop(plan, log) {
|
|
788
931
|
const adapter = this.engineRegistry.get(plan.engineId);
|
|
789
932
|
if (adapter === undefined) {
|
|
790
933
|
throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
|
|
@@ -794,8 +937,18 @@ export class LocalInfer extends Service {
|
|
|
794
937
|
// flashes the "Unable to link" banner for the moment the process takes to
|
|
795
938
|
// die. The flag clears when the process dies (the death observer) or the
|
|
796
939
|
// detector sees the process down (a stop of an engine it cannot watch).
|
|
797
|
-
this.
|
|
798
|
-
|
|
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
|
+
}
|
|
799
952
|
// Clear the live-port ledger and the plugin-launched flag, and cancel any
|
|
800
953
|
// adopted-process watcher standing on the port. The port-to-model tie is
|
|
801
954
|
// deferred to the death observer, so the provider keeps its linked read
|
|
@@ -910,7 +1063,10 @@ export class LocalInfer extends Service {
|
|
|
910
1063
|
preset: undefined,
|
|
911
1064
|
stored: false,
|
|
912
1065
|
degraded: true,
|
|
913
|
-
|
|
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`,
|
|
914
1070
|
};
|
|
915
1071
|
}
|
|
916
1072
|
const preset = presetFromFetch(source, content, todayIso());
|
|
@@ -919,13 +1075,26 @@ export class LocalInfer extends Service {
|
|
|
919
1075
|
return { preset, stored: true, degraded: false };
|
|
920
1076
|
}
|
|
921
1077
|
/**
|
|
922
|
-
* defaultModelDirectory is the plugin's model download directory:
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
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.
|
|
926
1084
|
*/
|
|
927
1085
|
defaultModelDirectory() {
|
|
928
|
-
const directory =
|
|
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();
|
|
929
1098
|
mkdirSync(directory, { recursive: true });
|
|
930
1099
|
return directory;
|
|
931
1100
|
}
|
|
@@ -938,7 +1107,10 @@ export class LocalInfer extends Service {
|
|
|
938
1107
|
const injected = options?.cliPath;
|
|
939
1108
|
if (injected !== undefined && injected.trim() !== '')
|
|
940
1109
|
return injected;
|
|
941
|
-
|
|
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']);
|
|
942
1114
|
if (which.ok && which.stdout.trim() !== '')
|
|
943
1115
|
return which.stdout.trim();
|
|
944
1116
|
return null;
|
|
@@ -947,7 +1119,10 @@ export class LocalInfer extends Service {
|
|
|
947
1119
|
* downloadModel runs the hf CLI download for one default model into the
|
|
948
1120
|
* model directory: it creates the target directory when absent (the
|
|
949
1121
|
* recursive flag covers the parents too), renders the command through the
|
|
950
|
-
* core builder (the single source the card shows),
|
|
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
|
|
951
1126
|
* injected runner when one is given, so a test captures the command without
|
|
952
1127
|
* a live hf. A failure keeps the runner's detail, so the card names the
|
|
953
1128
|
* broken step.
|
|
@@ -955,7 +1130,9 @@ export class LocalInfer extends Service {
|
|
|
955
1130
|
async downloadModel(entry, directory, options) {
|
|
956
1131
|
mkdirSync(directory, { recursive: true });
|
|
957
1132
|
const runner = options?.runner ?? downloadRunner;
|
|
958
|
-
const
|
|
1133
|
+
const command = hfDownloadCommand(entry, directory, options?.useHubCache === true);
|
|
1134
|
+
options?.onLog?.(commandLogLine(command));
|
|
1135
|
+
const result = await runner(command, options?.signal, options?.onChunk);
|
|
959
1136
|
if (result.ok)
|
|
960
1137
|
return { ok: true };
|
|
961
1138
|
return { ok: false, detail: `model download failed: ${result.detail ?? 'unknown'}` };
|
|
@@ -964,13 +1141,16 @@ export class LocalInfer extends Service {
|
|
|
964
1141
|
* downloadChatTemplate pulls the chat template file for one HF repo
|
|
965
1142
|
* reference into the model directory, so a selected template (the Froggeric
|
|
966
1143
|
* set, a custom repo) works out of the box. It shares the model download's
|
|
967
|
-
* recursive directory creation, runner,
|
|
968
|
-
* small file, but the hf CLI's cold start and auth
|
|
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.
|
|
969
1147
|
*/
|
|
970
1148
|
async downloadChatTemplate(repo, directory, options) {
|
|
971
1149
|
mkdirSync(directory, { recursive: true });
|
|
972
1150
|
const runner = options?.runner ?? downloadRunner;
|
|
973
|
-
const
|
|
1151
|
+
const command = hfChatTemplateCommand(repo, directory, options?.useHubCache === true);
|
|
1152
|
+
options?.onLog?.(commandLogLine(command));
|
|
1153
|
+
const result = await runner(command, options?.signal, options?.onChunk);
|
|
974
1154
|
if (result.ok)
|
|
975
1155
|
return { ok: true };
|
|
976
1156
|
return { ok: false, detail: `chat template download failed: ${result.detail ?? 'unknown'}` };
|
|
@@ -1020,6 +1200,71 @@ export class LocalInfer extends Service {
|
|
|
1020
1200
|
}
|
|
1021
1201
|
return entries;
|
|
1022
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
|
+
}
|
|
1023
1268
|
/**
|
|
1024
1269
|
* specsForEngine reads the parameter surface of the registered adapter for
|
|
1025
1270
|
* an engine id, so the defaults resolver floors the result on that engine's
|
|
@@ -1044,7 +1289,9 @@ export class LocalInfer extends Service {
|
|
|
1044
1289
|
const env = process.env.MODSEARCH_CLI;
|
|
1045
1290
|
if (env !== undefined && env.trim() !== '')
|
|
1046
1291
|
return env;
|
|
1047
|
-
|
|
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']);
|
|
1048
1295
|
if (which.ok && which.stdout.trim() !== '')
|
|
1049
1296
|
return which.stdout.trim();
|
|
1050
1297
|
return null;
|
|
@@ -1057,7 +1304,7 @@ export class LocalInfer extends Service {
|
|
|
1057
1304
|
*/
|
|
1058
1305
|
fetchPage(url, cliPath) {
|
|
1059
1306
|
return new Promise((resolve, reject) => {
|
|
1060
|
-
const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline',
|
|
1307
|
+
const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline', MODSEARCH_DEADLINE_ARG], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1061
1308
|
let stdout = '';
|
|
1062
1309
|
let stderr = '';
|
|
1063
1310
|
child.stdout.on('data', (chunk) => {
|
|
@@ -1313,36 +1560,132 @@ function runCommand(cmd, args) {
|
|
|
1313
1560
|
*/
|
|
1314
1561
|
const INSTALL_OUTPUT_TAIL = 1024;
|
|
1315
1562
|
/**
|
|
1316
|
-
*
|
|
1317
|
-
* the
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
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.
|
|
1320
1567
|
*/
|
|
1321
|
-
const
|
|
1322
|
-
|
|
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() {
|
|
1323
1577
|
let output = '';
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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),
|
|
1329
1586
|
};
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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 }),
|
|
1337
1618
|
});
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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;
|
|
1342
1683
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1684
|
+
finally {
|
|
1685
|
+
guard?.disarm();
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1346
1689
|
/**
|
|
1347
1690
|
* The download timeout. A model download moves tens of GiB, so the cap is
|
|
1348
1691
|
* hours, not the probe's seconds; a hung download must not kill the panel,
|
|
@@ -1350,20 +1693,126 @@ const shellRunCommand = (command) => new Promise(resolve => {
|
|
|
1350
1693
|
*/
|
|
1351
1694
|
const DOWNLOAD_TIMEOUT_MS = 7_200_000;
|
|
1352
1695
|
/**
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
|
|
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.
|
|
1699
|
+
*/
|
|
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.
|
|
1357
1707
|
*/
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
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) {
|
|
1361
1808
|
resolve({ ok: true });
|
|
1362
1809
|
return;
|
|
1363
1810
|
}
|
|
1364
|
-
const detail =
|
|
1365
|
-
|
|
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}` });
|
|
1366
1814
|
});
|
|
1815
|
+
watchChildAbort(child, signal, graceMs);
|
|
1367
1816
|
});
|
|
1368
1817
|
/**
|
|
1369
1818
|
* hasSafetensorsIn reports whether one directory holds a `.safetensors`
|
|
@@ -1405,15 +1854,21 @@ async function probeGpuUsage() {
|
|
|
1405
1854
|
return result.ok ? parseNvidiaSmiGpuUsage(result.stdout) : null;
|
|
1406
1855
|
}
|
|
1407
1856
|
/**
|
|
1408
|
-
* probeProcesses reads the running process table
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
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.
|
|
1415
1867
|
*/
|
|
1416
1868
|
async function probeProcesses(markers) {
|
|
1869
|
+
if (platform() === 'win32') {
|
|
1870
|
+
return probeWindowsProcesses();
|
|
1871
|
+
}
|
|
1417
1872
|
const result = await runCommand('ps', ['-eo', 'pid,args']);
|
|
1418
1873
|
if (!result.ok)
|
|
1419
1874
|
return [];
|
|
@@ -1436,6 +1891,51 @@ async function probeProcesses(markers) {
|
|
|
1436
1891
|
}
|
|
1437
1892
|
return lines;
|
|
1438
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
|
+
}
|
|
1439
1939
|
/**
|
|
1440
1940
|
* readProcEnviron reads one process's environment out of its /proc environ
|
|
1441
1941
|
* file (NUL-separated KEY=VALUE entries). Any failure (no /proc, the process
|
|
@@ -1519,12 +2019,14 @@ async function probeDocker() {
|
|
|
1519
2019
|
return result.ok;
|
|
1520
2020
|
}
|
|
1521
2021
|
/**
|
|
1522
|
-
* probePackageManager checks
|
|
1523
|
-
*
|
|
1524
|
-
* presence
|
|
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.
|
|
1525
2026
|
*/
|
|
1526
|
-
async function probePackageManager(binary) {
|
|
1527
|
-
const
|
|
2027
|
+
async function probePackageManager(binary, os) {
|
|
2028
|
+
const probe = os === 'windows' ? 'where' : 'which';
|
|
2029
|
+
const result = await runCommand(probe, [binary]);
|
|
1528
2030
|
return result.ok;
|
|
1529
2031
|
}
|
|
1530
2032
|
/**
|
|
@@ -1541,13 +2043,14 @@ export async function runHardwareProbes() {
|
|
|
1541
2043
|
darwin: 'macos',
|
|
1542
2044
|
};
|
|
1543
2045
|
const os = osMap[platform()] ?? 'linux';
|
|
1544
|
-
const [gpu, dockerAvailable, apt, dnf, pacman, brew] = await Promise.all([
|
|
2046
|
+
const [gpu, dockerAvailable, apt, dnf, pacman, brew, winget] = await Promise.all([
|
|
1545
2047
|
probeGpu(),
|
|
1546
2048
|
probeDocker(),
|
|
1547
|
-
probePackageManager('apt-get'),
|
|
1548
|
-
probePackageManager('dnf'),
|
|
1549
|
-
probePackageManager('pacman'),
|
|
1550
|
-
probePackageManager('brew'),
|
|
2049
|
+
probePackageManager('apt-get', os),
|
|
2050
|
+
probePackageManager('dnf', os),
|
|
2051
|
+
probePackageManager('pacman', os),
|
|
2052
|
+
probePackageManager('brew', os),
|
|
2053
|
+
probePackageManager('winget', os),
|
|
1551
2054
|
]);
|
|
1552
2055
|
return {
|
|
1553
2056
|
os,
|
|
@@ -1556,8 +2059,8 @@ export async function runHardwareProbes() {
|
|
|
1556
2059
|
wsl2: os === 'linux' && release().toLowerCase().includes('wsl'),
|
|
1557
2060
|
hostRamGiB: totalmem() / 2 ** 30,
|
|
1558
2061
|
gpu,
|
|
1559
|
-
packageManager: detectPackageManager({ os, apt, dnf, pacman, brew }) ?? null,
|
|
2062
|
+
packageManager: detectPackageManager({ os, apt, dnf, pacman, brew, winget }) ?? null,
|
|
1560
2063
|
};
|
|
1561
2064
|
}
|
|
1562
2065
|
export default LocalInfer;
|
|
1563
|
-
//# sourceMappingURL=index.js.map
|
|
2066
|
+
//# sourceMappingURL=index.js.map
|