@dsh-workbench/adapter-probe 0.0.0-stage → 1.0.0-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,190 @@
1
+ import { OPENCLI_TIMEOUT_MS, PROBE_TTL_MS, buildSupplyIndex, NEED_KIND_SPECS, needIdOf, commandNamesMatch, npmSourceOf, parseJsonLoose, quoteShellArg, runShell, } from '@dsh-workbench/shared/host';
2
+ import { npmRegistryOf } from '@dsh-workbench/adapter-supply';
3
+ import { probeRelease } from '@dsh-workbench/adapter-profile';
4
+ import { createTimedMemo } from './probe-cache.js';
5
+ import { probeLibs } from './probe-libs.js';
6
+ import { aggregate, binStateOf, envStateOf, extensionStateOf } from './probe-verdict.js';
7
+ const BINARY_PROBE_TIMEOUT_MS = 5_000;
8
+ const KNOWN_RUNTIMES = ['opencli'];
9
+ export function parsePluginList(stdout) {
10
+ const parsed = parseJsonLoose(stdout);
11
+ if (Array.isArray(parsed))
12
+ return parsed;
13
+ return /no plugins? installed/i.test(stdout) ? [] : undefined;
14
+ }
15
+ async function probeBinary(shell, name) {
16
+ const line = process.platform === 'win32' ? `where ${quoteShellArg(name)}` : `command -v ${quoteShellArg(name)}`;
17
+ const result = await runShell(shell, line, { timeoutMs: BINARY_PROBE_TIMEOUT_MS }).catch(() => undefined);
18
+ return result?.exitCode === 0;
19
+ }
20
+ async function runtimeList(options) {
21
+ if (!options.needsPlugins)
22
+ return {};
23
+ const result = await runShell(options.shell, `${options.command} plugin list -f json`, {
24
+ timeoutMs: OPENCLI_TIMEOUT_MS,
25
+ }).then((value) => ({ ok: true, stdout: value.exitCode === 0 ? value.stdout : '', stderr: value.stderr }), (error) => ({ ok: false, stdout: '', stderr: String(error) }));
26
+ if (!result.ok)
27
+ return { failure: result.stderr };
28
+ const list = parsePluginList(result.stdout);
29
+ return list === undefined ? { failure: 'plugin list 的输出读不懂(不是 JSON 数组)' } : { list };
30
+ }
31
+ function conflictOf(index, need) {
32
+ const entry = index.entryOf(need.kind, need.name);
33
+ if (entry?.conflicted !== true)
34
+ return undefined;
35
+ const conflict = index.conflicts.find((item) => item.names.includes(need.name) && item.key === entry.key);
36
+ return conflict?.detail ?? '来源声明有冲突';
37
+ }
38
+ async function releasesOf(index, options) {
39
+ const found = new Map();
40
+ for (const entry of index.entries.values()) {
41
+ if (entry.kind !== 'extension' || entry.conflicted)
42
+ continue;
43
+ const source = npmSourceOf(entry.sources);
44
+ if (source === undefined)
45
+ continue;
46
+ const registry = npmRegistryOf(source, process.env);
47
+ const signal = await probeRelease({
48
+ shell: options.shell,
49
+ package: entry.package,
50
+ ...(registry.ok ? { registry: registry.value } : {}),
51
+ force: options.force,
52
+ });
53
+ if (signal === undefined)
54
+ continue;
55
+ for (const name of entry.names)
56
+ found.set(`${entry.runtime ?? 'opencli'}:${name}`, signal);
57
+ }
58
+ return found;
59
+ }
60
+ const pendingProbes = new Map();
61
+ function cacheKeyOf(commandKey, units, unit) {
62
+ const scope = units.map((one) => one.unit).sort().join('|');
63
+ return `${commandKey}:${scope}:${unit}`;
64
+ }
65
+ function runtimeCommandOf(need, runtimeCommands) {
66
+ const resolved = runtimeCommands[need.runtime];
67
+ if (resolved !== undefined && resolved.trim() !== '')
68
+ return resolved;
69
+ return KNOWN_RUNTIMES.includes(need.runtime) ? need.runtime : undefined;
70
+ }
71
+ export async function probeUnits(options) {
72
+ const { shell, runtimeCommands, units, force = false } = options;
73
+ const commandKey = Object.values(runtimeCommands).sort().join('|');
74
+ const hits = units.map((unit) => unitCache.get(cacheKeyOf(commandKey, units, unit.unit)));
75
+ if (!force && hits.every((hit) => hit !== undefined)) {
76
+ return hits;
77
+ }
78
+ const key = `${commandKey}:${units.map((unit) => unit.unit).join('|')}`;
79
+ if (!force) {
80
+ const shared = pendingProbes.get(key);
81
+ if (shared !== undefined)
82
+ return shared;
83
+ }
84
+ const task = loadCapabilities({ shell, runtimeCommands, units, force });
85
+ pendingProbes.set(key, task);
86
+ try {
87
+ return await task;
88
+ }
89
+ finally {
90
+ if (pendingProbes.get(key) === task)
91
+ pendingProbes.delete(key);
92
+ }
93
+ }
94
+ async function loadCapabilities(options) {
95
+ const { shell, runtimeCommands, units, force } = options;
96
+ const index = buildSupplyIndex(units.map((unit) => ({ id: unit.unit, needs: unit.needs })));
97
+ const binNames = [
98
+ ...new Set(units.flatMap((unit) => unit.needs.filter((need) => need.kind === 'bin').map((need) => need.name.trim()).filter((name) => name !== ''))),
99
+ ];
100
+ const binaries = new Map();
101
+ const runtimeNames = Object.values(runtimeCommands);
102
+ await Promise.all(binNames.map(async (name) => {
103
+ const known = runtimeNames.some((command) => commandNamesMatch(name, command));
104
+ binaries.set(name, known ? true : await probeBinary(shell, name));
105
+ }));
106
+ const extensionNeeds = units.flatMap((unit) => unit.needs.filter((need) => need.kind === 'extension'));
107
+ const runtimes = [...new Set(extensionNeeds.map((need) => need.runtime))];
108
+ const lists = new Map();
109
+ await Promise.all(runtimes.map(async (runtime) => {
110
+ const command = runtimeCommands[runtime] ?? (KNOWN_RUNTIMES.includes(runtime) ? runtime : undefined);
111
+ if (command === undefined) {
112
+ lists.set(runtime, { failure: `工作台还不认识运行时 ${runtime}(没有对应的探法)` });
113
+ return;
114
+ }
115
+ const needsPlugins = extensionNeeds.some((need) => need.runtime === runtime && conflictOf(index, need) === undefined);
116
+ lists.set(runtime, await runtimeList({ shell, command, needsPlugins }));
117
+ }));
118
+ const libNeeds = units.flatMap((unit) => unit.needs.filter((need) => need.kind === 'lib'));
119
+ const libStates = await probeLibs({ needs: libNeeds, shell, force, index });
120
+ const releases = await releasesOf(index, { shell, force });
121
+ const statesOfUnit = (unit) => unit.needs.map((need) => {
122
+ const conflict = need.kind === 'extension' || need.kind === 'lib' ? conflictOf(index, need) : undefined;
123
+ if (conflict !== undefined) {
124
+ return {
125
+ id: needIdOf(need),
126
+ kind: need.kind,
127
+ name: need.name,
128
+ status: 'unchecked',
129
+ detail: `来源冲突:${conflict}`,
130
+ };
131
+ }
132
+ const probeClass = NEED_KIND_SPECS[need.kind].probe;
133
+ if (probeClass === 'bin' && need.kind === 'bin')
134
+ return binStateOf(need, binaries.get(need.name) === true);
135
+ if (probeClass === 'env' && need.kind === 'env')
136
+ return envStateOf(need, process.env[need.name]);
137
+ if (probeClass === 'package' && need.kind === 'lib') {
138
+ return (libStates.get(need.name) ?? {
139
+ id: needIdOf(need),
140
+ kind: 'lib',
141
+ name: need.name,
142
+ status: 'unchecked',
143
+ detail: '这个共用依赖没进这一轮探测',
144
+ });
145
+ }
146
+ if (need.kind !== 'extension') {
147
+ return {
148
+ id: needIdOf(need),
149
+ kind: need.kind,
150
+ name: need.name,
151
+ status: 'unchecked',
152
+ detail: '这个种类没有对应的探法 —— 升级工作台',
153
+ };
154
+ }
155
+ const probe = lists.get(need.runtime) ?? { failure: '没跑这一轮探测' };
156
+ const command = runtimeCommandOf(need, runtimeCommands) ?? need.runtime;
157
+ const hit = probe.list?.find((item) => typeof item === 'object' && item !== null && String(item.name) === need.name);
158
+ return extensionStateOf({
159
+ need,
160
+ ...(probe.list === undefined ? {} : { hit }),
161
+ ...(probe.failure === undefined ? {} : { failure: probe.failure }),
162
+ command,
163
+ ...(releases.get(`${need.runtime}:${need.name}`) === undefined
164
+ ? {}
165
+ : { release: releases.get(`${need.runtime}:${need.name}`) }),
166
+ sources: index.sourcesOf('extension', need.name),
167
+ });
168
+ });
169
+ return units.map((unit) => {
170
+ const capability = aggregate(unit.unit, unit.needs, statesOfUnit(unit));
171
+ unitCache.set(cacheKeyOf(Object.values(runtimeCommands).sort().join('|'), units, unit.unit), capability);
172
+ return capability;
173
+ });
174
+ }
175
+ export async function probeUnitCommands(options) {
176
+ const units = options.catalog === undefined ? [options.unit] : options.catalog;
177
+ const all = await probeUnits({
178
+ shell: options.shell,
179
+ runtimeCommands: options.runtimeCommands ?? {},
180
+ units,
181
+ ...(options.force === undefined ? {} : { force: options.force }),
182
+ });
183
+ const found = all.find((item) => item.unit === options.unit.unit);
184
+ return (found ?? aggregate(options.unit.unit, options.unit.needs, []));
185
+ }
186
+ const unitCache = createTimedMemo(PROBE_TTL_MS);
187
+ export const resetProbeCache = () => {
188
+ unitCache.clear();
189
+ pendingProbes.clear();
190
+ };
@@ -0,0 +1,16 @@
1
+ import type { BinNeed, EnvNeed, ExtensionNeed, Need, NeedState, PluginRelease, SupplySource, UnitCapability } from '@dsh-workbench/shared/host';
2
+ export declare function pluginUpdateCommand(plugin: string | undefined): string | undefined;
3
+ export declare function upgradeLine(need: ExtensionNeed, command: string, latest: string, sources: readonly SupplySource[]): string | undefined;
4
+ export declare function extensionStateOf(options: {
5
+ readonly need: ExtensionNeed;
6
+ readonly hit?: {
7
+ readonly commands?: unknown;
8
+ } | undefined;
9
+ readonly failure?: string | undefined;
10
+ readonly command: string;
11
+ readonly release?: PluginRelease | undefined;
12
+ readonly sources: readonly SupplySource[];
13
+ }): NeedState;
14
+ export declare function binStateOf(need: BinNeed, present: boolean): NeedState;
15
+ export declare function envStateOf(need: EnvNeed, value: string | undefined): NeedState;
16
+ export declare function aggregate(unit: string, needs: readonly Need[], states: readonly NeedState[]): UnitCapability;
@@ -0,0 +1,126 @@
1
+ import { needIdOf, quoteShellArg } from '@dsh-workbench/shared/host';
2
+ import { resolvePluginSource } from '@dsh-workbench/adapter-supply';
3
+ import { isNewerVersion } from '@dsh-workbench/adapter-profile';
4
+ const RANK = {
5
+ ok: 0,
6
+ unchecked: 1,
7
+ outdated: 2,
8
+ unsatisfied: 3,
9
+ missing: 4,
10
+ };
11
+ export function pluginUpdateCommand(plugin) {
12
+ return plugin === undefined
13
+ ? undefined
14
+ : `${quoteShellArg('opencli')} plugin update ${quoteShellArg(plugin)}`;
15
+ }
16
+ export function upgradeLine(need, command, latest, sources) {
17
+ for (const source of sources) {
18
+ const resolved = resolvePluginSource(source, process.env, {
19
+ command,
20
+ plugin: need.name,
21
+ version: latest,
22
+ mode: 'update',
23
+ });
24
+ if (resolved.ok)
25
+ return resolved.line;
26
+ }
27
+ return undefined;
28
+ }
29
+ function firstResolvable(need, command, sources) {
30
+ let problem = 'not-declared';
31
+ for (const source of sources) {
32
+ const resolved = resolvePluginSource(source, process.env, { command, plugin: need.name });
33
+ if (resolved.ok)
34
+ return { provision: 'ready', command: resolved.line };
35
+ problem = resolved.problem;
36
+ }
37
+ return { provision: problem };
38
+ }
39
+ export function extensionStateOf(options) {
40
+ const base = { id: needIdOf(options.need), kind: 'extension', name: options.need.name };
41
+ if (options.failure !== undefined) {
42
+ return { ...base, status: 'unchecked', detail: options.failure };
43
+ }
44
+ if (options.hit === undefined) {
45
+ const resolved = firstResolvable(options.need, options.command, options.sources);
46
+ return {
47
+ ...base,
48
+ status: 'missing',
49
+ detail: `已装插件里没有 ${options.need.name}`,
50
+ provision: resolved.provision,
51
+ ...(resolved.command === undefined ? {} : { command: resolved.command }),
52
+ };
53
+ }
54
+ const available = new Set((Array.isArray(options.hit.commands) ? options.hit.commands : []).map(String));
55
+ const missing = (options.need.commands ?? []).filter((name) => !available.has(name));
56
+ const release = options.release;
57
+ const behind = release?.latest !== undefined && isNewerVersion(release.latest, release.installed);
58
+ const outdated = missing.length > 0 || behind;
59
+ const upgrade = behind && release?.latest !== undefined
60
+ ? upgradeLine(options.need, options.command, release.latest, options.sources)
61
+ : undefined;
62
+ return {
63
+ ...base,
64
+ status: outdated ? 'outdated' : 'ok',
65
+ detail: outdated
66
+ ? missing.length > 0
67
+ ? `${options.need.name} 缺 ${missing.length} 条命令:${missing.join('、')}`
68
+ : `${options.need.name} 装的是 ${release?.installed ?? '未知'},源上最新 ${release?.latest ?? '未知'}`
69
+ : `${options.need.name} 的命令都在`,
70
+ ...(missing.length === 0 ? {} : { missing }),
71
+ ...(release?.installed === undefined ? {} : { installed: release.installed }),
72
+ ...(release?.latest === undefined ? {} : { latest: release.latest }),
73
+ ...(outdated || behind
74
+ ? {
75
+ command: upgrade ?? pluginUpdateCommand(options.need.name),
76
+ }
77
+ : {}),
78
+ };
79
+ }
80
+ export function binStateOf(need, present) {
81
+ return {
82
+ id: needIdOf(need),
83
+ kind: 'bin',
84
+ name: need.name,
85
+ status: present ? 'ok' : 'missing',
86
+ detail: present ? `${need.name} 命令在` : `这台机器上找不到 ${need.name} 命令(PATH 里没有)`,
87
+ };
88
+ }
89
+ export function envStateOf(need, value) {
90
+ const present = value !== undefined && value.trim() !== '';
91
+ return {
92
+ id: needIdOf(need),
93
+ kind: 'env',
94
+ name: need.name,
95
+ status: present ? 'ok' : 'missing',
96
+ detail: present
97
+ ? `${need.name} 已设置`
98
+ : `环境变量 ${need.name} 没设${need.secret === true ? '(值不会被任何界面回显)' : ''}`,
99
+ ...(need.secret === true ? { secret: true } : {}),
100
+ };
101
+ }
102
+ export function aggregate(unit, needs, states) {
103
+ const required = new Set(needs.filter((need) => need.required !== false).map((need) => needIdOf(need)));
104
+ let worst = 'ok';
105
+ for (const state of states) {
106
+ if (!required.has(state.id))
107
+ continue;
108
+ if (RANK[state.status] > RANK[worst])
109
+ worst = state.status;
110
+ }
111
+ const optionalGap = states.some((state) => state.status !== 'ok' && !required.has(state.id));
112
+ const status = worst === 'ok'
113
+ ? optionalGap
114
+ ? 'degraded'
115
+ : 'ok'
116
+ : worst === 'missing' || worst === 'unsatisfied'
117
+ ? 'missing'
118
+ : worst;
119
+ const first = states.find((state) => state.status !== 'ok');
120
+ return {
121
+ unit,
122
+ status,
123
+ needs: [...states],
124
+ detail: first === undefined ? '依赖都齐了' : first.detail,
125
+ };
126
+ }
@@ -0,0 +1,43 @@
1
+ import type { spawn } from 'node:child_process';
2
+ import type { DshHostContext } from '@dsh-workbench/shared';
3
+ export type SeedLog = (message: string) => void;
4
+ export interface RuntimeSeedPaths {
5
+ readonly root: string;
6
+ readonly entryDir: string;
7
+ readonly opencliRoot: string;
8
+ readonly opencliBinDir: string;
9
+ readonly opencliBin: string;
10
+ readonly opencliHome: string;
11
+ readonly workshop: string;
12
+ readonly workshopPackages: string;
13
+ readonly workshopManifest: string;
14
+ }
15
+ export interface RuntimeSeedOptions {
16
+ readonly root: string;
17
+ readonly home: string;
18
+ readonly packageRoot: string;
19
+ readonly platform?: NodeJS.Platform | undefined;
20
+ readonly log?: SeedLog;
21
+ }
22
+ export declare function opencliBinName(platform?: NodeJS.Platform): string;
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;
27
+ export declare function packageRootOf(moduleUrl: string): string;
28
+ export declare function isInstalledCopy(packageRoot: string): boolean;
29
+ export declare function opencliWrapperScript(packageRoot: string, home: string, platform?: NodeJS.Platform): string;
30
+ export interface SeedCommand {
31
+ readonly file: string;
32
+ readonly args: readonly string[];
33
+ readonly env: Readonly<Record<string, string>>;
34
+ readonly log: string;
35
+ }
36
+ export declare function workshopSeedPlan(packageRoot: string, home: string, root: string, platform?: NodeJS.Platform): {
37
+ readonly entry: SeedCommand;
38
+ readonly full: SeedCommand;
39
+ };
40
+ export declare function runDetached(command: SeedCommand, onError: SeedLog, spawnImpl?: typeof spawn): void;
41
+ export declare function prependPath(entryDir: string, env?: NodeJS.ProcessEnv): boolean;
42
+ export declare function seedWorkbenchToolchain(root: string, log: SeedLog): void;
43
+ export declare function seedRuntimeFiles(options: RuntimeSeedOptions): readonly string[];
@@ -0,0 +1,178 @@
1
+ import { execa } from 'execa';
2
+ import writeFileAtomic from 'write-file-atomic';
3
+ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, rmSync, } from 'node:fs';
4
+ import { delimiter, dirname, join, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { quoteShellArg } from '@dsh-workbench/shared/host';
7
+ import { ensureToolchain, localNpmCache, provisionLocalPnpmSync } from './toolchain.js';
8
+ export function opencliBinName(platform = process.platform) {
9
+ return platform === 'win32' ? 'opencli.cmd' : 'opencli';
10
+ }
11
+ export function runtimeSeedPaths(root, platform = process.platform) {
12
+ const opencliRoot = join(root, 'opencli');
13
+ const opencliBinDir = join(opencliRoot, 'bin');
14
+ const workshop = join(root, 'diy');
15
+ return {
16
+ root,
17
+ entryDir: join(root, 'bin'),
18
+ opencliRoot,
19
+ opencliBinDir,
20
+ opencliBin: join(opencliBinDir, opencliBinName(platform)),
21
+ opencliHome: join(opencliRoot, 'home'),
22
+ workshop,
23
+ workshopPackages: join(workshop, 'packages'),
24
+ workshopManifest: join(workshop, 'package.json'),
25
+ };
26
+ }
27
+ export function runtimeSeedRootOf(ctx) {
28
+ const configured = process.env.DSH_WORKBENCH_HOME?.trim();
29
+ if (configured !== undefined && configured !== '')
30
+ return configured;
31
+ const homePath = ctx.get('dshHomePath');
32
+ if (typeof homePath !== 'function')
33
+ return null;
34
+ try {
35
+ return homePath('workbench');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function coversWritableRoot(workspaceRoot, root) {
42
+ const prefix = workspaceRoot.endsWith(sep) ? workspaceRoot : `${workspaceRoot}${sep}`;
43
+ return root === workspaceRoot || root.startsWith(prefix);
44
+ }
45
+ export function warnUnwritableRoot(ctx) {
46
+ const root = runtimeSeedRootOf(ctx);
47
+ if (root === null)
48
+ return;
49
+ const workspaceRoot = ctx.get('sandboxPolicy')?.workspaceRoot;
50
+ if (workspaceRoot === undefined || coversWritableRoot(workspaceRoot, root))
51
+ return;
52
+ ctx.logger?.warn?.(`[dsh-workbench] 可写根 ${workspaceRoot} 盖不住 ${root}:市场装插件、自举命令可能被沙箱挡(把 profile/sandbox.patch.yml 那一行写进这个 profile 的 cordis.patch.yml 再重启)`);
53
+ }
54
+ export function packageRootOf(moduleUrl) {
55
+ return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
56
+ }
57
+ export function isInstalledCopy(packageRoot) {
58
+ return packageRoot.split(sep).includes('node_modules');
59
+ }
60
+ export function opencliWrapperScript(packageRoot, home, platform = process.platform) {
61
+ const script = join(packageRoot, 'bin', 'opencli-isolated.mjs');
62
+ if (platform === 'win32') {
63
+ return `@echo off\r\nset "DSH_HOME=${home}"\r\nnode "${script}" %*\r\n`;
64
+ }
65
+ return `#!/bin/sh\nexport DSH_HOME=${quoteShellArg(home)}\nexec node ${quoteShellArg(script)} "$@"\n`;
66
+ }
67
+ export function workshopSeedPlan(packageRoot, home, root, platform = process.platform) {
68
+ const script = join(packageRoot, 'scripts', 'seed-workshop.sh');
69
+ const env = { DSH_HOME: home, DSH_WB_SHELL_ROOT: packageRoot };
70
+ const log = join(root, 'seed.log');
71
+ if (platform === 'win32') {
72
+ const launcher = join(packageRoot, 'bin', 'seed-workshop-windows.mjs');
73
+ const windowsEnv = { ...env, ELECTRON_RUN_AS_NODE: '1' };
74
+ return {
75
+ entry: { file: process.execPath, args: [launcher, 'entry', script], env: windowsEnv, log },
76
+ full: { file: process.execPath, args: [launcher, 'full', script], env: windowsEnv, log },
77
+ };
78
+ }
79
+ return {
80
+ entry: { file: 'bash', args: [script, 'entry'], env, log },
81
+ full: { file: 'bash', args: [script], env, log },
82
+ };
83
+ }
84
+ const execaSpawn = ((file, args, options) => execa(file, [...args], { ...options, reject: false }));
85
+ export function runDetached(command, onError, spawnImpl = execaSpawn) {
86
+ let fd = null;
87
+ try {
88
+ fd = openSync(command.log, 'a');
89
+ const child = spawnImpl(command.file, [...command.args], {
90
+ env: { ...process.env, ...command.env },
91
+ detached: true,
92
+ stdio: ['ignore', fd, fd],
93
+ });
94
+ child.on('error', (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
95
+ child.unref();
96
+ }
97
+ catch (error) {
98
+ onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
99
+ }
100
+ finally {
101
+ if (fd !== null)
102
+ closeQuietly(fd);
103
+ }
104
+ }
105
+ function closeQuietly(fd) {
106
+ try {
107
+ closeSync(fd);
108
+ }
109
+ catch {
110
+ return;
111
+ }
112
+ }
113
+ export function prependPath(entryDir, env = process.env) {
114
+ const parts = (env.PATH ?? '').split(delimiter).filter((part) => part !== '');
115
+ if (parts.includes(entryDir))
116
+ return false;
117
+ env.PATH = [entryDir, ...parts].join(delimiter);
118
+ return true;
119
+ }
120
+ export function seedWorkbenchToolchain(root, log) {
121
+ const toolchain = ensureToolchain({ root, log });
122
+ for (const file of toolchain.files)
123
+ log(`工具链写下 ${file}`);
124
+ if (toolchain.pathDir !== undefined && prependPath(toolchain.pathDir)) {
125
+ log(`PATH 前插 ${toolchain.pathDir}`);
126
+ }
127
+ if (toolchain.npmDir !== undefined) {
128
+ if (prependPath(toolchain.npmDir))
129
+ log(`PATH 前插 ${toolchain.npmDir}`);
130
+ if (process.env.npm_config_cache === undefined) {
131
+ process.env.npm_config_cache = localNpmCache(root);
132
+ }
133
+ }
134
+ if (toolchain.pnpmDir !== undefined) {
135
+ if (prependPath(toolchain.pnpmDir))
136
+ log(`PATH 前插 ${toolchain.pnpmDir}`);
137
+ }
138
+ else if (toolchain.pathDir !== undefined) {
139
+ const installed = provisionLocalPnpmSync({ root });
140
+ if (installed.ok && installed.binDir !== undefined) {
141
+ if (prependPath(installed.binDir))
142
+ log(`PATH 前插 ${installed.binDir}`);
143
+ log(`工具链 provision ${installed.detail}`);
144
+ }
145
+ else {
146
+ log(`本地 pnpm 10 没装成:${installed.detail}`);
147
+ }
148
+ }
149
+ }
150
+ export function seedRuntimeFiles(options) {
151
+ const platform = options.platform ?? process.platform;
152
+ const paths = runtimeSeedPaths(options.root, platform);
153
+ const done = [];
154
+ try {
155
+ for (const dir of [paths.entryDir, paths.opencliBinDir, paths.opencliHome, paths.workshopPackages]) {
156
+ mkdirSync(dir, { recursive: true });
157
+ }
158
+ const wanted = opencliWrapperScript(options.packageRoot, options.home, platform);
159
+ if (readText(paths.opencliBin) !== wanted) {
160
+ rmSync(paths.opencliBin, { force: true });
161
+ writeFileAtomic.sync(paths.opencliBin, wanted, 'utf8');
162
+ chmodSync(paths.opencliBin, 0o755);
163
+ done.push(paths.opencliBin);
164
+ }
165
+ }
166
+ catch (error) {
167
+ options.log?.(`自举没做完:${String(error)}`);
168
+ }
169
+ return done;
170
+ }
171
+ function readText(file) {
172
+ try {
173
+ return readFileSync(file, 'utf8');
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
@@ -0,0 +1,63 @@
1
+ import type { spawn } from 'node:child_process';
2
+ import { type SeedCommand } from './runtime-seed.js';
3
+ import { SELF_UPDATE_STATES, type SelfUpdateState, type WorkbenchSelfUpdate } from '@dsh-workbench/shared/contract/protocol/bundle-install';
4
+ export { SELF_UPDATE_STATES, type SelfUpdateState };
5
+ export interface SelfUpdateCli {
6
+ readonly file: string;
7
+ readonly args: readonly string[];
8
+ readonly env: Readonly<Record<string, string>>;
9
+ }
10
+ export interface SelfUpdateProcess {
11
+ readonly execPath: string;
12
+ readonly argv: readonly string[];
13
+ readonly versions: {
14
+ readonly electron?: string | undefined;
15
+ readonly node?: string | undefined;
16
+ };
17
+ }
18
+ export declare function desktopSelfUpdateCli(proc?: SelfUpdateProcess, exists?: (path: string) => boolean): SelfUpdateCli | undefined;
19
+ export declare function selfUpdateCliFor(profile: string, proc?: SelfUpdateProcess, exists?: (path: string) => boolean): SelfUpdateCli;
20
+ export interface SelfUpdatePaths {
21
+ readonly dir: string;
22
+ readonly script: string;
23
+ readonly marker: string;
24
+ readonly log: string;
25
+ }
26
+ export interface SelfUpdateRequest {
27
+ readonly dshHome: string;
28
+ readonly profile: string;
29
+ readonly pkg: string;
30
+ readonly from: string;
31
+ readonly to: string;
32
+ readonly pinned: boolean;
33
+ }
34
+ export interface SelfUpdateMarker extends SelfUpdateRequest {
35
+ readonly state: SelfUpdateState;
36
+ readonly version: string;
37
+ readonly at: string;
38
+ }
39
+ export interface SelfUpdatePlan {
40
+ readonly paths: SelfUpdatePaths;
41
+ readonly script: string;
42
+ readonly marker: SelfUpdateMarker;
43
+ readonly command: SeedCommand;
44
+ }
45
+ export interface SelfUpdateStart {
46
+ readonly started: boolean;
47
+ readonly paths: SelfUpdatePaths;
48
+ readonly forward: string;
49
+ }
50
+ export declare function selfUpdatePaths(dshHome: string, platform?: NodeJS.Platform): SelfUpdatePaths;
51
+ export declare function recoveryCommand(input: {
52
+ readonly profile: string;
53
+ readonly pkg: string;
54
+ readonly version: string;
55
+ readonly cli?: SelfUpdateCli | undefined;
56
+ }): string;
57
+ export declare function selfUpdateMarker(request: SelfUpdateRequest, state: SelfUpdateState, version: string, at: string): SelfUpdateMarker;
58
+ export declare function selfUpdateScript(request: SelfUpdateRequest, paths: SelfUpdatePaths, cli?: SelfUpdateCli): string;
59
+ export declare function selfUpdateNodeScript(request: SelfUpdateRequest, paths: SelfUpdatePaths, cli?: SelfUpdateCli): string;
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;
62
+ export declare function readSelfUpdateView(home: string, profile?: string): WorkbenchSelfUpdate | undefined;
63
+ export { runningVersionOf } from '@dsh-workbench/shared/host';