@dsh-workbench/adapter-probe 0.0.0-stage → 1.0.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-workbench contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @dsh-workbench/adapter-probe
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 工作台的启动体检与运行时适配器。
4
+
5
+ ## 职责
6
+
7
+ - preflight / doctor / browser wake
8
+ - opencli 与 pnpm 的隔离安装命令
9
+ - runtime seed、toolchain、版本范围
10
+ - self-update(依赖 runtime seed 与工具链)
11
+
12
+ ## 入口
13
+
14
+ ```ts
15
+ import { createPreflight, probeUnits, startSelfUpdate } from '@dsh-workbench/adapter-probe';
16
+ ```
17
+
18
+ ## 边界
19
+
20
+ 这是唯一拥有 `runtime-seed` / `toolchain` / `platform` 的适配器;其他包不得复制这三份实现。
21
+
22
+ ## 验证
23
+
24
+ ```bash
25
+ pnpm --filter @dsh-workbench/adapter-probe run build
26
+ pnpm --filter @dsh-workbench/adapter-probe run test
27
+ ```
@@ -0,0 +1,21 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import type { WakeBrowserReply } from '@dsh-workbench/shared/contract/protocol/preflight-wake';
3
+ export interface DesktopBrowser {
4
+ observe(): Promise<void>;
5
+ wake(): Promise<WakeBrowserReply>;
6
+ }
7
+ export declare function createDesktopBrowser(options: {
8
+ shell: DshShell;
9
+ command: string;
10
+ stateFile: string;
11
+ platform?: string;
12
+ home?: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ logger?: {
15
+ warn?(message: string): void;
16
+ } | undefined;
17
+ exists?: (path: string) => boolean;
18
+ waitMs?: number;
19
+ shortWaitMs?: number;
20
+ pollMs?: number;
21
+ }): DesktopBrowser;
@@ -0,0 +1,263 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, win32 } from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { quoteShellArg, runShell } from '@dsh-workbench/shared/host';
6
+ const BROWSER_IDS = ['chrome', 'edge', 'brave', 'chromium', 'vivaldi'];
7
+ const BROWSERS = [
8
+ {
9
+ id: 'chrome',
10
+ label: 'Google Chrome',
11
+ macProcess: 'Google Chrome',
12
+ linuxBinary: '/usr/bin/google-chrome',
13
+ linuxProcess: 'chrome',
14
+ },
15
+ {
16
+ id: 'edge',
17
+ label: 'Microsoft Edge',
18
+ macProcess: 'Microsoft Edge',
19
+ linuxBinary: '/usr/bin/microsoft-edge',
20
+ linuxProcess: 'msedge',
21
+ },
22
+ {
23
+ id: 'brave',
24
+ label: 'Brave Browser',
25
+ macProcess: 'Brave Browser',
26
+ linuxBinary: '/usr/bin/brave-browser',
27
+ linuxProcess: 'brave',
28
+ },
29
+ {
30
+ id: 'chromium',
31
+ label: 'Chromium',
32
+ macProcess: 'Chromium',
33
+ linuxBinary: '/usr/bin/chromium',
34
+ linuxProcess: 'chromium',
35
+ },
36
+ {
37
+ id: 'vivaldi',
38
+ label: 'Vivaldi',
39
+ macProcess: 'Vivaldi',
40
+ linuxBinary: '/usr/bin/vivaldi',
41
+ linuxProcess: 'vivaldi-bin',
42
+ },
43
+ ];
44
+ const WAKE_WAIT_MS = 12_000;
45
+ const SHORT_WAIT_MS = 3_000;
46
+ const POLL_MS = 500;
47
+ const STATUS_TIMEOUT_MS = 5_000;
48
+ const LAUNCH_TIMEOUT_MS = 10_000;
49
+ function isBrowserId(value) {
50
+ return typeof value === 'string' && BROWSER_IDS.includes(value);
51
+ }
52
+ function specOf(id) {
53
+ return BROWSERS.find((item) => item.id === id) ?? BROWSERS[0];
54
+ }
55
+ export function createDesktopBrowser(options) {
56
+ const platform = options.platform ?? process.platform;
57
+ const home = options.home ?? homedir();
58
+ const env = options.env ?? process.env;
59
+ const exists = options.exists ?? existsSync;
60
+ const waitMs = options.waitMs ?? WAKE_WAIT_MS;
61
+ const shortWaitMs = options.shortWaitMs ?? SHORT_WAIT_MS;
62
+ const pollMs = options.pollMs ?? POLL_MS;
63
+ const command = options.command;
64
+ async function run(line, timeoutMs) {
65
+ try {
66
+ const result = await runShell(options.shell, line, { timeoutMs });
67
+ return { ok: result.exitCode === 0 && !result.timedOut, stdout: result.stdout };
68
+ }
69
+ catch {
70
+ return { ok: false, stdout: '' };
71
+ }
72
+ }
73
+ function winCandidates(id) {
74
+ const programFiles = env.ProgramFiles ?? env.PROGRAMFILES ?? 'C:\\Program Files';
75
+ const programFilesX86 = env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
76
+ const localAppData = env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
77
+ const relative = {
78
+ chrome: ['Google', 'Chrome', 'Application', 'chrome.exe'],
79
+ edge: ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
80
+ brave: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
81
+ chromium: ['Chromium', 'Application', 'chrome.exe'],
82
+ vivaldi: ['Vivaldi', 'Application', 'vivaldi.exe'],
83
+ };
84
+ const bases = id === 'edge' || id === 'chrome' || id === 'brave'
85
+ ? [programFiles, programFilesX86, localAppData]
86
+ : [localAppData, programFiles];
87
+ return bases.map((base) => win32.join(base, ...relative[id]));
88
+ }
89
+ function winExecutable(id) {
90
+ return winCandidates(id).find((path) => exists(path));
91
+ }
92
+ function installed() {
93
+ return BROWSERS.filter((spec) => {
94
+ if (platform === 'darwin') {
95
+ return (exists(`/Applications/${spec.label}.app`) ||
96
+ exists(join(home, 'Applications', `${spec.label}.app`)));
97
+ }
98
+ if (platform === 'linux')
99
+ return exists(spec.linuxBinary);
100
+ if (platform === 'win32')
101
+ return winExecutable(spec.id) !== undefined;
102
+ return false;
103
+ }).map((spec) => spec.id);
104
+ }
105
+ function readSaved() {
106
+ try {
107
+ const raw = JSON.parse(readFileSync(options.stateFile, 'utf8'));
108
+ if (typeof raw !== 'object' || raw === null)
109
+ return undefined;
110
+ const value = raw.browser;
111
+ return isBrowserId(value) ? value : undefined;
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ function save(id) {
118
+ try {
119
+ mkdirSync(dirname(options.stateFile), { recursive: true });
120
+ writeFileSync(options.stateFile, `${JSON.stringify({ browser: id }, null, 2)}\n`, 'utf8');
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ }
126
+ async function running() {
127
+ if (platform === 'darwin' || platform === 'linux') {
128
+ const names = BROWSERS.map((spec) => quoteShellArg(platform === 'darwin' ? spec.macProcess : spec.linuxProcess));
129
+ const result = await run(`pgrep -lx ${names.join(' ')}`, STATUS_TIMEOUT_MS);
130
+ const found = [];
131
+ for (const line of result.stdout.split('\n')) {
132
+ const match = /^\s*\d+\s+(.+?)\s*$/.exec(line);
133
+ const name = match?.[1];
134
+ if (name === undefined)
135
+ continue;
136
+ const spec = BROWSERS.find((item) => item.macProcess === name || item.linuxProcess === name);
137
+ if (spec !== undefined && !found.includes(spec.id))
138
+ found.push(spec.id);
139
+ }
140
+ return found;
141
+ }
142
+ if (platform === 'win32') {
143
+ const result = await run('tasklist /FO CSV /NH', STATUS_TIMEOUT_MS);
144
+ const found = [];
145
+ for (const line of result.stdout.split('\n')) {
146
+ const name = /^"([^"]+)"/.exec(line)?.[1]?.toLowerCase();
147
+ if (name === 'chrome.exe')
148
+ found.push('chrome');
149
+ if (name === 'msedge.exe')
150
+ found.push('edge');
151
+ if (name === 'brave.exe')
152
+ found.push('brave');
153
+ if (name === 'chromium.exe')
154
+ found.push('chromium');
155
+ if (name === 'vivaldi.exe')
156
+ found.push('vivaldi');
157
+ }
158
+ return [...new Set(found)];
159
+ }
160
+ return [];
161
+ }
162
+ async function daemonStatus() {
163
+ const result = await run(`${command} daemon status`, STATUS_TIMEOUT_MS);
164
+ if (!result.ok)
165
+ return 'unknown';
166
+ if (/^Daemon: not running/m.test(result.stdout))
167
+ return 'down';
168
+ if (/^Extension: connected/m.test(result.stdout))
169
+ return 'connected';
170
+ if (/^Extension:/m.test(result.stdout))
171
+ return 'disconnected';
172
+ return 'unknown';
173
+ }
174
+ async function waitConnected(ms) {
175
+ const deadline = Date.now() + ms;
176
+ for (;;) {
177
+ if ((await daemonStatus()) === 'connected')
178
+ return true;
179
+ if (Date.now() >= deadline)
180
+ return false;
181
+ await delay(Math.min(pollMs, Math.max(0, deadline - Date.now())));
182
+ }
183
+ }
184
+ function pickTarget(now) {
185
+ const have = installed();
186
+ if (have.length === 0)
187
+ return undefined;
188
+ const saved = readSaved();
189
+ if (saved !== undefined && have.includes(saved))
190
+ return saved;
191
+ const awake = have.filter((id) => now.includes(id));
192
+ if (awake.length === 1)
193
+ return awake[0];
194
+ return have[0];
195
+ }
196
+ function appPath(id) {
197
+ const label = specOf(id).label;
198
+ const local = `/Applications/${label}.app`;
199
+ return exists(local) ? local : join(home, 'Applications', `${label}.app`);
200
+ }
201
+ async function launch(id) {
202
+ if (platform === 'darwin') {
203
+ const result = await run(`open -g -j -a ${quoteShellArg(appPath(id))}`, LAUNCH_TIMEOUT_MS);
204
+ return result.ok;
205
+ }
206
+ if (platform === 'linux') {
207
+ const binary = quoteShellArg(specOf(id).linuxBinary);
208
+ const result = await run(`setsid ${binary} --no-startup-window >/dev/null 2>&1 < /dev/null &`, LAUNCH_TIMEOUT_MS);
209
+ return result.ok;
210
+ }
211
+ if (platform === 'win32') {
212
+ const exe = winExecutable(id);
213
+ if (exe === undefined)
214
+ return false;
215
+ const result = await run(`cmd /d /s /c start "" ${quoteShellArg(exe)}`, LAUNCH_TIMEOUT_MS);
216
+ return result.ok;
217
+ }
218
+ return false;
219
+ }
220
+ async function observe() {
221
+ const now = await running();
222
+ if (now.length === 0)
223
+ return;
224
+ const saved = readSaved();
225
+ if (saved !== undefined && now.includes(saved))
226
+ return;
227
+ if (now.length === 1) {
228
+ save(now[0]);
229
+ return;
230
+ }
231
+ if (now.includes('chrome'))
232
+ save('chrome');
233
+ }
234
+ async function wake() {
235
+ if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32')
236
+ return { status: 'unsupported' };
237
+ const before = await daemonStatus();
238
+ if (before === 'connected') {
239
+ await observe();
240
+ return { status: 'connected' };
241
+ }
242
+ if (before !== 'disconnected')
243
+ return { status: 'unknown' };
244
+ const now = await running();
245
+ const target = pickTarget(now);
246
+ if (target === undefined)
247
+ return { status: 'no-browser' };
248
+ if (now.includes(target)) {
249
+ if (await waitConnected(shortWaitMs))
250
+ return { status: 'connected', browser: target };
251
+ return { status: 'browser-open', browser: target };
252
+ }
253
+ if (!(await launch(target)))
254
+ return { status: 'launch-failed', browser: target };
255
+ if (await waitConnected(waitMs)) {
256
+ save(target);
257
+ options.logger?.warn?.(`[dsh-workbench] Desktop:已打开 ${specOf(target).label}`);
258
+ return { status: 'connected', browser: target };
259
+ }
260
+ return { status: 'not-connected', browser: target };
261
+ }
262
+ return { observe, wake };
263
+ }
@@ -0,0 +1,13 @@
1
+ import type { DshAgentPresetRow, DshHostContext } from '@dsh-workbench/shared';
2
+ export declare const DIY_UPGRADE_COMMAND = "npm i -g @deepseek-ai/dsh@latest";
3
+ export interface DiyPresetVerdict {
4
+ readonly ok: boolean;
5
+ readonly rows: readonly string[];
6
+ readonly detail: string;
7
+ }
8
+ export declare function brokenRowsOf(detail: string): readonly string[];
9
+ export declare function diyPresetVerdictOf(presets: readonly DshAgentPresetRow[]): DiyPresetVerdict | undefined;
10
+ export declare function createDiyPresetProbe(ctx: DshHostContext, options?: {
11
+ deadlineMs?: number;
12
+ everyMs?: number;
13
+ }): () => Promise<DiyPresetVerdict | undefined>;
@@ -0,0 +1,45 @@
1
+ import { DIY_PRESET_ID } from '@dsh-workbench/shared/contract/protocol/diy';
2
+ export const DIY_UPGRADE_COMMAND = 'npm i -g @deepseek-ai/dsh@latest';
3
+ export function brokenRowsOf(detail) {
4
+ const rows = [];
5
+ for (const line of String(detail ?? '').split('\n')) {
6
+ const text = line.trim();
7
+ if (text === '')
8
+ continue;
9
+ const at = text.indexOf(' (');
10
+ rows.push(at < 0 ? text : text.slice(0, at));
11
+ }
12
+ return rows;
13
+ }
14
+ export function diyPresetVerdictOf(presets) {
15
+ if (!Array.isArray(presets) || presets.length === 0)
16
+ return undefined;
17
+ const mine = presets.find((row) => row?.id === DIY_PRESET_ID);
18
+ if (mine === undefined)
19
+ return { ok: false, rows: [], detail: `preset 名单里没有 ${DIY_PRESET_ID}` };
20
+ const broken = typeof mine.broken === 'string' ? mine.broken.trim() : '';
21
+ if (broken === '')
22
+ return { ok: true, rows: [], detail: '' };
23
+ return { ok: false, rows: brokenRowsOf(broken), detail: broken };
24
+ }
25
+ export function createDiyPresetProbe(ctx, options = {}) {
26
+ const deadlineMs = options.deadlineMs ?? 15_000;
27
+ const everyMs = options.everyMs ?? 400;
28
+ return async () => {
29
+ const registry = ctx.get('agentPresets');
30
+ if (registry === undefined || typeof registry.list !== 'function')
31
+ return undefined;
32
+ const until = Date.now() + deadlineMs;
33
+ for (;;) {
34
+ const rows = await registry.list().catch(() => undefined);
35
+ if (rows === undefined)
36
+ return undefined;
37
+ const verdict = diyPresetVerdictOf(rows);
38
+ if (verdict !== undefined)
39
+ return verdict;
40
+ if (Date.now() >= until)
41
+ return undefined;
42
+ await new Promise((resolve) => setTimeout(resolve, everyMs));
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,14 @@
1
+ export * from './browser-wake.js';
2
+ export * from './diy-preset-health.js';
3
+ export * from './platform.js';
4
+ export * from './preflight-route.js';
5
+ export * from './preflight.js';
6
+ export * from './probe-cache.js';
7
+ export * from './probe-libs.js';
8
+ export * from './probe-units.js';
9
+ export * from './probe-verdict.js';
10
+ export * from './runtime-seed.js';
11
+ export * from './toolchain.js';
12
+ export * from './version-range.js';
13
+ export * from './install.js';
14
+ export * from './self-update.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export * from './browser-wake.js';
2
+ export * from './diy-preset-health.js';
3
+ export * from './platform.js';
4
+ export * from './preflight-route.js';
5
+ export * from './preflight.js';
6
+ export * from './probe-cache.js';
7
+ export * from './probe-libs.js';
8
+ export * from './probe-units.js';
9
+ export * from './probe-verdict.js';
10
+ export * from './runtime-seed.js';
11
+ export * from './toolchain.js';
12
+ export * from './version-range.js';
13
+ export * from './install.js';
14
+ export * from './self-update.js';
@@ -0,0 +1,2 @@
1
+ import { type PnpmProcess } from './toolchain.js';
2
+ export declare function opencliInstallCommand(proc?: PnpmProcess, exists?: (path: string) => boolean): string;
@@ -0,0 +1,7 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isolatedInstallCommand, isolatedPnpmInstallCommand } from '@dsh-workbench/adapter-runtime';
3
+ import { desktopPnpmPath } from './toolchain.js';
4
+ export function opencliInstallCommand(proc = process, exists = existsSync) {
5
+ const pnpm = desktopPnpmPath(proc, exists);
6
+ return pnpm === undefined ? isolatedInstallCommand() : isolatedPnpmInstallCommand(pnpm);
7
+ }
@@ -0,0 +1 @@
1
+ export { hostOsOf, hostSurface, profileNameOf, type HostSurfaceInput } from '@dsh-workbench/shared/host';
@@ -0,0 +1 @@
1
+ export { hostOsOf, hostSurface, profileNameOf } from '@dsh-workbench/shared/host';
@@ -0,0 +1,12 @@
1
+ import type { DshHostContext, DshShell } from '@dsh-workbench/shared';
2
+ import type { RouteTable } from '@dsh-workbench/shared/host';
3
+ import { type DesktopBrowser } from './browser-wake.js';
4
+ export declare function registerPreflight(input: {
5
+ routes: RouteTable;
6
+ ctx: DshHostContext;
7
+ shell: DshShell;
8
+ opencliCommand: string;
9
+ installCommand?: string | undefined;
10
+ warmReads: () => readonly (readonly string[])[];
11
+ desktopBrowser?: DesktopBrowser | undefined;
12
+ }): void;
@@ -0,0 +1,93 @@
1
+ import { basename, join } from 'node:path';
2
+ import { INSTALL_TIMEOUT_MS, preflightHandler, runShell, tailLines, workbenchRoot } from '@dsh-workbench/shared/host';
3
+ import { WAKE_BROWSER_ROUTE } from '@dsh-workbench/shared/contract/protocol/preflight-wake';
4
+ import { createDiyPresetProbe } from './diy-preset-health.js';
5
+ import { hostSurface } from './platform.js';
6
+ import { createPreflight, probeDesktopToolchain } from './preflight.js';
7
+ import { createDesktopBrowser } from './browser-wake.js';
8
+ import { profileDirOf } from '@dsh-workbench/adapter-supply';
9
+ import { prependPath, runtimeSeedRootOf } from './runtime-seed.js';
10
+ import { localNpmCache, provisionLocalNpm, provisionLocalPnpm } from './toolchain.js';
11
+ function desktopBrowserOf(input) {
12
+ const dir = profileDirOf(input.ctx);
13
+ const surface = hostSurface({ profile: dir === null ? undefined : basename(dir) });
14
+ if (!surface.capabilities.browserWake)
15
+ return undefined;
16
+ return createDesktopBrowser({
17
+ shell: input.shell,
18
+ command: input.command,
19
+ stateFile: join(workbenchRoot(), 'browser.json'),
20
+ ...(input.ctx.logger === undefined ? {} : { logger: input.ctx.logger }),
21
+ });
22
+ }
23
+ export function registerPreflight(input) {
24
+ const desktopBrowser = input.desktopBrowser ??
25
+ desktopBrowserOf({
26
+ ctx: input.ctx,
27
+ shell: input.shell,
28
+ command: input.opencliCommand,
29
+ });
30
+ const profileDir = profileDirOf(input.ctx);
31
+ const surface = hostSurface({ profile: profileDir === null ? undefined : basename(profileDir) });
32
+ const preflight = createPreflight({
33
+ shell: input.shell,
34
+ command: input.opencliCommand,
35
+ ...(input.installCommand === undefined ? {} : { installCommand: input.installCommand }),
36
+ logger: input.ctx.logger,
37
+ warmReads: input.warmReads,
38
+ probeDiy: createDiyPresetProbe(input.ctx),
39
+ ...(surface.deployment === 'desktop'
40
+ ? {
41
+ probeToolchain: () => probeDesktopToolchain(input.shell),
42
+ provisionNpm: async () => {
43
+ const root = runtimeSeedRootOf(input.ctx);
44
+ if (root === null)
45
+ return { ok: false, detail: '找不到工作台根' };
46
+ const result = await provisionLocalNpm({
47
+ root,
48
+ run: async (command) => {
49
+ const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
50
+ return {
51
+ ok: ran.exitCode === 0 && !ran.timedOut,
52
+ detail: tailLines(ran.stderr || ran.stdout, 3),
53
+ };
54
+ },
55
+ });
56
+ if (result.ok && result.binDir !== undefined) {
57
+ prependPath(result.binDir);
58
+ if (process.env.npm_config_cache === undefined) {
59
+ process.env.npm_config_cache = localNpmCache(root);
60
+ }
61
+ }
62
+ return result;
63
+ },
64
+ provisionPnpm: async () => {
65
+ const root = runtimeSeedRootOf(input.ctx);
66
+ if (root === null)
67
+ return { ok: false, detail: '找不到工作台根' };
68
+ const result = await provisionLocalPnpm({
69
+ root,
70
+ run: async (command) => {
71
+ const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
72
+ return {
73
+ ok: ran.exitCode === 0 && !ran.timedOut,
74
+ detail: tailLines(ran.stderr || ran.stdout, 3),
75
+ };
76
+ },
77
+ });
78
+ if (result.ok && result.binDir !== undefined)
79
+ prependPath(result.binDir);
80
+ return result;
81
+ },
82
+ }
83
+ : {}),
84
+ ...(desktopBrowser === undefined ? {} : { desktopBrowser }),
85
+ });
86
+ input.routes.preflight = preflightHandler(preflight);
87
+ if (desktopBrowser !== undefined) {
88
+ input.routes[WAKE_BROWSER_ROUTE] = async () => {
89
+ const result = await desktopBrowser.wake().catch(() => ({ status: 'unknown' }));
90
+ return { ok: true, ...result };
91
+ };
92
+ }
93
+ }
@@ -0,0 +1,81 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import type { DiyPresetVerdict } from './diy-preset-health.js';
3
+ import { type LocalNpmProvision, type LocalPnpmProvision } from './toolchain.js';
4
+ import type { DesktopBrowser } from './browser-wake.js';
5
+ export type PreflightStepId = 'toolchain' | 'opencli' | 'bridge' | 'diy' | 'warm';
6
+ export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
7
+ export interface PreflightStep {
8
+ readonly id: PreflightStepId;
9
+ readonly status: PreflightStepStatus;
10
+ readonly detail?: string;
11
+ readonly action?: string;
12
+ readonly command?: string;
13
+ }
14
+ export interface ToolchainReport {
15
+ readonly ok: boolean;
16
+ readonly detail: string;
17
+ readonly nodeMissing: boolean;
18
+ readonly npmMissing: boolean;
19
+ readonly pnpmProblem: boolean;
20
+ }
21
+ export type PreflightFailure = 'permission' | 'network' | 'missing' | 'unknown';
22
+ export declare function classifyFailure(text: string): PreflightFailure;
23
+ export type PreflightGuidance = 'install' | 'path' | 'shell';
24
+ export declare function probeDesktopToolchain(shell: DshShell): Promise<ToolchainReport>;
25
+ export type PreflightBridgeIssue = 'daemon' | 'extension' | 'profile' | 'stale' | 'unknown';
26
+ export declare function readBridgeReport(stdout: string): {
27
+ readonly ok: boolean;
28
+ readonly issues: readonly PreflightBridgeIssue[];
29
+ readonly detail: string;
30
+ };
31
+ export interface PreflightDiyGap {
32
+ readonly rows: readonly string[];
33
+ readonly detail: string;
34
+ readonly command: string;
35
+ }
36
+ export interface PreflightSnapshot {
37
+ readonly phase: 'idle' | 'checking' | 'installing' | 'ready' | 'failed';
38
+ readonly steps: readonly PreflightStep[];
39
+ readonly at: number;
40
+ readonly error?: string;
41
+ readonly failure?: PreflightFailure;
42
+ readonly guidance?: PreflightGuidance;
43
+ readonly bridgeIssues?: readonly PreflightBridgeIssue[];
44
+ readonly diyGap?: PreflightDiyGap;
45
+ readonly canWakeBrowser?: boolean;
46
+ }
47
+ export interface Preflight {
48
+ run(options?: {
49
+ hard?: boolean;
50
+ }): Promise<PreflightSnapshot>;
51
+ snapshot(): PreflightSnapshot;
52
+ }
53
+ export declare const OPENCLI_INSTALL_COMMAND: string;
54
+ export interface BridgeVerdict {
55
+ readonly ok: boolean;
56
+ readonly detail: string;
57
+ readonly issues: readonly PreflightBridgeIssue[];
58
+ readonly command?: string | undefined;
59
+ }
60
+ export declare function canWarm(bridge: {
61
+ ok: boolean;
62
+ issues: readonly PreflightBridgeIssue[];
63
+ }): boolean;
64
+ export declare function createPreflight(options: {
65
+ shell: DshShell;
66
+ command?: string;
67
+ installCommand?: string;
68
+ logger?: {
69
+ warn?(message: string): void;
70
+ } | undefined;
71
+ install?: (() => Promise<{
72
+ ok: boolean;
73
+ detail: string;
74
+ }>) | undefined;
75
+ warmReads?: (() => readonly (readonly string[])[]) | undefined;
76
+ probeDiy?: (() => Promise<DiyPresetVerdict | undefined>) | undefined;
77
+ probeToolchain?: (() => Promise<ToolchainReport>) | undefined;
78
+ provisionNpm?: (() => Promise<LocalNpmProvision>) | undefined;
79
+ provisionPnpm?: (() => Promise<LocalPnpmProvision>) | undefined;
80
+ desktopBrowser?: DesktopBrowser | undefined;
81
+ }): Preflight;