@dsh-workbench/shared 0.1.11 → 0.1.13

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.
Files changed (40) hide show
  1. package/dist/components/ChoiceGroup.css +49 -0
  2. package/dist/components/ChoiceGroup.d.ts +13 -3
  3. package/dist/components/ChoiceGroup.js +38 -5
  4. package/dist/components/StateDot.d.ts +5 -0
  5. package/dist/components/StateDot.js +10 -0
  6. package/dist/host/__tests__/fakes.d.ts +41 -0
  7. package/dist/host/__tests__/fakes.js +76 -0
  8. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  9. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  10. package/dist/host/context-inject.d.ts +24 -0
  11. package/dist/host/context-inject.js +74 -0
  12. package/dist/host/events.d.ts +9 -0
  13. package/dist/host/events.js +70 -0
  14. package/dist/host/mount-units.d.ts +52 -0
  15. package/dist/host/mount-units.js +186 -0
  16. package/dist/host/preflight.d.ts +58 -0
  17. package/dist/host/preflight.js +233 -0
  18. package/dist/host/probe-cache.d.ts +6 -0
  19. package/dist/host/probe-cache.js +19 -0
  20. package/dist/host/probe-libs.d.ts +45 -0
  21. package/dist/host/probe-libs.js +61 -0
  22. package/dist/host/probe-units.d.ts +17 -0
  23. package/dist/host/probe-units.js +102 -0
  24. package/dist/host/probe-verdict.d.ts +7 -0
  25. package/dist/host/probe-verdict.js +143 -0
  26. package/dist/host/tool-scope.d.ts +32 -0
  27. package/dist/host/tool-scope.js +84 -0
  28. package/dist/host/unit-readiness.d.ts +7 -0
  29. package/dist/host/unit-readiness.js +5 -0
  30. package/dist/host/unit-registry.d.ts +9 -0
  31. package/dist/host/unit-registry.js +33 -0
  32. package/dist/host/version-range.d.ts +2 -0
  33. package/dist/host/version-range.js +73 -0
  34. package/dist/lib/ui.js +8 -1
  35. package/package.json +1 -1
  36. package/src/components/ChoiceGroup.css +49 -0
  37. package/src/components/ChoiceGroup.test.tsx +201 -0
  38. package/src/components/ChoiceGroup.tsx +112 -19
  39. package/src/lib/ui.test.ts +8 -0
  40. package/src/lib/ui.ts +8 -1
@@ -0,0 +1,186 @@
1
+ import { runsRouteOf } from '@dsh-workbench/shared';
2
+ import { param } from './http';
3
+ import { createLongTaskHost } from './long-task';
4
+ import { unitReadiness } from './unit-readiness';
5
+ import { createWorkspaceStore } from './workspace-store';
6
+ function withRunsRoute(result, unit) {
7
+ const runs = result.longTasks;
8
+ if (runs === undefined)
9
+ return result;
10
+ return {
11
+ ...result,
12
+ routes: {
13
+ ...result.routes,
14
+ [runsRouteOf(unit)]: (search) => {
15
+ const path = (param(search, 'path') ?? '').trim();
16
+ return { ok: true, runs: path === '' ? [] : runs.current(path) };
17
+ },
18
+ },
19
+ };
20
+ }
21
+ function collectRoutes(mounted) {
22
+ const routes = {};
23
+ const owner = new Map();
24
+ const routesOfUnit = new Map();
25
+ for (const item of mounted) {
26
+ const unit = item.face.manifest.id;
27
+ for (const [key, handler] of Object.entries(item.result.routes)) {
28
+ if (key !== unit && !key.startsWith(`${unit}/`)) {
29
+ throw new Error(`路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— ` +
30
+ `装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`);
31
+ }
32
+ const previous = owner.get(key);
33
+ if (previous !== undefined) {
34
+ throw new Error(`路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`);
35
+ }
36
+ owner.set(key, unit);
37
+ routes[key] = handler;
38
+ const own = routesOfUnit.get(unit);
39
+ if (own === undefined)
40
+ routesOfUnit.set(unit, [key]);
41
+ else
42
+ own.push(key);
43
+ }
44
+ }
45
+ return { routes, owners: owner, routesOfUnit };
46
+ }
47
+ function pickGate(mounted) {
48
+ const providers = mounted.filter((item) => item.result.gate !== undefined);
49
+ if (providers.length > 1) {
50
+ throw new Error(`启停闸门只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
51
+ }
52
+ return providers[0]?.result.gate;
53
+ }
54
+ function pickProvision(mounted) {
55
+ const providers = mounted.filter((item) => item.result.provision !== undefined);
56
+ if (providers.length > 1) {
57
+ throw new Error(`机器供给只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
58
+ }
59
+ return providers[0]?.result.provision;
60
+ }
61
+ function collectPrefs(mounted) {
62
+ const owner = new Map();
63
+ const prefs = [];
64
+ for (const item of mounted) {
65
+ const entry = item.result.prefs;
66
+ if (entry === undefined)
67
+ continue;
68
+ const previous = owner.get(entry.resource);
69
+ if (previous !== undefined) {
70
+ throw new Error(`偏好资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
71
+ }
72
+ owner.set(entry.resource, item.face.manifest.id);
73
+ prefs.push({ ...entry, unit: item.face.manifest.id });
74
+ }
75
+ return prefs;
76
+ }
77
+ function collectActions(mounted) {
78
+ const owner = new Map();
79
+ const actions = [];
80
+ for (const item of mounted) {
81
+ for (const entry of item.result.actions ?? []) {
82
+ const previous = owner.get(entry.resource);
83
+ if (previous !== undefined) {
84
+ throw new Error(`动作资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
85
+ }
86
+ owner.set(entry.resource, item.face.manifest.id);
87
+ actions.push({ ...entry, unit: item.face.manifest.id });
88
+ }
89
+ }
90
+ return actions;
91
+ }
92
+ function collectWarmReads(mounted) {
93
+ const declarations = mounted
94
+ .map((item) => item.result.warmReads)
95
+ .filter((declare) => declare !== undefined);
96
+ return () => {
97
+ const seen = new Set();
98
+ const reads = [];
99
+ for (const read of declarations.flatMap((declare) => declare())) {
100
+ const key = read.join(' ');
101
+ if (seen.has(key))
102
+ continue;
103
+ seen.add(key);
104
+ reads.push(read);
105
+ }
106
+ return reads;
107
+ };
108
+ }
109
+ function collectContextRenderers(mounted) {
110
+ const table = new Map();
111
+ const owner = new Map();
112
+ for (const item of mounted) {
113
+ const context = item.result.context;
114
+ if (context === undefined)
115
+ continue;
116
+ for (const kind of context.kinds) {
117
+ const previous = owner.get(kind);
118
+ if (previous !== undefined) {
119
+ throw new Error(`对话引用的 kind 冲突:${kind} 由套件 ${previous} 与 ${item.face.manifest.id} 同时认领`);
120
+ }
121
+ owner.set(kind, item.face.manifest.id);
122
+ table.set(kind, (ref) => context.render(ref));
123
+ }
124
+ }
125
+ return table;
126
+ }
127
+ export function mountUnits(faces, deps) {
128
+ const store = deps.store ?? createWorkspaceStore({ root: null });
129
+ const mounted = [];
130
+ const skipped = [];
131
+ const longTaskDisposers = [];
132
+ for (const face of faces) {
133
+ const readiness = unitReadiness(face.manifest, (dep) => deps.ctx.get(dep));
134
+ if (!readiness.canMount) {
135
+ skipped.push({ id: face.manifest.id, missing: readiness.missingHard });
136
+ continue;
137
+ }
138
+ const unit = face.manifest.id;
139
+ const longTasks = deps.longTasks === undefined
140
+ ? undefined
141
+ : createLongTaskHost({ scope: deps.longTasks.scope, unit, bump: deps.longTasks.bump });
142
+ if (longTasks !== undefined)
143
+ longTaskDisposers.push(longTasks.dispose);
144
+ const result = face.mount({
145
+ ctx: deps.ctx,
146
+ logger: deps.logger,
147
+ manifests: deps.manifests,
148
+ ...(deps.opencliCommand === undefined ? {} : { opencliCommand: deps.opencliCommand }),
149
+ ...(deps.probe === undefined ? {} : { probe: deps.probe }),
150
+ ...(deps.values === undefined ? {} : { values: deps.values }),
151
+ ...(longTasks === undefined ? {} : { longTasks: longTasks.host }),
152
+ store,
153
+ });
154
+ mounted.push({ face, readiness, result: withRunsRoute(result, unit) });
155
+ }
156
+ const collected = collectRoutes(mounted);
157
+ const prefs = collectPrefs(mounted);
158
+ const actions = collectActions(mounted);
159
+ const owners = new Map(collected.owners);
160
+ for (const entry of [...prefs, ...actions])
161
+ owners.set(entry.resource, entry.unit);
162
+ const factsOf = new Map();
163
+ for (const item of mounted) {
164
+ const declare = item.result.factsOf;
165
+ if (declare !== undefined)
166
+ factsOf.set(item.face.manifest.id, declare);
167
+ }
168
+ return {
169
+ mounted,
170
+ routes: collected.routes,
171
+ owners,
172
+ routesOfUnit: collected.routesOfUnit,
173
+ factsOf,
174
+ gate: pickGate(mounted),
175
+ prefs,
176
+ actions,
177
+ provision: pickProvision(mounted),
178
+ contextRenderers: collectContextRenderers(mounted),
179
+ skipped,
180
+ warmReads: collectWarmReads(mounted),
181
+ disposeLongTasks: () => {
182
+ for (const dispose of longTaskDisposers.splice(0))
183
+ dispose();
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,58 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ export type PreflightStepId = 'opencli' | 'bridge' | 'warm';
3
+ export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
4
+ export interface PreflightStep {
5
+ readonly id: PreflightStepId;
6
+ readonly status: PreflightStepStatus;
7
+ readonly detail?: string;
8
+ readonly action?: string;
9
+ readonly command?: string;
10
+ }
11
+ export type PreflightFailure = 'permission' | 'network' | 'missing' | 'unknown';
12
+ export declare function classifyFailure(text: string): PreflightFailure;
13
+ export type PreflightGuidance = 'install' | 'path' | 'shell';
14
+ export type PreflightBridgeIssue = 'daemon' | 'extension' | 'profile' | 'stale' | 'unknown';
15
+ export declare function readBridgeReport(stdout: string): {
16
+ readonly ok: boolean;
17
+ readonly issues: readonly PreflightBridgeIssue[];
18
+ readonly detail: string;
19
+ };
20
+ export interface PreflightSnapshot {
21
+ readonly phase: 'idle' | 'checking' | 'installing' | 'ready' | 'failed';
22
+ readonly steps: readonly PreflightStep[];
23
+ readonly at: number;
24
+ readonly error?: string;
25
+ readonly failure?: PreflightFailure;
26
+ readonly guidance?: PreflightGuidance;
27
+ readonly bridgeIssues?: readonly PreflightBridgeIssue[];
28
+ }
29
+ export interface Preflight {
30
+ run(options?: {
31
+ force?: boolean;
32
+ }): Promise<PreflightSnapshot>;
33
+ snapshot(): PreflightSnapshot;
34
+ }
35
+ export declare const OPENCLI_INSTALL_COMMAND: string;
36
+ export interface BridgeVerdict {
37
+ readonly ok: boolean;
38
+ readonly detail: string;
39
+ readonly issues: readonly PreflightBridgeIssue[];
40
+ readonly command?: string | undefined;
41
+ }
42
+ export declare function canWarm(bridge: {
43
+ ok: boolean;
44
+ issues: readonly PreflightBridgeIssue[];
45
+ }): boolean;
46
+ export declare function createPreflight(options: {
47
+ shell: DshShell;
48
+ command?: string;
49
+ installCommand?: string;
50
+ logger?: {
51
+ warn?(message: string): void;
52
+ } | undefined;
53
+ install?: (() => Promise<{
54
+ ok: boolean;
55
+ detail: string;
56
+ }>) | undefined;
57
+ warmReads?: (() => readonly (readonly string[])[]) | undefined;
58
+ }): Preflight;
@@ -0,0 +1,233 @@
1
+ import { INSTALL_TIMEOUT_MS, OPENCLI_TIMEOUT_MS, WARM_TIMEOUT_MS } from '@dsh-workbench/shared';
2
+ import { tailLines } from '@dsh-workbench/shared';
3
+ import { opencliCommandLine } from './opencli';
4
+ import { isolatedInstallCommand } from './opencli-runtime';
5
+ import { runShell } from './shell';
6
+ export function classifyFailure(text) {
7
+ const lower = text.toLowerCase();
8
+ if (/eacces|eperm|permission denied|operation not permitted/.test(lower))
9
+ return 'permission';
10
+ if (/enotfound|etimedout|econnrefused|econnreset|network|proxy|fetch failed/.test(lower))
11
+ return 'network';
12
+ if (/command not found|not found|no such file/.test(lower))
13
+ return 'missing';
14
+ return 'unknown';
15
+ }
16
+ export function readBridgeReport(stdout) {
17
+ const text = String(stdout ?? '');
18
+ const ok = /^\[OK\] Daemon:/m.test(text) && /^\[OK\] Extension:/m.test(text) && /^\[OK\] Connectivity:/m.test(text);
19
+ if (ok) {
20
+ const good = text.split('\n').find((line) => line.trim() === 'Everything looks good!');
21
+ return { ok: true, issues: [], detail: good?.trim() ?? tailLines(text, 1) };
22
+ }
23
+ const found = new Set();
24
+ if (!/^\[OK\] Daemon:/m.test(text))
25
+ found.add('daemon');
26
+ if (!/^\[OK\] Extension:/m.test(text))
27
+ found.add('extension');
28
+ if (/Multiple Chrome profiles are connected/i.test(text))
29
+ found.add('profile');
30
+ if (/Default browser profile is stale|Selected browser profile is not connected/i.test(text)) {
31
+ found.add('stale');
32
+ }
33
+ if (found.has('profile') || found.has('stale'))
34
+ found.delete('extension');
35
+ if (found.size === 0)
36
+ found.add('unknown');
37
+ const order = ['profile', 'stale', 'daemon', 'extension', 'unknown'];
38
+ return { ok: false, issues: order.filter((issue) => found.has(issue)), detail: issuesSection(text) };
39
+ }
40
+ function issuesSection(text) {
41
+ const lines = text.split('\n');
42
+ const start = lines.findIndex((line) => /^Issues:/.test(line.trim()));
43
+ if (start < 0)
44
+ return tailLines(text, 6);
45
+ return lines.slice(start).join('\n').trim();
46
+ }
47
+ function bridgeFixCommand(command, issues) {
48
+ if (issues.includes('profile') || issues.includes('stale'))
49
+ return `${command} profile list`;
50
+ if (issues.includes('daemon') || issues.includes('extension'))
51
+ return `${command} daemon restart`;
52
+ return undefined;
53
+ }
54
+ export const OPENCLI_INSTALL_COMMAND = isolatedInstallCommand();
55
+ const WARMABLE_BRIDGE_ISSUES = ['daemon', 'stale'];
56
+ export function canWarm(bridge) {
57
+ return bridge.ok || bridge.issues.every((issue) => WARMABLE_BRIDGE_ISSUES.includes(issue));
58
+ }
59
+ function step(id) {
60
+ return { id, status: 'pending' };
61
+ }
62
+ export function createPreflight(options) {
63
+ const { shell, command = 'opencli', logger } = options;
64
+ const installCommand = options.installCommand ?? OPENCLI_INSTALL_COMMAND;
65
+ const warmReads = () => options.warmReads?.() ?? [];
66
+ const steps = () => [
67
+ step('opencli'),
68
+ step('bridge'),
69
+ ...(warmReads().length === 0 ? [] : [step('warm')]),
70
+ ];
71
+ let current = { phase: 'idle', steps: steps(), at: Date.now() };
72
+ let running = null;
73
+ let latest = null;
74
+ let rerun = false;
75
+ function set(next) {
76
+ current = { ...current, ...next, at: Date.now() };
77
+ return current;
78
+ }
79
+ function patchStep(id, patch) {
80
+ return set({ steps: current.steps.map((item) => (item.id === id ? { ...item, ...patch } : item)) });
81
+ }
82
+ async function exec(commandLine, timeoutMs) {
83
+ try {
84
+ const result = await runShell(shell, commandLine, { timeoutMs });
85
+ if (result.timedOut) {
86
+ return { kind: 'error', detail: `超时(${String(timeoutMs)} ms):${commandLine}` };
87
+ }
88
+ if (result.exitCode !== 0) {
89
+ return { kind: 'nonzero', detail: tailLines(result.stderr || result.stdout, 4) };
90
+ }
91
+ return { kind: 'ok', stdout: result.stdout };
92
+ }
93
+ catch (error) {
94
+ return { kind: 'error', detail: String(error) };
95
+ }
96
+ }
97
+ async function probeVersion() {
98
+ const result = await exec(`${command} --version`, OPENCLI_TIMEOUT_MS);
99
+ if (result.kind === 'ok')
100
+ return { ok: true, missing: false, detail: result.stdout.trim().split('\n')[0] ?? '' };
101
+ return { ok: false, missing: result.kind === 'nonzero', detail: result.detail };
102
+ }
103
+ async function probeBridge() {
104
+ const result = await exec(`${command} doctor`, OPENCLI_TIMEOUT_MS);
105
+ if (result.kind !== 'ok')
106
+ return { ok: false, detail: result.detail, issues: ['unknown'] };
107
+ const report = readBridgeReport(result.stdout);
108
+ if (report.ok)
109
+ return report;
110
+ return { ...report, command: bridgeFixCommand(command, report.issues) };
111
+ }
112
+ async function installOpencli() {
113
+ if (options.install !== undefined)
114
+ return options.install();
115
+ const result = await exec(installCommand, INSTALL_TIMEOUT_MS);
116
+ if (result.kind !== 'ok')
117
+ return { ok: false, detail: result.detail };
118
+ return { ok: true, detail: tailLines(result.stdout, 1) };
119
+ }
120
+ async function warmSites(bridge) {
121
+ const reads = warmReads();
122
+ if (reads.length === 0)
123
+ return bridge;
124
+ if (!canWarm(bridge)) {
125
+ patchStep('warm', { status: 'warn', detail: '' });
126
+ return bridge;
127
+ }
128
+ patchStep('warm', {
129
+ status: 'running',
130
+ action: `${command} <套件自己声明的那条读命令> --site-session persistent -f json`,
131
+ });
132
+ const results = await Promise.all(reads.map((read) => exec(opencliCommandLine(command, read), WARM_TIMEOUT_MS)));
133
+ const failed = reads.filter((_, index) => results[index]?.kind !== 'ok');
134
+ patchStep('warm', {
135
+ status: failed.length === 0 ? 'ok' : 'warn',
136
+ detail: failed.length === 0 ? String(reads.length) : failed.map((read) => read.join(' ')).join(' '),
137
+ action: undefined,
138
+ });
139
+ if (bridge.ok)
140
+ return bridge;
141
+ const again = await probeBridge();
142
+ patchStep('bridge', {
143
+ status: again.ok ? 'ok' : 'warn',
144
+ detail: again.detail,
145
+ action: undefined,
146
+ command: again.command,
147
+ });
148
+ return again;
149
+ }
150
+ async function doRun() {
151
+ set({ phase: 'checking', error: undefined, failure: undefined, steps: steps() });
152
+ patchStep('opencli', { status: 'running', action: `${command} --version` });
153
+ let version = await probeVersion();
154
+ if (!version.ok && !version.missing) {
155
+ patchStep('opencli', { status: 'failed', detail: version.detail, action: undefined });
156
+ return set({
157
+ phase: 'failed',
158
+ error: version.detail,
159
+ failure: classifyFailure(version.detail),
160
+ guidance: 'shell',
161
+ });
162
+ }
163
+ if (!version.ok) {
164
+ set({ phase: 'installing' });
165
+ patchStep('opencli', { status: 'running', action: installCommand, command: installCommand });
166
+ const installed = await installOpencli();
167
+ if (!installed.ok) {
168
+ patchStep('opencli', {
169
+ status: 'failed',
170
+ detail: installed.detail,
171
+ command: installCommand,
172
+ });
173
+ return set({
174
+ phase: 'failed',
175
+ error: installed.detail,
176
+ failure: classifyFailure(installed.detail),
177
+ guidance: 'install',
178
+ });
179
+ }
180
+ patchStep('opencli', { status: 'running', action: `${command} --version` });
181
+ version = await probeVersion();
182
+ if (!version.ok) {
183
+ patchStep('opencli', { status: 'failed', detail: version.detail, command: installCommand });
184
+ return set({
185
+ phase: 'failed',
186
+ error: version.detail,
187
+ failure: classifyFailure(version.detail),
188
+ guidance: 'path',
189
+ });
190
+ }
191
+ }
192
+ patchStep('opencli', { status: 'ok', detail: version.detail, action: undefined });
193
+ patchStep('bridge', { status: 'running', action: `${command} doctor` });
194
+ const bridge = await probeBridge();
195
+ const healable = !bridge.ok && warmReads().length > 0 && canWarm(bridge);
196
+ if (!healable) {
197
+ patchStep('bridge', {
198
+ status: bridge.ok ? 'ok' : 'warn',
199
+ detail: bridge.detail,
200
+ action: undefined,
201
+ ...(bridge.command === undefined ? {} : { command: bridge.command }),
202
+ });
203
+ }
204
+ const settled = await warmSites(bridge);
205
+ logger?.warn?.(`[dsh-workbench] 环境体检:${current.steps.map((item) => `${item.id}=${item.status}`).join(' ')}`);
206
+ return set({ phase: 'ready', ...(settled.ok ? {} : { bridgeIssues: settled.issues }) });
207
+ }
208
+ async function start(runOptions = {}) {
209
+ if (running !== null) {
210
+ if (runOptions.force === true)
211
+ rerun = true;
212
+ return running;
213
+ }
214
+ if (runOptions.force !== true && latest !== null)
215
+ return latest;
216
+ const inflight = doRun();
217
+ latest = inflight;
218
+ running = inflight;
219
+ await inflight.catch(() => undefined);
220
+ running = null;
221
+ if (rerun) {
222
+ rerun = false;
223
+ return start({ force: true });
224
+ }
225
+ return inflight;
226
+ }
227
+ return {
228
+ run: start,
229
+ snapshot() {
230
+ return current;
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,6 @@
1
+ export interface TimedMemo<T> {
2
+ get(key: string, now?: number): T | undefined;
3
+ set(key: string, value: T, now?: number): void;
4
+ clear(): void;
5
+ }
6
+ export declare function createTimedMemo<T>(ttlMs: number): TimedMemo<T>;
@@ -0,0 +1,19 @@
1
+ export function createTimedMemo(ttlMs) {
2
+ const entries = new Map();
3
+ return {
4
+ get(key, now = Date.now()) {
5
+ const hit = entries.get(key);
6
+ if (hit === undefined)
7
+ return undefined;
8
+ if (now - hit.at >= ttlMs)
9
+ return undefined;
10
+ return hit.value;
11
+ },
12
+ set(key, value, now = Date.now()) {
13
+ entries.set(key, { at: now, value });
14
+ },
15
+ clear() {
16
+ entries.clear();
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,45 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import type { PluginRelease, UnitLibNeed, UnitPluginNeed } from '@dsh-workbench/shared';
3
+ import type { PluginSourceProblem } from './plugin-source';
4
+ export interface UnitLibState {
5
+ readonly name: string;
6
+ readonly purpose: string;
7
+ readonly status: 'ok' | 'outdated' | 'missing' | 'unsatisfied';
8
+ readonly installed?: string;
9
+ readonly latest?: string;
10
+ readonly command?: string;
11
+ }
12
+ export declare function installedLibVersion(name: string): string | undefined;
13
+ export declare function probeLibs(options: {
14
+ readonly needs: readonly UnitLibNeed[];
15
+ readonly shell: DshShell;
16
+ readonly force: boolean;
17
+ }): Promise<ReadonlyMap<string, UnitLibState>>;
18
+ export interface UnitCommands {
19
+ readonly unit: string;
20
+ readonly plugins?: readonly UnitPluginNeed[];
21
+ readonly libs?: readonly UnitLibNeed[];
22
+ }
23
+ export interface UnitPluginState {
24
+ readonly name: string;
25
+ readonly status: "ok" | "outdated" | "missing";
26
+ readonly missing: readonly string[];
27
+ readonly install?: "ready" | PluginSourceProblem;
28
+ readonly command?: string;
29
+ readonly release?: PluginRelease;
30
+ }
31
+ export interface UnitCapability {
32
+ readonly unit: string;
33
+ readonly status: "ok" | "missing-commands" | "missing-plugin" | "unchecked";
34
+ readonly plugin?: string;
35
+ readonly required: readonly string[];
36
+ readonly missing: readonly string[];
37
+ readonly detail: string;
38
+ readonly command?: string;
39
+ readonly fix?: "install" | "update";
40
+ readonly install?: "ready" | PluginSourceProblem;
41
+ readonly plugins?: readonly UnitPluginState[];
42
+ readonly libs?: readonly UnitLibState[];
43
+ readonly released?: boolean;
44
+ }
45
+ export declare function resetLibCache(): void;
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { libDir, libInstallCommand } from './opencli-runtime';
4
+ import { isNewerVersion, latestVersion } from './release';
5
+ import { resetReleaseCache } from './release';
6
+ import { checkVersionRange } from './version-range';
7
+ export function installedLibVersion(name) {
8
+ try {
9
+ const parsed = JSON.parse(readFileSync(join(libDir(name), 'package.json'), 'utf8'));
10
+ const version = parsed.version;
11
+ return typeof version === 'string' && version.trim() !== ''
12
+ ? version.trim()
13
+ : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ export async function probeLibs(options) {
20
+ const { needs, shell, force } = options;
21
+ const found = new Map();
22
+ for (const need of needs) {
23
+ if (found.has(need.name))
24
+ continue;
25
+ const base = { name: need.name, purpose: need.purpose };
26
+ const installed = installedLibVersion(need.name);
27
+ if (installed === undefined) {
28
+ found.set(need.name, {
29
+ ...base,
30
+ status: 'missing',
31
+ command: libInstallCommand({ need }),
32
+ });
33
+ continue;
34
+ }
35
+ if (checkVersionRange(need.version, installed) !== 'satisfied') {
36
+ found.set(need.name, {
37
+ ...base,
38
+ status: 'unsatisfied',
39
+ installed,
40
+ command: libInstallCommand({ need }),
41
+ });
42
+ continue;
43
+ }
44
+ const registry = process.env[need.registryEnv]?.trim() || need.registryDefault;
45
+ const latest = await latestVersion({ shell, name: need.name, registry, force });
46
+ const behind = latest !== undefined && isNewerVersion(latest, installed);
47
+ found.set(need.name, {
48
+ ...base,
49
+ status: behind ? 'outdated' : 'ok',
50
+ installed,
51
+ ...(latest === undefined ? {} : { latest }),
52
+ ...(behind
53
+ ? { command: libInstallCommand({ need, version: latest }) }
54
+ : {}),
55
+ });
56
+ }
57
+ return found;
58
+ }
59
+ export function resetLibCache() {
60
+ resetReleaseCache();
61
+ }
@@ -0,0 +1,17 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import { type UnitCapability, type UnitCommands } from "./probe-libs";
3
+ export type { UnitCapability, UnitCommands, UnitLibState, UnitPluginState } from "./probe-libs";
4
+ export declare function parsePluginList(stdout: string): unknown[] | undefined;
5
+ export declare function probeUnitCommands(options: {
6
+ shell: DshShell;
7
+ command?: string;
8
+ unit: UnitCommands;
9
+ force?: boolean;
10
+ }): Promise<UnitCapability>;
11
+ export declare function probeUnits(options: {
12
+ shell: DshShell;
13
+ command?: string;
14
+ units: readonly UnitCommands[];
15
+ force?: boolean;
16
+ }): Promise<UnitCapability[]>;
17
+ export declare const resetProbeCache: () => void;