@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,82 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { readFlag, readText, writeFlag, writeText } from './storage';
|
|
3
|
+
|
|
4
|
+
function fakeStorage(options: { throwOnWrite?: boolean } = {}) {
|
|
5
|
+
const data = new Map<string, string>();
|
|
6
|
+
return {
|
|
7
|
+
data,
|
|
8
|
+
storage: {
|
|
9
|
+
getItem: (key: string) => data.get(key) ?? null,
|
|
10
|
+
setItem: (key: string, value: string) => {
|
|
11
|
+
if (options.throwOnWrite === true) throw new Error('QuotaExceededError');
|
|
12
|
+
data.set(key, value);
|
|
13
|
+
},
|
|
14
|
+
removeItem: (key: string) => {
|
|
15
|
+
data.delete(key);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useStorage(options: { throwOnWrite?: boolean } = {}) {
|
|
22
|
+
const fake = fakeStorage(options);
|
|
23
|
+
vi.stubGlobal('window', { localStorage: fake.storage });
|
|
24
|
+
return fake;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('storage', () => {
|
|
32
|
+
it('写进去能读回来', () => {
|
|
33
|
+
const fake = useStorage();
|
|
34
|
+
writeText('k', 'v');
|
|
35
|
+
expect(readText('k')).toBe('v');
|
|
36
|
+
expect(fake.data.get('k')).toBe('v');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('写 null 等于删除', () => {
|
|
40
|
+
const fake = useStorage();
|
|
41
|
+
writeText('k', 'v');
|
|
42
|
+
writeText('k', null);
|
|
43
|
+
expect(readText('k')).toBeNull();
|
|
44
|
+
expect(fake.data.has('k')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('没存过时读回 null / 默认值', () => {
|
|
48
|
+
useStorage();
|
|
49
|
+
expect(readText('missing')).toBeNull();
|
|
50
|
+
expect(readFlag('missing', true)).toBe(true);
|
|
51
|
+
expect(readFlag('missing', false)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('布尔开关按字面量往返', () => {
|
|
55
|
+
useStorage();
|
|
56
|
+
writeFlag('on', true);
|
|
57
|
+
writeFlag('off', false);
|
|
58
|
+
expect(readFlag('on', false)).toBe(true);
|
|
59
|
+
expect(readFlag('off', true)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('只有字面量 "true" 算真:脏数据不因为"非空"就把开关打开', () => {
|
|
63
|
+
useStorage();
|
|
64
|
+
writeText('weird', 'yes');
|
|
65
|
+
writeText('one', '1');
|
|
66
|
+
expect(readFlag('weird', true)).toBe(false);
|
|
67
|
+
expect(readFlag('one', true)).toBe(false);
|
|
68
|
+
expect(readFlag('never', true)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('localStorage 抛异常时静默降级(隐私模式不该让面板崩掉)', () => {
|
|
72
|
+
useStorage({ throwOnWrite: true });
|
|
73
|
+
expect(() => writeText('k', 'v')).not.toThrow();
|
|
74
|
+
expect(readText('k')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('window 上没有 localStorage 时也不抛', () => {
|
|
78
|
+
vi.stubGlobal('window', {});
|
|
79
|
+
expect(() => writeText('k', 'v')).not.toThrow();
|
|
80
|
+
expect(readText('k')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function readText(key: string): string | null {
|
|
2
|
+
try {
|
|
3
|
+
return window.localStorage.getItem(key);
|
|
4
|
+
} catch {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function writeText(key: string, value: string | null): void {
|
|
10
|
+
try {
|
|
11
|
+
if (value === null) window.localStorage.removeItem(key);
|
|
12
|
+
else window.localStorage.setItem(key, value);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readFlag(key: string, fallback: boolean): boolean {
|
|
18
|
+
const raw = readText(key);
|
|
19
|
+
if (raw === null) return fallback;
|
|
20
|
+
return raw === 'true';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeFlag(key: string, value: boolean): void {
|
|
24
|
+
writeText(key, value ? 'true' : 'false');
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import type { DshShell } from '../../api/dsh.js';
|
|
3
|
+
import type { Opencli, OpencliResult } from '../../host/opencli.js';
|
|
4
|
+
|
|
5
|
+
export interface ShellCall {
|
|
6
|
+
command: string;
|
|
7
|
+
workdir?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
onExpiry?: string;
|
|
10
|
+
stdoutMaxBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ShellReply {
|
|
14
|
+
stdout?: string;
|
|
15
|
+
stderr?: string;
|
|
16
|
+
exitCode?: number | null;
|
|
17
|
+
timedOut?: boolean;
|
|
18
|
+
aborted?: boolean;
|
|
19
|
+
failWith?: Error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tick = (): Promise<void> => delay(0);
|
|
23
|
+
|
|
24
|
+
export function createFakeShell(replies: ShellReply[] | ((call: ShellCall, index: number) => ShellReply) = []) {
|
|
25
|
+
const calls: ShellCall[] = [];
|
|
26
|
+
let running = 0;
|
|
27
|
+
let peak = 0;
|
|
28
|
+
|
|
29
|
+
const replyAt = (index: number): ShellReply =>
|
|
30
|
+
typeof replies === 'function' ? replies(calls[index] as ShellCall, index) : (replies[index] ?? {});
|
|
31
|
+
|
|
32
|
+
const shell: DshShell = {
|
|
33
|
+
resolve(request) {
|
|
34
|
+
calls.push(request as ShellCall);
|
|
35
|
+
return request;
|
|
36
|
+
},
|
|
37
|
+
async execute() {
|
|
38
|
+
const index = calls.length - 1;
|
|
39
|
+
const reply = replyAt(index);
|
|
40
|
+
if (reply.failWith !== undefined) throw reply.failWith;
|
|
41
|
+
|
|
42
|
+
running += 1;
|
|
43
|
+
peak = Math.max(peak, running);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async result() {
|
|
47
|
+
await tick();
|
|
48
|
+
running -= 1;
|
|
49
|
+
return {
|
|
50
|
+
exitCode: reply.exitCode ?? 0,
|
|
51
|
+
timedOut: reply.timedOut ?? false,
|
|
52
|
+
aborted: reply.aborted ?? false,
|
|
53
|
+
stdout: { text: reply.stdout ?? '', truncated: false },
|
|
54
|
+
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
shell,
|
|
63
|
+
calls,
|
|
64
|
+
peak: () => peak,
|
|
65
|
+
commands: () => calls.map((call) => call.command),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OpencliCall {
|
|
70
|
+
args: readonly string[];
|
|
71
|
+
key?: string;
|
|
72
|
+
workdir?: string;
|
|
73
|
+
refresh?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createFakeOpencli(
|
|
77
|
+
replies: Array<unknown | Error> | ((call: OpencliCall, index: number) => unknown | Error) = [],
|
|
78
|
+
) {
|
|
79
|
+
const calls: OpencliCall[] = [];
|
|
80
|
+
|
|
81
|
+
const opencli: Opencli = {
|
|
82
|
+
async json(args, options = {}) {
|
|
83
|
+
const call: OpencliCall = {
|
|
84
|
+
args,
|
|
85
|
+
...(options.key === undefined ? {} : { key: options.key }),
|
|
86
|
+
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
87
|
+
};
|
|
88
|
+
calls.push(call);
|
|
89
|
+
const index = calls.length - 1;
|
|
90
|
+
const reply =
|
|
91
|
+
typeof replies === 'function' ? replies(call, index) : (replies[index] ?? []);
|
|
92
|
+
|
|
93
|
+
if (reply instanceof Error) throw reply;
|
|
94
|
+
return { value: reply, at: Date.now() } satisfies OpencliResult;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
opencli,
|
|
100
|
+
calls,
|
|
101
|
+
keys: () => calls.map((call) => call.key),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createFakeLogger() {
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
return {
|
|
108
|
+
logger: {
|
|
109
|
+
info: () => {},
|
|
110
|
+
warn: (message: string) => {
|
|
111
|
+
warnings.push(message);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
warnings,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OPENCLI_PIN } from '../../config/constants.js';
|
|
3
|
+
|
|
4
|
+
async function load(dshHome: string, home: string) {
|
|
5
|
+
vi.resetModules();
|
|
6
|
+
vi.stubEnv('DSH_HOME', dshHome);
|
|
7
|
+
vi.stubEnv('HOME', home);
|
|
8
|
+
return import('../opencli-runtime');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.unstubAllEnvs();
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('opencli 隔离运行时', () => {
|
|
17
|
+
it('三条路径都从 $DSH_HOME 推(与 wrapper 必须一致)', async () => {
|
|
18
|
+
const runtime = await load('/tmp/dshhome', '/tmp/realhome');
|
|
19
|
+
|
|
20
|
+
expect(runtime.isolatedRoot()).toBe('/tmp/dshhome/workbench/opencli');
|
|
21
|
+
expect(runtime.isolatedHome()).toBe('/tmp/dshhome/workbench/opencli/home');
|
|
22
|
+
expect(runtime.toolchainDir()).toBe('/tmp/dshhome/workbench/opencli/toolchain');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('DSH_HOME 为空 → 退回 $HOME/.dsh(与 wrapper 的 `${DSH_HOME:-…}` 同义)', async () => {
|
|
26
|
+
const runtime = await load('', '/tmp/realhome');
|
|
27
|
+
|
|
28
|
+
expect(runtime.isolatedRoot()).toBe('/tmp/realhome/.dsh/workbench/opencli');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('★ 安装命令装进**隔离前缀**且钉住版本 —— 不是 npm i -g', async () => {
|
|
32
|
+
const runtime = await load('/tmp/dshhome', '/tmp/realhome');
|
|
33
|
+
const command = runtime.isolatedInstallCommand();
|
|
34
|
+
|
|
35
|
+
expect(command).toContain("--prefix '/tmp/dshhome/workbench/opencli/toolchain'");
|
|
36
|
+
expect(command).toContain(`@jackwener/opencli@${OPENCLI_PIN}`);
|
|
37
|
+
expect(command).not.toContain(' -g');
|
|
38
|
+
expect(command).toContain("--cache '/tmp/dshhome/workbench/opencli/npm-cache'");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('★ 对外给的入口是**稳定路径**(软链),不依赖本仓库在哪', async () => {
|
|
42
|
+
const runtime = await load('/tmp/dshhome', '/tmp/realhome');
|
|
43
|
+
|
|
44
|
+
expect(runtime.isolatedCommand()).toBe('/tmp/dshhome/workbench/opencli/bin/opencli');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface CliErrorEnvelope {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly help?: string;
|
|
5
|
+
readonly exitCode?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function unquote(raw: string): string {
|
|
9
|
+
const value = raw.trim();
|
|
10
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
11
|
+
return value.slice(1, -1);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function joinBlock(previous: string, line: string, fold: boolean): string {
|
|
17
|
+
if (previous === '') return line;
|
|
18
|
+
return fold ? `${previous} ${line}` : `${previous}\n${line}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCliErrorEnvelope(text: string | undefined): CliErrorEnvelope | undefined {
|
|
22
|
+
if (text === undefined || text.trim() === '') return undefined;
|
|
23
|
+
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
const start = lines.findIndex((line) => /^error:\s*$/.test(line.trim()) || /^error:\s*\S/.test(line.trim()));
|
|
26
|
+
if (start === -1) return undefined;
|
|
27
|
+
|
|
28
|
+
const scalars: Record<string, string> = {};
|
|
29
|
+
let current: string | undefined;
|
|
30
|
+
let block: { readonly key: string; readonly fold: boolean; readonly indent: number } | undefined;
|
|
31
|
+
const errorIndent = lines[start]?.search(/\S/) ?? 0;
|
|
32
|
+
for (const line of lines.slice(start + 1)) {
|
|
33
|
+
if (line.trim() === '') continue;
|
|
34
|
+
const indent = line.search(/\S/);
|
|
35
|
+
if (block !== undefined && indent > block.indent) {
|
|
36
|
+
scalars[block.key] = joinBlock(scalars[block.key] ?? '', line.trim(), block.fold);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
block = undefined;
|
|
40
|
+
if (indent <= errorIndent) break;
|
|
41
|
+
|
|
42
|
+
const match = /^\s*(code|message|help|exitCode):\s*(.*)$/.exec(line);
|
|
43
|
+
if (match === null) {
|
|
44
|
+
if (current !== undefined) scalars[current] = `${scalars[current] ?? ''} ${line.trim()}`.trim();
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const [, key, value] = match;
|
|
48
|
+
if (key === undefined) continue;
|
|
49
|
+
const raw = (value ?? '').trim();
|
|
50
|
+
const marker = /^([|>])[0-9]*([+-]?)$/.exec(raw);
|
|
51
|
+
if (marker !== null) {
|
|
52
|
+
scalars[key] = '';
|
|
53
|
+
block = { key, fold: marker[1] === '>', indent };
|
|
54
|
+
current = key;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
scalars[key] = unquote(raw);
|
|
58
|
+
current = key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const code = scalars['code'];
|
|
62
|
+
const message = scalars['message'];
|
|
63
|
+
if (code === undefined || code === '') return undefined;
|
|
64
|
+
|
|
65
|
+
const exitCode = scalars['exitCode'];
|
|
66
|
+
return {
|
|
67
|
+
code,
|
|
68
|
+
message: message ?? code,
|
|
69
|
+
...(scalars['help'] === undefined || scalars['help'] === '' ? {} : { help: scalars['help'] }),
|
|
70
|
+
...(exitCode === undefined || Number.isNaN(Number(exitCode)) ? {} : { exitCode: Number(exitCode) }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function helpUrlOf(envelope: CliErrorEnvelope): string | undefined {
|
|
75
|
+
const match = /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? '');
|
|
76
|
+
return match?.[0];
|
|
77
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function dshHome(): string {
|
|
2
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
3
|
+
if (configured !== undefined && configured !== '') return configured;
|
|
4
|
+
return `${process.env.HOME ?? ''}/.dsh`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function workbenchRoot(): string {
|
|
8
|
+
return `${dshHome()}/workbench`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function workspacesRoot(): string {
|
|
12
|
+
return `${workbenchRoot()}/workspaces`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { UnitManifest } from '../api/unit.js';
|
|
2
|
+
|
|
3
|
+
export interface FactChange {
|
|
4
|
+
readonly rev: number;
|
|
5
|
+
readonly facts: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FactSnapshot extends FactChange {
|
|
9
|
+
readonly epoch: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FactLog {
|
|
13
|
+
bump(facts: readonly string[]): FactChange;
|
|
14
|
+
revision(): number;
|
|
15
|
+
epoch(): string;
|
|
16
|
+
since(rev: number): FactSnapshot;
|
|
17
|
+
subscribe(listener: (change: FactChange) => void): () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createFactLog(options: { readonly epoch?: string } = {}): FactLog {
|
|
21
|
+
const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
let rev = 0;
|
|
23
|
+
const last = new Map<string, number>();
|
|
24
|
+
const listeners = new Set<(change: FactChange) => void>();
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
epoch: () => epoch,
|
|
28
|
+
revision: () => rev,
|
|
29
|
+
bump(facts) {
|
|
30
|
+
rev += 1;
|
|
31
|
+
const named = [...new Set(facts.filter((name) => name !== ''))];
|
|
32
|
+
for (const name of named) last.set(name, rev);
|
|
33
|
+
const change: FactChange = { rev, facts: named };
|
|
34
|
+
for (const listener of [...listeners]) listener(change);
|
|
35
|
+
return change;
|
|
36
|
+
},
|
|
37
|
+
since(seen) {
|
|
38
|
+
const facts = [...last.entries()].filter(([, at]) => at > seen).map(([name]) => name);
|
|
39
|
+
return { epoch, rev, facts };
|
|
40
|
+
},
|
|
41
|
+
subscribe(listener) {
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ToolUnit {
|
|
51
|
+
readonly unit: string;
|
|
52
|
+
readonly writes: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toolUnits(manifests: readonly UnitManifest[]): Map<string, ToolUnit> {
|
|
56
|
+
const map = new Map<string, ToolUnit>();
|
|
57
|
+
for (const manifest of manifests) {
|
|
58
|
+
for (const tool of manifest.tools ?? []) {
|
|
59
|
+
map.set(tool.name, { unit: manifest.id, writes: tool.writes });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function wroteNow(result: unknown): boolean | undefined {
|
|
66
|
+
const value = (result as { value?: unknown } | null)?.value;
|
|
67
|
+
const wrote = (value as { wrote?: unknown } | null)?.wrote;
|
|
68
|
+
return typeof wrote === 'boolean' ? wrote : undefined;
|
|
69
|
+
}
|
package/src/host/git.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
import { quoteShellArg, tailLines } from '../lib/text.js';
|
|
3
|
+
import { runShell } from './shell.js';
|
|
4
|
+
|
|
5
|
+
export type OriginRead =
|
|
6
|
+
| { readonly kind: 'ok'; readonly remote: string }
|
|
7
|
+
| { readonly kind: 'no-origin'; readonly detail?: string }
|
|
8
|
+
| { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string };
|
|
9
|
+
|
|
10
|
+
export type OriginReader = (path: string) => Promise<OriginRead>;
|
|
11
|
+
|
|
12
|
+
export type BranchRead =
|
|
13
|
+
| { readonly kind: 'ok'; readonly branch: string }
|
|
14
|
+
| { readonly kind: 'no-branch'; readonly detail?: string | undefined }
|
|
15
|
+
| { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string | undefined };
|
|
16
|
+
|
|
17
|
+
export type BranchReader = (path: string) => Promise<BranchRead>;
|
|
18
|
+
|
|
19
|
+
export type HeadRead =
|
|
20
|
+
| { readonly kind: 'ok'; readonly commit: string }
|
|
21
|
+
| { readonly kind: 'no-head'; readonly detail?: string }
|
|
22
|
+
| { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string };
|
|
23
|
+
|
|
24
|
+
export type HeadReader = (path: string) => Promise<HeadRead>;
|
|
25
|
+
|
|
26
|
+
export function createHeadReader(options: {
|
|
27
|
+
shell: DshShell;
|
|
28
|
+
logger?: { warn?(message: string): void } | undefined;
|
|
29
|
+
}): HeadReader {
|
|
30
|
+
const { shell, logger } = options;
|
|
31
|
+
|
|
32
|
+
return async function readHead(path: string): Promise<HeadRead> {
|
|
33
|
+
const key = String(path ?? '').trim();
|
|
34
|
+
if (!key) return { kind: 'unknown', reason: 'no-path' };
|
|
35
|
+
|
|
36
|
+
let result;
|
|
37
|
+
try {
|
|
38
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse HEAD`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger?.warn?.(`[dsh-workbench] git rev-parse 执行失败:${String(error)}`);
|
|
41
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (result.exitCode !== 0) {
|
|
45
|
+
return { kind: 'no-head', detail: tailLines(result.stderr, 3) };
|
|
46
|
+
}
|
|
47
|
+
return { kind: 'ok', commit: result.stdout.trim() };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createBranchReader(options: {
|
|
52
|
+
shell: DshShell;
|
|
53
|
+
logger?: { warn?(message: string): void } | undefined;
|
|
54
|
+
}): BranchReader {
|
|
55
|
+
const { shell, logger } = options;
|
|
56
|
+
|
|
57
|
+
return async function readBranch(path: string): Promise<BranchRead> {
|
|
58
|
+
const key = String(path ?? '').trim();
|
|
59
|
+
if (!key) return { kind: 'unknown', reason: 'no-path' };
|
|
60
|
+
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse --abbrev-ref HEAD`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger?.warn?.(`[dsh-workbench] git rev-parse --abbrev-ref 执行失败:${String(error)}`);
|
|
66
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const name = result.stdout.trim();
|
|
70
|
+
if (result.exitCode !== 0) return { kind: 'no-branch', detail: tailLines(result.stderr, 3) };
|
|
71
|
+
if (name === '' || name === 'HEAD') return { kind: 'no-branch', detail: name };
|
|
72
|
+
return { kind: 'ok', branch: name };
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createOriginReader(options: {
|
|
77
|
+
shell: DshShell;
|
|
78
|
+
logger?: { warn?(message: string): void } | undefined;
|
|
79
|
+
}): OriginReader {
|
|
80
|
+
const { shell, logger } = options;
|
|
81
|
+
|
|
82
|
+
return async function readOrigin(path: string): Promise<OriginRead> {
|
|
83
|
+
const key = String(path ?? '').trim();
|
|
84
|
+
if (!key) return { kind: 'unknown', reason: 'no-path' };
|
|
85
|
+
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await runShell(shell, `git -C ${quoteShellArg(key)} remote get-url origin`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger?.warn?.(`[dsh-workbench] git remote 执行失败:${String(error)}`);
|
|
91
|
+
return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.exitCode !== 0) {
|
|
95
|
+
return { kind: 'no-origin', detail: tailLines(result.stderr, 3) };
|
|
96
|
+
}
|
|
97
|
+
return { kind: 'ok', remote: result.stdout.trim() };
|
|
98
|
+
};
|
|
99
|
+
}
|