@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,146 @@
|
|
|
1
|
+
export const FACT_SCOPE_SUFFIX = '*';
|
|
2
|
+
export function unitFactScope(unit) {
|
|
3
|
+
return `${unit}${FACT_SCOPE_SUFFIX}`;
|
|
4
|
+
}
|
|
5
|
+
export function isFactScope(name) {
|
|
6
|
+
return name.length > 1 && name.endsWith(FACT_SCOPE_SUFFIX);
|
|
7
|
+
}
|
|
8
|
+
export function scopePrefixOf(name) {
|
|
9
|
+
return isFactScope(name) ? name.slice(0, -FACT_SCOPE_SUFFIX.length) : name;
|
|
10
|
+
}
|
|
11
|
+
export function createFactChannel(options) {
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let epoch = null;
|
|
14
|
+
let stopTransport = null;
|
|
15
|
+
let hostRev = 0;
|
|
16
|
+
let generation = 0;
|
|
17
|
+
const entryOf = (fact) => {
|
|
18
|
+
const existing = entries.get(fact);
|
|
19
|
+
if (existing !== undefined)
|
|
20
|
+
return existing;
|
|
21
|
+
const created = { rev: 0, listeners: new Set() };
|
|
22
|
+
entries.set(fact, created);
|
|
23
|
+
return created;
|
|
24
|
+
};
|
|
25
|
+
const targetsOf = (fact) => {
|
|
26
|
+
if (!isFactScope(fact)) {
|
|
27
|
+
const entry = entries.get(fact);
|
|
28
|
+
return entry === undefined ? [] : [entry];
|
|
29
|
+
}
|
|
30
|
+
const prefix = scopePrefixOf(fact);
|
|
31
|
+
const matched = [];
|
|
32
|
+
for (const [name, entry] of entries) {
|
|
33
|
+
if (name === prefix || name.startsWith(`${prefix}/`))
|
|
34
|
+
matched.push(entry);
|
|
35
|
+
}
|
|
36
|
+
return matched;
|
|
37
|
+
};
|
|
38
|
+
const mark = (fact, rev) => {
|
|
39
|
+
if (!isFactScope(fact)) {
|
|
40
|
+
entryOf(fact).rev = rev;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of targetsOf(fact))
|
|
44
|
+
entry.rev = rev;
|
|
45
|
+
};
|
|
46
|
+
const notify = (facts) => {
|
|
47
|
+
for (const fact of facts) {
|
|
48
|
+
for (const entry of targetsOf(fact)) {
|
|
49
|
+
for (const listener of [...entry.listeners])
|
|
50
|
+
listener();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const ensureTransport = () => {
|
|
55
|
+
if (stopTransport !== null)
|
|
56
|
+
return;
|
|
57
|
+
stopTransport = options.transport.start({
|
|
58
|
+
onFrame: (frame) => channel.applyFrame(frame),
|
|
59
|
+
cursor: () => hostRev,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const releaseTransport = () => {
|
|
63
|
+
if (stopTransport === null)
|
|
64
|
+
return;
|
|
65
|
+
stopTransport();
|
|
66
|
+
stopTransport = null;
|
|
67
|
+
};
|
|
68
|
+
const channel = {
|
|
69
|
+
revisionOf: (fact) => entries.get(fact)?.rev ?? 0,
|
|
70
|
+
subscribe(fact, listener) {
|
|
71
|
+
const entry = entryOf(fact);
|
|
72
|
+
entry.listeners.add(listener);
|
|
73
|
+
ensureTransport();
|
|
74
|
+
return () => {
|
|
75
|
+
entry.listeners.delete(listener);
|
|
76
|
+
if (channel.subscriberCount() === 0)
|
|
77
|
+
releaseTransport();
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
subscriberCount() {
|
|
81
|
+
let count = 0;
|
|
82
|
+
for (const entry of entries.values())
|
|
83
|
+
count += entry.listeners.size;
|
|
84
|
+
return count;
|
|
85
|
+
},
|
|
86
|
+
refresh(fact) {
|
|
87
|
+
generation += 1;
|
|
88
|
+
if (fact === undefined) {
|
|
89
|
+
for (const entry of entries.values())
|
|
90
|
+
entry.rev = generation;
|
|
91
|
+
notify(entries.keys());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
mark(fact, generation);
|
|
95
|
+
notify([fact]);
|
|
96
|
+
},
|
|
97
|
+
applyFrame(frame) {
|
|
98
|
+
const nextEpoch = frame.epoch ?? null;
|
|
99
|
+
const first = epoch === null;
|
|
100
|
+
const epochChanged = epoch !== null && nextEpoch !== null && epoch !== nextEpoch;
|
|
101
|
+
if (nextEpoch !== null)
|
|
102
|
+
epoch = nextEpoch;
|
|
103
|
+
hostRev = epochChanged ? frame.rev : Math.max(hostRev, frame.rev);
|
|
104
|
+
if (first) {
|
|
105
|
+
generation += 1;
|
|
106
|
+
for (const fact of frame.facts)
|
|
107
|
+
mark(fact, generation);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (epochChanged) {
|
|
111
|
+
generation += 1;
|
|
112
|
+
for (const entry of entries.values())
|
|
113
|
+
entry.rev = generation;
|
|
114
|
+
notify(entries.keys());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (frame.facts.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
generation += 1;
|
|
120
|
+
for (const fact of frame.facts)
|
|
121
|
+
mark(fact, generation);
|
|
122
|
+
notify(frame.facts);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
return channel;
|
|
126
|
+
}
|
|
127
|
+
export function parseFactFrame(data) {
|
|
128
|
+
if (typeof data !== 'string')
|
|
129
|
+
return null;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(data);
|
|
132
|
+
if (typeof parsed.rev !== 'number')
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
...(typeof parsed.epoch === 'string' ? { epoch: parsed.epoch } : {}),
|
|
136
|
+
rev: parsed.rev,
|
|
137
|
+
facts: Array.isArray(parsed.facts) ? parsed.facts.filter((f) => typeof f === 'string') : [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function factOfRoute(route) {
|
|
145
|
+
return route;
|
|
146
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApiResponse } from './workbench-client';
|
|
2
|
+
export type WriteChannel = 'prefs' | 'actions' | 'provision';
|
|
3
|
+
export declare function writeRoute(channel: WriteChannel, resource: string): string;
|
|
4
|
+
export type WriteOutcome<T> = {
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
readonly value: T;
|
|
7
|
+
} | {
|
|
8
|
+
readonly ok: false;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly detail?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function writeOutcomeOf<T>(response: ApiResponse<T> | null, networkText: string): WriteOutcome<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function writeRoute(channel, resource) {
|
|
2
|
+
if (channel === 'provision')
|
|
3
|
+
return 'provision';
|
|
4
|
+
return `${channel}/${resource}`;
|
|
5
|
+
}
|
|
6
|
+
export function writeOutcomeOf(response, networkText) {
|
|
7
|
+
if (response === null)
|
|
8
|
+
return { ok: false, message: networkText };
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
const box = response;
|
|
11
|
+
const own = typeof box.error?.message === 'string' && box.error.message !== '' ? box.error.message : undefined;
|
|
12
|
+
const fallback = typeof box.text === 'string' && box.text !== '' ? box.text : networkText;
|
|
13
|
+
const detail = typeof box.error?.detail === 'string' ? box.error.detail : undefined;
|
|
14
|
+
return { ok: false, message: own ?? fallback, ...(detail === undefined ? {} : { detail }) };
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, value: response };
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type LongTaskStatus = 'running' | 'needs-login' | 'ok' | 'failed';
|
|
2
|
+
export interface LongTaskStep {
|
|
3
|
+
readonly kind: 'start' | 'succeed' | 'fail' | 'info' | 'warn';
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly loginUrl?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function runsRouteOf(unit: string): string;
|
|
8
|
+
export interface LongTasksPayload {
|
|
9
|
+
readonly runs: readonly Readonly<Record<string, unknown>>[];
|
|
10
|
+
}
|
|
11
|
+
export declare const LONG_TASK_POLL_MS = 1500;
|
|
12
|
+
export declare const LONG_TASK_IDLE_MS = 10000;
|
|
13
|
+
export declare function longTaskPollMs(live: boolean, override?: number | undefined): number;
|
|
14
|
+
export declare function longTaskRunsOf<T>(state: {
|
|
15
|
+
readonly phase?: string | undefined;
|
|
16
|
+
readonly value?: LongTasksPayload | undefined;
|
|
17
|
+
}): readonly T[];
|
|
18
|
+
export declare function longTaskDot(status: LongTaskStatus): 'ongoing' | 'done' | 'error' | 'idle';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function runsRouteOf(unit) {
|
|
2
|
+
return `${unit}/runs`;
|
|
3
|
+
}
|
|
4
|
+
export const LONG_TASK_POLL_MS = 1500;
|
|
5
|
+
export const LONG_TASK_IDLE_MS = 10_000;
|
|
6
|
+
export function longTaskPollMs(live, override) {
|
|
7
|
+
if (override !== undefined)
|
|
8
|
+
return override;
|
|
9
|
+
return live ? LONG_TASK_POLL_MS : LONG_TASK_IDLE_MS;
|
|
10
|
+
}
|
|
11
|
+
export function longTaskRunsOf(state) {
|
|
12
|
+
return (state.value?.runs ?? []);
|
|
13
|
+
}
|
|
14
|
+
export function longTaskDot(status) {
|
|
15
|
+
if (status === 'ok')
|
|
16
|
+
return 'done';
|
|
17
|
+
if (status === 'failed')
|
|
18
|
+
return 'error';
|
|
19
|
+
if (status === 'running' || status === 'needs-login')
|
|
20
|
+
return 'ongoing';
|
|
21
|
+
return 'idle';
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModuleBlocker } from './blocker';
|
|
2
|
+
import type { CapabilityPayload } from './workbench-contract';
|
|
3
|
+
import type { ApiError } from './workbench-client';
|
|
4
|
+
export type GateMode = 'capability' | 'none';
|
|
5
|
+
export interface UnitGate {
|
|
6
|
+
readonly gated: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function gateOf(capability: CapabilityPayload | null, mode: GateMode): UnitGate;
|
|
9
|
+
export interface UnitBlockers {
|
|
10
|
+
readonly blocker?: ModuleBlocker;
|
|
11
|
+
readonly notice?: ModuleBlocker;
|
|
12
|
+
}
|
|
13
|
+
export declare function splitBlocker(candidates: readonly (ModuleBlocker | undefined)[]): UnitBlockers;
|
|
14
|
+
export declare function probeKeyOf(error: ApiError | undefined): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function gateOf(capability, mode) {
|
|
2
|
+
if (mode === 'none')
|
|
3
|
+
return { gated: true };
|
|
4
|
+
if (capability === null)
|
|
5
|
+
return { gated: true };
|
|
6
|
+
return { gated: capability.status === 'ok' || capability.status === 'unchecked' };
|
|
7
|
+
}
|
|
8
|
+
export function splitBlocker(candidates) {
|
|
9
|
+
let blocker;
|
|
10
|
+
let notice;
|
|
11
|
+
for (const candidate of candidates) {
|
|
12
|
+
if (candidate === undefined)
|
|
13
|
+
continue;
|
|
14
|
+
if (candidate.severity === 'block') {
|
|
15
|
+
blocker ??= candidate;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
notice ??= candidate;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...(blocker === undefined ? {} : { blocker }),
|
|
22
|
+
...(blocker !== undefined || notice === undefined ? {} : { notice }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function probeKeyOf(error) {
|
|
26
|
+
if (error === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
return error.code === '' ? 'unknown' : error.code;
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface UnitCard {
|
|
2
|
+
readonly titleKey: string;
|
|
3
|
+
readonly order: number;
|
|
4
|
+
readonly enabledByDefault: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface UnitTool {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly writes: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UnitHostNeeds {
|
|
11
|
+
readonly inject: readonly string[];
|
|
12
|
+
readonly optional?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export type PluginSource = {
|
|
15
|
+
readonly kind: 'git';
|
|
16
|
+
readonly url: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'local';
|
|
19
|
+
readonly env: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'npm';
|
|
22
|
+
readonly package: string;
|
|
23
|
+
readonly registryEnv: string;
|
|
24
|
+
readonly registryDefault?: string;
|
|
25
|
+
};
|
|
26
|
+
export interface UnitPluginNeed {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly commands?: readonly string[];
|
|
29
|
+
readonly installSource?: PluginSource;
|
|
30
|
+
}
|
|
31
|
+
export interface UnitLibNeed {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly version?: string | undefined;
|
|
34
|
+
readonly registryEnv: string;
|
|
35
|
+
readonly registryDefault: string;
|
|
36
|
+
readonly purpose: string;
|
|
37
|
+
}
|
|
38
|
+
export interface UnitEnvNeeds {
|
|
39
|
+
readonly command?: string;
|
|
40
|
+
readonly plugins?: readonly UnitPluginNeed[];
|
|
41
|
+
readonly libs?: readonly UnitLibNeed[];
|
|
42
|
+
}
|
|
43
|
+
export interface PluginRelease {
|
|
44
|
+
readonly installed: string;
|
|
45
|
+
readonly latest?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface UnitManifest {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly kind: 'board' | 'ui';
|
|
50
|
+
readonly card: UnitCard | null;
|
|
51
|
+
readonly tools: readonly UnitTool[];
|
|
52
|
+
readonly host: UnitHostNeeds;
|
|
53
|
+
readonly env?: UnitEnvNeeds;
|
|
54
|
+
}
|
package/dist/api/unit.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ApiError {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly detail?: string;
|
|
5
|
+
readonly hint?: string;
|
|
6
|
+
readonly helpUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
export type ApiResponse<T> = ({
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
} & T) | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly error: ApiError;
|
|
13
|
+
};
|
|
14
|
+
export type Query = Record<string, string | number | boolean | undefined>;
|
|
15
|
+
export interface ApiGetOptions {
|
|
16
|
+
readonly signal?: AbortSignal | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function apiGet<T>(route: string, query?: Query, options?: ApiGetOptions): Promise<ApiResponse<T>>;
|
|
19
|
+
export declare function apiPost<T>(route: string, body: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { API_PREFIX } from '../config/constants.js';
|
|
2
|
+
export async function apiGet(route, query = {}, options = {}) {
|
|
3
|
+
const { signal } = options;
|
|
4
|
+
const search = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(query)) {
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
search.set(key, String(value));
|
|
8
|
+
}
|
|
9
|
+
const suffix = search.toString();
|
|
10
|
+
const response = await fetch(`${API_PREFIX}/${route}${suffix ? `?${suffix}` : ''}`, {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
headers: { accept: 'application/json' },
|
|
13
|
+
...(signal === undefined ? {} : { signal }),
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
error: { code: 'http', message: `宿主返回 HTTP ${response.status}` },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return (await response.json());
|
|
22
|
+
}
|
|
23
|
+
export async function apiPost(route, body) {
|
|
24
|
+
const response = await fetch(`${API_PREFIX}/${route}`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return { ok: false, error: { code: 'http', message: `宿主返回 HTTP ${response.status}` } };
|
|
31
|
+
}
|
|
32
|
+
return (await response.json());
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PluginRelease } from '../api/unit.js';
|
|
2
|
+
export type ProvisionAction = 'install' | 'update' | 'uninstall' | 'update-dependency';
|
|
3
|
+
export interface PluginStatePayload {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly status: 'ok' | 'outdated' | 'missing';
|
|
6
|
+
readonly missing: readonly string[];
|
|
7
|
+
readonly install?: 'ready' | 'not-declared' | 'env-missing' | 'malformed';
|
|
8
|
+
readonly command?: string;
|
|
9
|
+
readonly release?: PluginRelease;
|
|
10
|
+
}
|
|
11
|
+
export interface CapabilityPayload {
|
|
12
|
+
readonly unit: string;
|
|
13
|
+
readonly status: 'ok' | 'missing-commands' | 'missing-plugin' | 'unchecked';
|
|
14
|
+
readonly plugin?: string;
|
|
15
|
+
readonly required: readonly string[];
|
|
16
|
+
readonly missing: readonly string[];
|
|
17
|
+
readonly detail: string;
|
|
18
|
+
readonly command?: string;
|
|
19
|
+
readonly fix?: 'install' | 'update';
|
|
20
|
+
readonly plugins?: readonly PluginStatePayload[];
|
|
21
|
+
readonly released?: boolean;
|
|
22
|
+
readonly install?: 'ready' | 'not-declared' | 'env-missing' | 'malformed';
|
|
23
|
+
readonly libs?: readonly LibStatePayload[];
|
|
24
|
+
}
|
|
25
|
+
export interface LibStatePayload {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly purpose: string;
|
|
28
|
+
readonly status: 'ok' | 'outdated' | 'missing' | 'unsatisfied';
|
|
29
|
+
readonly installed?: string;
|
|
30
|
+
readonly latest?: string;
|
|
31
|
+
readonly command?: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Translate } from '../api/dsh-client.js';
|
|
2
|
+
import type { UnitClientFace } from '../components/board-module.js';
|
|
3
|
+
interface ToolCallRoot {
|
|
4
|
+
readonly callId?: string;
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly phase?: string;
|
|
7
|
+
readonly argsRaw?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ChatSnapshot {
|
|
10
|
+
readonly nodes?: {
|
|
11
|
+
values(): Iterable<{
|
|
12
|
+
readonly kind?: string;
|
|
13
|
+
readonly data?: {
|
|
14
|
+
readonly root?: ToolCallRoot;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
type UseChat = <T>(selector: (snapshot: ChatSnapshot) => T) => T | undefined;
|
|
20
|
+
export interface ApprovalDetailProps {
|
|
21
|
+
readonly callId?: string;
|
|
22
|
+
readonly useChat?: UseChat;
|
|
23
|
+
readonly units?: readonly UnitClientFace[];
|
|
24
|
+
readonly t?: Translate;
|
|
25
|
+
}
|
|
26
|
+
export declare function ApprovalDetail(props: ApprovalDetailProps): JSX.Element | null;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { h } from '../lib/react.js';
|
|
2
|
+
function callOf(snapshot, callId) {
|
|
3
|
+
if (callId === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
for (const node of snapshot.nodes?.values() ?? []) {
|
|
6
|
+
const root = node.kind === 'tool-call' ? node.data?.root : undefined;
|
|
7
|
+
if (root?.callId !== callId || root.phase !== 'start')
|
|
8
|
+
continue;
|
|
9
|
+
return { ...(root.name === undefined ? {} : { name: root.name }), argsRaw: root.argsRaw ?? '' };
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function parseArgs(raw) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function ApprovalDetail(props) {
|
|
23
|
+
const useChat = props.useChat ?? (() => undefined);
|
|
24
|
+
const call = useChat((snapshot) => callOf(snapshot, props.callId));
|
|
25
|
+
const t = props.t ?? ((key) => key);
|
|
26
|
+
const args = parseArgs(call?.argsRaw ?? '');
|
|
27
|
+
const input = { tool: call?.name ?? '', args };
|
|
28
|
+
const command = typeof args.command === 'string' ? args.command : '';
|
|
29
|
+
const owner = (props.units ?? []).find((face) => face.approval?.tools.includes(input.tool) === true);
|
|
30
|
+
if (owner?.approval !== undefined)
|
|
31
|
+
return h(owner.approval.render, { input: input, t: t });
|
|
32
|
+
return command === '' ? null : h("code", { className: "wb-approvalCommand" }, command);
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import type { Translate } from '../api/dsh-client.js';
|
|
3
|
+
export interface CardHeadProps {
|
|
4
|
+
readonly t: Translate;
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly meta?: ReactNode | undefined;
|
|
7
|
+
readonly refreshing: boolean;
|
|
8
|
+
readonly onRefresh: () => void;
|
|
9
|
+
readonly gear?: string | undefined;
|
|
10
|
+
readonly gearOpen?: boolean | undefined;
|
|
11
|
+
readonly onToggleGear?: (() => void) | undefined;
|
|
12
|
+
readonly platformHref?: string | undefined;
|
|
13
|
+
readonly platform?: string | undefined;
|
|
14
|
+
readonly platformTitle?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function CardHead({ t, title, meta, refreshing, onRefresh, gear, gearOpen, onToggleGear, platformHref, platform, platformTitle, }: CardHeadProps): JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IconRefresh, IconRightUp, IconSettings } from '../components/icons.js';
|
|
2
|
+
import { h } from '../lib/react.js';
|
|
3
|
+
export function CardHead({ t, title, meta, refreshing, onRefresh, gear, gearOpen, onToggleGear, platformHref, platform, platformTitle, }) {
|
|
4
|
+
const refreshLabel = refreshing ? t('rightbar.refreshing') : t('rightbar.refresh');
|
|
5
|
+
return (h("header", { className: "wb-cardHead" },
|
|
6
|
+
h("h2", { className: "wb-cardTitle" }, title),
|
|
7
|
+
meta === undefined ? null : h("span", { className: "wb-cardMeta" }, meta),
|
|
8
|
+
platformHref === undefined || platform === undefined ? null : (h("a", { className: "wb-iconButton wb-cardOpen", href: platformHref, target: "_blank", rel: "noreferrer noopener", title: platformTitle ?? platform, "aria-label": platform },
|
|
9
|
+
h(IconRightUp, { size: 14 }))),
|
|
10
|
+
h("button", { type: "button", className: `wb-iconButton wb-cardRefresh${refreshing ? ' wb-spinning' : ''}`, "aria-label": refreshLabel, title: refreshLabel, disabled: refreshing, onClick: onRefresh },
|
|
11
|
+
h(IconRefresh, { size: 14 })),
|
|
12
|
+
gear === undefined || onToggleGear === undefined ? null : (h("button", { type: "button", className: `wb-iconButton wb-cardGear${gearOpen === true ? ' wb-iconButtonActive' : ''}`, "aria-label": gear, "aria-expanded": gearOpen === true, title: gear, onClick: onToggleGear },
|
|
13
|
+
h(IconSettings, { size: 15 })))));
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
export interface ChoiceGroupOption {
|
|
3
|
+
readonly value: string;
|
|
4
|
+
readonly label: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export interface ChoiceGroupProps {
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly options: readonly ChoiceGroupOption[];
|
|
11
|
+
readonly disabled?: boolean | undefined;
|
|
12
|
+
readonly onChange: (next: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function ChoiceGroup({ label, name, value, options, disabled, onChange }: ChoiceGroupProps): JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Fragment, h } from '../lib/react.js';
|
|
2
|
+
export function ChoiceGroup({ label, name, value, options, disabled, onChange }) {
|
|
3
|
+
return (h("div", { className: "wb-choiceMenu", role: "radiogroup", "aria-label": label }, options.map((option) => (h(Fragment, { key: option.value },
|
|
4
|
+
h("label", { className: "wb-choiceOption" },
|
|
5
|
+
h("input", { className: "wb-radio", type: "radio", name: name, checked: option.value === value, disabled: disabled === true, onChange: () => onChange(option.value) }),
|
|
6
|
+
h("span", { className: "wb-choiceLabel" }, option.label)))))));
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type LongTaskStatus } from '../api/long-task.js';
|
|
2
|
+
export interface LongTaskRowLink {
|
|
3
|
+
readonly href: string;
|
|
4
|
+
readonly text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LongTaskRowProps {
|
|
7
|
+
readonly status: LongTaskStatus;
|
|
8
|
+
readonly headline: string;
|
|
9
|
+
readonly problem?: string | undefined;
|
|
10
|
+
readonly links?: readonly LongTaskRowLink[] | undefined;
|
|
11
|
+
readonly steps?: readonly string[] | undefined;
|
|
12
|
+
readonly raw?: string | undefined;
|
|
13
|
+
readonly stepsTitle?: string | undefined;
|
|
14
|
+
readonly rawTitle?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function LongTaskRow(props: LongTaskRowProps): JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { longTaskDot } from '../api/long-task.js';
|
|
2
|
+
import { Fragment, h } from '../lib/react.js';
|
|
3
|
+
import { IconRightUp } from './icons';
|
|
4
|
+
import { StateDot } from './StateDot';
|
|
5
|
+
function RowLink({ href, text }) {
|
|
6
|
+
return (h("a", { className: "wb-linkButton wb-longTaskLink", href: href, target: "_blank", rel: "noreferrer noopener" },
|
|
7
|
+
text,
|
|
8
|
+
h(IconRightUp, { size: 12 })));
|
|
9
|
+
}
|
|
10
|
+
export function LongTaskRow(props) {
|
|
11
|
+
const failed = props.status === 'failed';
|
|
12
|
+
const problem = props.problem ?? '';
|
|
13
|
+
const raw = (props.raw ?? '').trim();
|
|
14
|
+
const steps = props.steps ?? [];
|
|
15
|
+
return (h("div", { className: "wb-longTask" },
|
|
16
|
+
h("p", { className: "wb-longTaskHead" },
|
|
17
|
+
h(StateDot, { state: longTaskDot(props.status), size: 10 }),
|
|
18
|
+
h("span", { className: "wb-longTaskHeadline" }, props.headline),
|
|
19
|
+
(props.links ?? []).map((link) => (h(RowLink, { href: link.href, key: `${link.text}@${link.href}`, text: link.text })))),
|
|
20
|
+
problem === '' ? null : h("p", { className: "wb-longTaskProblem" }, problem),
|
|
21
|
+
!failed ? null : (h(Fragment, null,
|
|
22
|
+
steps.length === 0 || props.stepsTitle === undefined ? null : (h("details", { className: "wb-longTaskDump" },
|
|
23
|
+
h("summary", null, props.stepsTitle),
|
|
24
|
+
h("pre", null, steps.join('\n')))),
|
|
25
|
+
raw === '' || props.rawTitle === undefined ? null : (h("details", { className: "wb-longTaskDump" },
|
|
26
|
+
h("summary", null, props.rawTitle),
|
|
27
|
+
h("pre", null, raw)))))));
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function MockStrip({ mocks, index, onPick, note, real, }: {
|
|
2
|
+
readonly mocks: readonly {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
}[];
|
|
6
|
+
readonly index: number;
|
|
7
|
+
readonly onPick: (next: number) => void;
|
|
8
|
+
readonly note?: string;
|
|
9
|
+
readonly real?: {
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly active: boolean;
|
|
12
|
+
readonly onPick: () => void;
|
|
13
|
+
} | undefined;
|
|
14
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { h } from '../lib/react.js';
|
|
2
|
+
export function MockStrip({ mocks, index, onPick, note, real, }) {
|
|
3
|
+
return (h("div", { className: "wb-mockStrip" },
|
|
4
|
+
h("span", { className: "wb-mockNote" }, note ?? '夹具:只切状态,不发请求'),
|
|
5
|
+
real === undefined ? null : (h("button", { type: "button", className: `wb-mockChip${real.active ? ' wb-mockChipOn' : ''}`, onClick: real.onPick }, real.label)),
|
|
6
|
+
mocks.map((mock, at) => (h("button", { key: mock.id, type: "button", className: `wb-mockChip${at === index ? ' wb-mockChipOn' : ''}`, onClick: () => onPick(at) }, mock.label)))));
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function SkeletonRows({ rows }: {
|
|
3
|
+
rows?: number;
|
|
4
|
+
}): JSX.Element;
|
|
5
|
+
export interface CenteredStateProps {
|
|
6
|
+
icon: ReactNode;
|
|
7
|
+
title: string;
|
|
8
|
+
hint?: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
action?: ReactNode;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function CenteredState({ icon, title, hint, detail, action, compact }: CenteredStateProps): JSX.Element;
|
|
14
|
+
export declare function EmptyState({ text, icon, compact }: {
|
|
15
|
+
text: string;
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { h } from '../lib/react.js';
|
|
2
|
+
export function SkeletonRows({ rows = 4 }) {
|
|
3
|
+
return (h("div", { className: "wb-skeleton", "aria-hidden": "true" }, Array.from({ length: rows }, (_, index) => (h("span", { key: index, className: "wb-skeletonRow" },
|
|
4
|
+
h("span", { className: "wb-skeletonBar wb-skeletonBarTitle" }),
|
|
5
|
+
h("span", { className: "wb-skeletonBar wb-skeletonBarMeta" }))))));
|
|
6
|
+
}
|
|
7
|
+
export function CenteredState({ icon, title, hint, detail, action, compact = false }) {
|
|
8
|
+
return (h("div", { className: compact ? 'wb-centered wb-centeredCompact' : 'wb-centered', role: "status" },
|
|
9
|
+
icon === null ? null : (h("span", { className: "wb-centeredHero", "aria-hidden": "true" }, icon)),
|
|
10
|
+
h("div", { className: "wb-centeredTitle" }, title),
|
|
11
|
+
hint === undefined ? null : h("div", { className: "wb-centeredHint" }, hint),
|
|
12
|
+
detail === undefined ? null : h("div", { className: "wb-centeredDetail" }, detail),
|
|
13
|
+
action === undefined ? null : h("div", { className: "wb-centeredAction" }, action)));
|
|
14
|
+
}
|
|
15
|
+
export function EmptyState({ text, icon = null, compact = false }) {
|
|
16
|
+
return h(CenteredState, { icon: icon, title: text, compact: compact });
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SaveBarProps {
|
|
2
|
+
readonly busy: boolean;
|
|
3
|
+
readonly disabled?: boolean | undefined;
|
|
4
|
+
readonly labels: {
|
|
5
|
+
readonly cancel: string;
|
|
6
|
+
readonly save: string;
|
|
7
|
+
readonly saving: string;
|
|
8
|
+
};
|
|
9
|
+
readonly onCancel: () => void;
|
|
10
|
+
readonly onSave: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function SaveBar({ busy, disabled, labels, onCancel, onSave }: SaveBarProps): JSX.Element;
|