@dsh-workbench/adapter-probe 1.1.12 → 1.1.14-beta.0

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,7 @@
1
+ export type PreflightBridgeIssue = 'daemon' | 'extension' | 'profile' | 'stale' | 'unknown';
2
+ export declare const BRIDGE_ISSUE_ORDER: readonly PreflightBridgeIssue[];
3
+ export declare function readBridgeReport(stdout: string): {
4
+ readonly ok: boolean;
5
+ readonly issues: readonly PreflightBridgeIssue[];
6
+ readonly detail: string;
7
+ };
@@ -0,0 +1,32 @@
1
+ import { tailLines } from '@dsh-workbench/shared/host';
2
+ export const BRIDGE_ISSUE_ORDER = ['profile', 'stale', 'daemon', 'extension', 'unknown'];
3
+ export function readBridgeReport(stdout) {
4
+ const text = String(stdout ?? '');
5
+ const ok = /^\[OK\] Daemon:/m.test(text) && /^\[OK\] Extension:/m.test(text) && /^\[OK\] Connectivity:/m.test(text);
6
+ if (ok) {
7
+ const good = text.split('\n').find((line) => line.trim() === 'Everything looks good!');
8
+ return { ok: true, issues: [], detail: good?.trim() ?? tailLines(text, 1) };
9
+ }
10
+ const found = new Set();
11
+ if (!/^\[OK\] Daemon:/m.test(text))
12
+ found.add('daemon');
13
+ if (!/^\[OK\] Extension:/m.test(text))
14
+ found.add('extension');
15
+ if (/Multiple Chrome profiles are connected/i.test(text))
16
+ found.add('profile');
17
+ if (/Default browser profile is stale|Selected browser profile is not connected/i.test(text)) {
18
+ found.add('stale');
19
+ }
20
+ if (found.has('profile') || found.has('stale'))
21
+ found.delete('extension');
22
+ if (found.size === 0)
23
+ found.add('unknown');
24
+ return { ok: false, issues: BRIDGE_ISSUE_ORDER.filter((issue) => found.has(issue)), detail: issuesSection(text) };
25
+ }
26
+ function issuesSection(text) {
27
+ const lines = text.split('\n');
28
+ const start = lines.findIndex((line) => /^Issues:/.test(line.trim()));
29
+ if (start < 0)
30
+ return tailLines(text, 6);
31
+ return lines.slice(start).join('\n').trim();
32
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './probe-units.js';
9
9
  export * from './probe-verdict.js';
10
10
  export * from './runtime-seed.js';
11
11
  export * from './toolchain.js';
12
+ export * from './toolchain-probe.js';
12
13
  export * from './version-range.js';
13
14
  export * from './install.js';
14
15
  export * from './self-update.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './probe-units.js';
9
9
  export * from './probe-verdict.js';
10
10
  export * from './runtime-seed.js';
11
11
  export * from './toolchain.js';
12
+ export * from './toolchain-probe.js';
12
13
  export * from './version-range.js';
13
14
  export * from './install.js';
14
15
  export * from './self-update.js';
package/dist/install.js CHANGED
@@ -2,6 +2,9 @@ import { existsSync } from 'node:fs';
2
2
  import { isolatedInstallCommand, isolatedPnpmInstallCommand } from '@dsh-workbench/adapter-runtime';
3
3
  import { desktopPnpmPath } from './toolchain.js';
4
4
  export function opencliInstallCommand(proc = process, exists = existsSync) {
5
+ const override = process.env.DSH_WB_PNPM?.trim();
6
+ if (override !== undefined && override !== '')
7
+ return isolatedInstallCommand();
5
8
  const pnpm = desktopPnpmPath(proc, exists);
6
9
  return pnpm === undefined ? isolatedInstallCommand() : isolatedPnpmInstallCommand(pnpm);
7
10
  }
@@ -3,11 +3,12 @@ import { INSTALL_TIMEOUT_MS, preflightHandler, runShell, tailLines, workbenchRoo
3
3
  import { WAKE_BROWSER_ROUTE } from '@dsh-workbench/shared/contract/protocol/preflight-wake';
4
4
  import { createDiyPresetProbe } from './diy-preset-health.js';
5
5
  import { hostSurface } from './platform.js';
6
- import { createPreflight, probeDesktopToolchain } from './preflight.js';
6
+ import { createPreflight } from './preflight.js';
7
+ import { probeDesktopToolchain } from './toolchain-probe.js';
7
8
  import { createDesktopBrowser } from './browser-wake.js';
8
9
  import { profileDirOf } from '@dsh-workbench/adapter-supply';
9
10
  import { prependPath, runtimeSeedRootOf } from './runtime-seed.js';
10
- import { localNpmCache, provisionLocalNpm, provisionLocalPnpm } from './toolchain.js';
11
+ import { localPnpmCommand, provisionLocalPnpm } from './toolchain.js';
11
12
  function desktopBrowserOf(input) {
12
13
  const dir = profileDirOf(input.ctx);
13
14
  const surface = hostSurface({ profile: dir === null ? undefined : basename(dir) });
@@ -29,8 +30,6 @@ export function registerPreflight(input) {
29
30
  shell: input.shell,
30
31
  command,
31
32
  });
32
- const profileDir = profileDirOf(input.ctx);
33
- const surface = hostSurface({ profile: profileDir === null ? undefined : basename(profileDir) });
34
33
  const preflight = createPreflight({
35
34
  shell: input.shell,
36
35
  command,
@@ -41,51 +40,29 @@ export function registerPreflight(input) {
41
40
  ...(input.runtimeCheck === undefined ? {} : { runtimeCheck: input.runtimeCheck }),
42
41
  ...(input.runtimeLabel === undefined ? {} : { runtimeLabel: input.runtimeLabel }),
43
42
  probeDiy: createDiyPresetProbe(input.ctx),
44
- ...(surface.deployment === 'desktop'
45
- ? {
46
- probeToolchain: () => probeDesktopToolchain(input.shell),
47
- provisionNpm: async () => {
48
- const root = runtimeSeedRootOf(input.ctx);
49
- if (root === null)
50
- return { ok: false, detail: '找不到工作台根' };
51
- const result = await provisionLocalNpm({
52
- root,
53
- run: async (command) => {
54
- const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
55
- return {
56
- ok: ran.exitCode === 0 && !ran.timedOut,
57
- detail: tailLines(ran.stderr || ran.stdout, 3),
58
- };
59
- },
60
- });
61
- if (result.ok && result.binDir !== undefined) {
62
- prependPath(result.binDir);
63
- if (process.env.npm_config_cache === undefined) {
64
- process.env.npm_config_cache = localNpmCache(root);
65
- }
66
- }
67
- return result;
68
- },
69
- provisionPnpm: async () => {
70
- const root = runtimeSeedRootOf(input.ctx);
71
- if (root === null)
72
- return { ok: false, detail: '找不到工作台根' };
73
- const result = await provisionLocalPnpm({
74
- root,
75
- run: async (command) => {
76
- const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
77
- return {
78
- ok: ran.exitCode === 0 && !ran.timedOut,
79
- detail: tailLines(ran.stderr || ran.stdout, 3),
80
- };
81
- },
82
- });
83
- if (result.ok && result.binDir !== undefined)
84
- prependPath(result.binDir);
85
- return result;
43
+ probeToolchain: () => probeDesktopToolchain(input.shell),
44
+ provisionPnpm: async () => {
45
+ const root = runtimeSeedRootOf(input.ctx);
46
+ if (root === null)
47
+ return { ok: false, detail: '找不到工作台根' };
48
+ const result = await provisionLocalPnpm({
49
+ root,
50
+ pnpm: process.env.DSH_WB_PNPM?.trim() || 'pnpm',
51
+ 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
+ };
86
58
  },
59
+ });
60
+ if (result.ok && result.binDir !== undefined) {
61
+ prependPath(result.binDir);
62
+ process.env.DSH_WB_PNPM = localPnpmCommand(root);
87
63
  }
88
- : {}),
64
+ return result;
65
+ },
89
66
  ...(desktopBrowser === undefined ? {} : { desktopBrowser }),
90
67
  });
91
68
  input.routes.preflight = preflightHandler(preflight);
@@ -1,7 +1,11 @@
1
1
  import type { DshShell, RuntimeCapabilities } from '@dsh-workbench/shared';
2
2
  import type { DiyPresetVerdict } from './diy-preset-health.js';
3
3
  import { type LocalNpmProvision, type LocalPnpmProvision } from './toolchain.js';
4
+ import type { ToolchainReport } from './toolchain-probe.js';
4
5
  import type { DesktopBrowser } from './browser-wake.js';
6
+ export type PreflightFailure = 'permission' | 'network' | 'missing' | 'host' | 'unknown';
7
+ export declare function classifyFailure(text: string): PreflightFailure;
8
+ export type PreflightGuidance = 'install' | 'path' | 'shell' | 'host';
5
9
  export type PreflightStepId = 'toolchain' | 'runtime' | 'cli' | 'bridge' | 'diy' | 'warm';
6
10
  export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
7
11
  export interface PreflightStep {
@@ -10,25 +14,11 @@ export interface PreflightStep {
10
14
  readonly detail?: string;
11
15
  readonly action?: string;
12
16
  readonly command?: string;
17
+ readonly log?: readonly string[];
13
18
  }
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 const BRIDGE_ISSUE_ORDER: readonly PreflightBridgeIssue[];
27
- export declare function readBridgeReport(stdout: string): {
28
- readonly ok: boolean;
29
- readonly issues: readonly PreflightBridgeIssue[];
30
- readonly detail: string;
31
- };
19
+ import { type PreflightBridgeIssue } from './bridge-report.js';
20
+ export { BRIDGE_ISSUE_ORDER, readBridgeReport } from './bridge-report.js';
21
+ export type { PreflightBridgeIssue } from './bridge-report.js';
32
22
  export interface PreflightDiyGap {
33
23
  readonly rows: readonly string[];
34
24
  readonly detail: string;
@@ -44,6 +34,7 @@ export interface PreflightSnapshot {
44
34
  readonly bridgeIssues?: readonly PreflightBridgeIssue[];
45
35
  readonly diyGap?: PreflightDiyGap;
46
36
  readonly canWakeBrowser?: boolean;
37
+ readonly log?: readonly string[];
47
38
  }
48
39
  export interface Preflight {
49
40
  run(options?: {
@@ -57,6 +48,7 @@ export interface BridgeVerdict {
57
48
  readonly detail: string;
58
49
  readonly issues: readonly PreflightBridgeIssue[];
59
50
  readonly command?: string | undefined;
51
+ readonly stalled?: boolean | undefined;
60
52
  }
61
53
  export declare function canWarm(bridge: {
62
54
  ok: boolean;
package/dist/preflight.js CHANGED
@@ -1,10 +1,10 @@
1
- import { INSTALL_TIMEOUT_MS, OPENCLI_TIMEOUT_MS, PREFLIGHT_FRESH_MS, WARM_TIMEOUT_MS } from '@dsh-workbench/shared/host';
1
+ import { INSTALL_TIMEOUT_MS, OPENCLI_TIMEOUT_MS, PREFLIGHT_FRESH_MS, PREFLIGHT_VERSION_TIMEOUT_MS, WARM_TIMEOUT_MS, } from '@dsh-workbench/shared/host';
2
2
  import { tailLines } from '@dsh-workbench/shared/host';
3
3
  import { opencliCommandLine } from '@dsh-workbench/adapter-runtime';
4
4
  import { runShell } from '@dsh-workbench/shared/host';
5
5
  import { DIY_UPGRADE_COMMAND } from './diy-preset-health.js';
6
6
  import { opencliInstallCommand } from './install.js';
7
- import { desktopPnpmPath, LOCAL_NPM_VERSION, LOCAL_PNPM_VERSION, } from './toolchain.js';
7
+ import { LOCAL_PNPM_VERSION } from './toolchain.js';
8
8
  import { bridgeFixCommand } from './browser-wake.js';
9
9
  export function classifyFailure(text) {
10
10
  const lower = text.toLowerCase();
@@ -16,73 +16,8 @@ export function classifyFailure(text) {
16
16
  return 'missing';
17
17
  return 'unknown';
18
18
  }
19
- async function toolVersion(shell, line) {
20
- try {
21
- const result = await runShell(shell, line, { timeoutMs: 10_000 });
22
- const text = (result.stdout || result.stderr).trim().split('\n')[0] ?? '';
23
- return { ok: result.exitCode === 0 && !result.timedOut, text };
24
- }
25
- catch {
26
- return { ok: false, text: '' };
27
- }
28
- }
29
- export async function probeDesktopToolchain(shell) {
30
- const [node, npm, pnpm] = await Promise.all([
31
- toolVersion(shell, 'node --version'),
32
- toolVersion(shell, 'npm --version'),
33
- toolVersion(shell, 'pnpm --version'),
34
- ]);
35
- const nodeMissing = !node.ok;
36
- const npmMissing = !npm.ok;
37
- const hasBundledPnpm = desktopPnpmPath() !== undefined;
38
- const pnpmMajor = /^v?(\d+)/.exec(pnpm.text)?.[1];
39
- const pnpmProblem = !pnpm.ok || pnpmMajor !== '10';
40
- const parts = [
41
- nodeMissing ? 'node 不在 PATH(Desktop 自带 Node 的 shim 没生效)' : `node ${node.text}(Desktop 自带)`,
42
- npmMissing
43
- ? hasBundledPnpm
44
- ? 'npm 缺(opencli 安装走 pnpm;插件安装/模块库仍需 npm)'
45
- : 'npm 缺(装不了 opencli:bundled pnpm 也没找到)'
46
- : `npm ${npm.text}`,
47
- pnpmProblem
48
- ? pnpm.ok
49
- ? `pnpm ${pnpm.text}(工坊按 10 方言)`
50
- : 'pnpm 缺(只影响工坊 build/install)'
51
- : `pnpm ${pnpm.text}`,
52
- ];
53
- return { ok: !nodeMissing, detail: parts.join(' · '), nodeMissing, npmMissing, pnpmProblem };
54
- }
55
- export const BRIDGE_ISSUE_ORDER = ['profile', 'stale', 'daemon', 'extension', 'unknown'];
56
- export function readBridgeReport(stdout) {
57
- const text = String(stdout ?? '');
58
- const ok = /^\[OK\] Daemon:/m.test(text) && /^\[OK\] Extension:/m.test(text) && /^\[OK\] Connectivity:/m.test(text);
59
- if (ok) {
60
- const good = text.split('\n').find((line) => line.trim() === 'Everything looks good!');
61
- return { ok: true, issues: [], detail: good?.trim() ?? tailLines(text, 1) };
62
- }
63
- const found = new Set();
64
- if (!/^\[OK\] Daemon:/m.test(text))
65
- found.add('daemon');
66
- if (!/^\[OK\] Extension:/m.test(text))
67
- found.add('extension');
68
- if (/Multiple Chrome profiles are connected/i.test(text))
69
- found.add('profile');
70
- if (/Default browser profile is stale|Selected browser profile is not connected/i.test(text)) {
71
- found.add('stale');
72
- }
73
- if (found.has('profile') || found.has('stale'))
74
- found.delete('extension');
75
- if (found.size === 0)
76
- found.add('unknown');
77
- return { ok: false, issues: BRIDGE_ISSUE_ORDER.filter((issue) => found.has(issue)), detail: issuesSection(text) };
78
- }
79
- function issuesSection(text) {
80
- const lines = text.split('\n');
81
- const start = lines.findIndex((line) => /^Issues:/.test(line.trim()));
82
- if (start < 0)
83
- return tailLines(text, 6);
84
- return lines.slice(start).join('\n').trim();
85
- }
19
+ import { readBridgeReport } from './bridge-report.js';
20
+ export { BRIDGE_ISSUE_ORDER, readBridgeReport } from './bridge-report.js';
86
21
  export function opencliInstallCommandDefault() {
87
22
  return opencliInstallCommand();
88
23
  }
@@ -108,43 +43,91 @@ export function createPreflight(options) {
108
43
  ...(options.probeDiy === undefined ? [] : [step('diy')]),
109
44
  ...(warmReads().length === 0 ? [] : [step('warm')]),
110
45
  ];
46
+ let logLines = [];
47
+ const stepLogs = new Map();
48
+ function appendLog(stepId, text) {
49
+ const stepLines = stepLogs.get(stepId) ?? [];
50
+ for (const line of String(text).split('\n')) {
51
+ if (line.trim() === '')
52
+ continue;
53
+ const clipped = line.length > 400 ? `${line.slice(0, 400)}…` : line;
54
+ stepLines.push(clipped);
55
+ logLines.push(clipped);
56
+ }
57
+ if (stepLines.length > 200)
58
+ stepLines.splice(0, stepLines.length - 200);
59
+ if (logLines.length > 200)
60
+ logLines = logLines.slice(-200);
61
+ stepLogs.set(stepId, stepLines);
62
+ set({
63
+ steps: current.steps.map((item) => item.id === stepId ? { ...item, log: [...stepLines] } : item),
64
+ });
65
+ }
111
66
  let current = {
112
67
  phase: 'idle',
113
68
  steps: steps(),
114
69
  at: Date.now(),
70
+ log: [],
115
71
  ...(options.desktopBrowser === undefined ? {} : { canWakeBrowser: true }),
116
72
  };
117
73
  let running = null;
74
+ let shellStalled = false;
118
75
  function set(next) {
119
- current = { ...current, ...next, at: Date.now() };
76
+ current = { ...current, ...next, log: [...logLines], at: Date.now() };
120
77
  return current;
121
78
  }
122
79
  function patchStep(id, patch) {
123
80
  return set({ steps: current.steps.map((item) => (item.id === id ? { ...item, ...patch } : item)) });
124
81
  }
125
- async function exec(commandLine, timeoutMs) {
82
+ function hostFailure(detail) {
83
+ return set({ phase: 'failed', error: detail, failure: 'host', guidance: 'host' });
84
+ }
85
+ async function exec(stepId, commandLine, timeoutMs) {
86
+ if (shellStalled) {
87
+ return { kind: 'stalled', detail: `前一条命令就没交回结果,这一步没跑:${commandLine}` };
88
+ }
89
+ appendLog(stepId, `> ${commandLine}`);
126
90
  try {
127
91
  const result = await runShell(shell, commandLine, { timeoutMs });
92
+ if (result.stalled) {
93
+ shellStalled = true;
94
+ appendLog(stepId, `没返回:${commandLine}`);
95
+ return { kind: 'stalled', detail: `没有返回(宿主没把结果交回来):${commandLine}` };
96
+ }
128
97
  if (result.timedOut) {
98
+ appendLog(stepId, `超时(${String(timeoutMs)} ms):${commandLine}`);
129
99
  return { kind: 'error', detail: `超时(${String(timeoutMs)} ms):${commandLine}` };
130
100
  }
101
+ const output = result.stdout || result.stderr;
102
+ if (output.trim() !== '')
103
+ appendLog(stepId, tailLines(output, 12));
131
104
  if (result.exitCode !== 0) {
132
105
  return { kind: 'nonzero', detail: tailLines(result.stderr || result.stdout, 4) };
133
106
  }
134
107
  return { kind: 'ok', stdout: result.stdout };
135
108
  }
136
109
  catch (error) {
110
+ appendLog(stepId, String(error));
137
111
  return { kind: 'error', detail: String(error) };
138
112
  }
139
113
  }
140
114
  async function probeVersion() {
141
- const result = await exec(`${command} --version`, OPENCLI_TIMEOUT_MS);
142
- if (result.kind === 'ok')
143
- return { ok: true, missing: false, detail: result.stdout.trim().split('\n')[0] ?? '' };
144
- return { ok: false, missing: result.kind === 'nonzero', detail: result.detail };
115
+ const result = await exec('cli', `${command} --version`, PREFLIGHT_VERSION_TIMEOUT_MS);
116
+ if (result.kind === 'ok') {
117
+ return { ok: true, missing: false, detail: result.stdout.trim().split('\n')[0] ?? '', stalled: false };
118
+ }
119
+ return {
120
+ ok: false,
121
+ missing: result.kind === 'nonzero',
122
+ detail: result.detail,
123
+ stalled: result.kind === 'stalled',
124
+ };
145
125
  }
146
126
  async function probeBridge() {
147
- const result = await exec(`${command} doctor`, OPENCLI_TIMEOUT_MS);
127
+ const result = await exec('bridge', `${command} doctor`, OPENCLI_TIMEOUT_MS);
128
+ if (result.kind === 'stalled') {
129
+ return { ok: false, detail: result.detail, issues: ['unknown'], stalled: true };
130
+ }
148
131
  if (result.kind !== 'ok')
149
132
  return { ok: false, detail: result.detail, issues: ['unknown'] };
150
133
  const report = readBridgeReport(result.stdout);
@@ -153,9 +136,14 @@ export function createPreflight(options) {
153
136
  return { ...report, command: bridgeFixCommand(command, report.issues) };
154
137
  }
155
138
  async function installOpencli() {
156
- if (options.install !== undefined)
157
- return options.install();
158
- const result = await exec(installCommand, INSTALL_TIMEOUT_MS);
139
+ if (options.install !== undefined) {
140
+ appendLog('cli', '> 安装 opencli');
141
+ const installed = await options.install();
142
+ if (installed.detail.trim() !== '')
143
+ appendLog('cli', installed.detail);
144
+ return installed;
145
+ }
146
+ const result = await exec('cli', installCommand, INSTALL_TIMEOUT_MS);
159
147
  if (result.kind !== 'ok')
160
148
  return { ok: false, detail: result.detail };
161
149
  return { ok: true, detail: tailLines(result.stdout, 1) };
@@ -170,9 +158,9 @@ export function createPreflight(options) {
170
158
  }
171
159
  patchStep('warm', {
172
160
  status: 'running',
173
- action: `${command} <套件自己声明的那条读命令> --site-session persistent -f json`,
161
+ action: '预热浏览器站点',
174
162
  });
175
- const results = await Promise.all(reads.map((read) => exec(opencliCommandLine(command, read), WARM_TIMEOUT_MS)));
163
+ const results = await Promise.all(reads.map((read) => exec('warm', opencliCommandLine(command, read), WARM_TIMEOUT_MS)));
176
164
  const failed = reads.filter((_, index) => results[index]?.kind !== 'ok');
177
165
  patchStep('warm', {
178
166
  status: failed.length === 0 ? 'ok' : 'warn',
@@ -206,6 +194,9 @@ export function createPreflight(options) {
206
194
  return { rows: verdict.rows, detail: verdict.detail, command: DIY_UPGRADE_COMMAND };
207
195
  }
208
196
  async function doRun() {
197
+ shellStalled = false;
198
+ logLines = [];
199
+ stepLogs.clear();
209
200
  set({
210
201
  phase: 'checking',
211
202
  error: undefined,
@@ -227,30 +218,20 @@ export function createPreflight(options) {
227
218
  nodeMissing: true,
228
219
  npmMissing: true,
229
220
  pnpmProblem: true,
221
+ stalled: false,
230
222
  }));
231
- if (toolchain !== undefined && toolchain.ok && toolchain.npmMissing && options.provisionNpm !== undefined) {
232
- set({ phase: 'installing' });
233
- patchStep('toolchain', { status: 'running', action: `装本地 npm@${LOCAL_NPM_VERSION}` });
234
- const provisioned = await options.provisionNpm().catch((error) => ({
235
- ok: false,
236
- detail: String(error),
237
- }));
238
- if (provisioned.ok) {
239
- const reprobed = await options.probeToolchain?.();
240
- if (reprobed !== undefined)
241
- toolchain = reprobed;
242
- }
243
- else {
244
- toolchain = { ...toolchain, detail: `${toolchain.detail};本地 npm 没装成:${provisioned.detail}` };
245
- }
246
- }
223
+ if (toolchain !== undefined && toolchain.detail.trim() !== '')
224
+ appendLog('toolchain', toolchain.detail);
247
225
  if (toolchain !== undefined && toolchain.ok && toolchain.pnpmProblem && options.provisionPnpm !== undefined) {
248
226
  set({ phase: 'installing' });
249
227
  patchStep('toolchain', { status: 'running', action: `装本地 pnpm@${LOCAL_PNPM_VERSION}` });
228
+ appendLog('toolchain', '> 安装本地 pnpm');
250
229
  const provisioned = await options.provisionPnpm().catch((error) => ({
251
230
  ok: false,
252
231
  detail: String(error),
253
232
  }));
233
+ if (provisioned.detail.trim() !== '')
234
+ appendLog('toolchain', provisioned.detail);
254
235
  if (provisioned.ok) {
255
236
  const reprobed = await options.probeToolchain?.();
256
237
  if (reprobed !== undefined)
@@ -263,35 +244,39 @@ export function createPreflight(options) {
263
244
  if (toolchain !== undefined) {
264
245
  set({ phase: 'checking' });
265
246
  patchStep('toolchain', {
266
- status: toolchain.ok ? (toolchain.npmMissing || toolchain.pnpmProblem ? 'warn' : 'ok') : 'failed',
247
+ status: toolchain.ok && !toolchain.pnpmProblem ? 'ok' : 'failed',
267
248
  detail: toolchain.detail,
268
249
  action: undefined,
269
250
  });
270
- if (!toolchain.ok) {
271
- return set({
272
- phase: 'failed',
273
- error: toolchain.detail,
274
- failure: 'missing',
275
- guidance: 'install',
276
- });
251
+ if (!toolchain.ok || toolchain.pnpmProblem) {
252
+ return toolchain.stalled === true
253
+ ? hostFailure(toolchain.detail)
254
+ : set({ phase: 'failed', error: toolchain.detail, failure: 'missing', guidance: 'install' });
277
255
  }
278
256
  }
279
257
  if (options.runtimeCheck !== undefined) {
258
+ appendLog('runtime', '> 检查运行环境');
280
259
  patchStep('runtime', { status: 'running' });
281
260
  const verdict = await options.runtimeCheck().catch((error) => ({
282
261
  ok: false,
283
262
  detail: error instanceof Error ? error.message : String(error),
284
263
  }));
264
+ if (verdict.detail.trim() !== '')
265
+ appendLog('runtime', verdict.detail);
285
266
  patchStep('runtime', { status: verdict.ok ? 'ok' : 'failed', detail: verdict.detail, action: undefined });
286
267
  if (!verdict.ok)
287
268
  return set({ phase: 'failed', error: verdict.detail, failure: 'missing', guidance: 'install' });
288
269
  }
289
270
  const diyStarted = options.probeDiy?.();
290
271
  if (withConsole) {
291
- patchStep('cli', { status: 'running', action: `${command} --version` });
272
+ patchStep('cli', { status: 'running', action: '检查 opencli 版本' });
292
273
  if (diyStarted !== undefined)
293
274
  patchStep('diy', { status: 'running' });
294
275
  let version = await probeVersion();
276
+ if (version.stalled) {
277
+ patchStep('cli', { status: 'failed', detail: version.detail, action: undefined });
278
+ return hostFailure(version.detail);
279
+ }
295
280
  if (!version.ok && !version.missing) {
296
281
  patchStep('cli', { status: 'failed', detail: version.detail, action: undefined });
297
282
  return set({
@@ -312,7 +297,7 @@ export function createPreflight(options) {
312
297
  });
313
298
  }
314
299
  set({ phase: 'installing' });
315
- patchStep('cli', { status: 'running', action: installCommand, command: installCommand });
300
+ patchStep('cli', { status: 'running', action: '安装 opencli', command: installCommand });
316
301
  const installed = await installOpencli();
317
302
  if (!installed.ok) {
318
303
  patchStep('cli', {
@@ -327,8 +312,12 @@ export function createPreflight(options) {
327
312
  guidance: 'install',
328
313
  });
329
314
  }
330
- patchStep('cli', { status: 'running', action: `${command} --version` });
315
+ patchStep('cli', { status: 'running', action: '检查 opencli 版本' });
331
316
  version = await probeVersion();
317
+ if (version.stalled) {
318
+ patchStep('cli', { status: 'failed', detail: version.detail, action: undefined });
319
+ return hostFailure(version.detail);
320
+ }
332
321
  if (!version.ok) {
333
322
  patchStep('cli', { status: 'failed', detail: version.detail, command: installCommand });
334
323
  return set({
@@ -341,8 +330,12 @@ export function createPreflight(options) {
341
330
  }
342
331
  patchStep('cli', { status: 'ok', detail: version.detail, action: undefined });
343
332
  }
344
- patchStep('bridge', { status: 'running', action: `${command} doctor` });
333
+ patchStep('bridge', { status: 'running', action: '检查浏览器通道' });
345
334
  const bridge = withBridge ? await probeBridge() : { ok: true, detail: '', issues: [] };
335
+ if (bridge.stalled === true) {
336
+ patchStep('bridge', { status: 'failed', detail: bridge.detail, action: undefined });
337
+ return hostFailure(bridge.detail);
338
+ }
346
339
  if (withBridge && options.desktopBrowser !== undefined && bridge.ok) {
347
340
  await options.desktopBrowser.observe().catch(() => undefined);
348
341
  }
@@ -4,8 +4,8 @@ import { createRequire } from 'node:module';
4
4
  import writeFileAtomic from 'write-file-atomic';
5
5
  import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, rmSync, } from 'node:fs';
6
6
  import { delimiter, dirname, join, sep } from 'node:path';
7
- import { quoteShellArg } from '@dsh-workbench/shared/host';
8
- import { ensureToolchain, localNpmCache, provisionLocalPnpmSync } from './toolchain.js';
7
+ import { quoteShellArg, quoteShellArgFor } from '@dsh-workbench/shared/host';
8
+ import { desktopPnpmPath, desktopPnpmVersion, ensureToolchain, localNpmCache, provisionLocalPnpmSync } from './toolchain.js';
9
9
  export function opencliBinName(platform = process.platform) {
10
10
  return platform === 'win32' ? 'opencli.cmd' : 'opencli';
11
11
  }
@@ -125,6 +125,15 @@ export function prependPath(entryDir, env = process.env) {
125
125
  return true;
126
126
  }
127
127
  export function seedWorkbenchToolchain(root, log) {
128
+ const bundledPnpm = desktopPnpmPath();
129
+ const bundledVersion = desktopPnpmVersion()?.split('.')[0];
130
+ if (bundledPnpm !== undefined && bundledVersion === '10') {
131
+ process.env.DSH_WB_PNPM = `node ${quoteShellArgFor(bundledPnpm, process.platform)}`;
132
+ log(`pnpm 用 Desktop 自带的:${bundledPnpm}`);
133
+ }
134
+ else if (bundledPnpm !== undefined) {
135
+ log(`Desktop 自带 pnpm 版本不是 10,优先用 PATH 上的 pnpm`);
136
+ }
128
137
  const toolchain = ensureToolchain({ root, log });
129
138
  for (const file of toolchain.files)
130
139
  log(`工具链写下 ${file}`);
@@ -0,0 +1,10 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ export interface ToolchainReport {
3
+ readonly ok: boolean;
4
+ readonly detail: string;
5
+ readonly nodeMissing: boolean;
6
+ readonly npmMissing: boolean;
7
+ readonly pnpmProblem: boolean;
8
+ readonly stalled?: boolean | undefined;
9
+ }
10
+ export declare function probeDesktopToolchain(shell: DshShell): Promise<ToolchainReport>;
@@ -0,0 +1,60 @@
1
+ import { runShell } from '@dsh-workbench/shared/host';
2
+ import { quoteShellArg } from '@dsh-workbench/shared/host';
3
+ import { desktopPnpmPath, desktopPnpmVersion } from './toolchain.js';
4
+ import { hostSurface } from './platform.js';
5
+ async function toolVersion(shell, line) {
6
+ try {
7
+ const result = await runShell(shell, line, { timeoutMs: 10_000 });
8
+ const text = (result.stdout || result.stderr).trim().split('\n')[0] ?? '';
9
+ return { ok: result.exitCode === 0 && !result.timedOut, text, stalled: result.stalled };
10
+ }
11
+ catch {
12
+ return { ok: false, text: '', stalled: false };
13
+ }
14
+ }
15
+ export async function probeDesktopToolchain(shell) {
16
+ const bundledPnpm = desktopPnpmPath();
17
+ const bundledMajor = desktopPnpmVersion()?.split('.')[0];
18
+ const preferBundled = bundledPnpm !== undefined && bundledMajor === '10';
19
+ const pnpmLine = preferBundled
20
+ ? `node ${quoteShellArg(bundledPnpm ?? '')} --version`
21
+ : 'pnpm --version';
22
+ const [node, npm, pnpm] = await Promise.all([
23
+ toolVersion(shell, 'node --version'),
24
+ toolVersion(shell, 'npm --version'),
25
+ toolVersion(shell, pnpmLine),
26
+ ]);
27
+ if (node.stalled || npm.stalled || pnpm.stalled) {
28
+ return {
29
+ ok: false,
30
+ detail: '版本探测没有返回:node / npm / pnpm 的命令都没把结果交回来(不是没装)',
31
+ nodeMissing: false,
32
+ npmMissing: false,
33
+ pnpmProblem: false,
34
+ stalled: true,
35
+ };
36
+ }
37
+ const nodeMissing = !node.ok;
38
+ const npmMissing = !npm.ok;
39
+ const pnpmMajor = /^v?(\d+)/.exec(pnpm.text)?.[1];
40
+ const pnpmProblem = !pnpm.ok || pnpmMajor !== '10';
41
+ const desktop = hostSurface({ proc: process }).deployment === 'desktop';
42
+ const parts = [
43
+ nodeMissing
44
+ ? desktop
45
+ ? 'node 不在 PATH(Desktop 自带 Node 的 shim 没生效)'
46
+ : 'node 不在 PATH'
47
+ : desktop
48
+ ? `node ${node.text}(Desktop 自带)`
49
+ : `node ${node.text}`,
50
+ npmMissing ? 'npm 缺(已改用 pnpm)' : `npm ${npm.text}`,
51
+ pnpmProblem
52
+ ? pnpm.ok
53
+ ? `pnpm ${pnpm.text}(工坊按 10 方言)`
54
+ : 'pnpm 缺(装不了 opencli/插件/模块库)'
55
+ : preferBundled
56
+ ? `pnpm ${pnpm.text}(Desktop 自带)`
57
+ : `pnpm ${pnpm.text}`,
58
+ ];
59
+ return { ok: !nodeMissing, detail: parts.join(' · '), nodeMissing, npmMissing, pnpmProblem };
60
+ }
@@ -14,6 +14,7 @@ export interface PnpmProcess {
14
14
  readonly resourcesPath?: string | undefined;
15
15
  }
16
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;
17
18
  export interface ToolchainOptions {
18
19
  readonly root: string;
19
20
  readonly proc?: ToolchainProcess | undefined;
@@ -68,6 +69,8 @@ export declare function provisionLocalPnpm(options: {
68
69
  readonly platform?: NodeJS.Platform | undefined;
69
70
  readonly proc?: PnpmProcess | undefined;
70
71
  readonly exists?: ((path: string) => boolean) | undefined;
72
+ readonly pnpm?: string | undefined;
73
+ readonly npm?: string | undefined;
71
74
  }): Promise<LocalPnpmProvision>;
72
75
  export declare function provisionLocalPnpmSync(options: {
73
76
  readonly root: string;
package/dist/toolchain.js CHANGED
@@ -14,6 +14,23 @@ export function desktopPnpmPath(proc = process, exists = existsSync) {
14
14
  const path = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.cjs');
15
15
  return exists(path) ? path : undefined;
16
16
  }
17
+ export function desktopPnpmVersion(proc = process, exists = existsSync) {
18
+ if (hostSurface({ proc }).deployment !== 'desktop')
19
+ return undefined;
20
+ const resources = proc.resourcesPath;
21
+ if (resources === undefined || resources === '')
22
+ return undefined;
23
+ const manifest = join(resources, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'package.json');
24
+ if (!exists(manifest))
25
+ return undefined;
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
28
+ return typeof parsed.version === 'string' ? parsed.version : undefined;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
17
34
  export const LOCAL_NPM_VERSION = [10, 9, 4].join('.');
18
35
  export const LOCAL_PNPM_VERSION = [10, 32, 1].join('.');
19
36
  export function localNpmRoot(root) {
@@ -75,27 +92,43 @@ export function localPnpmInstallCommand(root, proc = process, exists = existsSyn
75
92
  `pnpm@${LOCAL_PNPM_VERSION} --store-dir ${quoteShellArgFor(join(root, 'pnpm-store'), platform)}`);
76
93
  }
77
94
  export async function provisionLocalPnpm(options) {
78
- const command = localPnpmInstallCommand(options.root, options.proc ?? process, options.exists ?? existsSync);
79
- if (command === undefined)
80
- return { ok: false, detail: '找不到 Desktop 自带 pnpm,装不了本地 pnpm 10' };
95
+ const platform = options.platform ?? process.platform;
96
+ const bootstrap = options.pnpm?.trim();
97
+ const pnpmCommand = bootstrap === undefined || bootstrap === ''
98
+ ? localPnpmInstallCommand(options.root, options.proc ?? process, options.exists ?? existsSync)
99
+ : `${bootstrap} add --dir ${quoteShellArgFor(localPnpmRoot(options.root), platform)} pnpm@${LOCAL_PNPM_VERSION} --store-dir ${quoteShellArgFor(join(options.root, 'pnpm-store'), platform)}`;
100
+ const npm = options.npm?.trim();
101
+ const npmCommand = npm === undefined || npm === ''
102
+ ? undefined
103
+ : `${npm} install --prefix ${quoteShellArgFor(localPnpmRoot(options.root), platform)} pnpm@${LOCAL_PNPM_VERSION} --cache ${quoteShellArgFor(join(options.root, 'npm-cache'), platform)} --no-audit --no-fund`;
104
+ const commands = [pnpmCommand, npmCommand].filter((item) => item !== undefined);
105
+ if (commands.length === 0)
106
+ return { ok: false, detail: '找不到可用的 pnpm 或 npm,装不了本地 pnpm 10' };
81
107
  try {
82
108
  mkdirSync(localPnpmRoot(options.root), { recursive: true });
83
109
  }
84
110
  catch (error) {
85
111
  return { ok: false, detail: `建不了 ${localPnpmRoot(options.root)}:${String(error)}` };
86
112
  }
87
- const result = await options.run(command);
88
- if (!result.ok)
89
- return { ok: false, detail: result.detail };
90
- const platform = options.platform ?? process.platform;
91
- const installed = options.installed?.() ?? existsSync(localPnpmCommand(options.root, platform));
92
- if (!installed)
93
- return { ok: false, detail: 'pnpm 跑完但本地 pnpm 10 不在' };
94
- return {
95
- ok: true,
96
- binDir: localPnpmBin(options.root),
97
- detail: `pnpm@${LOCAL_PNPM_VERSION} 装到 ${localPnpmRoot(options.root)}`,
98
- };
113
+ let detail = '';
114
+ for (const command of commands) {
115
+ const result = await options.run(command);
116
+ if (result.ok) {
117
+ const installed = options.installed?.() ?? existsSync(localPnpmCommand(options.root, platform));
118
+ if (installed) {
119
+ return {
120
+ ok: true,
121
+ binDir: localPnpmBin(options.root),
122
+ detail: `pnpm@${LOCAL_PNPM_VERSION} 装到 ${localPnpmRoot(options.root)}`,
123
+ };
124
+ }
125
+ detail = 'pnpm 跑完但本地 pnpm 10 不在';
126
+ }
127
+ else {
128
+ detail = result.detail;
129
+ }
130
+ }
131
+ return { ok: false, detail };
99
132
  }
100
133
  export function provisionLocalPnpmSync(options) {
101
134
  const command = localPnpmInstallCommand(options.root, options.proc ?? process, options.exists ?? existsSync);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/adapter-probe",
3
- "version": "1.1.12",
3
+ "version": "1.1.14-beta.0",
4
4
  "description": "Workbench adapter-probe adapter.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,10 +16,10 @@
16
16
  "execa": "^10.2.0",
17
17
  "proper-lockfile": "^4.1.2",
18
18
  "write-file-atomic": "^8.0.0",
19
- "@dsh-workbench/shared": "1.1.12",
20
- "@dsh-workbench/adapter-runtime": "1.1.12",
21
- "@dsh-workbench/adapter-profile": "1.1.12",
22
- "@dsh-workbench/adapter-supply": "1.1.12"
19
+ "@dsh-workbench/adapter-profile": "1.1.14-beta.0",
20
+ "@dsh-workbench/adapter-runtime": "1.1.14-beta.0",
21
+ "@dsh-workbench/adapter-supply": "1.1.14-beta.0",
22
+ "@dsh-workbench/shared": "1.1.14-beta.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.19.0",