@dsh-workbench/adapter-probe 1.1.14-beta.2 → 1.1.15

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.
@@ -1,4 +1,4 @@
1
- import type { ShellKind } from '@dsh-workbench/shared/host';
1
+ import type { Seam, ShellKind } from '@dsh-workbench/shared/host';
2
2
  export interface BootstrapEnvOptions {
3
3
  readonly opencliRoot?: string | undefined;
4
4
  readonly realHome?: string | undefined;
@@ -8,8 +8,6 @@ export interface BootstrapEnvOptions {
8
8
  export interface BootstrapSpawnOptions {
9
9
  readonly env: Readonly<Record<string, string>>;
10
10
  readonly timeout: number;
11
- readonly reject: false;
12
- readonly stripFinalNewline: false;
13
11
  }
14
12
  export interface BootstrapSpawnResult {
15
13
  readonly exitCode: number | null;
@@ -23,6 +21,7 @@ export interface BootstrapRunOptions {
23
21
  readonly env?: Readonly<Record<string, string>> | undefined;
24
22
  readonly shellKind?: ShellKind | undefined;
25
23
  readonly spawn?: BootstrapSpawn | undefined;
24
+ readonly seam?: Seam | undefined;
26
25
  }
27
26
  export interface BootstrapRunResult {
28
27
  readonly ok: boolean;
@@ -1,65 +1,43 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { execa } from 'execa';
5
2
  import { isolatedRoot } from '@dsh-workbench/adapter-runtime';
6
- import { tailLines } from '@dsh-workbench/shared/host';
3
+ import { bootstrapEnvOf, invocationOf, tailLines } from '@dsh-workbench/shared/host';
4
+ import { desktopRuntimeEnvPath } from './desktop-runtime.js';
7
5
  import { currentShellKind } from './platform.js';
8
6
  export function bootstrapEnv(options = {}) {
9
- const shellKind = options.shellKind ?? currentShellKind();
10
- const opencliRoot = options.opencliRoot ?? isolatedRoot();
11
- const realHome = options.realHome ?? homedir();
12
7
  const parent = options.parent ?? process.env;
13
- const env = {};
14
- for (const [key, value] of Object.entries(parent)) {
15
- if (typeof value === 'string')
16
- env[key] = value;
17
- }
18
- const home = join(opencliRoot, 'home');
19
- env['HOME'] = home;
20
- if (shellKind === 'pwsh')
21
- env['USERPROFILE'] = home;
22
- const userConfig = join(realHome, '.npmrc');
23
- if (existsSync(userConfig))
24
- env['NPM_CONFIG_USERCONFIG'] = userConfig;
25
- const cache = join(opencliRoot, 'npm-cache');
26
- env['npm_config_cache'] = cache;
27
- env['NPM_CONFIG_CACHE'] = cache;
28
- env['NO_COLOR'] = '1';
29
- env['TERM'] = 'dumb';
30
- return env;
31
- }
32
- function shellOf(line, shellKind) {
33
- return shellKind === 'pwsh'
34
- ? { file: 'pwsh', args: ['-NoProfile', '-NonInteractive', '-Command', line] }
35
- : { file: 'bash', args: ['-c', line] };
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
+ });
36
15
  }
37
- const defaultSpawn = async (file, args, options) => {
38
- const result = await execa(file, [...args], {
39
- env: { ...options.env },
40
- timeout: options.timeout,
41
- reject: options.reject,
42
- stripFinalNewline: options.stripFinalNewline,
16
+ async function seamSpawn(seam, line, shellKind, env, timeoutMs) {
17
+ if (seam === undefined)
18
+ throw new Error('没有执行缝,自举命令起不来');
19
+ const result = await seam.run({
20
+ command: line,
21
+ shellKind,
22
+ intent: { kind: 'host' },
23
+ env,
24
+ timeoutMs,
43
25
  });
44
26
  return {
45
- exitCode: result.exitCode ?? null,
46
- timedOut: result.timedOut === true,
27
+ exitCode: result.exitCode,
28
+ timedOut: result.timedOut,
47
29
  stdout: result.stdout,
48
30
  stderr: result.stderr,
49
31
  };
50
- };
32
+ }
51
33
  export async function runBootstrapCommand(line, options) {
52
34
  const shellKind = options.shellKind ?? currentShellKind();
53
- const spawn = options.spawn ?? defaultSpawn;
54
35
  const env = { ...bootstrapEnv({ shellKind }), ...(options.env ?? {}) };
55
- const command = shellOf(line, shellKind);
36
+ const command = invocationOf(shellKind, line);
56
37
  try {
57
- const result = await spawn(command.file, command.args, {
58
- env,
59
- timeout: options.timeoutMs,
60
- reject: false,
61
- stripFinalNewline: false,
62
- });
38
+ const result = options.spawn === undefined
39
+ ? await seamSpawn(options.seam, line, shellKind, env, options.timeoutMs)
40
+ : await options.spawn(command.file, command.args, { env, timeout: options.timeoutMs });
63
41
  if (result.timedOut === true)
64
42
  return { ok: false, detail: `超时(${String(options.timeoutMs)} ms):${line}` };
65
43
  if (result.exitCode === 0)
@@ -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
@@ -9,6 +9,7 @@ export * from './probe-libs.js';
9
9
  export * from './probe-units.js';
10
10
  export * from './probe-verdict.js';
11
11
  export * from './runtime-seed.js';
12
+ export * from './desktop-runtime.js';
12
13
  export * from './toolchain.js';
13
14
  export * from './toolchain-probe.js';
14
15
  export * from './version-range.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './probe-libs.js';
9
9
  export * from './probe-units.js';
10
10
  export * from './probe-verdict.js';
11
11
  export * from './runtime-seed.js';
12
+ export * from './desktop-runtime.js';
12
13
  export * from './toolchain.js';
13
14
  export * from './toolchain-probe.js';
14
15
  export * from './version-range.js';
@@ -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<{
@@ -17,7 +17,7 @@ function desktopBrowserOf(input) {
17
17
  if (!surface.capabilities.browserWake)
18
18
  return undefined;
19
19
  return createDesktopBrowser({
20
- shell: input.shell,
20
+ seam: input.seam,
21
21
  command: input.command,
22
22
  stateFile: join(workbenchRoot(), 'browser.json'),
23
23
  ...(input.ctx.logger === undefined ? {} : { logger: input.ctx.logger }),
@@ -32,21 +32,24 @@ export function registerPreflight(input) {
32
32
  const desktopBrowser = input.desktopBrowser ??
33
33
  desktopBrowserOf({
34
34
  ctx: input.ctx,
35
- shell: input.shell,
35
+ seam: input.seam,
36
36
  command,
37
37
  });
38
38
  const preflight = createPreflight({
39
- shell: input.shell,
39
+ seam: input.seam,
40
40
  command,
41
41
  installCommand,
42
- install: () => runBootstrapCommand(installCommand, { timeoutMs: PREFLIGHT_INSTALL_TIMEOUT_MS }),
42
+ install: () => runBootstrapCommand(installCommand, {
43
+ timeoutMs: PREFLIGHT_INSTALL_TIMEOUT_MS,
44
+ seam: input.seam,
45
+ }),
43
46
  logger: input.ctx.logger,
44
47
  warmReads: input.warmReads,
45
48
  ...(input.runtimeCapabilities === undefined ? {} : { capabilities: input.runtimeCapabilities }),
46
49
  ...(input.runtimeCheck === undefined ? {} : { runtimeCheck: input.runtimeCheck }),
47
50
  ...(input.runtimeLabel === undefined ? {} : { runtimeLabel: input.runtimeLabel }),
48
51
  probeDiy: createDiyPresetProbe(input.ctx),
49
- probeToolchain: () => probeDesktopToolchain(input.shell),
52
+ probeToolchain: () => probeDesktopToolchain(input.seam),
50
53
  provisionPnpm: async () => {
51
54
  const root = runtimeSeedRootOf(input.ctx);
52
55
  if (root === null)
@@ -55,7 +58,10 @@ export function registerPreflight(input) {
55
58
  root,
56
59
  pnpm: process.env.DSH_WB_PNPM?.trim() || 'pnpm',
57
60
  npm: 'npm',
58
- run: (command) => runBootstrapCommand(command, { timeoutMs: PREFLIGHT_INSTALL_TIMEOUT_MS }),
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 { PREFLIGHT_DOCTOR_TIMEOUT_MS, PREFLIGHT_FRESH_MS, PREFLIGHT_INSTALL_TIMEOUT_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}`);
@@ -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, PREFLIGHT_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;
@@ -1,5 +1,5 @@
1
1
  import { commandNamesMatch } from './command-name.js';
2
- import { OPENCLI_TIMEOUT_MS, PROBE_TTL_MS, buildSupplyIndex, NEED_KIND_SPECS, needIdOf, npmSourceOf, parseJsonLoose, quoteShellArg, runShell, } from '@dsh-workbench/shared/host';
2
+ import { OPENCLI_TIMEOUT_MS, PROBE_TTL_MS, buildSupplyIndex, NEED_KIND_SPECS, needIdOf, noWrite, npmSourceOf, parseJsonLoose, productWrite, quoteShellArg, } from '@dsh-workbench/shared/host';
3
3
  import { npmRegistryOf } from '@dsh-workbench/adapter-supply';
4
4
  import { probeRelease } from './release-port.js';
5
5
  import { createTimedMemo } from './probe-cache.js';
@@ -13,17 +13,24 @@ export function parsePluginList(stdout) {
13
13
  return parsed;
14
14
  return /no plugins? installed/i.test(stdout) ? [] : undefined;
15
15
  }
16
- async function probeBinary(shell, name) {
16
+ async function probeBinary(seam, name) {
17
17
  const line = process.platform === 'win32' ? `where ${quoteShellArg(name)}` : `command -v ${quoteShellArg(name)}`;
18
- const result = await runShell(shell, line, { timeoutMs: BINARY_PROBE_TIMEOUT_MS }).catch(() => undefined);
18
+ const result = await seam
19
+ .run({ command: line, intent: noWrite(), retry: 'safe', timeoutMs: BINARY_PROBE_TIMEOUT_MS, bestEffort: true })
20
+ .catch(() => undefined);
19
21
  return result?.exitCode === 0;
20
22
  }
21
23
  async function runtimeList(options) {
22
24
  if (!options.needsPlugins)
23
25
  return {};
24
- const result = await runShell(options.shell, `${options.command} plugin list -f json`, {
26
+ const result = await options.seam
27
+ .run({
28
+ command: `${options.command} plugin list -f json`,
29
+ intent: productWrite(),
30
+ retry: 'safe',
25
31
  timeoutMs: OPENCLI_TIMEOUT_MS,
26
- }).then((value) => ({ ok: true, stdout: value.exitCode === 0 ? value.stdout : '', stderr: value.stderr }), (error) => ({ ok: false, stdout: '', stderr: String(error) }));
32
+ })
33
+ .then((value) => ({ ok: true, stdout: value.exitCode === 0 ? value.stdout : '', stderr: value.stderr }), (error) => ({ ok: false, stdout: '', stderr: String(error) }));
27
34
  if (!result.ok)
28
35
  return { failure: result.stderr };
29
36
  const list = parsePluginList(result.stdout);
@@ -46,7 +53,7 @@ async function releasesOf(index, options) {
46
53
  continue;
47
54
  const registry = npmRegistryOf(source, process.env);
48
55
  const signal = await probeRelease({
49
- shell: options.shell,
56
+ seam: options.seam,
50
57
  package: entry.package,
51
58
  ...(registry.ok ? { registry: registry.value } : {}),
52
59
  force: options.force,
@@ -70,7 +77,7 @@ function runtimeCommandOf(need, runtimeCommands) {
70
77
  return KNOWN_RUNTIMES.includes(need.runtime) ? need.runtime : undefined;
71
78
  }
72
79
  export async function probeUnits(options) {
73
- const { shell, runtimeCommands, units, force = false } = options;
80
+ const { seam, runtimeCommands, units, force = false } = options;
74
81
  const commandKey = Object.values(runtimeCommands).sort().join('|');
75
82
  const hits = units.map((unit) => unitCache.get(cacheKeyOf(commandKey, units, unit.unit)));
76
83
  if (!force && hits.every((hit) => hit !== undefined)) {
@@ -82,7 +89,7 @@ export async function probeUnits(options) {
82
89
  if (shared !== undefined)
83
90
  return shared;
84
91
  }
85
- const task = loadCapabilities({ shell, runtimeCommands, units, force });
92
+ const task = loadCapabilities({ seam, runtimeCommands, units, force });
86
93
  pendingProbes.set(key, task);
87
94
  try {
88
95
  return await task;
@@ -93,7 +100,7 @@ export async function probeUnits(options) {
93
100
  }
94
101
  }
95
102
  async function loadCapabilities(options) {
96
- const { shell, runtimeCommands, units, force } = options;
103
+ const { seam, runtimeCommands, units, force } = options;
97
104
  const index = buildSupplyIndex(units.map((unit) => ({ id: unit.unit, needs: unit.needs })), {
98
105
  env: process.env,
99
106
  packagePrefix: 'opencli-plugin-',
@@ -105,7 +112,7 @@ async function loadCapabilities(options) {
105
112
  const runtimeNames = Object.values(runtimeCommands);
106
113
  await Promise.all(binNames.map(async (name) => {
107
114
  const known = runtimeNames.some((command) => commandNamesMatch(name, command));
108
- binaries.set(name, known ? true : await probeBinary(shell, name));
115
+ binaries.set(name, known ? true : await probeBinary(seam, name));
109
116
  }));
110
117
  const extensionNeeds = units.flatMap((unit) => unit.needs.filter((need) => need.kind === 'extension'));
111
118
  const runtimes = [...new Set(extensionNeeds.map((need) => need.runtime))];
@@ -117,11 +124,11 @@ async function loadCapabilities(options) {
117
124
  return;
118
125
  }
119
126
  const needsPlugins = extensionNeeds.some((need) => need.runtime === runtime && conflictOf(index, need) === undefined);
120
- lists.set(runtime, await runtimeList({ shell, command, needsPlugins }));
127
+ lists.set(runtime, await runtimeList({ seam, command, needsPlugins }));
121
128
  }));
122
129
  const libNeeds = units.flatMap((unit) => unit.needs.filter((need) => need.kind === 'lib'));
123
- const libStates = await probeLibs({ needs: libNeeds, shell, force, index });
124
- const releases = await releasesOf(index, { shell, force });
130
+ const libStates = await probeLibs({ needs: libNeeds, seam, force, index });
131
+ const releases = await releasesOf(index, { seam, force });
125
132
  const statesOfUnit = (unit) => unit.needs.map((need) => {
126
133
  const conflict = need.kind === 'extension' || need.kind === 'lib' ? conflictOf(index, need) : undefined;
127
134
  if (conflict !== undefined) {
@@ -179,7 +186,7 @@ async function loadCapabilities(options) {
179
186
  export async function probeUnitCommands(options) {
180
187
  const units = options.catalog === undefined ? [options.unit] : options.catalog;
181
188
  const all = await probeUnits({
182
- shell: options.shell,
189
+ seam: options.seam,
183
190
  runtimeCommands: options.runtimeCommands ?? {},
184
191
  units,
185
192
  ...(options.force === undefined ? {} : { force: options.force }),