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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/install.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { isolatedInstallCommand, isolatedPnpmInstallCommand } from '@dsh-workbench/adapter-runtime';
3
- import { desktopPnpmPath } from './toolchain.js';
3
+ import { desktopPnpmPath, desktopPnpmVersion } 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
- return pnpm === undefined ? isolatedInstallCommand() : isolatedPnpmInstallCommand(pnpm);
9
+ const major = Number(desktopPnpmVersion(proc, exists)?.split('.')[0] ?? '0');
10
+ return pnpm === undefined || (major !== 0 && major < 10)
11
+ ? isolatedInstallCommand()
12
+ : isolatedPnpmInstallCommand(pnpm);
7
13
  }
@@ -8,7 +8,7 @@ import { probeDesktopToolchain } from './toolchain-probe.js';
8
8
  import { createDesktopBrowser } from './browser-wake.js';
9
9
  import { profileDirOf } from '@dsh-workbench/adapter-supply';
10
10
  import { prependPath, runtimeSeedRootOf } from './runtime-seed.js';
11
- import { localNpmCache, provisionLocalNpm, provisionLocalPnpm } from './toolchain.js';
11
+ import { localPnpmCommand, provisionLocalPnpm } from './toolchain.js';
12
12
  function desktopBrowserOf(input) {
13
13
  const dir = profileDirOf(input.ctx);
14
14
  const surface = hostSurface({ profile: dir === null ? undefined : basename(dir) });
@@ -30,8 +30,6 @@ export function registerPreflight(input) {
30
30
  shell: input.shell,
31
31
  command,
32
32
  });
33
- const profileDir = profileDirOf(input.ctx);
34
- const surface = hostSurface({ profile: profileDir === null ? undefined : basename(profileDir) });
35
33
  const preflight = createPreflight({
36
34
  shell: input.shell,
37
35
  command,
@@ -42,51 +40,29 @@ export function registerPreflight(input) {
42
40
  ...(input.runtimeCheck === undefined ? {} : { runtimeCheck: input.runtimeCheck }),
43
41
  ...(input.runtimeLabel === undefined ? {} : { runtimeLabel: input.runtimeLabel }),
44
42
  probeDiy: createDiyPresetProbe(input.ctx),
45
- ...(surface.deployment === 'desktop'
46
- ? {
47
- probeToolchain: () => probeDesktopToolchain(input.shell),
48
- provisionNpm: async () => {
49
- const root = runtimeSeedRootOf(input.ctx);
50
- if (root === null)
51
- return { ok: false, detail: '找不到工作台根' };
52
- const result = await provisionLocalNpm({
53
- root,
54
- run: async (command) => {
55
- const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
56
- return {
57
- ok: ran.exitCode === 0 && !ran.timedOut,
58
- detail: tailLines(ran.stderr || ran.stdout, 3),
59
- };
60
- },
61
- });
62
- if (result.ok && result.binDir !== undefined) {
63
- prependPath(result.binDir);
64
- if (process.env.npm_config_cache === undefined) {
65
- process.env.npm_config_cache = localNpmCache(root);
66
- }
67
- }
68
- return result;
69
- },
70
- provisionPnpm: async () => {
71
- const root = runtimeSeedRootOf(input.ctx);
72
- if (root === null)
73
- return { ok: false, detail: '找不到工作台根' };
74
- const result = await provisionLocalPnpm({
75
- root,
76
- run: async (command) => {
77
- const ran = await runShell(input.shell, command, { timeoutMs: INSTALL_TIMEOUT_MS });
78
- return {
79
- ok: ran.exitCode === 0 && !ran.timedOut,
80
- detail: tailLines(ran.stderr || ran.stdout, 3),
81
- };
82
- },
83
- });
84
- if (result.ok && result.binDir !== undefined)
85
- prependPath(result.binDir);
86
- 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
+ };
87
58
  },
59
+ });
60
+ if (result.ok && result.binDir !== undefined) {
61
+ prependPath(result.binDir);
62
+ process.env.DSH_WB_PNPM = localPnpmCommand(root);
88
63
  }
89
- : {}),
64
+ return result;
65
+ },
90
66
  ...(desktopBrowser === undefined ? {} : { desktopBrowser }),
91
67
  });
92
68
  input.routes.preflight = preflightHandler(preflight);
@@ -14,14 +14,11 @@ export interface PreflightStep {
14
14
  readonly detail?: string;
15
15
  readonly action?: string;
16
16
  readonly command?: string;
17
+ readonly log?: readonly string[];
17
18
  }
18
- export type PreflightBridgeIssue = 'daemon' | 'extension' | 'profile' | 'stale' | 'unknown';
19
- export declare const BRIDGE_ISSUE_ORDER: readonly PreflightBridgeIssue[];
20
- export declare function readBridgeReport(stdout: string): {
21
- readonly ok: boolean;
22
- readonly issues: readonly PreflightBridgeIssue[];
23
- readonly detail: string;
24
- };
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';
25
22
  export interface PreflightDiyGap {
26
23
  readonly rows: readonly string[];
27
24
  readonly detail: string;
@@ -37,6 +34,7 @@ export interface PreflightSnapshot {
37
34
  readonly bridgeIssues?: readonly PreflightBridgeIssue[];
38
35
  readonly diyGap?: PreflightDiyGap;
39
36
  readonly canWakeBrowser?: boolean;
37
+ readonly log?: readonly string[];
40
38
  }
41
39
  export interface Preflight {
42
40
  run(options?: {
package/dist/preflight.js CHANGED
@@ -4,7 +4,7 @@ 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 { 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,37 +16,8 @@ export function classifyFailure(text) {
16
16
  return 'missing';
17
17
  return 'unknown';
18
18
  }
19
- export const BRIDGE_ISSUE_ORDER = ['profile', 'stale', 'daemon', 'extension', 'unknown'];
20
- export function readBridgeReport(stdout) {
21
- const text = String(stdout ?? '');
22
- const ok = /^\[OK\] Daemon:/m.test(text) && /^\[OK\] Extension:/m.test(text) && /^\[OK\] Connectivity:/m.test(text);
23
- if (ok) {
24
- const good = text.split('\n').find((line) => line.trim() === 'Everything looks good!');
25
- return { ok: true, issues: [], detail: good?.trim() ?? tailLines(text, 1) };
26
- }
27
- const found = new Set();
28
- if (!/^\[OK\] Daemon:/m.test(text))
29
- found.add('daemon');
30
- if (!/^\[OK\] Extension:/m.test(text))
31
- found.add('extension');
32
- if (/Multiple Chrome profiles are connected/i.test(text))
33
- found.add('profile');
34
- if (/Default browser profile is stale|Selected browser profile is not connected/i.test(text)) {
35
- found.add('stale');
36
- }
37
- if (found.has('profile') || found.has('stale'))
38
- found.delete('extension');
39
- if (found.size === 0)
40
- found.add('unknown');
41
- return { ok: false, issues: BRIDGE_ISSUE_ORDER.filter((issue) => found.has(issue)), detail: issuesSection(text) };
42
- }
43
- function issuesSection(text) {
44
- const lines = text.split('\n');
45
- const start = lines.findIndex((line) => /^Issues:/.test(line.trim()));
46
- if (start < 0)
47
- return tailLines(text, 6);
48
- return lines.slice(start).join('\n').trim();
49
- }
19
+ import { readBridgeReport } from './bridge-report.js';
20
+ export { BRIDGE_ISSUE_ORDER, readBridgeReport } from './bridge-report.js';
50
21
  export function opencliInstallCommandDefault() {
51
22
  return opencliInstallCommand();
52
23
  }
@@ -72,16 +43,37 @@ export function createPreflight(options) {
72
43
  ...(options.probeDiy === undefined ? [] : [step('diy')]),
73
44
  ...(warmReads().length === 0 ? [] : [step('warm')]),
74
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
+ }
75
66
  let current = {
76
67
  phase: 'idle',
77
68
  steps: steps(),
78
69
  at: Date.now(),
70
+ log: [],
79
71
  ...(options.desktopBrowser === undefined ? {} : { canWakeBrowser: true }),
80
72
  };
81
73
  let running = null;
82
74
  let shellStalled = false;
83
75
  function set(next) {
84
- current = { ...current, ...next, at: Date.now() };
76
+ current = { ...current, ...next, log: [...logLines], at: Date.now() };
85
77
  return current;
86
78
  }
87
79
  function patchStep(id, patch) {
@@ -90,30 +82,37 @@ export function createPreflight(options) {
90
82
  function hostFailure(detail) {
91
83
  return set({ phase: 'failed', error: detail, failure: 'host', guidance: 'host' });
92
84
  }
93
- async function exec(commandLine, timeoutMs) {
85
+ async function exec(stepId, commandLine, timeoutMs) {
94
86
  if (shellStalled) {
95
87
  return { kind: 'stalled', detail: `前一条命令就没交回结果,这一步没跑:${commandLine}` };
96
88
  }
89
+ appendLog(stepId, `> ${commandLine}`);
97
90
  try {
98
91
  const result = await runShell(shell, commandLine, { timeoutMs });
99
92
  if (result.stalled) {
100
93
  shellStalled = true;
94
+ appendLog(stepId, `没返回:${commandLine}`);
101
95
  return { kind: 'stalled', detail: `没有返回(宿主没把结果交回来):${commandLine}` };
102
96
  }
103
97
  if (result.timedOut) {
98
+ appendLog(stepId, `超时(${String(timeoutMs)} ms):${commandLine}`);
104
99
  return { kind: 'error', detail: `超时(${String(timeoutMs)} ms):${commandLine}` };
105
100
  }
101
+ const output = result.stdout || result.stderr;
102
+ if (output.trim() !== '')
103
+ appendLog(stepId, tailLines(output, 12));
106
104
  if (result.exitCode !== 0) {
107
105
  return { kind: 'nonzero', detail: tailLines(result.stderr || result.stdout, 4) };
108
106
  }
109
107
  return { kind: 'ok', stdout: result.stdout };
110
108
  }
111
109
  catch (error) {
110
+ appendLog(stepId, String(error));
112
111
  return { kind: 'error', detail: String(error) };
113
112
  }
114
113
  }
115
114
  async function probeVersion() {
116
- const result = await exec(`${command} --version`, PREFLIGHT_VERSION_TIMEOUT_MS);
115
+ const result = await exec('cli', `${command} --version`, PREFLIGHT_VERSION_TIMEOUT_MS);
117
116
  if (result.kind === 'ok') {
118
117
  return { ok: true, missing: false, detail: result.stdout.trim().split('\n')[0] ?? '', stalled: false };
119
118
  }
@@ -125,7 +124,7 @@ export function createPreflight(options) {
125
124
  };
126
125
  }
127
126
  async function probeBridge() {
128
- const result = await exec(`${command} doctor`, OPENCLI_TIMEOUT_MS);
127
+ const result = await exec('bridge', `${command} doctor`, OPENCLI_TIMEOUT_MS);
129
128
  if (result.kind === 'stalled') {
130
129
  return { ok: false, detail: result.detail, issues: ['unknown'], stalled: true };
131
130
  }
@@ -137,9 +136,14 @@ export function createPreflight(options) {
137
136
  return { ...report, command: bridgeFixCommand(command, report.issues) };
138
137
  }
139
138
  async function installOpencli() {
140
- if (options.install !== undefined)
141
- return options.install();
142
- 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);
143
147
  if (result.kind !== 'ok')
144
148
  return { ok: false, detail: result.detail };
145
149
  return { ok: true, detail: tailLines(result.stdout, 1) };
@@ -154,9 +158,9 @@ export function createPreflight(options) {
154
158
  }
155
159
  patchStep('warm', {
156
160
  status: 'running',
157
- action: `${command} <套件自己声明的那条读命令> --site-session persistent -f json`,
161
+ action: '预热浏览器站点',
158
162
  });
159
- 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)));
160
164
  const failed = reads.filter((_, index) => results[index]?.kind !== 'ok');
161
165
  patchStep('warm', {
162
166
  status: failed.length === 0 ? 'ok' : 'warn',
@@ -191,6 +195,8 @@ export function createPreflight(options) {
191
195
  }
192
196
  async function doRun() {
193
197
  shellStalled = false;
198
+ logLines = [];
199
+ stepLogs.clear();
194
200
  set({
195
201
  phase: 'checking',
196
202
  error: undefined,
@@ -214,29 +220,18 @@ export function createPreflight(options) {
214
220
  pnpmProblem: true,
215
221
  stalled: false,
216
222
  }));
217
- if (toolchain !== undefined && toolchain.ok && toolchain.npmMissing && options.provisionNpm !== undefined) {
218
- set({ phase: 'installing' });
219
- patchStep('toolchain', { status: 'running', action: `装本地 npm@${LOCAL_NPM_VERSION}` });
220
- const provisioned = await options.provisionNpm().catch((error) => ({
221
- ok: false,
222
- detail: String(error),
223
- }));
224
- if (provisioned.ok) {
225
- const reprobed = await options.probeToolchain?.();
226
- if (reprobed !== undefined)
227
- toolchain = reprobed;
228
- }
229
- else {
230
- toolchain = { ...toolchain, detail: `${toolchain.detail};本地 npm 没装成:${provisioned.detail}` };
231
- }
232
- }
223
+ if (toolchain !== undefined && toolchain.detail.trim() !== '')
224
+ appendLog('toolchain', toolchain.detail);
233
225
  if (toolchain !== undefined && toolchain.ok && toolchain.pnpmProblem && options.provisionPnpm !== undefined) {
234
226
  set({ phase: 'installing' });
235
227
  patchStep('toolchain', { status: 'running', action: `装本地 pnpm@${LOCAL_PNPM_VERSION}` });
228
+ appendLog('toolchain', '> 安装本地 pnpm');
236
229
  const provisioned = await options.provisionPnpm().catch((error) => ({
237
230
  ok: false,
238
231
  detail: String(error),
239
232
  }));
233
+ if (provisioned.detail.trim() !== '')
234
+ appendLog('toolchain', provisioned.detail);
240
235
  if (provisioned.ok) {
241
236
  const reprobed = await options.probeToolchain?.();
242
237
  if (reprobed !== undefined)
@@ -249,29 +244,32 @@ export function createPreflight(options) {
249
244
  if (toolchain !== undefined) {
250
245
  set({ phase: 'checking' });
251
246
  patchStep('toolchain', {
252
- status: toolchain.ok ? (toolchain.npmMissing || toolchain.pnpmProblem ? 'warn' : 'ok') : 'failed',
247
+ status: toolchain.ok && !toolchain.pnpmProblem ? 'ok' : 'failed',
253
248
  detail: toolchain.detail,
254
249
  action: undefined,
255
250
  });
256
- if (!toolchain.ok) {
251
+ if (!toolchain.ok || toolchain.pnpmProblem) {
257
252
  return toolchain.stalled === true
258
253
  ? hostFailure(toolchain.detail)
259
254
  : set({ phase: 'failed', error: toolchain.detail, failure: 'missing', guidance: 'install' });
260
255
  }
261
256
  }
262
257
  if (options.runtimeCheck !== undefined) {
258
+ appendLog('runtime', '> 检查运行环境');
263
259
  patchStep('runtime', { status: 'running' });
264
260
  const verdict = await options.runtimeCheck().catch((error) => ({
265
261
  ok: false,
266
262
  detail: error instanceof Error ? error.message : String(error),
267
263
  }));
264
+ if (verdict.detail.trim() !== '')
265
+ appendLog('runtime', verdict.detail);
268
266
  patchStep('runtime', { status: verdict.ok ? 'ok' : 'failed', detail: verdict.detail, action: undefined });
269
267
  if (!verdict.ok)
270
268
  return set({ phase: 'failed', error: verdict.detail, failure: 'missing', guidance: 'install' });
271
269
  }
272
270
  const diyStarted = options.probeDiy?.();
273
271
  if (withConsole) {
274
- patchStep('cli', { status: 'running', action: `${command} --version` });
272
+ patchStep('cli', { status: 'running', action: '检查 opencli 版本' });
275
273
  if (diyStarted !== undefined)
276
274
  patchStep('diy', { status: 'running' });
277
275
  let version = await probeVersion();
@@ -299,7 +297,7 @@ export function createPreflight(options) {
299
297
  });
300
298
  }
301
299
  set({ phase: 'installing' });
302
- patchStep('cli', { status: 'running', action: installCommand, command: installCommand });
300
+ patchStep('cli', { status: 'running', action: '安装 opencli', command: installCommand });
303
301
  const installed = await installOpencli();
304
302
  if (!installed.ok) {
305
303
  patchStep('cli', {
@@ -314,7 +312,7 @@ export function createPreflight(options) {
314
312
  guidance: 'install',
315
313
  });
316
314
  }
317
- patchStep('cli', { status: 'running', action: `${command} --version` });
315
+ patchStep('cli', { status: 'running', action: '检查 opencli 版本' });
318
316
  version = await probeVersion();
319
317
  if (version.stalled) {
320
318
  patchStep('cli', { status: 'failed', detail: version.detail, action: undefined });
@@ -332,7 +330,7 @@ export function createPreflight(options) {
332
330
  }
333
331
  patchStep('cli', { status: 'ok', detail: version.detail, action: undefined });
334
332
  }
335
- patchStep('bridge', { status: 'running', action: `${command} doctor` });
333
+ patchStep('bridge', { status: 'running', action: '检查浏览器通道' });
336
334
  const bridge = withBridge ? await probeBridge() : { ok: true, detail: '', issues: [] };
337
335
  if (bridge.stalled === true) {
338
336
  patchStep('bridge', { status: 'failed', detail: bridge.detail, action: undefined });
@@ -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, localPnpmCommand, 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,16 @@ 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 bundledMajor = Number(desktopPnpmVersion()?.split('.')[0] ?? '0');
130
+ const bundledUsable = bundledPnpm !== undefined && bundledMajor >= 10;
131
+ if (bundledPnpm !== undefined && bundledUsable) {
132
+ process.env.DSH_WB_PNPM = `node ${quoteShellArgFor(bundledPnpm, process.platform)}`;
133
+ log(`pnpm 用 Desktop 自带的:${bundledPnpm}`);
134
+ }
135
+ else if (bundledPnpm !== undefined) {
136
+ log(`Desktop 自带 pnpm 版本低于 10,优先用 PATH 上的 pnpm`);
137
+ }
128
138
  const toolchain = ensureToolchain({ root, log });
129
139
  for (const file of toolchain.files)
130
140
  log(`工具链写下 ${file}`);
@@ -141,12 +151,15 @@ export function seedWorkbenchToolchain(root, log) {
141
151
  if (toolchain.pnpmDir !== undefined) {
142
152
  if (prependPath(toolchain.pnpmDir))
143
153
  log(`PATH 前插 ${toolchain.pnpmDir}`);
154
+ if (!bundledUsable)
155
+ process.env.DSH_WB_PNPM = localPnpmCommand(root);
144
156
  }
145
- else if (toolchain.pathDir !== undefined) {
157
+ else if (!bundledUsable && toolchain.pathDir !== undefined) {
146
158
  const installed = provisionLocalPnpmSync({ root });
147
159
  if (installed.ok && installed.binDir !== undefined) {
148
160
  if (prependPath(installed.binDir))
149
161
  log(`PATH 前插 ${installed.binDir}`);
162
+ process.env.DSH_WB_PNPM = localPnpmCommand(root);
150
163
  log(`工具链 provision ${installed.detail}`);
151
164
  }
152
165
  else {
@@ -1,5 +1,7 @@
1
1
  import { runShell } from '@dsh-workbench/shared/host';
2
- import { desktopPnpmPath } from './toolchain.js';
2
+ import { quoteShellArg } from '@dsh-workbench/shared/host';
3
+ import { desktopPnpmPath, desktopPnpmVersion } from './toolchain.js';
4
+ import { hostSurface } from './platform.js';
3
5
  async function toolVersion(shell, line) {
4
6
  try {
5
7
  const result = await runShell(shell, line, { timeoutMs: 10_000 });
@@ -11,10 +13,16 @@ async function toolVersion(shell, line) {
11
13
  }
12
14
  }
13
15
  export async function probeDesktopToolchain(shell) {
16
+ const bundledPnpm = desktopPnpmPath();
17
+ const bundledMajor = desktopPnpmVersion()?.split('.')[0];
18
+ const preferBundled = bundledPnpm !== undefined && Number(bundledMajor ?? '0') >= 10;
19
+ const pnpmLine = preferBundled
20
+ ? `node ${quoteShellArg(bundledPnpm ?? '')} --version`
21
+ : 'pnpm --version';
14
22
  const [node, npm, pnpm] = await Promise.all([
15
23
  toolVersion(shell, 'node --version'),
16
24
  toolVersion(shell, 'npm --version'),
17
- toolVersion(shell, 'pnpm --version'),
25
+ toolVersion(shell, pnpmLine),
18
26
  ]);
19
27
  if (node.stalled || npm.stalled || pnpm.stalled) {
20
28
  return {
@@ -28,21 +36,25 @@ export async function probeDesktopToolchain(shell) {
28
36
  }
29
37
  const nodeMissing = !node.ok;
30
38
  const npmMissing = !npm.ok;
31
- const hasBundledPnpm = desktopPnpmPath() !== undefined;
32
- const pnpmMajor = /^v?(\d+)/.exec(pnpm.text)?.[1];
33
- const pnpmProblem = !pnpm.ok || pnpmMajor !== '10';
39
+ const pnpmMajor = Number(/^v?(\d+)/.exec(pnpm.text)?.[1] ?? '0');
40
+ const pnpmProblem = !pnpm.ok || pnpmMajor < 10;
41
+ const desktop = hostSurface({ proc: process }).deployment === 'desktop';
34
42
  const parts = [
35
- nodeMissing ? 'node 不在 PATH(Desktop 自带 Node 的 shim 没生效)' : `node ${node.text}(Desktop 自带)`,
36
- npmMissing
37
- ? hasBundledPnpm
38
- ? 'npm 缺(opencli 安装走 pnpm;插件安装/模块库仍需 npm)'
39
- : 'npm 缺(装不了 opencli:bundled pnpm 也没找到)'
40
- : `npm ${npm.text}`,
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}`,
41
51
  pnpmProblem
42
52
  ? pnpm.ok
43
53
  ? `pnpm ${pnpm.text}(工坊按 10 方言)`
44
- : 'pnpm 缺(只影响工坊 build/install)'
45
- : `pnpm ${pnpm.text}`,
54
+ : 'pnpm 缺(装不了 opencli/插件/模块库)'
55
+ : preferBundled
56
+ ? `pnpm ${pnpm.text}(Desktop 自带)`
57
+ : `pnpm ${pnpm.text}`,
46
58
  ];
47
59
  return { ok: !nodeMissing, detail: parts.join(' · '), nodeMissing, npmMissing, pnpmProblem };
48
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.13",
3
+ "version": "1.1.14-beta.1",
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.13",
20
- "@dsh-workbench/adapter-runtime": "1.1.13",
21
- "@dsh-workbench/adapter-profile": "1.1.13",
22
- "@dsh-workbench/adapter-supply": "1.1.13"
19
+ "@dsh-workbench/adapter-supply": "1.1.14-beta.1",
20
+ "@dsh-workbench/adapter-runtime": "1.1.14-beta.1",
21
+ "@dsh-workbench/shared": "1.1.14-beta.1",
22
+ "@dsh-workbench/adapter-profile": "1.1.14-beta.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.19.0",