@dsh-workbench/shared 0.1.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.
- package/dist/api/blocker-table.d.ts +9 -0
- package/dist/api/blocker-table.js +130 -0
- package/dist/api/blocker.d.ts +30 -0
- package/dist/api/blocker.js +1 -0
- package/dist/api/context-ref.d.ts +10 -0
- package/dist/api/context-ref.js +40 -0
- package/dist/api/dsh-client.d.ts +170 -0
- package/dist/api/dsh-client.js +1 -0
- package/dist/api/dsh.d.ts +153 -0
- package/dist/api/dsh.js +1 -0
- package/dist/api/facts.d.ts +27 -0
- package/dist/api/facts.js +146 -0
- package/dist/api/host-write.d.ts +12 -0
- package/dist/api/host-write.js +17 -0
- package/dist/api/long-task.d.ts +18 -0
- package/dist/api/long-task.js +22 -0
- package/dist/api/unit-board.d.ts +14 -0
- package/dist/api/unit-board.js +29 -0
- package/dist/api/unit.d.ts +54 -0
- package/dist/api/unit.js +1 -0
- package/dist/api/workbench-client.d.ts +19 -0
- package/dist/api/workbench-client.js +33 -0
- package/dist/api/workbench-contract.d.ts +32 -0
- package/dist/api/workbench-contract.js +1 -0
- package/dist/components/ApprovalDetail.d.ts +27 -0
- package/dist/components/ApprovalDetail.js +33 -0
- package/dist/components/CardHead.d.ts +16 -0
- package/dist/components/CardHead.js +14 -0
- package/dist/components/ChoiceGroup.d.ts +14 -0
- package/dist/components/ChoiceGroup.js +7 -0
- package/dist/components/LongTaskRow.d.ts +16 -0
- package/dist/components/LongTaskRow.js +28 -0
- package/dist/components/MockStrip.d.ts +14 -0
- package/dist/components/MockStrip.js +7 -0
- package/dist/components/ResourceStates.d.ts +18 -0
- package/dist/components/ResourceStates.js +17 -0
- package/dist/components/SaveBar.d.ts +12 -0
- package/dist/components/SaveBar.js +6 -0
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/components/board-module.d.ts +67 -0
- package/dist/components/board-module.js +1 -0
- package/dist/components/icons.d.ts +18 -0
- package/dist/components/icons.js +81 -0
- package/dist/config/constants.d.ts +27 -0
- package/dist/config/constants.js +27 -0
- package/dist/config/storage.d.ts +4 -0
- package/dist/config/storage.js +27 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/cli-error.d.ts +8 -0
- package/dist/host/cli-error.js +70 -0
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/data-root.d.ts +3 -0
- package/dist/host/data-root.js +12 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +24 -0
- package/dist/host/facts.js +44 -0
- package/dist/host/git.d.ts +55 -0
- package/dist/host/git.js +64 -0
- package/dist/host/http.d.ts +36 -0
- package/dist/host/http.js +242 -0
- package/dist/host/long-task.d.ts +58 -0
- package/dist/host/long-task.js +148 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.d.ts +22 -0
- package/dist/host/opencli-runtime.js +46 -0
- package/dist/host/opencli.d.ts +37 -0
- package/dist/host/opencli.js +152 -0
- package/dist/host/plugin-source.d.ts +31 -0
- package/dist/host/plugin-source.js +81 -0
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/provision.d.ts +14 -0
- package/dist/host/provision.js +12 -0
- package/dist/host/shell.d.ts +14 -0
- package/dist/host/shell.js +23 -0
- package/dist/host/tool-approval.d.ts +29 -0
- package/dist/host/tool-approval.js +190 -0
- package/dist/host/tool-kit.d.ts +19 -0
- package/dist/host/tool-kit.js +37 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-face.d.ts +66 -0
- package/dist/host/unit-face.js +1 -0
- package/dist/host/unit-libs.d.ts +37 -0
- package/dist/host/unit-libs.js +1 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/value-handoff.d.ts +16 -0
- package/dist/host/value-handoff.js +21 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host/workspace-store.d.ts +19 -0
- package/dist/host/workspace-store.js +133 -0
- package/dist/host/write-result.d.ts +1 -0
- package/dist/host/write-result.js +18 -0
- package/dist/host.d.ts +19 -0
- package/dist/host.js +19 -0
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.js +162 -0
- package/dist/i18n/index.d.ts +172 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/merge.d.ts +6 -0
- package/dist/i18n/merge.js +40 -0
- package/dist/i18n/zh.d.ts +163 -0
- package/dist/i18n/zh.js +162 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/lib/blocker-action.d.ts +13 -0
- package/dist/lib/blocker-action.js +12 -0
- package/dist/lib/busy.d.ts +5 -0
- package/dist/lib/busy.js +7 -0
- package/dist/lib/react.d.ts +10 -0
- package/dist/lib/react.js +9 -0
- package/dist/lib/text.d.ts +4 -0
- package/dist/lib/text.js +48 -0
- package/dist/lib/ui.d.ts +11 -0
- package/dist/lib/ui.js +37 -0
- package/package.json +44 -0
- package/src/api/__tests__/blocker-table.test.ts +253 -0
- package/src/api/__tests__/facts.test.ts +274 -0
- package/src/api/__tests__/host-write.test.ts +55 -0
- package/src/api/__tests__/long-task.test.ts +52 -0
- package/src/api/__tests__/unit-board.test.ts +76 -0
- package/src/api/blocker-table.ts +155 -0
- package/src/api/blocker.ts +25 -0
- package/src/api/context-ref.ts +47 -0
- package/src/api/dsh-client.ts +176 -0
- package/src/api/dsh.ts +156 -0
- package/src/api/facts.ts +166 -0
- package/src/api/host-write.ts +27 -0
- package/src/api/long-task.ts +38 -0
- package/src/api/unit-board.ts +44 -0
- package/src/api/unit.ts +59 -0
- package/src/api/workbench-client.test.ts +92 -0
- package/src/api/workbench-client.ts +59 -0
- package/src/api/workbench-contract.ts +40 -0
- package/src/components/ApprovalDetail.tsx +57 -0
- package/src/components/CardHead.tsx +74 -0
- package/src/components/ChoiceGroup.tsx +38 -0
- package/src/components/LongTaskRow.tsx +65 -0
- package/src/components/MockStrip.tsx +40 -0
- package/src/components/ResourceStates.tsx +44 -0
- package/src/components/SaveBar.tsx +31 -0
- package/src/components/StateDot.tsx +31 -0
- package/src/components/board-module.ts +73 -0
- package/src/components/icons.tsx +232 -0
- package/src/config/constants.test.ts +41 -0
- package/src/config/constants.ts +52 -0
- package/src/config/storage.test.ts +82 -0
- package/src/config/storage.ts +25 -0
- package/src/host/__tests__/fakes.ts +116 -0
- package/src/host/__tests__/opencli-runtime.test.ts +46 -0
- package/src/host/cli-error.ts +77 -0
- package/src/host/data-root.ts +13 -0
- package/src/host/facts.ts +69 -0
- package/src/host/git.ts +99 -0
- package/src/host/http.ts +311 -0
- package/src/host/long-task.ts +235 -0
- package/src/host/opencli-runtime.ts +66 -0
- package/src/host/opencli.ts +210 -0
- package/src/host/plugin-source.ts +145 -0
- package/src/host/provision.ts +34 -0
- package/src/host/shell.ts +42 -0
- package/src/host/tool-approval.ts +243 -0
- package/src/host/tool-kit.ts +56 -0
- package/src/host/unit-face.ts +65 -0
- package/src/host/unit-libs.ts +41 -0
- package/src/host/value-handoff.ts +38 -0
- package/src/host/workspace-store.ts +161 -0
- package/src/host/write-result.ts +16 -0
- package/src/host.ts +19 -0
- package/src/i18n/en.ts +170 -0
- package/src/i18n/index.test.ts +15 -0
- package/src/i18n/index.ts +12 -0
- package/src/i18n/merge.test.ts +60 -0
- package/src/i18n/merge.ts +54 -0
- package/src/i18n/zh.test.ts +40 -0
- package/src/i18n/zh.ts +170 -0
- package/src/index.ts +33 -0
- package/src/lib/blocker-action.test.ts +40 -0
- package/src/lib/blocker-action.ts +28 -0
- package/src/lib/busy.test.ts +27 -0
- package/src/lib/busy.ts +11 -0
- package/src/lib/react.test.ts +26 -0
- package/src/lib/react.ts +16 -0
- package/src/lib/text.test.ts +92 -0
- package/src/lib/text.ts +46 -0
- package/src/lib/ui.test.ts +93 -0
- package/src/lib/ui.ts +38 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { OPENCLI_MAX_CONCURRENT, OPENCLI_TIMEOUT_MS } from '../config/constants.js';
|
|
2
|
+
import { parseJsonLoose, quoteShellArg, tailLines } from '../lib/text.js';
|
|
3
|
+
import { helpUrlOf, parseCliErrorEnvelope } from './cli-error';
|
|
4
|
+
import { runShell } from './shell';
|
|
5
|
+
const FORBIDDEN_CODES = new Set(['FORBIDDEN', 'PERMISSION_DENIED']);
|
|
6
|
+
const SITE_SESSION_ARGS = ['--site-session', 'persistent'];
|
|
7
|
+
export class WorkbenchDataError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
detail;
|
|
10
|
+
hint;
|
|
11
|
+
project;
|
|
12
|
+
helpUrl;
|
|
13
|
+
constructor(code, message, extra = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'WorkbenchDataError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
if (extra.detail !== undefined)
|
|
18
|
+
this.detail = extra.detail;
|
|
19
|
+
if (extra.hint !== undefined)
|
|
20
|
+
this.hint = extra.hint;
|
|
21
|
+
if (extra.project !== undefined)
|
|
22
|
+
this.project = extra.project;
|
|
23
|
+
if (extra.helpUrl !== undefined)
|
|
24
|
+
this.helpUrl = extra.helpUrl;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function opencliCommandLine(command, argv, session = 'persistent') {
|
|
28
|
+
return [
|
|
29
|
+
quoteShellArg(command),
|
|
30
|
+
...argv.map(quoteShellArg),
|
|
31
|
+
...(session === 'persistent' ? SITE_SESSION_ARGS : []),
|
|
32
|
+
'-f',
|
|
33
|
+
'json',
|
|
34
|
+
].join(' ');
|
|
35
|
+
}
|
|
36
|
+
export function createOpencli(options) {
|
|
37
|
+
const { shell, plugin, logger, command = 'opencli', timeoutMs = OPENCLI_TIMEOUT_MS, maxConcurrent = OPENCLI_MAX_CONCURRENT, } = options;
|
|
38
|
+
const inflight = new Map();
|
|
39
|
+
const MAX_CONCURRENT = maxConcurrent;
|
|
40
|
+
let running = 0;
|
|
41
|
+
const waiting = [];
|
|
42
|
+
async function acquire() {
|
|
43
|
+
if (running < MAX_CONCURRENT) {
|
|
44
|
+
running += 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
48
|
+
}
|
|
49
|
+
function release() {
|
|
50
|
+
const next = waiting.shift();
|
|
51
|
+
if (next === undefined)
|
|
52
|
+
running -= 1;
|
|
53
|
+
else
|
|
54
|
+
next();
|
|
55
|
+
}
|
|
56
|
+
async function runLimited(task) {
|
|
57
|
+
await acquire();
|
|
58
|
+
try {
|
|
59
|
+
return await task();
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
release();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function call(commandLine, workdir) {
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = await runShell(shell, commandLine, {
|
|
69
|
+
...(workdir === undefined ? {} : { workdir }),
|
|
70
|
+
timeoutMs,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new WorkbenchDataError('cli-exec-failed', `执行 opencli 失败:${String(error)}`, {
|
|
75
|
+
hint: '确认 opencli 已安装且在 PATH 里(which opencli)。',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (result.timedOut) {
|
|
79
|
+
throw new WorkbenchDataError('cli-timeout', `opencli 超过 ${Math.round(timeoutMs / 1000)} 秒没有返回`, { hint: '无头浏览器可能没起来,重试一次通常就好。' });
|
|
80
|
+
}
|
|
81
|
+
if (result.exitCode !== 0) {
|
|
82
|
+
const detail = tailLines(result.stderr || result.stdout, 6);
|
|
83
|
+
const envelope = parseCliErrorEnvelope(result.stderr) ?? parseCliErrorEnvelope(result.stdout);
|
|
84
|
+
const url = envelope === undefined ? undefined : helpUrlOf(envelope);
|
|
85
|
+
if (envelope?.code === 'AUTH_REQUIRED') {
|
|
86
|
+
throw new WorkbenchDataError('auth-required', envelope.message, {
|
|
87
|
+
detail,
|
|
88
|
+
...(url === undefined ? {} : { helpUrl: url }),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (envelope !== undefined && FORBIDDEN_CODES.has(envelope.code)) {
|
|
92
|
+
throw new WorkbenchDataError('permission-denied', envelope.message, {
|
|
93
|
+
detail,
|
|
94
|
+
...(url === undefined ? {} : { helpUrl: url }),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (envelope !== undefined) {
|
|
98
|
+
throw new WorkbenchDataError('cli-command-failed', `${envelope.code}:${envelope.message}`, {
|
|
99
|
+
detail,
|
|
100
|
+
...(envelope.help === undefined ? {} : { hint: envelope.help }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw new WorkbenchDataError('cli-unclassified', `opencli 这条命令没跑通(退出码 ${result.exitCode}),而且没有结构化错误码`, {
|
|
104
|
+
detail,
|
|
105
|
+
hint: '多半是插件里没有这条命令(版本旧就会缺),也可能是命令自己失败了:先确认命令存在,再更新插件重试。',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const parsed = parseJsonLoose(result.stdout);
|
|
109
|
+
if (parsed === undefined) {
|
|
110
|
+
throw new WorkbenchDataError('cli-bad-output', 'opencli 输出不是 JSON', {
|
|
111
|
+
detail: tailLines(result.stdout, 4),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
async function json(args, callOptions = {}) {
|
|
117
|
+
const { key, workdir } = callOptions;
|
|
118
|
+
const session = callOptions.session ?? 'persistent';
|
|
119
|
+
const callKey = key ?? args.map((arg) => String(arg)).join(' ');
|
|
120
|
+
const pending = inflight.get(callKey);
|
|
121
|
+
if (pending !== undefined)
|
|
122
|
+
return pending;
|
|
123
|
+
const commandLine = opencliCommandLine(command, [plugin, ...args], session);
|
|
124
|
+
const once = async () => {
|
|
125
|
+
const value = await call(commandLine, workdir);
|
|
126
|
+
return { value, at: Date.now() };
|
|
127
|
+
};
|
|
128
|
+
const task = runLimited(async () => {
|
|
129
|
+
try {
|
|
130
|
+
return await once();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!(error instanceof WorkbenchDataError) || error.code !== 'auth-required')
|
|
134
|
+
throw error;
|
|
135
|
+
return await once();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
inflight.set(callKey, task);
|
|
139
|
+
try {
|
|
140
|
+
return await task;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
logger?.warn?.(`[dsh-workbench] opencli 调用失败:${message}`);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
inflight.delete(callKey);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { json };
|
|
152
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PluginSource } from '../api/unit.js';
|
|
2
|
+
export type PluginSourceProblem = "not-declared" | "env-missing" | "malformed";
|
|
3
|
+
export interface PluginSourceCommand {
|
|
4
|
+
readonly line: string;
|
|
5
|
+
readonly optional?: true;
|
|
6
|
+
}
|
|
7
|
+
export type ResolvedPluginSource = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly commands: readonly PluginSourceCommand[];
|
|
10
|
+
readonly line: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly problem: PluginSourceProblem;
|
|
14
|
+
readonly env?: string;
|
|
15
|
+
};
|
|
16
|
+
export interface PluginSourceOptions {
|
|
17
|
+
readonly command?: string;
|
|
18
|
+
readonly plugin?: string;
|
|
19
|
+
readonly version?: string;
|
|
20
|
+
readonly mode?: "install" | "update";
|
|
21
|
+
}
|
|
22
|
+
export declare function npmRegistryOf(source: {
|
|
23
|
+
readonly registryEnv: string;
|
|
24
|
+
readonly registryDefault?: string;
|
|
25
|
+
}, env: Readonly<Record<string, string | undefined>>): {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly value: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
};
|
|
31
|
+
export declare function resolvePluginSource(source: PluginSource | undefined, env: Readonly<Record<string, string | undefined>>, options?: PluginSourceOptions): ResolvedPluginSource;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { quoteShellArg } from '../lib/text.js';
|
|
2
|
+
import { npmCacheDir, toolchainOpencliDir, vendorPeerLink, vendorPrefix } from '../host/opencli-runtime.js';
|
|
3
|
+
function mergeLines(commands) {
|
|
4
|
+
return commands
|
|
5
|
+
.map((item) => item.optional === true ? `(${item.line} || true)` : item.line)
|
|
6
|
+
.join(" && ");
|
|
7
|
+
}
|
|
8
|
+
function envValue(env, name) {
|
|
9
|
+
const value = env[name]?.trim();
|
|
10
|
+
return value === undefined || value === ""
|
|
11
|
+
? { ok: false }
|
|
12
|
+
: { ok: true, value };
|
|
13
|
+
}
|
|
14
|
+
export function npmRegistryOf(source, env) {
|
|
15
|
+
const fromEnv = envValue(env, source.registryEnv);
|
|
16
|
+
if (fromEnv.ok)
|
|
17
|
+
return fromEnv;
|
|
18
|
+
const fallback = source.registryDefault?.trim();
|
|
19
|
+
return fallback === undefined || fallback === ""
|
|
20
|
+
? { ok: false }
|
|
21
|
+
: { ok: true, value: fallback };
|
|
22
|
+
}
|
|
23
|
+
function installOne(command, spec) {
|
|
24
|
+
const line = `${quoteShellArg(command)} plugin install ${quoteShellArg(spec)}`;
|
|
25
|
+
return { ok: true, commands: [{ line }], line };
|
|
26
|
+
}
|
|
27
|
+
function npmSource(source, env, options) {
|
|
28
|
+
const registry = npmRegistryOf(source, env);
|
|
29
|
+
if (!registry.ok)
|
|
30
|
+
return { ok: false, problem: "env-missing", env: source.registryEnv };
|
|
31
|
+
const name = source.package.trim();
|
|
32
|
+
if (name === "")
|
|
33
|
+
return { ok: false, problem: "malformed" };
|
|
34
|
+
const { command = "opencli", plugin = "", version = "latest", mode = "install", } = options;
|
|
35
|
+
const prefix = vendorPrefix();
|
|
36
|
+
const dirInPrefix = `${prefix}/node_modules/${name}`;
|
|
37
|
+
const peer = vendorPeerLink();
|
|
38
|
+
const commands = [
|
|
39
|
+
{
|
|
40
|
+
line: [
|
|
41
|
+
"npm i",
|
|
42
|
+
`--prefix ${quoteShellArg(prefix)}`,
|
|
43
|
+
quoteShellArg(`${name}@${version}`),
|
|
44
|
+
`--registry ${quoteShellArg(registry.value)}`,
|
|
45
|
+
`--cache ${quoteShellArg(npmCacheDir())}`,
|
|
46
|
+
].join(" "),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
line: `rm -rf ${quoteShellArg(peer.dir)} && mkdir -p ${quoteShellArg(peer.parent)} && ln -s ${quoteShellArg(toolchainOpencliDir())} ${quoteShellArg(peer.dir)}`,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
if (mode === "install") {
|
|
53
|
+
if (plugin !== "") {
|
|
54
|
+
commands.push({
|
|
55
|
+
line: `${quoteShellArg(command)} plugin uninstall ${quoteShellArg(plugin)}`,
|
|
56
|
+
optional: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
commands.push({
|
|
60
|
+
line: `NPM_CONFIG_REGISTRY=${quoteShellArg(registry.value)} ${quoteShellArg(command)} plugin install ${quoteShellArg(dirInPrefix)}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, commands, line: mergeLines(commands) };
|
|
64
|
+
}
|
|
65
|
+
export function resolvePluginSource(source, env, options = {}) {
|
|
66
|
+
if (source === undefined)
|
|
67
|
+
return { ok: false, problem: "not-declared" };
|
|
68
|
+
const command = options.command ?? "opencli";
|
|
69
|
+
if (source.kind === "git") {
|
|
70
|
+
const url = source.url.trim();
|
|
71
|
+
return url === ""
|
|
72
|
+
? { ok: false, problem: "malformed" }
|
|
73
|
+
: installOne(command, url);
|
|
74
|
+
}
|
|
75
|
+
if (source.kind === "npm")
|
|
76
|
+
return npmSource(source, env, options);
|
|
77
|
+
const value = envValue(env, source.env);
|
|
78
|
+
return value.ok
|
|
79
|
+
? installOne(command, value.value)
|
|
80
|
+
: { ok: false, problem: "env-missing", env: source.env };
|
|
81
|
+
}
|
|
@@ -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,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;
|