@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.
Files changed (206) hide show
  1. package/dist/api/blocker-table.d.ts +9 -0
  2. package/dist/api/blocker-table.js +130 -0
  3. package/dist/api/blocker.d.ts +30 -0
  4. package/dist/api/blocker.js +1 -0
  5. package/dist/api/context-ref.d.ts +10 -0
  6. package/dist/api/context-ref.js +40 -0
  7. package/dist/api/dsh-client.d.ts +170 -0
  8. package/dist/api/dsh-client.js +1 -0
  9. package/dist/api/dsh.d.ts +153 -0
  10. package/dist/api/dsh.js +1 -0
  11. package/dist/api/facts.d.ts +27 -0
  12. package/dist/api/facts.js +146 -0
  13. package/dist/api/host-write.d.ts +12 -0
  14. package/dist/api/host-write.js +17 -0
  15. package/dist/api/long-task.d.ts +18 -0
  16. package/dist/api/long-task.js +22 -0
  17. package/dist/api/unit-board.d.ts +14 -0
  18. package/dist/api/unit-board.js +29 -0
  19. package/dist/api/unit.d.ts +54 -0
  20. package/dist/api/unit.js +1 -0
  21. package/dist/api/workbench-client.d.ts +19 -0
  22. package/dist/api/workbench-client.js +33 -0
  23. package/dist/api/workbench-contract.d.ts +32 -0
  24. package/dist/api/workbench-contract.js +1 -0
  25. package/dist/components/ApprovalDetail.d.ts +27 -0
  26. package/dist/components/ApprovalDetail.js +33 -0
  27. package/dist/components/CardHead.d.ts +16 -0
  28. package/dist/components/CardHead.js +14 -0
  29. package/dist/components/ChoiceGroup.d.ts +14 -0
  30. package/dist/components/ChoiceGroup.js +7 -0
  31. package/dist/components/LongTaskRow.d.ts +16 -0
  32. package/dist/components/LongTaskRow.js +28 -0
  33. package/dist/components/MockStrip.d.ts +14 -0
  34. package/dist/components/MockStrip.js +7 -0
  35. package/dist/components/ResourceStates.d.ts +18 -0
  36. package/dist/components/ResourceStates.js +17 -0
  37. package/dist/components/SaveBar.d.ts +12 -0
  38. package/dist/components/SaveBar.js +6 -0
  39. package/dist/components/StateDot.d.ts +5 -0
  40. package/dist/components/StateDot.js +10 -0
  41. package/dist/components/board-module.d.ts +67 -0
  42. package/dist/components/board-module.js +1 -0
  43. package/dist/components/icons.d.ts +18 -0
  44. package/dist/components/icons.js +81 -0
  45. package/dist/config/constants.d.ts +27 -0
  46. package/dist/config/constants.js +27 -0
  47. package/dist/config/storage.d.ts +4 -0
  48. package/dist/config/storage.js +27 -0
  49. package/dist/host/__tests__/fakes.d.ts +41 -0
  50. package/dist/host/__tests__/fakes.js +76 -0
  51. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  52. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  53. package/dist/host/cli-error.d.ts +8 -0
  54. package/dist/host/cli-error.js +70 -0
  55. package/dist/host/context-inject.d.ts +24 -0
  56. package/dist/host/context-inject.js +74 -0
  57. package/dist/host/data-root.d.ts +3 -0
  58. package/dist/host/data-root.js +12 -0
  59. package/dist/host/events.d.ts +9 -0
  60. package/dist/host/events.js +70 -0
  61. package/dist/host/facts.d.ts +24 -0
  62. package/dist/host/facts.js +44 -0
  63. package/dist/host/git.d.ts +55 -0
  64. package/dist/host/git.js +64 -0
  65. package/dist/host/http.d.ts +36 -0
  66. package/dist/host/http.js +242 -0
  67. package/dist/host/long-task.d.ts +58 -0
  68. package/dist/host/long-task.js +148 -0
  69. package/dist/host/mount-units.d.ts +52 -0
  70. package/dist/host/mount-units.js +186 -0
  71. package/dist/host/opencli-runtime.d.ts +22 -0
  72. package/dist/host/opencli-runtime.js +46 -0
  73. package/dist/host/opencli.d.ts +37 -0
  74. package/dist/host/opencli.js +152 -0
  75. package/dist/host/plugin-source.d.ts +31 -0
  76. package/dist/host/plugin-source.js +81 -0
  77. package/dist/host/preflight.d.ts +58 -0
  78. package/dist/host/preflight.js +233 -0
  79. package/dist/host/probe-cache.d.ts +6 -0
  80. package/dist/host/probe-cache.js +19 -0
  81. package/dist/host/probe-libs.d.ts +45 -0
  82. package/dist/host/probe-libs.js +61 -0
  83. package/dist/host/probe-units.d.ts +17 -0
  84. package/dist/host/probe-units.js +102 -0
  85. package/dist/host/probe-verdict.d.ts +7 -0
  86. package/dist/host/probe-verdict.js +143 -0
  87. package/dist/host/provision.d.ts +14 -0
  88. package/dist/host/provision.js +12 -0
  89. package/dist/host/shell.d.ts +14 -0
  90. package/dist/host/shell.js +23 -0
  91. package/dist/host/tool-approval.d.ts +29 -0
  92. package/dist/host/tool-approval.js +190 -0
  93. package/dist/host/tool-kit.d.ts +19 -0
  94. package/dist/host/tool-kit.js +37 -0
  95. package/dist/host/tool-scope.d.ts +32 -0
  96. package/dist/host/tool-scope.js +84 -0
  97. package/dist/host/unit-face.d.ts +66 -0
  98. package/dist/host/unit-face.js +1 -0
  99. package/dist/host/unit-libs.d.ts +37 -0
  100. package/dist/host/unit-libs.js +1 -0
  101. package/dist/host/unit-readiness.d.ts +7 -0
  102. package/dist/host/unit-readiness.js +5 -0
  103. package/dist/host/unit-registry.d.ts +9 -0
  104. package/dist/host/unit-registry.js +33 -0
  105. package/dist/host/value-handoff.d.ts +16 -0
  106. package/dist/host/value-handoff.js +21 -0
  107. package/dist/host/version-range.d.ts +2 -0
  108. package/dist/host/version-range.js +73 -0
  109. package/dist/host/workspace-store.d.ts +19 -0
  110. package/dist/host/workspace-store.js +133 -0
  111. package/dist/host/write-result.d.ts +1 -0
  112. package/dist/host/write-result.js +18 -0
  113. package/dist/host.d.ts +19 -0
  114. package/dist/host.js +19 -0
  115. package/dist/i18n/en.d.ts +2 -0
  116. package/dist/i18n/en.js +162 -0
  117. package/dist/i18n/index.d.ts +172 -0
  118. package/dist/i18n/index.js +3 -0
  119. package/dist/i18n/merge.d.ts +6 -0
  120. package/dist/i18n/merge.js +40 -0
  121. package/dist/i18n/zh.d.ts +163 -0
  122. package/dist/i18n/zh.js +162 -0
  123. package/dist/index.d.ts +33 -0
  124. package/dist/index.js +33 -0
  125. package/dist/lib/blocker-action.d.ts +13 -0
  126. package/dist/lib/blocker-action.js +12 -0
  127. package/dist/lib/busy.d.ts +5 -0
  128. package/dist/lib/busy.js +7 -0
  129. package/dist/lib/react.d.ts +10 -0
  130. package/dist/lib/react.js +9 -0
  131. package/dist/lib/text.d.ts +4 -0
  132. package/dist/lib/text.js +48 -0
  133. package/dist/lib/ui.d.ts +11 -0
  134. package/dist/lib/ui.js +37 -0
  135. package/package.json +44 -0
  136. package/src/api/__tests__/blocker-table.test.ts +253 -0
  137. package/src/api/__tests__/facts.test.ts +274 -0
  138. package/src/api/__tests__/host-write.test.ts +55 -0
  139. package/src/api/__tests__/long-task.test.ts +52 -0
  140. package/src/api/__tests__/unit-board.test.ts +76 -0
  141. package/src/api/blocker-table.ts +155 -0
  142. package/src/api/blocker.ts +25 -0
  143. package/src/api/context-ref.ts +47 -0
  144. package/src/api/dsh-client.ts +176 -0
  145. package/src/api/dsh.ts +156 -0
  146. package/src/api/facts.ts +166 -0
  147. package/src/api/host-write.ts +27 -0
  148. package/src/api/long-task.ts +38 -0
  149. package/src/api/unit-board.ts +44 -0
  150. package/src/api/unit.ts +59 -0
  151. package/src/api/workbench-client.test.ts +92 -0
  152. package/src/api/workbench-client.ts +59 -0
  153. package/src/api/workbench-contract.ts +40 -0
  154. package/src/components/ApprovalDetail.tsx +57 -0
  155. package/src/components/CardHead.tsx +74 -0
  156. package/src/components/ChoiceGroup.tsx +38 -0
  157. package/src/components/LongTaskRow.tsx +65 -0
  158. package/src/components/MockStrip.tsx +40 -0
  159. package/src/components/ResourceStates.tsx +44 -0
  160. package/src/components/SaveBar.tsx +31 -0
  161. package/src/components/StateDot.tsx +31 -0
  162. package/src/components/board-module.ts +73 -0
  163. package/src/components/icons.tsx +232 -0
  164. package/src/config/constants.test.ts +41 -0
  165. package/src/config/constants.ts +52 -0
  166. package/src/config/storage.test.ts +82 -0
  167. package/src/config/storage.ts +25 -0
  168. package/src/host/__tests__/fakes.ts +116 -0
  169. package/src/host/__tests__/opencli-runtime.test.ts +46 -0
  170. package/src/host/cli-error.ts +77 -0
  171. package/src/host/data-root.ts +13 -0
  172. package/src/host/facts.ts +69 -0
  173. package/src/host/git.ts +99 -0
  174. package/src/host/http.ts +311 -0
  175. package/src/host/long-task.ts +235 -0
  176. package/src/host/opencli-runtime.ts +66 -0
  177. package/src/host/opencli.ts +210 -0
  178. package/src/host/plugin-source.ts +145 -0
  179. package/src/host/provision.ts +34 -0
  180. package/src/host/shell.ts +42 -0
  181. package/src/host/tool-approval.ts +243 -0
  182. package/src/host/tool-kit.ts +56 -0
  183. package/src/host/unit-face.ts +65 -0
  184. package/src/host/unit-libs.ts +41 -0
  185. package/src/host/value-handoff.ts +38 -0
  186. package/src/host/workspace-store.ts +161 -0
  187. package/src/host/write-result.ts +16 -0
  188. package/src/host.ts +19 -0
  189. package/src/i18n/en.ts +170 -0
  190. package/src/i18n/index.test.ts +15 -0
  191. package/src/i18n/index.ts +12 -0
  192. package/src/i18n/merge.test.ts +60 -0
  193. package/src/i18n/merge.ts +54 -0
  194. package/src/i18n/zh.test.ts +40 -0
  195. package/src/i18n/zh.ts +170 -0
  196. package/src/index.ts +33 -0
  197. package/src/lib/blocker-action.test.ts +40 -0
  198. package/src/lib/blocker-action.ts +28 -0
  199. package/src/lib/busy.test.ts +27 -0
  200. package/src/lib/busy.ts +11 -0
  201. package/src/lib/react.test.ts +26 -0
  202. package/src/lib/react.ts +16 -0
  203. package/src/lib/text.test.ts +92 -0
  204. package/src/lib/text.ts +46 -0
  205. package/src/lib/ui.test.ts +93 -0
  206. package/src/lib/ui.ts +38 -0
@@ -0,0 +1,56 @@
1
+ import type { DshToolRunContext } from '../api/dsh.js';
2
+ import { WorkbenchDataError } from './opencli';
3
+
4
+ export function sessionWorkspace(exec: DshToolRunContext | undefined): string | null {
5
+ const cwd = exec?.agent?.session?.header?.cwd;
6
+ return typeof cwd === 'string' && cwd.trim() !== '' ? cwd.trim() : null;
7
+ }
8
+
9
+ export function textOutput(text: string): Array<Record<string, unknown>> {
10
+ return [{ type: 'text', text }];
11
+ }
12
+
13
+ export function textRender(_args: unknown, value: unknown): Array<Record<string, unknown>> {
14
+ const text = (value as { text?: unknown } | null)?.text;
15
+ return textOutput(typeof text === 'string' ? text : JSON.stringify(value));
16
+ }
17
+
18
+ export const TEXT_OUTPUT = { schema: { type: 'object', properties: {}, additionalProperties: true }, render: textRender };
19
+
20
+ export function jsonText(value: unknown): string {
21
+ return JSON.stringify(value);
22
+ }
23
+
24
+ const UNCLASSIFIED = 'cli-unclassified';
25
+
26
+ export interface CommandHintNames {
27
+ readonly plugin: string;
28
+ readonly suiteId: string;
29
+ }
30
+
31
+ export type CommandHint = <T>(command: string, run: () => Promise<T>) => Promise<T>;
32
+
33
+ export function commandHintFor(names: CommandHintNames): CommandHint {
34
+ return async (command, run) => {
35
+ try {
36
+ return await run();
37
+ } catch (error) {
38
+ if (!(error instanceof WorkbenchDataError) || error.code !== UNCLASSIFIED) throw error;
39
+
40
+ const advice =
41
+ `这条命令(${command})在这份 ${names.plugin} 插件里可能不存在(插件版本旧就会缺):` +
42
+ `用 workbench_modules(action=update, id=${names.suiteId})升级插件,或在面板的模块状态里点更新,再重试。`;
43
+
44
+ throw new WorkbenchDataError(error.code, `${error.message}。${advice}`, {
45
+ ...(error.detail === undefined ? {} : { detail: error.detail }),
46
+ hint:
47
+ `升级:workbench_modules(action=update, id=${names.suiteId})。` +
48
+ `终端确认:opencli ${names.plugin} ${command} --help${
49
+ error.hint === undefined ? '' : `\n${error.hint}`
50
+ }`,
51
+ ...(error.helpUrl === undefined ? {} : { helpUrl: error.helpUrl }),
52
+ ...(error.project === undefined ? {} : { project: error.project }),
53
+ });
54
+ }
55
+ };
56
+ }
@@ -0,0 +1,65 @@
1
+ import type { DshHostContext } from '../api/dsh.js';
2
+ import type { UnitManifest } from '../api/unit.js';
3
+ import type { RouteTable } from '../host/http.js';
4
+ import type { LongTaskBase, LongTaskHost, LongTaskRegistry } from '../host/long-task.js';
5
+ import type { ProvisionHandler } from '../host/provision.js';
6
+ import type { UnitCapability } from './unit-libs';
7
+ import type { ApprovalCall, ApprovalFacts } from '../host/tool-approval.js';
8
+ import type { ValueHandoff } from '../host/value-handoff.js';
9
+ import type { WorkspaceStore } from '../host/workspace-store.js';
10
+
11
+ export interface UnitMountDeps {
12
+ readonly ctx: DshHostContext;
13
+ readonly logger?: { info?(message: string): void; warn?(message: string): void } | undefined;
14
+ readonly manifests: () => readonly UnitManifest[];
15
+ readonly opencliCommand?: string;
16
+ readonly store: WorkspaceStore;
17
+ readonly values?: ValueHandoff | undefined;
18
+ readonly probe?: (
19
+ unit: string,
20
+ options?: { readonly force?: boolean },
21
+ ) => Promise<UnitCapability | undefined>;
22
+ readonly longTasks?: LongTaskHost | undefined;
23
+ }
24
+
25
+ export type ContextRenderer = (ref: {
26
+ readonly kind: string;
27
+ readonly parts: readonly string[];
28
+ }) => Promise<string> | string;
29
+
30
+ export type UnitWarmRead = readonly string[];
31
+
32
+ export interface UnitCall {
33
+ readonly source: string;
34
+ readonly args: Readonly<Record<string, unknown>>;
35
+ readonly result: unknown;
36
+ }
37
+
38
+ export interface UnitMountResult {
39
+ readonly routes: RouteTable;
40
+ registerTools(): Array<() => void>;
41
+ readonly factsOf?: ((call: UnitCall) => readonly string[] | undefined) | undefined;
42
+ readonly gate?: {
43
+ deniedTools(workspace: string | null): readonly string[];
44
+ subscribe(listener: () => void): () => void;
45
+ };
46
+ readonly context?: {
47
+ readonly kinds: readonly string[];
48
+ render(ref: { readonly kind: string; readonly parts: readonly string[] }): Promise<string> | string;
49
+ };
50
+
51
+ readonly warmReads?: (() => readonly UnitWarmRead[]) | undefined;
52
+
53
+ readonly prefs?: { readonly resource: string; handle(payload: unknown): unknown };
54
+ readonly actions?: readonly { readonly resource: string; handle(payload: unknown): unknown }[];
55
+ readonly provision?: ProvisionHandler;
56
+ readonly longTasks?: LongTaskRegistry<LongTaskBase> | undefined;
57
+ readonly approvalDetail?:
58
+ | ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>)
59
+ | undefined;
60
+ }
61
+
62
+ export interface UnitHostFace {
63
+ readonly manifest: UnitManifest;
64
+ mount(deps: UnitMountDeps): UnitMountResult;
65
+ }
@@ -0,0 +1,41 @@
1
+ import type { PluginSourceProblem } from './plugin-source';
2
+ import type { PluginRelease, UnitLibNeed, UnitPluginNeed } from '../api/unit';
3
+
4
+ export interface UnitLibState {
5
+ readonly name: string;
6
+ readonly purpose: string;
7
+ readonly status: 'ok' | 'outdated' | 'missing' | 'unsatisfied';
8
+ readonly installed?: string;
9
+ readonly latest?: string;
10
+ readonly command?: string;
11
+ }
12
+
13
+ export interface UnitCommands {
14
+ readonly unit: string;
15
+ readonly plugins?: readonly UnitPluginNeed[];
16
+ readonly libs?: readonly UnitLibNeed[];
17
+ }
18
+
19
+ export interface UnitPluginState {
20
+ readonly name: string;
21
+ readonly status: "ok" | "outdated" | "missing";
22
+ readonly missing: readonly string[];
23
+ readonly install?: "ready" | PluginSourceProblem;
24
+ readonly command?: string;
25
+ readonly release?: PluginRelease;
26
+ }
27
+
28
+ export interface UnitCapability {
29
+ readonly unit: string;
30
+ readonly status: "ok" | "missing-commands" | "missing-plugin" | "unchecked";
31
+ readonly plugin?: string;
32
+ readonly required: readonly string[];
33
+ readonly missing: readonly string[];
34
+ readonly detail: string;
35
+ readonly command?: string;
36
+ readonly fix?: "install" | "update";
37
+ readonly install?: "ready" | PluginSourceProblem;
38
+ readonly plugins?: readonly UnitPluginState[];
39
+ readonly libs?: readonly UnitLibState[];
40
+ readonly released?: boolean;
41
+ }
@@ -0,0 +1,38 @@
1
+ import { HANDOFF_TTL_MS } from '../config/constants.js';
2
+
3
+ export interface HandoffEntry {
4
+ readonly fact: string;
5
+ readonly key: string;
6
+ }
7
+
8
+ export interface HandoffValue {
9
+ readonly value: unknown;
10
+ readonly at: number;
11
+ }
12
+
13
+ export interface ValueHandoff {
14
+ hand(entry: HandoffEntry, value: unknown, now?: number): void;
15
+ take(entry: HandoffEntry, now?: number): HandoffValue | undefined;
16
+ }
17
+
18
+ export function handoffId(entry: HandoffEntry): string {
19
+ return `${String(entry.fact.length)}:${entry.fact}${entry.key}`;
20
+ }
21
+
22
+ export function createValueHandoff(options: { ttlMs?: number } = {}): ValueHandoff {
23
+ const ttlMs = options.ttlMs ?? HANDOFF_TTL_MS;
24
+ const held = new Map<string, HandoffValue>();
25
+
26
+ return {
27
+ hand(entry, value, now = Date.now()) {
28
+ held.set(handoffId(entry), { value, at: now });
29
+ },
30
+ take(entry, now = Date.now()) {
31
+ const id = handoffId(entry);
32
+ const hit = held.get(id);
33
+ if (hit === undefined) return undefined;
34
+ held.delete(id);
35
+ return now - hit.at >= ttlMs ? undefined : hit;
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,161 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { workspacesRoot } from '../host/data-root.js';
5
+
6
+ export interface ModuleStore {
7
+ readonly dir: string;
8
+ read(name: string): unknown | null;
9
+ write(name: string, value: unknown): void;
10
+ remove(name: string): void;
11
+ list(): string[];
12
+ }
13
+
14
+ export interface WorkspaceStore {
15
+ for(unitId: string, workspace: string | null): ModuleStore;
16
+ machine(unitId: string): ModuleStore;
17
+ scan(unitId: string): ModuleStore[];
18
+ }
19
+
20
+ function slug(raw: string, max = 40): string {
21
+ const cleaned = raw
22
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
23
+ .replace(/^[-.]+|[-.]+$/g, '')
24
+ .slice(0, max);
25
+ return cleaned === '' ? 'workspace' : cleaned;
26
+ }
27
+
28
+ export function workspaceKey(workspace: string): string {
29
+ const abs = path.resolve(workspace);
30
+ return `${slug(path.basename(abs))}-${createHash('sha1').update(abs).digest('hex').slice(0, 8)}`;
31
+ }
32
+
33
+ function inside(dir: string, name: string): string {
34
+ const trimmed = name.trim();
35
+ if (trimmed === '' || trimmed === '.' || trimmed === '..') {
36
+ throw new Error(`模块文件夹里只接受相对文件名,收到的是「${name}」`);
37
+ }
38
+ if (trimmed.includes('\0') || path.isAbsolute(trimmed)) {
39
+ throw new Error(`模块只能写自己的文件夹:不接受绝对路径「${name}」`);
40
+ }
41
+ const full = path.resolve(dir, trimmed);
42
+ if (full !== dir && !full.startsWith(dir + path.sep)) {
43
+ throw new Error(`模块只能写自己的文件夹:路径越界「${name}」`);
44
+ }
45
+ return full;
46
+ }
47
+
48
+ function memoryStore(dir: string): ModuleStore {
49
+ const files = new Map<string, unknown>();
50
+ return {
51
+ dir,
52
+ read: (name) => {
53
+ inside(dir, name);
54
+ return files.get(name) ?? null;
55
+ },
56
+ write: (name, value) => {
57
+ inside(dir, name);
58
+ files.set(name, value);
59
+ },
60
+ remove: (name) => {
61
+ inside(dir, name);
62
+ files.delete(name);
63
+ },
64
+ list: () => [...files.keys()].sort(),
65
+ };
66
+ }
67
+
68
+ function fileStore(dir: string): ModuleStore {
69
+ const ensureFor = (file: string): void => {
70
+ fs.mkdirSync(path.dirname(file), { recursive: true });
71
+ };
72
+ return {
73
+ dir,
74
+ read: (name) => {
75
+ const file = inside(dir, name);
76
+ try {
77
+ return JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;
78
+ } catch {
79
+ return null;
80
+ }
81
+ },
82
+ write: (name, value) => {
83
+ const file = inside(dir, name);
84
+ ensureFor(file);
85
+ const tmp = `${file}.tmp`;
86
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
87
+ fs.renameSync(tmp, file);
88
+ },
89
+ remove: (name) => {
90
+ const file = inside(dir, name);
91
+ try {
92
+ fs.unlinkSync(file);
93
+ } catch {
94
+ }
95
+ },
96
+ list: () => {
97
+ try {
98
+ return fs
99
+ .readdirSync(dir, { withFileTypes: true })
100
+ .filter((entry) => entry.isFile())
101
+ .map((entry) => entry.name)
102
+ .sort();
103
+ } catch {
104
+ return [];
105
+ }
106
+ },
107
+ };
108
+ }
109
+
110
+ export function createWorkspaceStore(options: {
111
+ readonly root?: string | null | undefined;
112
+ readonly logger?: { warn?(message: string): void } | undefined;
113
+ }): WorkspaceStore {
114
+ const root = options.root === null ? null : options.root ?? workspacesRoot();
115
+ let warned = false;
116
+ const memory = new Map<string, ModuleStore>();
117
+ const fallback = (dir: string): ModuleStore => {
118
+ if (!warned) {
119
+ warned = true;
120
+ options.logger?.warn?.(
121
+ '[dsh-workbench] 没有可写的数据根:模块配置只在本进程内生效(见 shared/api/host/data-root.ts)',
122
+ );
123
+ }
124
+ const hit = memory.get(dir);
125
+ if (hit !== undefined) return hit;
126
+ const created = memoryStore(dir);
127
+ memory.set(dir, created);
128
+ return created;
129
+ };
130
+
131
+ const open = (parts: readonly string[]): ModuleStore =>
132
+ root === null
133
+ ? fallback(path.join('/_memory', ...parts))
134
+ : fileStore(
135
+ path.join(
136
+ root,
137
+ ...parts.map((part) => slug(part)),
138
+ ),
139
+ );
140
+
141
+ return {
142
+ for: (unitId, workspace) =>
143
+ workspace === null || workspace.trim() === ''
144
+ ? fallback(path.join('/_memory', slug(unitId)))
145
+ : open([workspaceKey(workspace), unitId]),
146
+ machine: (unitId) => open(['_machine', unitId]),
147
+ scan: (unitId) => {
148
+ if (root === null) return [];
149
+ let entries: string[];
150
+ try {
151
+ entries = fs.readdirSync(root, { withFileTypes: true })
152
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('_'))
153
+ .map((entry) => entry.name)
154
+ .sort();
155
+ } catch {
156
+ return [];
157
+ }
158
+ return entries.map((key) => fileStore(path.join(root, key, slug(unitId))));
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,16 @@
1
+ export function writeResultForClient(value: unknown, code: string): unknown {
2
+ if (typeof value !== 'object' || value === null) return value;
3
+ const box = value as Record<string, unknown>;
4
+ const existing = box['error'];
5
+ if (box['ok'] !== false || (typeof existing === 'object' && existing !== null)) return value;
6
+ const text = box['text'];
7
+ const detail = box['detail'];
8
+ return {
9
+ ok: false,
10
+ error: {
11
+ code,
12
+ message: typeof text === 'string' && text.trim() !== '' ? text : '这一步没有成功,也没说为什么',
13
+ ...(typeof detail === 'string' ? { detail } : {}),
14
+ },
15
+ };
16
+ }
package/src/host.ts ADDED
@@ -0,0 +1,19 @@
1
+ export * from './host/__tests__/fakes';
2
+ export * from './host/cli-error';
3
+ export * from './host/data-root';
4
+ export * from './host/facts';
5
+ export * from './host/git';
6
+ export * from './host/http';
7
+ export * from './host/long-task';
8
+ export * from './host/opencli-runtime';
9
+ export * from './host/opencli';
10
+ export * from './host/plugin-source';
11
+ export * from './host/provision';
12
+ export * from './host/shell';
13
+ export * from './host/tool-approval';
14
+ export * from './host/tool-kit';
15
+ export * from './host/unit-face';
16
+ export * from './host/unit-libs';
17
+ export * from './host/value-handoff';
18
+ export * from './host/workspace-store';
19
+ export * from './host/write-result';
package/src/i18n/en.ts ADDED
@@ -0,0 +1,170 @@
1
+ import type { WorkbenchMessages } from './zh';
2
+
3
+ export const en: Record<keyof WorkbenchMessages, string> = {
4
+ 'brand.name': 'Workbench',
5
+
6
+ 'toggle.open': 'Open sidebar',
7
+ 'toggle.collapse': 'Collapse sidebar',
8
+ 'project.label': 'Switch project',
9
+ 'workspace.section': 'Workspaces',
10
+ 'project.none': 'No project',
11
+ 'project.empty': 'No projects yet. Use “Add project” below.',
12
+ 'project.add': 'Add project…',
13
+ 'session.new': 'New session',
14
+ 'session.new.label': 'Start a session in the current project',
15
+ 'session.empty': 'No sessions in this project yet.',
16
+ 'session.noProject': 'Add a project first.',
17
+ 'session.pinned': 'Pinned',
18
+ 'session.actions': 'Actions for {name}',
19
+ 'status.idle': 'Idle',
20
+ 'status.running': 'Running',
21
+ 'status.waitingApproval': 'Waiting for approval',
22
+ 'status.planReview': 'Waiting for plan review',
23
+ 'status.waitingAnswer': 'Waiting for your answer',
24
+ 'status.completed': 'Completed',
25
+ 'status.subagentsRunning.one': '{n} subagent running',
26
+ 'status.subagentsRunning.other': '{n} subagents running',
27
+
28
+ 'time.now': 'just now',
29
+ 'time.minutes': '{n} min ago',
30
+ 'time.hours': '{n} h ago',
31
+ 'time.days': '{n} d ago',
32
+
33
+ 'panels.title': 'Panel modules',
34
+ 'panels.reset': 'Reset',
35
+ 'panels.enabled': 'enabled in this project',
36
+ 'panels.disabled': 'not enabled in this project',
37
+ 'panels.choose': 'Choose modules',
38
+ 'panels.failed': 'Could not save the change. Try again.',
39
+ 'panels.hintEffect': 'Disabling = the agent cannot reach that data',
40
+ 'panels.dragHint': 'Drag to reorder',
41
+ 'panels.unknownStep': 'No suite provides this view',
42
+ 'panels.labelWithHint': '{label} ({hint})',
43
+ 'panels.dep.missingPlugin': '⚠ not ready yet',
44
+ 'panels.dep.missingCommands': '⚠ needs an update',
45
+ 'panels.dep.unchecked': '⚠ cannot check',
46
+ 'panels.tab.modules': 'Panel setup',
47
+ 'panels.tab.market': 'Workbench module market',
48
+ 'panels.browseMarket': 'Browse the module market',
49
+ 'panels.noneInstalled': 'None of these modules has its plugin installed yet',
50
+ 'panels.noneInstalledHint': 'Once installed they come back here, and you pick which go on the panel.',
51
+ 'panels.market.installed': 'installed',
52
+ 'panels.market.outdated': 'needs update',
53
+ 'panels.market.hasNew': 'New version',
54
+ 'panels.market.missing': 'not installed',
55
+ 'panels.market.unchecked': 'unknown',
56
+ 'panels.market.checking': 'checking',
57
+ 'panels.market.sectionInstalled': 'Installed',
58
+ 'panels.market.sectionPending': 'Not installed',
59
+ 'panels.market.sectionShared': 'Shared dependencies',
60
+ 'panels.market.sharedUsedBy': 'Used by {modules}',
61
+ 'panels.market.listSeparator': ', ',
62
+ 'panels.market.sharedMissing': 'not installed',
63
+ 'panels.market.sharedOutdated': 'update available',
64
+ 'panels.market.sharedUnsatisfied': 'wrong version',
65
+ 'panels.market.sharedOk': 'installed',
66
+ 'panels.market.versionArrow': '{from} → {to}',
67
+ 'panels.market.lead': 'Installed on this machine, shared by every project.',
68
+ 'panels.market.install': 'Install',
69
+ 'panels.market.update': 'Update',
70
+ 'panels.market.uninstall': 'Uninstall',
71
+ 'panels.market.installing': 'Installing…',
72
+ 'panels.market.updating': 'Updating…',
73
+ 'panels.market.uninstalling': 'Uninstalling…',
74
+ 'panels.market.confirmUninstall': 'Uninstall?',
75
+ 'panels.market.confirmUninstallDetail':
76
+ 'It will be gone from this machine, and removed from every project panel (bindings and settings are kept).',
77
+ 'panels.market.confirmYes': 'Uninstall',
78
+ 'panels.market.confirmNo': 'Cancel',
79
+ 'panels.market.failed': 'Failed: {reason}',
80
+ 'panels.market.needEnv': 'Set {env} in the terminal that started the workbench first',
81
+ 'panels.market.noSource': 'This module declares no install method — install it yourself',
82
+ 'panels.market.badSource': 'The install method is incomplete — install it yourself',
83
+ 'panels.market.empty': 'No modules available',
84
+ 'panels.market.foot': 'After installing, enable it in the panel page of that project.',
85
+ 'panels.market.recheck': 'Check for updates',
86
+ 'panels.market.checkUpdates': 'Check for updates',
87
+ 'panels.market.checkedAt': 'Last checked: {time}',
88
+ 'panels.market.justNow': 'just now',
89
+ 'panels.market.minutesAgo': '{n} min ago',
90
+ 'panels.market.hoursAgo': '{n} h ago',
91
+ 'blocker.fix.install': 'Install for me',
92
+ 'blocker.fix.update': 'Upgrade for me',
93
+ 'blocker.fix.running': 'Running…',
94
+ 'blocker.fix.failed': 'Failed: ',
95
+ 'blocker.retry': 'Retry',
96
+ 'blocker.retry.running': 'Retrying…',
97
+ 'blocker.copy': 'Copy command',
98
+ 'blocker.market.open': 'Open the module market',
99
+ 'blocker.login.title': 'Sign in to {platform}',
100
+ 'blocker.login.titleGeneric': 'Sign-in required',
101
+ 'blocker.login.hint': 'The workbench uses your browser session; sign in once and you are done.',
102
+ 'blocker.login.go': 'Go sign in',
103
+ 'blocker.login.manual': 'Run `opencli coding team-current` in a terminal, finish signing in, then retry.',
104
+ 'blocker.forbidden.title': 'No access to {platform}',
105
+ 'blocker.forbidden.titleGeneric': 'No access',
106
+ 'blocker.forbidden.hint': 'You are signed in, but this project/repo is not permitted for your account.',
107
+ 'blocker.forbidden.open': 'Open permissions page',
108
+ 'blocker.timeout.hint': 'The headless browser may not have started; retrying usually helps.',
109
+ 'blocker.exec.hint': 'Make sure opencli is installed and on PATH (which opencli).',
110
+ 'blocker.unknown.title': 'Something went wrong',
111
+ 'panels.reading': 'Reading the panel setup…',
112
+ 'panels.empty': 'No modules on the panel yet',
113
+ 'panels.emptyHint': 'You can also just tell the agent: “add a module to the panel”.',
114
+ 'env.title': 'Preparing the workbench',
115
+ 'env.checking': 'Checking the runtime environment…',
116
+ 'env.installing': 'Installing the missing dependency…',
117
+ 'env.failed': 'One step away',
118
+ 'env.elapsed': 'Waiting {seconds}s',
119
+ 'env.stepFailed.install': 'install failed',
120
+ 'env.stepFailed.path': 'installed but not found',
121
+ 'env.stepFailed.shell': 'no shell channel',
122
+ 'env.bridge.ok': 'Connected',
123
+ 'env.bridge.short': 'Not connected',
124
+ 'env.bridge.why': 'Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup, but fix it first.',
125
+ 'env.bridge.daemon': 'The background service is not running. It usually starts by itself \u2014 run the command below in your terminal first.',
126
+ 'env.bridge.extension': 'The browser extension is not connected. Install it once from the Chrome Web Store (then hit Retry here). If it is already installed, run the command below first.',
127
+ 'env.bridge.extensionLink': 'Get the extension in the Chrome Web Store',
128
+ 'env.bridge.profile': 'Several browser profiles are connected but none is the default: list them (command below), then pick one.',
129
+ 'env.bridge.stale': 'The default browser profile is stale (it names an extension instance that no longer exists): pick one again.',
130
+ 'env.bridge.unknown': 'Could not tell what went wrong: the raw output below has the steps opencli itself suggests.',
131
+ 'env.fix.title': 'How to fix it',
132
+ 'env.fix.why': 'The workbench uses its own opencli (installed separately from the one in your terminal), and it is not set up on this machine yet.',
133
+ 'env.fix.whyPath': 'opencli is installed, but this environment cannot find it \u2014 usually a PATH or shell-session issue.',
134
+ 'env.fix.whyShell': 'The workbench has no shell channel, so it cannot fetch data \u2014 nothing the user can install.',
135
+ 'env.fix.step': 'Run this in a terminal, then hit Retry:',
136
+ 'env.fix.stepPath': 'Open a new terminal window (or make sure npm\u2019s global bin directory is on PATH), then hit Retry.',
137
+ 'env.fix.stepShell': 'Restart the workbench (or dsh); if it keeps happening, the shell channel is not wired up.',
138
+ 'env.fix.permission': 'Permission denied: rerun with sudo, or point npm\u2019s global directory at your home.',
139
+ 'env.fix.network': 'Network unreachable: check your network or proxy, then retry.',
140
+ 'env.fix.details': 'Details (for troubleshooting)',
141
+ 'env.retry': 'Retry',
142
+ 'env.copyCommand': 'Copy command',
143
+ 'env.copied': 'Copied',
144
+ 'env.step.opencli': 'Checking the workbench opencli',
145
+ 'env.step.codingPlugin': 'Checking the coding plugin',
146
+ 'env.step.bridge': 'Checking the browser bridge',
147
+ 'env.step.warm': 'Warming up site sessions',
148
+ 'env.warm.done': '{n} site(s) warmed up',
149
+ 'env.warm.short': 'Not warmed this time — harmless; the panel heals on first read',
150
+ 'env.unit.missingPlugin': 'This module is not ready yet',
151
+ 'env.unit.missingCommandsHint': 'Updating it should fix this.',
152
+ 'env.unit.unchecked': 'Cannot check: no shell channel',
153
+ 'env.unit.uncheckedHint': 'The module is not the problem \u2014 the shell channel is unavailable. Restart the workbench.',
154
+ 'env.unit.missingCommands': 'This module is out of date',
155
+ 'env.unit.hint': 'Retry after installing; other modules are unaffected.',
156
+ 'rightbar.label': 'Data panels',
157
+ 'rightbar.expand': 'Expand the data panel',
158
+ 'rightbar.collapse': 'Collapse the data panel',
159
+ 'rightbar.refresh': 'Refresh',
160
+ 'rightbar.refreshing': 'Refreshing…',
161
+ 'rightbar.updatedAt': 'updated {time}',
162
+ 'rightbar.noProject': 'The current session has no workspace',
163
+ 'coding.notARepo.title': 'This workspace is not a Coding repository',
164
+ 'error.unknown': 'Something went wrong',
165
+
166
+ 'board.title': 'Board',
167
+
168
+ 'panel.conversation': 'Conversation',
169
+ 'panels.label': 'Global panels',
170
+ };
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { en } from './en';
3
+ import { dictionaries } from './index';
4
+ import { zh } from './zh';
5
+
6
+ describe('dictionaries', () => {
7
+ it('同时导出两种语言,且就是那两份字典', () => {
8
+ expect(dictionaries.zh).toBe(zh);
9
+ expect(dictionaries.en).toBe(en);
10
+ });
11
+
12
+ it('只有这两种语言(多出来的键会被 locale 服务忽略,属于死代码)', () => {
13
+ expect(Object.keys(dictionaries).sort()).toEqual(['en', 'zh']);
14
+ });
15
+ });
@@ -0,0 +1,12 @@
1
+ import { en } from './en';
2
+ import { zh, type WorkbenchMessages } from './zh';
3
+
4
+ export const dictionaries = { zh, en };
5
+
6
+ export type { WorkbenchMessages };
7
+
8
+ export interface Dictionaries {
9
+ readonly zh: Readonly<Record<string, string>>;
10
+ readonly en: Readonly<Record<string, string>>;
11
+ readonly [language: string]: Readonly<Record<string, string>>;
12
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mergeMessages } from './merge';
3
+
4
+ const base = { zh: { 'board.title': '看板' }, en: { 'board.title': 'Board' } };
5
+
6
+ describe('mergeMessages', () => {
7
+ it('把套件的键并进来;外壳原有的键不动', () => {
8
+ const merged = mergeMessages(base, [
9
+ {
10
+ unit: 'issues',
11
+ messages: { zh: { 'issues.mine': '我的缺陷' }, en: { 'issues.mine': 'My defects' } },
12
+ },
13
+ ]);
14
+
15
+ expect(merged.zh).toEqual({ 'board.title': '看板', 'issues.mine': '我的缺陷' });
16
+ expect(merged.en).toEqual({ 'board.title': 'Board', 'issues.mine': 'My defects' });
17
+ expect(base.zh).toEqual({ 'board.title': '看板' });
18
+ });
19
+
20
+ it('没贡献文案的套件直接跳过', () => {
21
+ expect(mergeMessages(base, [{ unit: 'nothing' }])).toEqual(base);
22
+ });
23
+
24
+ it('★ 键前缀必须等于套件 id(否则"这个键是谁的"说不清)', () => {
25
+ expect(() =>
26
+ mergeMessages(base, [
27
+ { unit: 'issues', messages: { zh: { 'builds.title': '构建' }, en: { 'builds.title': 'Builds' } } },
28
+ ]),
29
+ ).toThrow(/必须以 "issues\." 开头/);
30
+ });
31
+
32
+ it('★ 与外壳自己的键撞名 → 抛,并报出双方是谁', () => {
33
+ const legacy = { zh: { 'issues.title': '缺陷' }, en: { 'issues.title': 'Defects' } };
34
+
35
+ expect(() =>
36
+ mergeMessages(legacy, [
37
+ { unit: 'issues', messages: { zh: { 'issues.title': '缺陷' }, en: { 'issues.title': 'Defects' } } },
38
+ ]),
39
+ ).toThrow(/文案键冲突:issues\.title 由 外壳 与 issues/);
40
+ });
41
+
42
+ it('★ 套件自己的两语言不同构 → 抛(外壳的类型管不到别的套件)', () => {
43
+ expect(() =>
44
+ mergeMessages(base, [
45
+ {
46
+ unit: 'issues',
47
+ messages: { zh: { 'issues.a': '甲', 'issues.b': '乙' }, en: { 'issues.a': 'A' } },
48
+ },
49
+ ]),
50
+ ).toThrow(/两语言不同构/);
51
+ });
52
+
53
+ it('★ 空文案 → 抛(空值会静默显示成空白)', () => {
54
+ expect(() =>
55
+ mergeMessages(base, [
56
+ { unit: 'issues', messages: { zh: { 'issues.a': ' ' }, en: { 'issues.a': 'A' } } },
57
+ ]),
58
+ ).toThrow(/是空的/);
59
+ });
60
+ });