@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.
@@ -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 }),
@@ -1,18 +1,18 @@
1
1
  export declare const latestVersion: (options: {
2
- readonly shell: import("@dsh-workbench/shared").DshShell;
2
+ readonly seam: import("@dsh-workbench/shared/host").Seam;
3
3
  readonly name: string;
4
4
  readonly registry: string;
5
5
  readonly force?: boolean;
6
6
  }) => Promise<string | undefined>;
7
7
  export declare const missingReleaseParts: (options: {
8
- readonly shell: import("@dsh-workbench/shared").DshShell;
8
+ readonly seam: import("@dsh-workbench/shared/host").Seam;
9
9
  readonly name: string;
10
10
  readonly registry: string;
11
11
  readonly version: string;
12
12
  readonly force?: boolean;
13
13
  }) => Promise<readonly string[]>;
14
14
  export declare const probeRelease: (options: {
15
- readonly shell: import("@dsh-workbench/shared").DshShell;
15
+ readonly seam: import("@dsh-workbench/shared/host").Seam;
16
16
  readonly package: string;
17
17
  readonly registry?: string | undefined;
18
18
  readonly force?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { createReleasePort } from '@dsh-workbench/adapter-profile';
2
- import { npmCacheDir, vendorPrefix } from '@dsh-workbench/adapter-runtime';
3
- const port = createReleasePort({ cacheDir: npmCacheDir(), prefix: vendorPrefix() });
2
+ import { vendorPrefix } from '@dsh-workbench/adapter-runtime';
3
+ const port = createReleasePort({ prefix: vendorPrefix() });
4
4
  export const latestVersion = port.latestVersion;
5
5
  export const missingReleaseParts = port.missingReleaseParts;
6
6
  export const probeRelease = port.probeRelease;
@@ -1,5 +1,5 @@
1
- import type { spawn } from 'node:child_process';
2
1
  import type { DshHostContext } from '@dsh-workbench/shared';
2
+ import type { Seam } from '@dsh-workbench/shared/host';
3
3
  export type SeedLog = (message: string) => void;
4
4
  export interface RuntimeSeedPaths {
5
5
  readonly root: string;
@@ -21,9 +21,7 @@ export interface RuntimeSeedOptions {
21
21
  }
22
22
  export declare function opencliBinName(platform?: NodeJS.Platform): string;
23
23
  export declare function runtimeSeedPaths(root: string, platform?: NodeJS.Platform): RuntimeSeedPaths;
24
- export declare function runtimeSeedRootOf(ctx: DshHostContext): string | null;
25
- export declare function coversWritableRoot(workspaceRoot: string, root: string): boolean;
26
- export declare function warnUnwritableRoot(ctx: DshHostContext): void;
24
+ export declare function runtimeSeedRootOf(_ctx: DshHostContext): string | null;
27
25
  export declare function wrapperRootOf(fallbackRoot: string): string;
28
26
  export declare function isInstalledCopy(packageRoot: string): boolean;
29
27
  export declare function opencliWrapperScript(packageRoot: string, home: string, platform?: NodeJS.Platform): string;
@@ -38,7 +36,7 @@ export declare function workshopSeedPlan(packageRoot: string, home: string, root
38
36
  readonly entry: SeedCommand;
39
37
  readonly full: SeedCommand;
40
38
  };
41
- export declare function runDetached(command: SeedCommand, onError: SeedLog, spawnImpl?: typeof spawn): void;
39
+ export declare function runDetached(command: SeedCommand, onError: SeedLog, seam?: Seam | undefined): void;
42
40
  export declare function prependPath(entryDir: string, env?: NodeJS.ProcessEnv): boolean;
43
- export declare function seedWorkbenchToolchain(root: string, log: SeedLog): void;
41
+ export declare function seedWorkbenchToolchain(root: string, log: SeedLog, seam?: Seam | undefined): Promise<void>;
44
42
  export declare function seedRuntimeFiles(options: RuntimeSeedOptions): readonly string[];
@@ -1,11 +1,9 @@
1
- import { execa } from 'execa';
2
1
  import { toolchainManifestOf } from '@dsh-workbench/adapter-runtime';
3
2
  import { createRequire } from 'node:module';
4
- import writeFileAtomic from 'write-file-atomic';
5
- import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, rmSync, } from 'node:fs';
3
+ import { mkdirSync, readFileSync, rmSync } from 'node:fs';
6
4
  import { delimiter, dirname, join, sep } from 'node:path';
7
- import { quoteShellArg, quoteShellArgFor } from '@dsh-workbench/shared/host';
8
- import { desktopPnpmPath, desktopPnpmVersion, ensureToolchain, localNpmCache, localPnpmCommand, provisionLocalPnpmSync, } from './toolchain.js';
5
+ import { pathWithBinsOf, quoteShellArg, quoteShellArgFor, workbenchRoot, writeTextFile } from '@dsh-workbench/shared/host';
6
+ import { desktopBundledBins, desktopNodeExecutable, desktopPnpmPath, desktopPnpmVersion, ensureToolchain, localNpmCache, localPnpmCommand, provisionLocalPnpm, } from './toolchain.js';
9
7
  export function opencliBinName(platform = process.platform) {
10
8
  return platform === 'win32' ? 'opencli.cmd' : 'opencli';
11
9
  }
@@ -25,32 +23,8 @@ export function runtimeSeedPaths(root, platform = process.platform) {
25
23
  workshopManifest: join(workshop, 'package.json'),
26
24
  };
27
25
  }
28
- export function runtimeSeedRootOf(ctx) {
29
- const configured = process.env.DSH_WORKBENCH_HOME?.trim();
30
- if (configured !== undefined && configured !== '')
31
- return configured;
32
- const homePath = ctx.get('dshHomePath');
33
- if (typeof homePath !== 'function')
34
- return null;
35
- try {
36
- return homePath('workbench');
37
- }
38
- catch {
39
- return null;
40
- }
41
- }
42
- export function coversWritableRoot(workspaceRoot, root) {
43
- const prefix = workspaceRoot.endsWith(sep) ? workspaceRoot : `${workspaceRoot}${sep}`;
44
- return root === workspaceRoot || root.startsWith(prefix);
45
- }
46
- export function warnUnwritableRoot(ctx) {
47
- const root = runtimeSeedRootOf(ctx);
48
- if (root === null)
49
- return;
50
- const workspaceRoot = ctx.get('sandboxPolicy')?.workspaceRoot;
51
- if (workspaceRoot === undefined || coversWritableRoot(workspaceRoot, root))
52
- return;
53
- ctx.logger?.warn?.(`[dsh-workbench] 可写根 ${workspaceRoot} 盖不住 ${root}:市场装插件、自举命令可能被沙箱挡(把 profile/sandbox.patch.yml 那一行写进这个 profile 的 cordis.patch.yml 再重启)`);
26
+ export function runtimeSeedRootOf(_ctx) {
27
+ return workbenchRoot();
54
28
  }
55
29
  export function wrapperRootOf(fallbackRoot) {
56
30
  try {
@@ -88,49 +62,46 @@ export function workshopSeedPlan(packageRoot, home, root, platform = process.pla
88
62
  full: { file: 'bash', args: [script], env, log },
89
63
  };
90
64
  }
91
- const execaSpawn = ((file, args, options) => execa(file, [...args], { ...options, reject: false }));
92
- export function runDetached(command, onError, spawnImpl = execaSpawn) {
93
- let fd = null;
65
+ export function runDetached(command, onError, seam) {
66
+ if (seam === undefined) {
67
+ onError(`自举脚本没跑起来(${command.file}):宿主没给执行缝`);
68
+ return;
69
+ }
94
70
  try {
95
- fd = openSync(command.log, 'a');
96
- const child = spawnImpl(command.file, [...command.args], {
97
- env: { ...process.env, ...command.env },
98
- detached: true,
99
- stdio: ['ignore', fd, fd],
71
+ const handle = seam.spawn({
72
+ target: { argv: [command.file, ...command.args] },
73
+ intent: { kind: 'host' },
74
+ lifetime: 'host',
75
+ logFile: command.log,
76
+ env: command.env,
77
+ label: 'seed',
100
78
  });
101
- child.on('error', (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
102
- child.unref();
79
+ handle.onError((error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
103
80
  }
104
81
  catch (error) {
105
82
  onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
106
83
  }
107
- finally {
108
- if (fd !== null)
109
- closeQuietly(fd);
110
- }
111
- }
112
- function closeQuietly(fd) {
113
- try {
114
- closeSync(fd);
115
- }
116
- catch {
117
- return;
118
- }
119
84
  }
120
85
  export function prependPath(entryDir, env = process.env) {
121
86
  const parts = (env.PATH ?? '').split(delimiter).filter((part) => part !== '');
122
87
  if (parts.includes(entryDir))
123
88
  return false;
124
- env.PATH = [entryDir, ...parts].join(delimiter);
89
+ env.PATH = pathWithBinsOf(env.PATH, [entryDir]);
125
90
  return true;
126
91
  }
127
- export function seedWorkbenchToolchain(root, log) {
92
+ export async function seedWorkbenchToolchain(root, log, seam) {
93
+ for (const dir of desktopBundledBins()) {
94
+ if (prependPath(dir))
95
+ log(`PATH 前插 Desktop 自带 ${dir}`);
96
+ }
128
97
  const bundledPnpm = desktopPnpmPath();
129
98
  const bundledMajor = Number(desktopPnpmVersion()?.split('.')[0] ?? '0');
130
99
  const bundledUsable = bundledPnpm !== undefined && bundledMajor >= 10;
131
100
  if (bundledPnpm !== undefined && bundledUsable) {
132
- process.env.DSH_WB_PNPM = `node ${quoteShellArgFor(bundledPnpm, process.platform)}`;
133
- log(`pnpm 用 Desktop 自带的:${bundledPnpm}`);
101
+ const bundledNode = desktopNodeExecutable();
102
+ const nodePrefix = bundledNode === undefined ? 'node' : quoteShellArgFor(bundledNode, process.platform);
103
+ process.env.DSH_WB_PNPM = `${nodePrefix} ${quoteShellArgFor(bundledPnpm, process.platform)}`;
104
+ log(`pnpm 用 Desktop 自带的:${bundledPnpm}(node: ${nodePrefix})`);
134
105
  }
135
106
  else if (bundledPnpm !== undefined) {
136
107
  log(`Desktop 自带 pnpm 版本低于 10,优先用 PATH 上的 pnpm`);
@@ -155,7 +126,7 @@ export function seedWorkbenchToolchain(root, log) {
155
126
  process.env.DSH_WB_PNPM = localPnpmCommand(root);
156
127
  }
157
128
  else if (!bundledUsable && toolchain.pathDir !== undefined) {
158
- const installed = provisionLocalPnpmSync({ root });
129
+ const installed = await provisionLocalPnpm({ root, seam });
159
130
  if (installed.ok && installed.binDir !== undefined) {
160
131
  if (prependPath(installed.binDir))
161
132
  log(`PATH 前插 ${installed.binDir}`);
@@ -178,15 +149,14 @@ export function seedRuntimeFiles(options) {
178
149
  const manifest = join(paths.opencliRoot, 'toolchain', 'package.json');
179
150
  mkdirSync(dirname(manifest), { recursive: true });
180
151
  if (readText(manifest) !== toolchainManifestOf()) {
181
- writeFileAtomic.sync(manifest, toolchainManifestOf(), 'utf8');
152
+ writeTextFile(manifest, toolchainManifestOf());
182
153
  done.push(manifest);
183
154
  }
184
155
  rmSync(join(paths.opencliRoot, 'toolchain', 'pnpm-workspace.yaml'), { force: true });
185
156
  const wanted = opencliWrapperScript(options.packageRoot, options.home, platform);
186
157
  if (readText(paths.opencliBin) !== wanted) {
187
158
  rmSync(paths.opencliBin, { force: true });
188
- writeFileAtomic.sync(paths.opencliBin, wanted, 'utf8');
189
- chmodSync(paths.opencliBin, 0o755);
159
+ writeTextFile(paths.opencliBin, wanted, { mode: 0o755 });
190
160
  done.push(paths.opencliBin);
191
161
  }
192
162
  }
@@ -1,4 +1,4 @@
1
- import type { spawn } from 'node:child_process';
1
+ import type { Seam } from '@dsh-workbench/shared/host';
2
2
  import { type SeedCommand } from './runtime-seed.js';
3
3
  import { SELF_UPDATE_STATES, type SelfUpdateState, type WorkbenchSelfUpdate } from '@dsh-workbench/shared/contract/protocol/bundle-install';
4
4
  export { SELF_UPDATE_STATES, type SelfUpdateState };
@@ -47,7 +47,7 @@ export interface SelfUpdateStart {
47
47
  readonly paths: SelfUpdatePaths;
48
48
  readonly forward: string;
49
49
  }
50
- export declare function selfUpdatePaths(dshHome: string, platform?: NodeJS.Platform): SelfUpdatePaths;
50
+ export declare function selfUpdatePaths(_dshHome: string, platform?: NodeJS.Platform): SelfUpdatePaths;
51
51
  export declare function recoveryCommand(input: {
52
52
  readonly profile: string;
53
53
  readonly pkg: string;
@@ -58,6 +58,6 @@ export declare function selfUpdateMarker(request: SelfUpdateRequest, state: Self
58
58
  export declare function selfUpdateScript(request: SelfUpdateRequest, paths: SelfUpdatePaths, cli?: SelfUpdateCli): string;
59
59
  export declare function selfUpdateNodeScript(request: SelfUpdateRequest, paths: SelfUpdatePaths, cli?: SelfUpdateCli): string;
60
60
  export declare function planSelfUpdate(request: SelfUpdateRequest, now?: Date, cli?: SelfUpdateCli, platform?: NodeJS.Platform): SelfUpdatePlan;
61
- export declare function startSelfUpdate(request: SelfUpdateRequest, onError: (message: string) => void, spawnImpl?: typeof spawn, cli?: SelfUpdateCli): SelfUpdateStart;
61
+ export declare function startSelfUpdate(request: SelfUpdateRequest, onError: (message: string) => void, seam?: Seam | undefined, cli?: SelfUpdateCli): SelfUpdateStart;
62
62
  export declare function readSelfUpdateView(home: string, profile?: string): WorkbenchSelfUpdate | undefined;
63
63
  export { runningVersionOf } from '@dsh-workbench/shared/host';
@@ -1,7 +1,6 @@
1
- import writeFileAtomic from 'write-file-atomic';
2
- import { quoteShellArg } from '@dsh-workbench/shared/host';
1
+ import { now as clockNow, quoteShellArg, workbenchRoot, writeJsonFile, writeTextFile } from '@dsh-workbench/shared/host';
3
2
  import { hostSurface } from './platform.js';
4
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, } from 'node:fs';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, } from 'node:fs';
5
4
  import { dirname, join } from 'node:path';
6
5
  import { runDetached } from './runtime-seed.js';
7
6
  import { SELF_UPDATE_STATES, } from '@dsh-workbench/shared/contract/protocol/bundle-install';
@@ -41,9 +40,8 @@ export function selfUpdateCliFor(profile, proc = process, exists = existsSync) {
41
40
  return PATH_DSH;
42
41
  return desktopSelfUpdateCli(proc, exists) ?? PATH_DSH;
43
42
  }
44
- export function selfUpdatePaths(dshHome, platform = process.platform) {
45
- const configured = process.env.DSH_WORKBENCH_HOME?.trim();
46
- const dir = configured === undefined || configured === '' ? join(dshHome, 'workbench') : configured;
43
+ export function selfUpdatePaths(_dshHome, platform = process.platform) {
44
+ const dir = workbenchRoot();
47
45
  return {
48
46
  dir,
49
47
  script: join(dir, platform === 'win32' ? 'self-update.mjs' : 'self-update.sh'),
@@ -178,7 +176,7 @@ export function selfUpdateNodeScript(request, paths, cli = selfUpdateCliFor(requ
178
176
  '',
179
177
  ].join('\n');
180
178
  }
181
- export function planSelfUpdate(request, now = new Date(), cli = selfUpdateCliFor(request.profile), platform = process.platform) {
179
+ export function planSelfUpdate(request, now = new Date(clockNow()), cli = selfUpdateCliFor(request.profile), platform = process.platform) {
182
180
  const paths = selfUpdatePaths(request.dshHome, platform);
183
181
  const windows = platform === 'win32';
184
182
  return {
@@ -195,13 +193,12 @@ export function planSelfUpdate(request, now = new Date(), cli = selfUpdateCliFor
195
193
  : { file: 'bash', args: [paths.script], env: {}, log: paths.log },
196
194
  };
197
195
  }
198
- export function startSelfUpdate(request, onError, spawnImpl, cli = selfUpdateCliFor(request.profile)) {
199
- const plan = planSelfUpdate(request, new Date(), cli);
196
+ export function startSelfUpdate(request, onError, seam, cli = selfUpdateCliFor(request.profile)) {
197
+ const plan = planSelfUpdate(request, new Date(clockNow()), cli);
200
198
  mkdirSync(plan.paths.dir, { recursive: true });
201
- writeFileAtomic.sync(plan.paths.script, plan.script, { mode: 0o755 });
202
- chmodSync(plan.paths.script, 0o755);
203
- writeFileAtomic.sync(plan.paths.marker, `${JSON.stringify(plan.marker)}\n`);
204
- runDetached(plan.command, onError, spawnImpl);
199
+ writeTextFile(plan.paths.script, plan.script, { mode: 0o755 });
200
+ writeJsonFile(plan.paths.marker, plan.marker);
201
+ runDetached(plan.command, onError, seam);
205
202
  const forward = recoveryCommand({
206
203
  profile: request.profile,
207
204
  pkg: request.pkg,
@@ -1,2 +1,5 @@
1
- import type { SupplyPort } from '@dsh-workbench/shared/host';
2
- export declare function createSupplyPort(): SupplyPort;
1
+ import type { Seam, SupplyPort } from '@dsh-workbench/shared/host';
2
+ export interface ProbeSupplyOptions {
3
+ readonly seam?: Seam | undefined;
4
+ }
5
+ export declare function createSupplyPort(options?: ProbeSupplyOptions): SupplyPort;
@@ -7,7 +7,8 @@ import { startSelfUpdate } from './self-update.js';
7
7
  import { declarationGaps, declarationRefusalOf, moduleDeclarationOf, packageRouteOf, pathDeclarationOf, pluginInstallPlan, profileDirOf, profileModules, readInstalledModule, readProfilePackage, registryDeclarationOf, resolvePluginSource, splitNpmSpec, } from '@dsh-workbench/adapter-supply';
8
8
  import { readBundleManager, refusalReason } from '@dsh-workbench/shared/host';
9
9
  import { OPENCLI_MANIFEST_FILE } from '@dsh-workbench/adapter-runtime';
10
- export function createSupplyPort() {
10
+ export function createSupplyPort(options = {}) {
11
+ const seam = options.seam;
11
12
  const port = {
12
13
  isolatedRoot,
13
14
  isolatedHome,
@@ -42,7 +43,7 @@ export function createSupplyPort() {
42
43
  splitNpmSpec,
43
44
  libInstallCommand,
44
45
  libUninstallCommand,
45
- startSelfUpdate,
46
+ startSelfUpdate: (request, onError) => startSelfUpdate(request, onError, seam),
46
47
  };
47
48
  return { ...port, env: port, modules: port };
48
49
  }
@@ -1,4 +1,4 @@
1
- import type { DshShell } from '@dsh-workbench/shared';
1
+ import { type Seam } from '@dsh-workbench/shared/host';
2
2
  export interface ToolchainReport {
3
3
  readonly ok: boolean;
4
4
  readonly detail: string;
@@ -6,5 +6,7 @@ export interface ToolchainReport {
6
6
  readonly npmMissing: boolean;
7
7
  readonly pnpmProblem: boolean;
8
8
  readonly stalled?: boolean | undefined;
9
+ readonly diagnostics?: readonly string[] | undefined;
10
+ readonly hostBroken?: boolean | undefined;
9
11
  }
10
- export declare function probeDesktopToolchain(shell: DshShell): Promise<ToolchainReport>;
12
+ export declare function probeDesktopToolchain(seam: Seam): Promise<ToolchainReport>;
@@ -1,28 +1,49 @@
1
- import { runShell } from '@dsh-workbench/shared/host';
2
- import { quoteShellArg } from '@dsh-workbench/shared/host';
3
- import { desktopPnpmPath, desktopPnpmVersion } from './toolchain.js';
1
+ import { productWrite, quoteShellArg } from '@dsh-workbench/shared/host';
2
+ import { delimiter } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { desktopBundledBins, desktopNodeExecutable, desktopPnpmPath, desktopPnpmVersion, desktopResourcesDir, desktopRuntimeEnv, } from './desktop-runtime.js';
4
5
  import { hostSurface } from './platform.js';
5
- async function toolVersion(shell, line) {
6
+ async function toolVersion(seam, line, extra) {
6
7
  try {
7
- const result = await runShell(shell, line, { timeoutMs: 10_000 });
8
+ const result = await seam.run({
9
+ command: line,
10
+ intent: productWrite(),
11
+ retry: 'safe',
12
+ timeoutMs: 10_000,
13
+ ...(extra?.workdir === undefined ? {} : { workdir: extra.workdir }),
14
+ });
8
15
  const text = (result.stdout || result.stderr).trim().split('\n')[0] ?? '';
9
- return { ok: result.exitCode === 0 && !result.timedOut, text, stalled: result.stalled };
16
+ const fact = seam.facts().filter((entry) => entry.command === line).at(-1);
17
+ const sandbox = fact === undefined
18
+ ? '(无)'
19
+ : `mode=${fact.mode} root=${fact.workspaceRoot}${fact.denial === undefined ? '' : ` denial=${fact.denial.scope}`}`;
20
+ return {
21
+ ok: result.exitCode === 0 && !result.timedOut,
22
+ text,
23
+ stalled: result.stalled,
24
+ code: result.exitCode,
25
+ raw: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().split('\n').slice(0, 2).join(' / '),
26
+ sandbox,
27
+ };
10
28
  }
11
- catch {
12
- return { ok: false, text: '', stalled: false };
29
+ catch (error) {
30
+ return { ok: false, text: '', stalled: false, code: null, raw: String(error), sandbox: '(抛错)' };
13
31
  }
14
32
  }
15
- export async function probeDesktopToolchain(shell) {
33
+ export async function probeDesktopToolchain(seam) {
34
+ const bundledNode = desktopNodeExecutable();
16
35
  const bundledPnpm = desktopPnpmPath();
17
36
  const bundledMajor = desktopPnpmVersion()?.split('.')[0];
18
37
  const preferBundled = bundledPnpm !== undefined && Number(bundledMajor ?? '0') >= 10;
38
+ const nodeCommand = bundledNode === undefined ? 'node' : quoteShellArg(bundledNode);
19
39
  const pnpmLine = preferBundled
20
- ? `node ${quoteShellArg(bundledPnpm ?? '')} --version`
40
+ ? `${nodeCommand} ${quoteShellArg(bundledPnpm ?? '')} --version`
21
41
  : 'pnpm --version';
42
+ const path = desktopRuntimeEnv().PATH;
22
43
  const [node, npm, pnpm] = await Promise.all([
23
- toolVersion(shell, 'node --version'),
24
- toolVersion(shell, 'npm --version'),
25
- toolVersion(shell, pnpmLine),
44
+ toolVersion(seam, `${nodeCommand} --version`),
45
+ toolVersion(seam, 'npm --version'),
46
+ toolVersion(seam, pnpmLine),
26
47
  ]);
27
48
  if (node.stalled || npm.stalled || pnpm.stalled) {
28
49
  return {
@@ -56,5 +77,50 @@ export async function probeDesktopToolchain(shell) {
56
77
  ? `pnpm ${pnpm.text}(Desktop 自带)`
57
78
  : `pnpm ${pnpm.text}`,
58
79
  ];
59
- return { ok: !nodeMissing, detail: parts.join(' · '), nodeMissing, npmMissing, pnpmProblem };
80
+ const control = [];
81
+ if (nodeMissing) {
82
+ for (const [label, file] of [
83
+ ['direct bundled node', desktopNodeExecutable()],
84
+ ['direct host node', process.execPath],
85
+ ]) {
86
+ if (file === undefined) {
87
+ control.push(`control ${label}: (没有可执行文件)`);
88
+ continue;
89
+ }
90
+ try {
91
+ const out = await seam.run({
92
+ argv: [file, '--version'],
93
+ intent: { kind: 'host' },
94
+ timeoutMs: 10_000,
95
+ });
96
+ control.push(`control ${label}: exit=${String(out.exitCode)} out=${(out.stdout || out.stderr).slice(0, 80)}`);
97
+ }
98
+ catch (error) {
99
+ control.push(`control ${label}: 起不来 ${String(error).slice(0, 120)}`);
100
+ }
101
+ }
102
+ for (const [label, workdir] of [
103
+ ['default', undefined],
104
+ ['home', homedir()],
105
+ ['cwd', process.cwd()],
106
+ ]) {
107
+ const probe = await toolVersion(seam, 'echo workbench-probe', { workdir });
108
+ control.push(`control ${label}: exit=${String(probe.code)} out=${probe.raw.slice(0, 80) || '(空)'}`);
109
+ }
110
+ }
111
+ const hostBroken = control.some((line) => line.includes('exit=3221225794'));
112
+ const diagnostics = [
113
+ `deployment=${desktop ? 'desktop' : 'host'} · resources=${desktopResourcesDir() ?? '(无)'} · execPath=${process.execPath}`,
114
+ `bundled node=${bundledNode ?? '(找不到)'} · bundled pnpm=${bundledPnpm ?? '(找不到)'} · bins=${desktopBundledBins().join('|') || '(无)'}`,
115
+ `PATH 前两段=${(path ?? '').split(delimiter).slice(0, 2).join('|') || '(空)'}`,
116
+ `node: exit=${String(node.code)} sandbox=${node.sandbox} out=${node.raw.slice(0, 160) || '(空)'} cmd=${nodeCommand}`,
117
+ `pnpm: exit=${String(pnpm.code)} sandbox=${pnpm.sandbox} out=${pnpm.raw.slice(0, 160) || '(空)'} cmd=${pnpmLine}`,
118
+ `npm: exit=${String(npm.code)} sandbox=${npm.sandbox} out=${npm.raw.slice(0, 120) || '(空)'}`,
119
+ ...control,
120
+ ];
121
+ const base = parts.join(' · ');
122
+ const detail = hostBroken
123
+ ? `宿主执行通道起不了子进程(0xC0000142 · DLL 初始化失败)——不是没装 node:${base}`
124
+ : base;
125
+ return { ok: !nodeMissing, detail, nodeMissing, npmMissing, pnpmProblem, diagnostics, hostBroken };
60
126
  }
@@ -1,3 +1,7 @@
1
+ import type { Seam } from '@dsh-workbench/shared/host';
2
+ import type { PnpmProcess } from './desktop-runtime.js';
3
+ export { desktopBundledBins, desktopNodeExecutable, desktopPnpmPath, desktopPnpmVersion, desktopResourcesDir, desktopRuntimeEnv, desktopRuntimeEnvPath } from './desktop-runtime.js';
4
+ export type { PnpmProcess } from './desktop-runtime.js';
1
5
  export interface ToolchainProcess {
2
6
  readonly versions: {
3
7
  readonly electron?: string | undefined;
@@ -6,15 +10,6 @@ export interface ToolchainProcess {
6
10
  readonly execPath: string;
7
11
  readonly resourcesPath?: string | undefined;
8
12
  }
9
- export interface PnpmProcess {
10
- readonly versions: {
11
- readonly electron?: string | undefined;
12
- readonly node?: string | undefined;
13
- };
14
- readonly resourcesPath?: string | undefined;
15
- }
16
- export declare function desktopPnpmPath(proc?: PnpmProcess, exists?: (path: string) => boolean): string | undefined;
17
- export declare function desktopPnpmVersion(proc?: PnpmProcess, exists?: (path: string) => boolean): string | undefined;
18
13
  export interface ToolchainOptions {
19
14
  readonly root: string;
20
15
  readonly proc?: ToolchainProcess | undefined;
@@ -61,10 +56,11 @@ export interface LocalPnpmProvision {
61
56
  }
62
57
  export declare function provisionLocalPnpm(options: {
63
58
  readonly root: string;
64
- readonly run: (command: string) => Promise<{
59
+ readonly seam?: Seam | undefined;
60
+ readonly run?: ((command: string) => Promise<{
65
61
  ok: boolean;
66
62
  detail: string;
67
- }>;
63
+ }>) | undefined;
68
64
  readonly installed?: (() => boolean) | undefined;
69
65
  readonly platform?: NodeJS.Platform | undefined;
70
66
  readonly proc?: PnpmProcess | undefined;
@@ -72,14 +68,6 @@ export declare function provisionLocalPnpm(options: {
72
68
  readonly pnpm?: string | undefined;
73
69
  readonly npm?: string | undefined;
74
70
  }): Promise<LocalPnpmProvision>;
75
- export declare function provisionLocalPnpmSync(options: {
76
- readonly root: string;
77
- readonly run?: ((command: string) => void) | undefined;
78
- readonly installed?: (() => boolean) | undefined;
79
- readonly platform?: NodeJS.Platform | undefined;
80
- readonly proc?: PnpmProcess | undefined;
81
- readonly exists?: ((path: string) => boolean) | undefined;
82
- }): LocalPnpmProvision;
83
71
  export declare function desktopNodeBinary(proc?: ToolchainProcess, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | undefined;
84
72
  export declare function desktopNodeShim(proc?: ToolchainProcess, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | undefined;
85
73
  export declare function toolchainBinDir(root: string): string;