@dsh-workbench/shared 0.3.6 → 0.3.8
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/context-ref.d.ts +0 -12
- package/dist/api/context-ref.js +0 -40
- package/dist/api/facts.d.ts +0 -6
- package/dist/api/facts.js +0 -8
- package/dist/api/long-task.d.ts +8 -0
- package/dist/api/long-task.js +10 -0
- package/dist/components/ConfirmBar.css +26 -0
- package/dist/components/ConfirmBar.d.ts +36 -0
- package/dist/components/ConfirmBar.js +26 -0
- package/dist/components/Form.d.ts +7 -5
- package/dist/components/Form.js +5 -3
- package/dist/components/FormNodes.d.ts +17 -13
- package/dist/components/FormNodes.js +13 -5
- package/dist/components/FormTypes.d.ts +39 -14
- package/dist/components/MockStrip.css +11 -1
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -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/board-fact.d.ts +8 -0
- package/dist/host/board-fact.js +16 -0
- package/dist/host/browser-read.js +2 -1
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/context-ref.d.ts +13 -0
- package/dist/host/context-ref.js +41 -0
- package/dist/host/diy-debug.d.ts +80 -0
- package/dist/host/diy-debug.js +88 -0
- package/dist/host/diy-record.d.ts +135 -0
- package/dist/host/diy-record.js +162 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +6 -0
- package/dist/host/facts.js +9 -0
- package/dist/host/git.js +1 -1
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +3 -3
- package/dist/host/long-task.d.ts +5 -0
- package/dist/host/long-task.js +3 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.js +1 -1
- package/dist/host/opencli-window.js +1 -1
- package/dist/host/opencli.js +2 -1
- package/dist/host/panel-state.d.ts +120 -0
- package/dist/host/panel-state.js +176 -0
- package/dist/host/plugin-source.js +1 -1
- 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/shell-text.d.ts +13 -0
- package/dist/host/shell-text.js +20 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-declaration.d.ts +65 -0
- package/dist/host/unit-declaration.js +213 -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/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +7 -4
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/index.d.ts +5 -1
- package/dist/i18n/zh.d.ts +4 -0
- package/dist/i18n/zh.js +4 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/board-fact.d.ts +0 -7
- package/dist/lib/board-fact.js +0 -15
- package/dist/lib/diy-record.d.ts +38 -1
- package/dist/lib/diy-record.js +66 -0
- package/dist/lib/text.d.ts +0 -13
- package/dist/lib/text.js +0 -20
- package/dist/testing/declaration.js +1 -1
- package/package.json +1 -1
- package/src/api/__tests__/facts.test.ts +0 -13
- package/src/api/__tests__/long-task.test.ts +15 -0
- package/src/api/context-ref.ts +0 -39
- package/src/api/facts.ts +0 -9
- package/src/api/long-task.ts +11 -0
- package/src/components/ConfirmBar.css +26 -0
- package/src/components/ConfirmBar.test.tsx +130 -0
- package/src/components/ConfirmBar.tsx +63 -0
- package/src/components/Form.test.tsx +15 -0
- package/src/components/Form.tsx +7 -5
- package/src/components/FormNodes.tsx +50 -26
- package/src/components/FormTypes.test.ts +31 -0
- package/src/components/FormTypes.ts +46 -15
- package/src/components/FormWiring.test.tsx +1 -0
- package/src/config/constants.ts +3 -0
- package/src/host/__tests__/board-fact.test.ts +47 -0
- package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
- package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
- package/src/host/__tests__/facts.test.ts +13 -0
- package/src/host/__tests__/long-task.test.ts +42 -0
- package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
- package/src/host/__tests__/preflight-route.test.ts +33 -0
- package/src/host/__tests__/shell-text.test.ts +44 -0
- package/src/host/board-fact.ts +14 -0
- package/src/host/browser-read.ts +2 -1
- package/src/host/context-ref.ts +40 -0
- package/src/{lib → host}/diy-record.ts +95 -1
- package/src/host/facts.ts +10 -0
- package/src/host/git.ts +1 -1
- package/src/host/http.ts +4 -4
- package/src/host/long-task.ts +9 -0
- package/src/host/opencli-runtime.ts +1 -1
- package/src/host/opencli-window.ts +1 -1
- package/src/host/opencli.ts +2 -1
- package/src/host/plugin-source.ts +1 -1
- package/src/host/shell-text.ts +21 -0
- package/src/{api → host}/unit-declaration.ts +1 -1
- package/src/host.ts +7 -4
- package/src/i18n/en.ts +4 -0
- package/src/i18n/zh.ts +4 -0
- package/src/index.ts +1 -4
- package/src/lib/board-fact.test.ts +1 -33
- package/src/lib/board-fact.ts +0 -13
- package/src/lib/text.test.ts +1 -43
- package/src/lib/text.ts +0 -22
- package/src/testing/declaration.ts +1 -1
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
- /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
- /package/src/{lib → host}/diy-debug.ts +0 -0
- /package/src/{lib → host}/panel-state.ts +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一张面板卡片取数走到哪一步:`idle` 还没取、`loading` 取着、`ready` 拿到了、`error` 失败了。
|
|
3
|
+
*/
|
|
4
|
+
export type PanelCardPhase = 'idle' | 'loading' | 'ready' | 'error';
|
|
5
|
+
/**
|
|
6
|
+
* 一次取数失败:机器码 + 给人看的一句话。
|
|
7
|
+
*/
|
|
8
|
+
export interface PanelCardError {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 一张卡片最近一次取数的读数:哪个单元、哪条路由、什么相位、什么时候、失败原因。
|
|
14
|
+
*/
|
|
15
|
+
export interface PanelCardRead {
|
|
16
|
+
readonly unit: string;
|
|
17
|
+
readonly route: string;
|
|
18
|
+
readonly phase: PanelCardPhase;
|
|
19
|
+
readonly at: number;
|
|
20
|
+
readonly error?: PanelCardError;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 一张卡片崩了的读数:单元、原因、时间,以及同一原因连着崩了几次(窗口见 `PANEL_STATE_CRASH_WINDOW_MS`)。
|
|
24
|
+
*/
|
|
25
|
+
export interface PanelCardCrash {
|
|
26
|
+
readonly unit: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
readonly at: number;
|
|
29
|
+
readonly count: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 一个单元此刻的读数:它的卡片、最近一次读成的时间(`okAt`,没成过给 `null`),以及崩了的那一条。
|
|
33
|
+
*/
|
|
34
|
+
export interface PanelUnitReport {
|
|
35
|
+
readonly unit: string;
|
|
36
|
+
readonly at: number;
|
|
37
|
+
readonly okAt: number | null;
|
|
38
|
+
readonly cards: readonly PanelCardRead[];
|
|
39
|
+
readonly crash?: PanelCardCrash;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 一个工作区此刻的全量读数(面板按工作区存):所有单元的读数 + 整块最近动过的时间。
|
|
43
|
+
*/
|
|
44
|
+
export interface PanelWorkspaceReport {
|
|
45
|
+
readonly workspace: string | null;
|
|
46
|
+
readonly at: number;
|
|
47
|
+
readonly units: readonly PanelUnitReport[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 客户端往上报的一批卡片读数(`POST` 请求体的形状)。
|
|
51
|
+
*/
|
|
52
|
+
export interface PanelCardsReport {
|
|
53
|
+
readonly workspace: string;
|
|
54
|
+
readonly cards: readonly PanelCardRead[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 客户端往上报的一次卡片崩溃(`POST` 请求体的形状)。
|
|
58
|
+
*/
|
|
59
|
+
export interface PanelCrashReport {
|
|
60
|
+
readonly workspace: string;
|
|
61
|
+
readonly unit: string;
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
readonly at: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 面板运行态的留存口(宿主持有一份):收卡片读数与崩溃,按工作区取快照。
|
|
67
|
+
*/
|
|
68
|
+
export interface PanelStateStore {
|
|
69
|
+
cards(report: PanelCardsReport): void;
|
|
70
|
+
crash(report: PanelCrashReport): void;
|
|
71
|
+
snapshot(workspace: string | null): PanelWorkspaceReport;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 留存的工作区上限(LRU,满 64 个丢最旧的)。
|
|
75
|
+
*/
|
|
76
|
+
export declare const PANEL_STATE_MAX_UNITS = 64;
|
|
77
|
+
/**
|
|
78
|
+
* 每个单元留存的卡片上限(按路由 LRU,满 12 张丢最旧的)。
|
|
79
|
+
*/
|
|
80
|
+
export declare const PANEL_STATE_MAX_CARDS = 12;
|
|
81
|
+
/**
|
|
82
|
+
* 一次上报最多读入的卡片条数(32 条之后的丢掉)。
|
|
83
|
+
*/
|
|
84
|
+
export declare const PANEL_STATE_MAX_REPORTS = 32;
|
|
85
|
+
/**
|
|
86
|
+
* 同一原因的连续崩溃算进同一次计数的窗口(10 分钟,毫秒)。
|
|
87
|
+
*/
|
|
88
|
+
export declare const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
89
|
+
/**
|
|
90
|
+
* 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
|
|
91
|
+
*/
|
|
92
|
+
export declare const PANEL_STATE_MAX_TEXT = 2000;
|
|
93
|
+
/**
|
|
94
|
+
* 上报解析的结果:`ok: true` 带收好的值,`ok: false` 带一句中文原因(直接回给上报方)。
|
|
95
|
+
*/
|
|
96
|
+
export type PanelParse<T> = {
|
|
97
|
+
readonly ok: true;
|
|
98
|
+
readonly value: T;
|
|
99
|
+
} | {
|
|
100
|
+
readonly ok: false;
|
|
101
|
+
readonly message: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 收一批卡片读数上报。
|
|
105
|
+
* @param payload - `POST` 的请求体。
|
|
106
|
+
* @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
|
|
107
|
+
* 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
|
|
108
|
+
*/
|
|
109
|
+
export declare function parseCardsReport(payload: unknown): PanelParse<PanelCardsReport>;
|
|
110
|
+
/**
|
|
111
|
+
* 收一次卡片崩溃上报。
|
|
112
|
+
* @param payload - `POST` 的请求体。
|
|
113
|
+
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
114
|
+
*/
|
|
115
|
+
export declare function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport>;
|
|
116
|
+
/**
|
|
117
|
+
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
118
|
+
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
119
|
+
*/
|
|
120
|
+
export declare function createPanelStateStore(): PanelStateStore;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 留存的工作区上限(LRU,满 64 个丢最旧的)。
|
|
3
|
+
*/
|
|
4
|
+
export const PANEL_STATE_MAX_UNITS = 64;
|
|
5
|
+
/**
|
|
6
|
+
* 每个单元留存的卡片上限(按路由 LRU,满 12 张丢最旧的)。
|
|
7
|
+
*/
|
|
8
|
+
export const PANEL_STATE_MAX_CARDS = 12;
|
|
9
|
+
/**
|
|
10
|
+
* 一次上报最多读入的卡片条数(32 条之后的丢掉)。
|
|
11
|
+
*/
|
|
12
|
+
export const PANEL_STATE_MAX_REPORTS = 32;
|
|
13
|
+
/**
|
|
14
|
+
* 同一原因的连续崩溃算进同一次计数的窗口(10 分钟,毫秒)。
|
|
15
|
+
*/
|
|
16
|
+
export const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
17
|
+
/**
|
|
18
|
+
* 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
|
|
19
|
+
*/
|
|
20
|
+
export const PANEL_STATE_MAX_TEXT = 2000;
|
|
21
|
+
const PHASES = ['idle', 'loading', 'ready', 'error'];
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function textOf(value, max) {
|
|
26
|
+
if (typeof value !== 'string')
|
|
27
|
+
return '';
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
30
|
+
}
|
|
31
|
+
function atOf(value) {
|
|
32
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
33
|
+
return null;
|
|
34
|
+
if (value < 0)
|
|
35
|
+
return null;
|
|
36
|
+
return Math.trunc(value);
|
|
37
|
+
}
|
|
38
|
+
function errorOf(raw) {
|
|
39
|
+
const record = asRecord(raw);
|
|
40
|
+
if (record === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const code = textOf(record['code'], 120);
|
|
43
|
+
const message = textOf(record['message'], PANEL_STATE_MAX_TEXT);
|
|
44
|
+
if (code === '' && message === '')
|
|
45
|
+
return undefined;
|
|
46
|
+
return { code: code === '' ? 'unknown' : code, message };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 收一批卡片读数上报。
|
|
50
|
+
* @param payload - `POST` 的请求体。
|
|
51
|
+
* @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
|
|
52
|
+
* 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
|
|
53
|
+
*/
|
|
54
|
+
export function parseCardsReport(payload) {
|
|
55
|
+
const record = asRecord(payload);
|
|
56
|
+
if (record === undefined)
|
|
57
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
58
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
59
|
+
if (workspace === '')
|
|
60
|
+
return { ok: false, message: 'workspace 不能是空的 —— 面板按工作区存' };
|
|
61
|
+
const raw = record['cards'];
|
|
62
|
+
if (!Array.isArray(raw))
|
|
63
|
+
return { ok: false, message: 'cards 要是一个数组' };
|
|
64
|
+
const cards = [];
|
|
65
|
+
for (const item of raw.slice(0, PANEL_STATE_MAX_REPORTS)) {
|
|
66
|
+
const card = asRecord(item);
|
|
67
|
+
if (card === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
const unit = textOf(card['unit'], 200);
|
|
70
|
+
const route = textOf(card['route'], 400);
|
|
71
|
+
const phase = textOf(card['phase'], 20);
|
|
72
|
+
const at = atOf(card['at']);
|
|
73
|
+
if (unit === '' || route === '' || at === null || !PHASES.includes(phase))
|
|
74
|
+
continue;
|
|
75
|
+
const error = errorOf(card['error']);
|
|
76
|
+
cards.push({ unit, route, phase, at, ...(error === undefined ? {} : { error }) });
|
|
77
|
+
}
|
|
78
|
+
if (cards.length === 0)
|
|
79
|
+
return { ok: false, message: 'cards 里没有一条能认的(unit / route / phase / at 都要有)' };
|
|
80
|
+
return { ok: true, value: { workspace, cards } };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 收一次卡片崩溃上报。
|
|
84
|
+
* @param payload - `POST` 的请求体。
|
|
85
|
+
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
86
|
+
*/
|
|
87
|
+
export function parseCrashReport(payload) {
|
|
88
|
+
const record = asRecord(payload);
|
|
89
|
+
if (record === undefined)
|
|
90
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
91
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
92
|
+
const unit = textOf(record['unit'], 200);
|
|
93
|
+
const reason = textOf(record['reason'], PANEL_STATE_MAX_TEXT);
|
|
94
|
+
if (workspace === '')
|
|
95
|
+
return { ok: false, message: 'workspace 不能是空的' };
|
|
96
|
+
if (unit === '')
|
|
97
|
+
return { ok: false, message: 'unit 不能是空的' };
|
|
98
|
+
if (reason === '')
|
|
99
|
+
return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
|
|
100
|
+
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
104
|
+
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
105
|
+
*/
|
|
106
|
+
export function createPanelStateStore() {
|
|
107
|
+
const workspaces = new Map();
|
|
108
|
+
function trim(entry, max) {
|
|
109
|
+
while (entry.size > max) {
|
|
110
|
+
const oldest = entry.keys().next();
|
|
111
|
+
if (oldest.done === true)
|
|
112
|
+
return;
|
|
113
|
+
entry.delete(oldest.value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function workspaceEntryOf(workspace) {
|
|
117
|
+
const existing = workspaces.get(workspace);
|
|
118
|
+
if (existing !== undefined)
|
|
119
|
+
return existing;
|
|
120
|
+
const created = { at: 0, units: new Map() };
|
|
121
|
+
workspaces.set(workspace, created);
|
|
122
|
+
trim(workspaces, PANEL_STATE_MAX_UNITS);
|
|
123
|
+
return created;
|
|
124
|
+
}
|
|
125
|
+
function unitEntryOf(entry, unit) {
|
|
126
|
+
const existing = entry.units.get(unit);
|
|
127
|
+
if (existing !== undefined)
|
|
128
|
+
return existing;
|
|
129
|
+
const created = { at: 0, cards: new Map() };
|
|
130
|
+
entry.units.set(unit, created);
|
|
131
|
+
return created;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
cards(report) {
|
|
135
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
136
|
+
for (const card of report.cards) {
|
|
137
|
+
const unit = unitEntryOf(entry, card.unit);
|
|
138
|
+
unit.cards.delete(card.route);
|
|
139
|
+
unit.cards.set(card.route, card);
|
|
140
|
+
trim(unit.cards, PANEL_STATE_MAX_CARDS);
|
|
141
|
+
if (card.at > unit.at)
|
|
142
|
+
unit.at = card.at;
|
|
143
|
+
if (card.at > entry.at)
|
|
144
|
+
entry.at = card.at;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
crash(report) {
|
|
148
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
149
|
+
const unit = unitEntryOf(entry, report.unit);
|
|
150
|
+
const previous = unit.crash;
|
|
151
|
+
const counted = previous !== undefined &&
|
|
152
|
+
previous.reason === report.reason &&
|
|
153
|
+
report.at >= previous.at &&
|
|
154
|
+
report.at - previous.at <= PANEL_STATE_CRASH_WINDOW_MS;
|
|
155
|
+
unit.crash = { unit: report.unit, reason: report.reason, at: report.at, count: counted ? previous.count + 1 : 1 };
|
|
156
|
+
if (report.at > unit.at)
|
|
157
|
+
unit.at = report.at;
|
|
158
|
+
if (report.at > entry.at)
|
|
159
|
+
entry.at = report.at;
|
|
160
|
+
},
|
|
161
|
+
snapshot(workspace) {
|
|
162
|
+
if (workspace === null)
|
|
163
|
+
return { workspace: null, at: 0, units: [] };
|
|
164
|
+
const entry = workspaces.get(workspace);
|
|
165
|
+
if (entry === undefined)
|
|
166
|
+
return { workspace, at: 0, units: [] };
|
|
167
|
+
const units = [...entry.units.entries()].map(([unit, one]) => {
|
|
168
|
+
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
169
|
+
const okAt = cards.reduce((best, card) => (card.phase === 'ready' && (best === null || card.at > best) ? card.at : best), null);
|
|
170
|
+
return { unit, at: one.at, okAt, cards, ...(one.crash === undefined ? {} : { crash: one.crash }) };
|
|
171
|
+
});
|
|
172
|
+
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
173
|
+
return { workspace, at: entry.at, units };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -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;
|