@dsh-workbench/adapter-probe 1.1.14-beta.1 → 1.1.14

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.
@@ -0,0 +1,31 @@
1
+ import type { Seam, ShellKind } from '@dsh-workbench/shared/host';
2
+ export interface BootstrapEnvOptions {
3
+ readonly opencliRoot?: string | undefined;
4
+ readonly realHome?: string | undefined;
5
+ readonly parent?: NodeJS.ProcessEnv | undefined;
6
+ readonly shellKind?: ShellKind | undefined;
7
+ }
8
+ export interface BootstrapSpawnOptions {
9
+ readonly env: Readonly<Record<string, string>>;
10
+ readonly timeout: number;
11
+ }
12
+ export interface BootstrapSpawnResult {
13
+ readonly exitCode: number | null;
14
+ readonly timedOut?: boolean | undefined;
15
+ readonly stdout: string;
16
+ readonly stderr: string;
17
+ }
18
+ export type BootstrapSpawn = (file: string, args: readonly string[], options: BootstrapSpawnOptions) => Promise<BootstrapSpawnResult>;
19
+ export interface BootstrapRunOptions {
20
+ readonly timeoutMs: number;
21
+ readonly env?: Readonly<Record<string, string>> | undefined;
22
+ readonly shellKind?: ShellKind | undefined;
23
+ readonly spawn?: BootstrapSpawn | undefined;
24
+ readonly seam?: Seam | undefined;
25
+ }
26
+ export interface BootstrapRunResult {
27
+ readonly ok: boolean;
28
+ readonly detail: string;
29
+ }
30
+ export declare function bootstrapEnv(options?: BootstrapEnvOptions): Record<string, string>;
31
+ export declare function runBootstrapCommand(line: string, options: BootstrapRunOptions): Promise<BootstrapRunResult>;
@@ -0,0 +1,59 @@
1
+ import { homedir } from 'node:os';
2
+ import { isolatedRoot } from '@dsh-workbench/adapter-runtime';
3
+ import { bootstrapEnvOf, tailLines } from '@dsh-workbench/shared/host';
4
+ import { desktopRuntimeEnvPath } from './desktop-runtime.js';
5
+ import { currentShellKind } from './platform.js';
6
+ export function bootstrapEnv(options = {}) {
7
+ const parent = options.parent ?? process.env;
8
+ return bootstrapEnvOf({
9
+ root: options.opencliRoot ?? isolatedRoot(),
10
+ realHome: options.realHome ?? homedir(),
11
+ shellKind: options.shellKind ?? currentShellKind(),
12
+ parent,
13
+ path: desktopRuntimeEnvPath(parent.PATH),
14
+ });
15
+ }
16
+ function commandArgsOf(line, shellKind) {
17
+ return shellKind === 'pwsh'
18
+ ? { file: 'pwsh', args: ['-NoProfile', '-NonInteractive', '-Command', line] }
19
+ : { file: 'bash', args: ['-c', line] };
20
+ }
21
+ async function seamSpawn(seam, line, shellKind, env, timeoutMs) {
22
+ if (seam === undefined)
23
+ throw new Error('没有执行缝,自举命令起不来');
24
+ const result = await seam.run({
25
+ command: line,
26
+ shellKind,
27
+ intent: { kind: 'host' },
28
+ env,
29
+ timeoutMs,
30
+ });
31
+ return {
32
+ exitCode: result.exitCode,
33
+ timedOut: result.timedOut,
34
+ stdout: result.stdout,
35
+ stderr: result.stderr,
36
+ };
37
+ }
38
+ export async function runBootstrapCommand(line, options) {
39
+ const shellKind = options.shellKind ?? currentShellKind();
40
+ const env = { ...bootstrapEnv({ shellKind }), ...(options.env ?? {}) };
41
+ const command = commandArgsOf(line, shellKind);
42
+ try {
43
+ const result = options.spawn === undefined
44
+ ? await seamSpawn(options.seam, line, shellKind, env, options.timeoutMs)
45
+ : await options.spawn(command.file, command.args, { env, timeout: options.timeoutMs });
46
+ if (result.timedOut === true)
47
+ return { ok: false, detail: `超时(${String(options.timeoutMs)} ms):${line}` };
48
+ if (result.exitCode === 0)
49
+ return { ok: true, detail: tailLines(result.stdout, 1) };
50
+ const detail = tailLines(result.stderr || result.stdout, 4);
51
+ return {
52
+ ok: false,
53
+ detail: detail === '' ? `没跑通(退出码 ${String(result.exitCode)}):${line}` : detail,
54
+ };
55
+ }
56
+ catch (error) {
57
+ return { ok: false, detail: `${String(error)}:${line}` };
58
+ }
59
+ }
@@ -1,11 +1,11 @@
1
- import type { DshShell } from '@dsh-workbench/shared';
1
+ import { type Seam } from '@dsh-workbench/shared/host';
2
2
  import type { WakeBrowserReply } from '@dsh-workbench/shared/contract/protocol/preflight-wake';
3
3
  export interface DesktopBrowser {
4
4
  observe(): Promise<void>;
5
5
  wake(): Promise<WakeBrowserReply>;
6
6
  }
7
7
  export declare function createDesktopBrowser(options: {
8
- shell: DshShell;
8
+ seam: Seam;
9
9
  command: string;
10
10
  stateFile: string;
11
11
  platform?: string;
@@ -1,8 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { dirname, join, win32 } from 'node:path';
3
+ import { join, win32 } from 'node:path';
4
4
  import { setTimeout as delay } from 'node:timers/promises';
5
- import { quoteShellArg, runShell } from '@dsh-workbench/shared/host';
5
+ import { noWrite, now, quoteShellArg, writeJsonFile } from '@dsh-workbench/shared/host';
6
6
  const BROWSER_IDS = ['chrome', 'edge', 'brave', 'chromium', 'vivaldi'];
7
7
  const BROWSERS = [
8
8
  {
@@ -62,13 +62,10 @@ export function createDesktopBrowser(options) {
62
62
  const pollMs = options.pollMs ?? POLL_MS;
63
63
  const command = options.command;
64
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
- }
65
+ const result = await options.seam
66
+ .run({ command: line, intent: noWrite(), retry: 'safe', timeoutMs, bestEffort: true })
67
+ .catch(() => undefined);
68
+ return { ok: result !== undefined && result.exitCode === 0 && !result.timedOut, stdout: result?.stdout ?? '' };
72
69
  }
73
70
  function winCandidates(id) {
74
71
  const programFiles = env.ProgramFiles ?? env.PROGRAMFILES ?? 'C:\\Program Files';
@@ -116,8 +113,7 @@ export function createDesktopBrowser(options) {
116
113
  }
117
114
  function save(id) {
118
115
  try {
119
- mkdirSync(dirname(options.stateFile), { recursive: true });
120
- writeFileSync(options.stateFile, `${JSON.stringify({ browser: id }, null, 2)}\n`, 'utf8');
116
+ writeJsonFile(options.stateFile, { browser: id });
121
117
  }
122
118
  catch {
123
119
  return;
@@ -172,13 +168,13 @@ export function createDesktopBrowser(options) {
172
168
  return 'unknown';
173
169
  }
174
170
  async function waitConnected(ms) {
175
- const deadline = Date.now() + ms;
171
+ const deadline = now() + ms;
176
172
  for (;;) {
177
173
  if ((await daemonStatus()) === 'connected')
178
174
  return true;
179
- if (Date.now() >= deadline)
175
+ if (now() >= deadline)
180
176
  return false;
181
- await delay(Math.min(pollMs, Math.max(0, deadline - Date.now())));
177
+ await delay(Math.min(pollMs, Math.max(0, deadline - now())));
182
178
  }
183
179
  }
184
180
  function pickTarget(now) {
@@ -0,0 +1,14 @@
1
+ export interface PnpmProcess {
2
+ readonly versions: {
3
+ readonly electron?: string | undefined;
4
+ readonly node?: string | undefined;
5
+ };
6
+ readonly resourcesPath?: string | undefined;
7
+ }
8
+ export declare function desktopResourcesDir(proc?: PnpmProcess): string | undefined;
9
+ export declare function desktopPnpmPath(proc?: PnpmProcess, exists?: (path: string) => boolean): string | undefined;
10
+ export declare function desktopBundledBins(proc?: PnpmProcess, exists?: (path: string) => boolean): readonly string[];
11
+ export declare function desktopNodeExecutable(proc?: PnpmProcess, exists?: (path: string) => boolean): string | undefined;
12
+ export declare function desktopRuntimeEnvPath(parentPath: string | undefined, proc?: PnpmProcess, exists?: (path: string) => boolean, separator?: string): string | undefined;
13
+ export declare function desktopRuntimeEnv(parent?: NodeJS.ProcessEnv, proc?: PnpmProcess, exists?: (path: string) => boolean): Record<string, string>;
14
+ export declare function desktopPnpmVersion(proc?: PnpmProcess, exists?: (path: string) => boolean): string | undefined;
@@ -0,0 +1,59 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { delimiter, join } from 'node:path';
3
+ import { inheritedEnvOf, pathWithBinsOf } from '@dsh-workbench/shared/host';
4
+ import { hostSurface } from './platform.js';
5
+ export function desktopResourcesDir(proc = process) {
6
+ if (hostSurface({ proc }).deployment !== 'desktop')
7
+ return undefined;
8
+ const declared = proc.resourcesPath;
9
+ return declared === undefined || declared === '' ? undefined : declared;
10
+ }
11
+ export function desktopPnpmPath(proc = process, exists = existsSync) {
12
+ const resources = desktopResourcesDir(proc);
13
+ if (resources === undefined)
14
+ return undefined;
15
+ const path = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.cjs');
16
+ return exists(path) ? path : undefined;
17
+ }
18
+ export function desktopBundledBins(proc = process, exists = existsSync) {
19
+ const resources = desktopResourcesDir(proc);
20
+ if (resources === undefined)
21
+ return [];
22
+ const dependencies = join(resources, 'runtime', 'primary-runtime', 'dependencies');
23
+ return [join(dependencies, 'node', 'bin'), join(dependencies, 'pnpm', 'bin')].filter((dir) => exists(dir));
24
+ }
25
+ export function desktopNodeExecutable(proc = process, exists = existsSync) {
26
+ const resources = desktopResourcesDir(proc);
27
+ if (resources === undefined)
28
+ return undefined;
29
+ const bin = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'node', 'bin');
30
+ return ['node.exe', 'node'].map((name) => join(bin, name)).find((path) => exists(path));
31
+ }
32
+ export function desktopRuntimeEnvPath(parentPath, proc = process, exists = existsSync, separator = delimiter) {
33
+ const bins = desktopBundledBins(proc, exists);
34
+ if (bins.length === 0)
35
+ return undefined;
36
+ const parts = (parentPath ?? '').split(separator).filter((part) => part !== '' && !bins.includes(part));
37
+ return [...bins, ...parts].join(separator);
38
+ }
39
+ export function desktopRuntimeEnv(parent = process.env, proc = process, exists = existsSync) {
40
+ const bins = desktopBundledBins(proc, exists);
41
+ if (bins.length === 0)
42
+ return {};
43
+ return { ...inheritedEnvOf(parent), PATH: pathWithBinsOf(parent.PATH, bins) };
44
+ }
45
+ export function desktopPnpmVersion(proc = process, exists = existsSync) {
46
+ const resources = desktopResourcesDir(proc);
47
+ if (resources === undefined)
48
+ return undefined;
49
+ const manifest = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'package.json');
50
+ if (!exists(manifest))
51
+ return undefined;
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
54
+ return typeof parsed.version === 'string' ? parsed.version : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
@@ -1,3 +1,4 @@
1
+ import { now } from '@dsh-workbench/shared/host';
1
2
  import { DIY_PRESET_ID } from '@dsh-workbench/shared/contract/protocol/diy';
2
3
  export const DIY_UPGRADE_COMMAND = 'npm i -g @deepseek-ai/dsh@latest';
3
4
  export function brokenRowsOf(detail) {
@@ -29,7 +30,7 @@ export function createDiyPresetProbe(ctx, options = {}) {
29
30
  const registry = ctx.get('agentPresets');
30
31
  if (registry === undefined || typeof registry.list !== 'function')
31
32
  return undefined;
32
- const until = Date.now() + deadlineMs;
33
+ const until = now() + deadlineMs;
33
34
  for (;;) {
34
35
  const rows = await registry.list().catch(() => undefined);
35
36
  if (rows === undefined)
@@ -37,7 +38,7 @@ export function createDiyPresetProbe(ctx, options = {}) {
37
38
  const verdict = diyPresetVerdictOf(rows);
38
39
  if (verdict !== undefined)
39
40
  return verdict;
40
- if (Date.now() >= until)
41
+ if (now() >= until)
41
42
  return undefined;
42
43
  await new Promise((resolve) => setTimeout(resolve, everyMs));
43
44
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './bootstrap-run.js';
1
2
  export * from './browser-wake.js';
2
3
  export * from './diy-preset-health.js';
3
4
  export * from './platform.js';
@@ -8,6 +9,7 @@ export * from './probe-libs.js';
8
9
  export * from './probe-units.js';
9
10
  export * from './probe-verdict.js';
10
11
  export * from './runtime-seed.js';
12
+ export * from './desktop-runtime.js';
11
13
  export * from './toolchain.js';
12
14
  export * from './toolchain-probe.js';
13
15
  export * from './version-range.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './bootstrap-run.js';
1
2
  export * from './browser-wake.js';
2
3
  export * from './diy-preset-health.js';
3
4
  export * from './platform.js';
@@ -8,6 +9,7 @@ export * from './probe-libs.js';
8
9
  export * from './probe-units.js';
9
10
  export * from './probe-verdict.js';
10
11
  export * from './runtime-seed.js';
12
+ export * from './desktop-runtime.js';
11
13
  export * from './toolchain.js';
12
14
  export * from './toolchain-probe.js';
13
15
  export * from './version-range.js';
@@ -1,3 +1,5 @@
1
+ import type { ShellKind } from '@dsh-workbench/shared/host';
1
2
  import type { HostOs } from '@dsh-workbench/shared/contract/protocol/platform';
2
- export { hostOsOf, hostSurface, profileNameOf, type HostSurfaceInput } from '@dsh-workbench/shared/host';
3
+ export { hostOsOf, hostSurface, profileNameOf, shellKindOf, type HostSurfaceInput } from '@dsh-workbench/shared/host';
3
4
  export declare function currentHostOs(): HostOs;
5
+ export declare function currentShellKind(): ShellKind;
package/dist/platform.js CHANGED
@@ -1,5 +1,8 @@
1
- import { hostOsOf } from '@dsh-workbench/shared/host';
2
- export { hostOsOf, hostSurface, profileNameOf } from '@dsh-workbench/shared/host';
1
+ import { hostOsOf, shellKindOf } from '@dsh-workbench/shared/host';
2
+ export { hostOsOf, hostSurface, profileNameOf, shellKindOf } from '@dsh-workbench/shared/host';
3
3
  export function currentHostOs() {
4
4
  return hostOsOf(process.platform);
5
5
  }
6
+ export function currentShellKind() {
7
+ return shellKindOf(process.platform);
8
+ }
@@ -1,10 +1,11 @@
1
- import type { DshHostContext, DshShell, RuntimeCapabilities } from '@dsh-workbench/shared';
1
+ import type { DshHostContext, RuntimeCapabilities } from '@dsh-workbench/shared';
2
2
  import type { RouteTable } from '@dsh-workbench/shared/host';
3
+ import { type Seam } from '@dsh-workbench/shared/host';
3
4
  import { type DesktopBrowser } from './browser-wake.js';
4
5
  export declare function registerPreflight(input: {
5
6
  routes: RouteTable;
6
7
  ctx: DshHostContext;
7
- shell: DshShell;
8
+ seam: Seam;
8
9
  runtimeCommands: Readonly<Record<string, string>>;
9
10
  runtimeCapabilities?: RuntimeCapabilities | undefined;
10
11
  runtimeCheck?: (() => Promise<{
@@ -1,7 +1,9 @@
1
1
  import { basename, join } from 'node:path';
2
- import { INSTALL_TIMEOUT_MS, preflightHandler, runShell, tailLines, workbenchRoot } from '@dsh-workbench/shared/host';
2
+ import { PREFLIGHT_INSTALL_TIMEOUT_MS, preflightHandler, workbenchRoot } from '@dsh-workbench/shared/host';
3
3
  import { WAKE_BROWSER_ROUTE } from '@dsh-workbench/shared/contract/protocol/preflight-wake';
4
+ import { runBootstrapCommand } from './bootstrap-run.js';
4
5
  import { createDiyPresetProbe } from './diy-preset-health.js';
6
+ import { opencliInstallCommand } from './install.js';
5
7
  import { hostSurface } from './platform.js';
6
8
  import { createPreflight } from './preflight.js';
7
9
  import { probeDesktopToolchain } from './toolchain-probe.js';
@@ -15,7 +17,7 @@ function desktopBrowserOf(input) {
15
17
  if (!surface.capabilities.browserWake)
16
18
  return undefined;
17
19
  return createDesktopBrowser({
18
- shell: input.shell,
20
+ seam: input.seam,
19
21
  command: input.command,
20
22
  stateFile: join(workbenchRoot(), 'browser.json'),
21
23
  ...(input.ctx.logger === undefined ? {} : { logger: input.ctx.logger }),
@@ -23,24 +25,31 @@ function desktopBrowserOf(input) {
23
25
  }
24
26
  export function registerPreflight(input) {
25
27
  const command = input.runtimeCommands['opencli'] ?? Object.values(input.runtimeCommands)[0] ?? 'opencli';
26
- const installCommand = process.env.DSH_WORKBENCH_OPENCLI_INSTALL;
28
+ const configuredInstall = process.env.DSH_WORKBENCH_OPENCLI_INSTALL;
29
+ const installCommand = configuredInstall === undefined || configuredInstall === ''
30
+ ? opencliInstallCommand()
31
+ : configuredInstall;
27
32
  const desktopBrowser = input.desktopBrowser ??
28
33
  desktopBrowserOf({
29
34
  ctx: input.ctx,
30
- shell: input.shell,
35
+ seam: input.seam,
31
36
  command,
32
37
  });
33
38
  const preflight = createPreflight({
34
- shell: input.shell,
39
+ seam: input.seam,
35
40
  command,
36
- ...(installCommand === undefined || installCommand === '' ? {} : { installCommand }),
41
+ installCommand,
42
+ install: () => runBootstrapCommand(installCommand, {
43
+ timeoutMs: PREFLIGHT_INSTALL_TIMEOUT_MS,
44
+ seam: input.seam,
45
+ }),
37
46
  logger: input.ctx.logger,
38
47
  warmReads: input.warmReads,
39
48
  ...(input.runtimeCapabilities === undefined ? {} : { capabilities: input.runtimeCapabilities }),
40
49
  ...(input.runtimeCheck === undefined ? {} : { runtimeCheck: input.runtimeCheck }),
41
50
  ...(input.runtimeLabel === undefined ? {} : { runtimeLabel: input.runtimeLabel }),
42
51
  probeDiy: createDiyPresetProbe(input.ctx),
43
- probeToolchain: () => probeDesktopToolchain(input.shell),
52
+ probeToolchain: () => probeDesktopToolchain(input.seam),
44
53
  provisionPnpm: async () => {
45
54
  const root = runtimeSeedRootOf(input.ctx);
46
55
  if (root === null)
@@ -49,13 +58,10 @@ export function registerPreflight(input) {
49
58
  root,
50
59
  pnpm: process.env.DSH_WB_PNPM?.trim() || 'pnpm',
51
60
  npm: 'npm',
52
- run: async (command) => {
53
- const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
54
- return {
55
- ok: ran.exitCode === 0 && !ran.timedOut,
56
- detail: tailLines(ran.stderr || ran.stdout, 3),
57
- };
58
- },
61
+ run: (command) => runBootstrapCommand(command, {
62
+ timeoutMs: PREFLIGHT_INSTALL_TIMEOUT_MS,
63
+ seam: input.seam,
64
+ }),
59
65
  });
60
66
  if (result.ok && result.binDir !== undefined) {
61
67
  prependPath(result.binDir);
@@ -1,4 +1,5 @@
1
- import type { DshShell, RuntimeCapabilities } from '@dsh-workbench/shared';
1
+ import type { RuntimeCapabilities } from '@dsh-workbench/shared';
2
+ import { type Seam } from '@dsh-workbench/shared/host';
2
3
  import type { DiyPresetVerdict } from './diy-preset-health.js';
3
4
  import { type LocalNpmProvision, type LocalPnpmProvision } from './toolchain.js';
4
5
  import type { ToolchainReport } from './toolchain-probe.js';
@@ -55,7 +56,7 @@ export declare function canWarm(bridge: {
55
56
  issues: readonly PreflightBridgeIssue[];
56
57
  }): boolean;
57
58
  export declare function createPreflight(options: {
58
- shell: DshShell;
59
+ seam: Seam;
59
60
  command?: string;
60
61
  installCommand?: string;
61
62
  logger?: {
package/dist/preflight.js CHANGED
@@ -1,7 +1,7 @@
1
- import { INSTALL_TIMEOUT_MS, OPENCLI_TIMEOUT_MS, PREFLIGHT_FRESH_MS, PREFLIGHT_VERSION_TIMEOUT_MS, WARM_TIMEOUT_MS, } from '@dsh-workbench/shared/host';
1
+ import { PREFLIGHT_DOCTOR_TIMEOUT_MS, PREFLIGHT_FRESH_MS, PREFLIGHT_INSTALL_TIMEOUT_MS, PREFLIGHT_VERSION_TIMEOUT_MS, WARM_TIMEOUT_MS, now } from '@dsh-workbench/shared/host';
2
2
  import { tailLines } from '@dsh-workbench/shared/host';
3
3
  import { opencliCommandLine } from '@dsh-workbench/adapter-runtime';
4
- import { runShell } from '@dsh-workbench/shared/host';
4
+ import { productWrite } from '@dsh-workbench/shared/host';
5
5
  import { DIY_UPGRADE_COMMAND } from './diy-preset-health.js';
6
6
  import { opencliInstallCommand } from './install.js';
7
7
  import { LOCAL_PNPM_VERSION } from './toolchain.js';
@@ -29,7 +29,7 @@ function step(id, label) {
29
29
  return { id, status: 'pending', ...(label === undefined || label === '' ? {} : { label }) };
30
30
  }
31
31
  export function createPreflight(options) {
32
- const { shell, command = 'opencli', logger } = options;
32
+ const { seam, command = 'opencli', logger } = options;
33
33
  const capabilities = options.capabilities ?? { console: true, pluginMarket: true, browserBridge: true };
34
34
  const withConsole = capabilities.console;
35
35
  const withBridge = capabilities.browserBridge;
@@ -66,14 +66,14 @@ export function createPreflight(options) {
66
66
  let current = {
67
67
  phase: 'idle',
68
68
  steps: steps(),
69
- at: Date.now(),
69
+ at: now(),
70
70
  log: [],
71
71
  ...(options.desktopBrowser === undefined ? {} : { canWakeBrowser: true }),
72
72
  };
73
73
  let running = null;
74
74
  let shellStalled = false;
75
75
  function set(next) {
76
- current = { ...current, ...next, log: [...logLines], at: Date.now() };
76
+ current = { ...current, ...next, log: [...logLines], at: now() };
77
77
  return current;
78
78
  }
79
79
  function patchStep(id, patch) {
@@ -82,13 +82,13 @@ export function createPreflight(options) {
82
82
  function hostFailure(detail) {
83
83
  return set({ phase: 'failed', error: detail, failure: 'host', guidance: 'host' });
84
84
  }
85
- async function exec(stepId, commandLine, timeoutMs) {
85
+ async function exec(stepId, commandLine, timeoutMs, retry = 'safe') {
86
86
  if (shellStalled) {
87
87
  return { kind: 'stalled', detail: `前一条命令就没交回结果,这一步没跑:${commandLine}` };
88
88
  }
89
89
  appendLog(stepId, `> ${commandLine}`);
90
90
  try {
91
- const result = await runShell(shell, commandLine, { timeoutMs });
91
+ const result = await seam.run({ command: commandLine, intent: productWrite(), timeoutMs, retry });
92
92
  if (result.stalled) {
93
93
  shellStalled = true;
94
94
  appendLog(stepId, `没返回:${commandLine}`);
@@ -124,7 +124,7 @@ export function createPreflight(options) {
124
124
  };
125
125
  }
126
126
  async function probeBridge() {
127
- const result = await exec('bridge', `${command} doctor`, OPENCLI_TIMEOUT_MS);
127
+ const result = await exec('bridge', `${command} doctor`, PREFLIGHT_DOCTOR_TIMEOUT_MS);
128
128
  if (result.kind === 'stalled') {
129
129
  return { ok: false, detail: result.detail, issues: ['unknown'], stalled: true };
130
130
  }
@@ -143,7 +143,7 @@ export function createPreflight(options) {
143
143
  appendLog('cli', installed.detail);
144
144
  return installed;
145
145
  }
146
- const result = await exec('cli', installCommand, INSTALL_TIMEOUT_MS);
146
+ const result = await exec('cli', installCommand, PREFLIGHT_INSTALL_TIMEOUT_MS, 'never');
147
147
  if (result.kind !== 'ok')
148
148
  return { ok: false, detail: result.detail };
149
149
  return { ok: true, detail: tailLines(result.stdout, 1) };
@@ -222,6 +222,8 @@ export function createPreflight(options) {
222
222
  }));
223
223
  if (toolchain !== undefined && toolchain.detail.trim() !== '')
224
224
  appendLog('toolchain', toolchain.detail);
225
+ for (const line of toolchain?.diagnostics ?? [])
226
+ appendLog('toolchain', line);
225
227
  if (toolchain !== undefined && toolchain.ok && toolchain.pnpmProblem && options.provisionPnpm !== undefined) {
226
228
  set({ phase: 'installing' });
227
229
  patchStep('toolchain', { status: 'running', action: `装本地 pnpm@${LOCAL_PNPM_VERSION}` });
@@ -249,7 +251,7 @@ export function createPreflight(options) {
249
251
  action: undefined,
250
252
  });
251
253
  if (!toolchain.ok || toolchain.pnpmProblem) {
252
- return toolchain.stalled === true
254
+ return toolchain.stalled === true || toolchain.hostBroken === true
253
255
  ? hostFailure(toolchain.detail)
254
256
  : set({ phase: 'failed', error: toolchain.detail, failure: 'missing', guidance: 'install' });
255
257
  }
@@ -358,7 +360,7 @@ export function createPreflight(options) {
358
360
  });
359
361
  }
360
362
  function freshReady() {
361
- return current.phase === 'ready' && Date.now() - current.at < PREFLIGHT_FRESH_MS;
363
+ return current.phase === 'ready' && now() - current.at < PREFLIGHT_FRESH_MS;
362
364
  }
363
365
  async function start(runOptions = {}) {
364
366
  if (running !== null)
@@ -1,7 +1,8 @@
1
+ import { now as clockNow } from '@dsh-workbench/shared/host';
1
2
  export function createTimedMemo(ttlMs) {
2
3
  const entries = new Map();
3
4
  return {
4
- get(key, now = Date.now()) {
5
+ get(key, now = clockNow()) {
5
6
  const hit = entries.get(key);
6
7
  if (hit === undefined)
7
8
  return undefined;
@@ -9,7 +10,7 @@ export function createTimedMemo(ttlMs) {
9
10
  return undefined;
10
11
  return hit.value;
11
12
  },
12
- set(key, value, now = Date.now()) {
13
+ set(key, value, now = clockNow()) {
13
14
  entries.set(key, { at: now, value });
14
15
  },
15
16
  clear() {
@@ -1,9 +1,8 @@
1
- import type { DshShell } from '@dsh-workbench/shared';
2
- import type { LibNeed, NeedState, SupplyIndex } from '@dsh-workbench/shared/host';
1
+ import type { LibNeed, NeedState, Seam, SupplyIndex } from '@dsh-workbench/shared/host';
3
2
  export declare function installedLibVersion(name: string): string | undefined;
4
3
  export declare function probeLibs(options: {
5
4
  readonly needs: readonly LibNeed[];
6
- readonly shell: DshShell;
5
+ readonly seam: Seam;
7
6
  readonly force: boolean;
8
7
  readonly index?: SupplyIndex | undefined;
9
8
  }): Promise<ReadonlyMap<string, NeedState>>;
@@ -28,7 +28,7 @@ function stateOf(need, status, detail, extra = {}) {
28
28
  };
29
29
  }
30
30
  export async function probeLibs(options) {
31
- const { needs, shell, force, index } = options;
31
+ const { needs, seam, force, index } = options;
32
32
  const found = new Map();
33
33
  for (const need of needs) {
34
34
  if (found.has(need.name))
@@ -52,7 +52,7 @@ export async function probeLibs(options) {
52
52
  const registry = source === undefined ? undefined : npmRegistryOf(source, process.env);
53
53
  const latest = registry === undefined || !registry.ok
54
54
  ? undefined
55
- : await latestVersion({ shell, name: need.name, registry: registry.value, force });
55
+ : await latestVersion({ seam, name: need.name, registry: registry.value, force });
56
56
  const behind = latest !== undefined && isNewerVersion(latest, installed);
57
57
  found.set(need.name, stateOf(need, behind ? 'outdated' : 'ok', behind ? `装的 ${installed},源上最新 ${latest ?? ''}` : `${installed} 版本对`, {
58
58
  installed,
@@ -1,15 +1,14 @@
1
- import type { DshShell } from '@dsh-workbench/shared';
2
- import { type NeedProbeInput, type UnitCapability } from '@dsh-workbench/shared/host';
1
+ import { type Seam, type NeedProbeInput, type UnitCapability } from '@dsh-workbench/shared/host';
3
2
  export type { NeedProbeInput, NeedState, UnitCapability, } from '@dsh-workbench/shared/host';
4
3
  export declare function parsePluginList(stdout: string): unknown[] | undefined;
5
4
  export declare function probeUnits(options: {
6
- shell: DshShell;
5
+ seam: Seam;
7
6
  runtimeCommands: Readonly<Record<string, string>>;
8
7
  units: readonly NeedProbeInput[];
9
8
  force?: boolean;
10
9
  }): Promise<UnitCapability[]>;
11
10
  export declare function probeUnitCommands(options: {
12
- shell: DshShell;
11
+ seam: Seam;
13
12
  runtimeCommands?: Readonly<Record<string, string>> | undefined;
14
13
  unit: NeedProbeInput;
15
14
  force?: boolean;