@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,74 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { encodeRef, parseMarkers } from '@dsh-workbench/shared';
|
|
3
|
+
export const CONTEXT_SOURCE_KIND = 'workbench';
|
|
4
|
+
export function collectContextRefs(messages) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const refs = [];
|
|
7
|
+
for (const message of messages) {
|
|
8
|
+
if (message.source?.kind !== 'user')
|
|
9
|
+
continue;
|
|
10
|
+
for (const block of message.content ?? []) {
|
|
11
|
+
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
12
|
+
continue;
|
|
13
|
+
for (const ref of parseMarkers(block.text)) {
|
|
14
|
+
const key = encodeRef(ref);
|
|
15
|
+
if (seen.has(key))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(key);
|
|
18
|
+
refs.push(ref);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return refs;
|
|
23
|
+
}
|
|
24
|
+
export function buildContextMessage(text, summary) {
|
|
25
|
+
return {
|
|
26
|
+
role: 'user',
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
content: [{ type: 'text', text }],
|
|
29
|
+
source: { kind: CONTEXT_SOURCE_KIND, form: 'notice', summary },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createContextInjector(options) {
|
|
33
|
+
const done = new WeakMap();
|
|
34
|
+
return {
|
|
35
|
+
async inject(messages, agent) {
|
|
36
|
+
const refs = collectContextRefs(messages);
|
|
37
|
+
if (refs.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
const key = typeof agent === 'object' && agent !== null ? agent : undefined;
|
|
40
|
+
const seen = key === undefined ? undefined : (done.get(key) ?? new Set());
|
|
41
|
+
if (key !== undefined && seen !== undefined)
|
|
42
|
+
done.set(key, seen);
|
|
43
|
+
const fresh = [];
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
const encoded = encodeRef(ref);
|
|
46
|
+
if (seen?.has(encoded) === true)
|
|
47
|
+
continue;
|
|
48
|
+
seen?.add(encoded);
|
|
49
|
+
fresh.push(ref);
|
|
50
|
+
}
|
|
51
|
+
if (fresh.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const texts = [];
|
|
54
|
+
for (const ref of fresh) {
|
|
55
|
+
const render = options.renderers.get(ref.kind);
|
|
56
|
+
if (render === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = await render(ref);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
options.logger?.warn?.(`[dsh-workbench] 渲染对话引用失败:${String(error)}`);
|
|
64
|
+
text = `工作台面板引用了一条内容(${encodeRef(ref)}),但当前渲染不出来(项目未选中或数据源不可用)。`;
|
|
65
|
+
}
|
|
66
|
+
if (text !== '')
|
|
67
|
+
texts.push(text);
|
|
68
|
+
}
|
|
69
|
+
if (texts.length === 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return buildContextMessage(texts.join('\n\n'), `工作台引用:${fresh.length} 条`);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function dshHome() {
|
|
2
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
3
|
+
if (configured !== undefined && configured !== '')
|
|
4
|
+
return configured;
|
|
5
|
+
return `${process.env.HOME ?? ''}/.dsh`;
|
|
6
|
+
}
|
|
7
|
+
export function workbenchRoot() {
|
|
8
|
+
return `${dshHome()}/workbench`;
|
|
9
|
+
}
|
|
10
|
+
export function workspacesRoot() {
|
|
11
|
+
return `${workbenchRoot()}/workspaces`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DshHostContext, DshRequest, DshResponse } from '@dsh-workbench/shared';
|
|
2
|
+
import type { FactLog } from './facts';
|
|
3
|
+
export interface FactStream {
|
|
4
|
+
handler(req: DshRequest, res: DshResponse): void;
|
|
5
|
+
closeAll(): void;
|
|
6
|
+
size(): number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createFactStream(log: FactLog): FactStream;
|
|
9
|
+
export declare function registerFactStream(ctx: DshHostContext, log: FactLog): () => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EVENTS_PATH } from '@dsh-workbench/shared';
|
|
2
|
+
const HEARTBEAT_MS = 25_000;
|
|
3
|
+
function frame(payload) {
|
|
4
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
5
|
+
}
|
|
6
|
+
export function createFactStream(log) {
|
|
7
|
+
const connections = new Set();
|
|
8
|
+
const cleanups = new Map();
|
|
9
|
+
const drop = (res) => {
|
|
10
|
+
cleanups.get(res)?.();
|
|
11
|
+
cleanups.delete(res);
|
|
12
|
+
connections.delete(res);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
size: () => connections.size,
|
|
16
|
+
closeAll() {
|
|
17
|
+
for (const res of [...connections]) {
|
|
18
|
+
drop(res);
|
|
19
|
+
res.end?.();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
handler(req, res) {
|
|
23
|
+
if (req.method !== undefined && req.method !== 'GET') {
|
|
24
|
+
res.statusCode = 405;
|
|
25
|
+
res.end();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const url = req.url ?? '/';
|
|
29
|
+
const cut = url.indexOf('?');
|
|
30
|
+
const since = Number(new URLSearchParams(cut === -1 ? '' : url.slice(cut + 1)).get('since') ?? 0) || 0;
|
|
31
|
+
const opening = log.since(since);
|
|
32
|
+
if (typeof res.write !== 'function') {
|
|
33
|
+
res.statusCode = 200;
|
|
34
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
35
|
+
res.end(JSON.stringify({ ok: true, ...opening }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const headers = {
|
|
39
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
40
|
+
'cache-control': 'no-store, no-transform',
|
|
41
|
+
connection: 'keep-alive',
|
|
42
|
+
};
|
|
43
|
+
if (typeof res.writeHead === 'function')
|
|
44
|
+
res.writeHead(200, headers);
|
|
45
|
+
else {
|
|
46
|
+
res.statusCode = 200;
|
|
47
|
+
for (const [name, value] of Object.entries(headers))
|
|
48
|
+
res.setHeader(name, value);
|
|
49
|
+
}
|
|
50
|
+
res.write(': connected\n\n');
|
|
51
|
+
res.write(frame({ type: 'sync', ...opening }));
|
|
52
|
+
connections.add(res);
|
|
53
|
+
const timer = setInterval(() => res.write?.(': ping\n\n'), HEARTBEAT_MS);
|
|
54
|
+
const unsubscribe = log.subscribe((change) => res.write?.(frame({ type: 'bump', ...change })));
|
|
55
|
+
cleanups.set(res, () => {
|
|
56
|
+
clearInterval(timer);
|
|
57
|
+
unsubscribe();
|
|
58
|
+
});
|
|
59
|
+
res.on?.('close', () => drop(res));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function registerFactStream(ctx, log) {
|
|
64
|
+
const stream = createFactStream(log);
|
|
65
|
+
const unregister = ctx.webServer.register({ kind: 'exact', path: EVENTS_PATH, handler: stream.handler });
|
|
66
|
+
return () => {
|
|
67
|
+
stream.closeAll();
|
|
68
|
+
unregister();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UnitManifest } from '../api/unit.js';
|
|
2
|
+
export interface FactChange {
|
|
3
|
+
readonly rev: number;
|
|
4
|
+
readonly facts: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface FactSnapshot extends FactChange {
|
|
7
|
+
readonly epoch: string;
|
|
8
|
+
}
|
|
9
|
+
export interface FactLog {
|
|
10
|
+
bump(facts: readonly string[]): FactChange;
|
|
11
|
+
revision(): number;
|
|
12
|
+
epoch(): string;
|
|
13
|
+
since(rev: number): FactSnapshot;
|
|
14
|
+
subscribe(listener: (change: FactChange) => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createFactLog(options?: {
|
|
17
|
+
readonly epoch?: string;
|
|
18
|
+
}): FactLog;
|
|
19
|
+
export interface ToolUnit {
|
|
20
|
+
readonly unit: string;
|
|
21
|
+
readonly writes: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function toolUnits(manifests: readonly UnitManifest[]): Map<string, ToolUnit>;
|
|
24
|
+
export declare function wroteNow(result: unknown): boolean | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createFactLog(options = {}) {
|
|
2
|
+
const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
3
|
+
let rev = 0;
|
|
4
|
+
const last = new Map();
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
return {
|
|
7
|
+
epoch: () => epoch,
|
|
8
|
+
revision: () => rev,
|
|
9
|
+
bump(facts) {
|
|
10
|
+
rev += 1;
|
|
11
|
+
const named = [...new Set(facts.filter((name) => name !== ''))];
|
|
12
|
+
for (const name of named)
|
|
13
|
+
last.set(name, rev);
|
|
14
|
+
const change = { rev, facts: named };
|
|
15
|
+
for (const listener of [...listeners])
|
|
16
|
+
listener(change);
|
|
17
|
+
return change;
|
|
18
|
+
},
|
|
19
|
+
since(seen) {
|
|
20
|
+
const facts = [...last.entries()].filter(([, at]) => at > seen).map(([name]) => name);
|
|
21
|
+
return { epoch, rev, facts };
|
|
22
|
+
},
|
|
23
|
+
subscribe(listener) {
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
return () => {
|
|
26
|
+
listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function toolUnits(manifests) {
|
|
32
|
+
const map = new Map();
|
|
33
|
+
for (const manifest of manifests) {
|
|
34
|
+
for (const tool of manifest.tools ?? []) {
|
|
35
|
+
map.set(tool.name, { unit: manifest.id, writes: tool.writes });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
export function wroteNow(result) {
|
|
41
|
+
const value = result?.value;
|
|
42
|
+
const wrote = value?.wrote;
|
|
43
|
+
return typeof wrote === 'boolean' ? wrote : undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
export type OriginRead = {
|
|
3
|
+
readonly kind: 'ok';
|
|
4
|
+
readonly remote: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: 'no-origin';
|
|
7
|
+
readonly detail?: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: 'unknown';
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
readonly detail?: string;
|
|
12
|
+
};
|
|
13
|
+
export type OriginReader = (path: string) => Promise<OriginRead>;
|
|
14
|
+
export type BranchRead = {
|
|
15
|
+
readonly kind: 'ok';
|
|
16
|
+
readonly branch: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'no-branch';
|
|
19
|
+
readonly detail?: string | undefined;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'unknown';
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
readonly detail?: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
export type BranchReader = (path: string) => Promise<BranchRead>;
|
|
26
|
+
export type HeadRead = {
|
|
27
|
+
readonly kind: 'ok';
|
|
28
|
+
readonly commit: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'no-head';
|
|
31
|
+
readonly detail?: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'unknown';
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly detail?: string;
|
|
36
|
+
};
|
|
37
|
+
export type HeadReader = (path: string) => Promise<HeadRead>;
|
|
38
|
+
export declare function createHeadReader(options: {
|
|
39
|
+
shell: DshShell;
|
|
40
|
+
logger?: {
|
|
41
|
+
warn?(message: string): void;
|
|
42
|
+
} | undefined;
|
|
43
|
+
}): HeadReader;
|
|
44
|
+
export declare function createBranchReader(options: {
|
|
45
|
+
shell: DshShell;
|
|
46
|
+
logger?: {
|
|
47
|
+
warn?(message: string): void;
|
|
48
|
+
} | undefined;
|
|
49
|
+
}): BranchReader;
|
|
50
|
+
export declare function createOriginReader(options: {
|
|
51
|
+
shell: DshShell;
|
|
52
|
+
logger?: {
|
|
53
|
+
warn?(message: string): void;
|
|
54
|
+
} | undefined;
|
|
55
|
+
}): OriginReader;
|
package/dist/host/git.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { quoteShellArg, tailLines } from '../lib/text.js';
|
|
2
|
+
import { runShell } from './shell.js';
|
|
3
|
+
export function createHeadReader(options) {
|
|
4
|
+
const { shell, logger } = options;
|
|
5
|
+
return async function readHead(path) {
|
|
6
|
+
const key = String(path ?? '').trim();
|
|
7
|
+
if (!key)
|
|
8
|
+
return { kind: 'unknown', reason: 'no-path' };
|
|
9
|
+
let result;
|
|
10
|
+
try {
|
|
11
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse HEAD`);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
logger?.warn?.(`[dsh-workbench] git rev-parse 执行失败:${String(error)}`);
|
|
15
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
16
|
+
}
|
|
17
|
+
if (result.exitCode !== 0) {
|
|
18
|
+
return { kind: 'no-head', detail: tailLines(result.stderr, 3) };
|
|
19
|
+
}
|
|
20
|
+
return { kind: 'ok', commit: result.stdout.trim() };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createBranchReader(options) {
|
|
24
|
+
const { shell, logger } = options;
|
|
25
|
+
return async function readBranch(path) {
|
|
26
|
+
const key = String(path ?? '').trim();
|
|
27
|
+
if (!key)
|
|
28
|
+
return { kind: 'unknown', reason: 'no-path' };
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse --abbrev-ref HEAD`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger?.warn?.(`[dsh-workbench] git rev-parse --abbrev-ref 执行失败:${String(error)}`);
|
|
35
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
36
|
+
}
|
|
37
|
+
const name = result.stdout.trim();
|
|
38
|
+
if (result.exitCode !== 0)
|
|
39
|
+
return { kind: 'no-branch', detail: tailLines(result.stderr, 3) };
|
|
40
|
+
if (name === '' || name === 'HEAD')
|
|
41
|
+
return { kind: 'no-branch', detail: name };
|
|
42
|
+
return { kind: 'ok', branch: name };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createOriginReader(options) {
|
|
46
|
+
const { shell, logger } = options;
|
|
47
|
+
return async function readOrigin(path) {
|
|
48
|
+
const key = String(path ?? '').trim();
|
|
49
|
+
if (!key)
|
|
50
|
+
return { kind: 'unknown', reason: 'no-path' };
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} remote get-url origin`);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
logger?.warn?.(`[dsh-workbench] git remote 执行失败:${String(error)}`);
|
|
57
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
58
|
+
}
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
return { kind: 'no-origin', detail: tailLines(result.stderr, 3) };
|
|
61
|
+
}
|
|
62
|
+
return { kind: 'ok', remote: result.stdout.trim() };
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DshHostContext } from '../api/dsh.js';
|
|
2
|
+
import { type ProvisionHandler } from './provision';
|
|
3
|
+
export type RouteHandler = (search: URLSearchParams) => Promise<unknown> | unknown;
|
|
4
|
+
export type RouteTable = Record<string, RouteHandler>;
|
|
5
|
+
export declare function flag(search: URLSearchParams, name: string): boolean;
|
|
6
|
+
export declare function param(search: URLSearchParams, name: string): string | undefined;
|
|
7
|
+
export declare function registerApi(ctx: DshHostContext, routes: RouteTable): () => void;
|
|
8
|
+
export declare function registerProvision(ctx: DshHostContext, provision: ProvisionHandler, log?: {
|
|
9
|
+
bump(facts: readonly string[]): unknown;
|
|
10
|
+
} | undefined): () => void;
|
|
11
|
+
export interface PostResource {
|
|
12
|
+
readonly resource: string;
|
|
13
|
+
readonly unit?: string;
|
|
14
|
+
handle(payload: unknown): Promise<unknown> | unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface PostWrite {
|
|
17
|
+
readonly args: unknown;
|
|
18
|
+
readonly result: unknown;
|
|
19
|
+
}
|
|
20
|
+
export declare function changesHandler(log: {
|
|
21
|
+
epoch(): string;
|
|
22
|
+
since(rev: number): {
|
|
23
|
+
rev: number;
|
|
24
|
+
facts: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
}): RouteHandler;
|
|
27
|
+
export declare function preflightHandler(preflight: {
|
|
28
|
+
run(options?: {
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
snapshot(): unknown;
|
|
32
|
+
}): RouteHandler;
|
|
33
|
+
export declare function capabilityHandler(probe: (unit: string | undefined, force: boolean) => Promise<unknown> | unknown): RouteHandler;
|
|
34
|
+
export declare function healthHandler(ctx: DshHostContext): RouteHandler;
|
|
35
|
+
export declare function registerPrefs(ctx: DshHostContext, resources: readonly PostResource[], wrote?: ((resource: string, write: PostWrite) => void) | undefined): () => void;
|
|
36
|
+
export declare function registerActions(ctx: DshHostContext, resources: readonly PostResource[], wrote?: ((resource: string, write: PostWrite) => void) | undefined): () => void;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { unitFactScope } from '../api/facts.js';
|
|
2
|
+
import { ACTIONS_PREFIX, API_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
|
|
3
|
+
import { isDependencyAction, PROVISION_ACTIONS, } from './provision';
|
|
4
|
+
import { WorkbenchDataError } from '../host/opencli.js';
|
|
5
|
+
import { writeResultForClient } from './write-result';
|
|
6
|
+
const MAX_PREFS_BODY_BYTES = 8 * 1024;
|
|
7
|
+
const PROVISION_FACTS = ['capability', 'panels'];
|
|
8
|
+
function provisionFacts(provision, request) {
|
|
9
|
+
const scopes = (provision.affectedUnits?.(request) ?? []).map(unitFactScope);
|
|
10
|
+
return [...PROVISION_FACTS, ...scopes];
|
|
11
|
+
}
|
|
12
|
+
function sendJson(res, status, body) {
|
|
13
|
+
res.statusCode = status;
|
|
14
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
15
|
+
res.setHeader('cache-control', 'no-store');
|
|
16
|
+
res.end(JSON.stringify(body));
|
|
17
|
+
}
|
|
18
|
+
function toErrorPayload(error) {
|
|
19
|
+
if (error instanceof WorkbenchDataError) {
|
|
20
|
+
return {
|
|
21
|
+
code: error.code,
|
|
22
|
+
message: error.message,
|
|
23
|
+
...(error.detail === undefined ? {} : { detail: error.detail }),
|
|
24
|
+
...(error.hint === undefined ? {} : { hint: error.hint }),
|
|
25
|
+
...(error.helpUrl === undefined ? {} : { helpUrl: error.helpUrl }),
|
|
26
|
+
...(error.project === undefined ? {} : { project: error.project }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { code: 'internal', message: error instanceof Error ? error.message : String(error) };
|
|
30
|
+
}
|
|
31
|
+
export function flag(search, name) {
|
|
32
|
+
const raw = search.get(name);
|
|
33
|
+
if (raw === null)
|
|
34
|
+
return false;
|
|
35
|
+
return raw === '' || raw === '1' || raw.toLowerCase() === 'true';
|
|
36
|
+
}
|
|
37
|
+
export function param(search, name) {
|
|
38
|
+
const raw = search.get(name);
|
|
39
|
+
return raw === null || raw === '' ? undefined : raw;
|
|
40
|
+
}
|
|
41
|
+
export function registerApi(ctx, routes) {
|
|
42
|
+
return ctx.webServer.register({
|
|
43
|
+
kind: 'prefix',
|
|
44
|
+
path: API_PREFIX,
|
|
45
|
+
handler: async (req, res) => {
|
|
46
|
+
const raw = req.url ?? '/';
|
|
47
|
+
const cut = raw.indexOf('?');
|
|
48
|
+
const pathname = cut === -1 ? raw : raw.slice(0, cut);
|
|
49
|
+
const search = new URLSearchParams(cut === -1 ? '' : raw.slice(cut + 1));
|
|
50
|
+
const route = pathname.slice(API_PREFIX.length).replace(/^\//, '');
|
|
51
|
+
if (req.method !== 'GET') {
|
|
52
|
+
sendJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: '只支持 GET' } });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const handler = Object.prototype.hasOwnProperty.call(routes, route) ? routes[route] : undefined;
|
|
56
|
+
if (handler === undefined) {
|
|
57
|
+
sendJson(res, 404, { ok: false, error: { code: 'not-found', message: `没有这个接口:${route}` } });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
sendJson(res, 200, await handler(search));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof WorkbenchDataError) {
|
|
65
|
+
sendJson(res, 200, { ok: false, error: toErrorPayload(error) });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
ctx.logger?.warn?.(`[dsh-workbench] 路由 ${route} 失败:${String(error)}`);
|
|
69
|
+
sendJson(res, 500, { ok: false, error: toErrorPayload(error) });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function registerProvision(ctx, provision, log) {
|
|
75
|
+
return ctx.webServer.register({
|
|
76
|
+
kind: 'prefix',
|
|
77
|
+
path: PROVISION_PREFIX,
|
|
78
|
+
handler: async (req, res) => {
|
|
79
|
+
if (req.method !== 'POST') {
|
|
80
|
+
sendJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: '机器供给只支持 POST' } });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
const raw = await readBoundedBody(req);
|
|
86
|
+
if (raw === null) {
|
|
87
|
+
sendJson(res, 413, { ok: false, error: { code: 'body-too-large', message: '请求体太大' } });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
payload = raw === '' ? {} : JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
sendJson(res, 400, { ok: false, error: { code: 'bad-json', message: String(error) } });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const body = typeof payload === 'object' && payload !== null
|
|
97
|
+
? payload
|
|
98
|
+
: {};
|
|
99
|
+
const action = body.action;
|
|
100
|
+
if (typeof action !== 'string' || !PROVISION_ACTIONS.includes(action)) {
|
|
101
|
+
sendJson(res, 400, {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: {
|
|
104
|
+
code: 'workbench/bad-action',
|
|
105
|
+
message: `不认识的 action:${String(action)}(可用:${PROVISION_ACTIONS.join(' / ')})`,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const wanted = action;
|
|
111
|
+
const target = isDependencyAction(wanted) ? body.id : body.unit;
|
|
112
|
+
if (typeof target !== 'string' || target.trim() === '') {
|
|
113
|
+
sendJson(res, 400, {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: {
|
|
116
|
+
code: isDependencyAction(wanted) ? 'workbench/no-id' : 'workbench/no-unit',
|
|
117
|
+
message: isDependencyAction(wanted) ? '缺少 id' : '缺少 unit',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const key = target.trim();
|
|
123
|
+
const request = isDependencyAction(wanted)
|
|
124
|
+
? { action: wanted, id: key }
|
|
125
|
+
: { action: wanted, unit: key };
|
|
126
|
+
try {
|
|
127
|
+
const result = await provision.handle(request);
|
|
128
|
+
log?.bump(provisionFacts(provision, request));
|
|
129
|
+
sendJson(res, 200, result);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
log?.bump(provisionFacts(provision, request));
|
|
133
|
+
sendJson(res, 200, { ok: false, error: toErrorPayload(error) });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function registerPost(ctx, options) {
|
|
139
|
+
const { prefix, what, resources, refusedCode, wrote } = options;
|
|
140
|
+
const table = new Map(resources.map((item) => [item.resource, item.handle]));
|
|
141
|
+
return ctx.webServer.register({
|
|
142
|
+
kind: 'prefix',
|
|
143
|
+
path: prefix,
|
|
144
|
+
handler: async (req, res) => {
|
|
145
|
+
if (req.method !== 'POST') {
|
|
146
|
+
sendJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: `${what}只支持 POST` } });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const raw = await readBoundedBody(req);
|
|
150
|
+
if (raw === null) {
|
|
151
|
+
sendJson(res, 413, { ok: false, error: { code: 'payload-too-large', message: '请求体过大' } });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let payload;
|
|
155
|
+
try {
|
|
156
|
+
payload = raw === '' ? {} : JSON.parse(raw);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
sendJson(res, 400, { ok: false, error: { code: 'bad-json', message: '请求体不是 JSON' } });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const requested = (req.url ?? '/').slice(prefix.length).replace(/^\//, '').split('?')[0] ?? '';
|
|
163
|
+
const handle = table.get(requested);
|
|
164
|
+
if (handle === undefined) {
|
|
165
|
+
sendJson(res, 404, { ok: false, error: { code: 'not-found', message: `没有这个${what}资源:${requested}` } });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const result = await handle(payload);
|
|
170
|
+
wrote?.(requested, { args: payload, result });
|
|
171
|
+
sendJson(res, 200, writeResultForClient(result, refusedCode));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
sendJson(res, 200, { ok: false, error: toErrorPayload(error) });
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function readBoundedBody(req) {
|
|
180
|
+
if (typeof req[Symbol.asyncIterator] !== 'function')
|
|
181
|
+
return '';
|
|
182
|
+
const chunks = [];
|
|
183
|
+
let size = 0;
|
|
184
|
+
for await (const chunk of req) {
|
|
185
|
+
size += chunk.byteLength;
|
|
186
|
+
if (size > MAX_PREFS_BODY_BYTES)
|
|
187
|
+
return null;
|
|
188
|
+
chunks.push(chunk);
|
|
189
|
+
}
|
|
190
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
191
|
+
}
|
|
192
|
+
export function changesHandler(log) {
|
|
193
|
+
return (search) => ({ ok: true, epoch: log.epoch(), ...log.since(Number(param(search, 'since') ?? 0) || 0) });
|
|
194
|
+
}
|
|
195
|
+
export function preflightHandler(preflight) {
|
|
196
|
+
return (search) => {
|
|
197
|
+
void preflight.run({ force: flag(search, 'force') });
|
|
198
|
+
return { ok: true, ...preflight.snapshot() };
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function capabilityHandler(probe) {
|
|
202
|
+
return async (search) => {
|
|
203
|
+
const unit = param(search, 'unit');
|
|
204
|
+
const result = await probe(unit, flag(search, 'force'));
|
|
205
|
+
if (result === undefined) {
|
|
206
|
+
return { ok: false, error: { code: 'workbench/unknown-unit', message: `没有这个单元:${unit}` } };
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, ...result };
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function healthHandler(ctx) {
|
|
212
|
+
return () => {
|
|
213
|
+
const profile = ctx.get('profileContext');
|
|
214
|
+
const dir = profile?.dir;
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
plugin: PLUGIN_ID,
|
|
218
|
+
profile: dir === undefined ? null : dir.split('/').pop(),
|
|
219
|
+
profileDir: dir ?? null,
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
now: new Date().toISOString(),
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function registerPrefs(ctx, resources, wrote) {
|
|
226
|
+
return registerPost(ctx, {
|
|
227
|
+
prefix: PREFS_PREFIX,
|
|
228
|
+
what: '偏好写',
|
|
229
|
+
refusedCode: 'write-refused',
|
|
230
|
+
resources,
|
|
231
|
+
...(wrote === undefined ? {} : { wrote }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export function registerActions(ctx, resources, wrote) {
|
|
235
|
+
return registerPost(ctx, {
|
|
236
|
+
prefix: ACTIONS_PREFIX,
|
|
237
|
+
what: '动作',
|
|
238
|
+
refusedCode: 'action-refused',
|
|
239
|
+
resources,
|
|
240
|
+
...(wrote === undefined ? {} : { wrote }),
|
|
241
|
+
});
|
|
242
|
+
}
|