@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,61 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { libDir, libInstallCommand } from './opencli-runtime';
4
+ import { isNewerVersion, latestVersion } from './release';
5
+ import { resetReleaseCache } from './release';
6
+ import { checkVersionRange } from './version-range';
7
+ export function installedLibVersion(name) {
8
+ try {
9
+ const parsed = JSON.parse(readFileSync(join(libDir(name), 'package.json'), 'utf8'));
10
+ const version = parsed.version;
11
+ return typeof version === 'string' && version.trim() !== ''
12
+ ? version.trim()
13
+ : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ export async function probeLibs(options) {
20
+ const { needs, shell, force } = options;
21
+ const found = new Map();
22
+ for (const need of needs) {
23
+ if (found.has(need.name))
24
+ continue;
25
+ const base = { name: need.name, purpose: need.purpose };
26
+ const installed = installedLibVersion(need.name);
27
+ if (installed === undefined) {
28
+ found.set(need.name, {
29
+ ...base,
30
+ status: 'missing',
31
+ command: libInstallCommand({ need }),
32
+ });
33
+ continue;
34
+ }
35
+ if (checkVersionRange(need.version, installed) !== 'satisfied') {
36
+ found.set(need.name, {
37
+ ...base,
38
+ status: 'unsatisfied',
39
+ installed,
40
+ command: libInstallCommand({ need }),
41
+ });
42
+ continue;
43
+ }
44
+ const registry = process.env[need.registryEnv]?.trim() || need.registryDefault;
45
+ const latest = await latestVersion({ shell, name: need.name, registry, force });
46
+ const behind = latest !== undefined && isNewerVersion(latest, installed);
47
+ found.set(need.name, {
48
+ ...base,
49
+ status: behind ? 'outdated' : 'ok',
50
+ installed,
51
+ ...(latest === undefined ? {} : { latest }),
52
+ ...(behind
53
+ ? { command: libInstallCommand({ need, version: latest }) }
54
+ : {}),
55
+ });
56
+ }
57
+ return found;
58
+ }
59
+ export function resetLibCache() {
60
+ resetReleaseCache();
61
+ }
@@ -0,0 +1,17 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import { type UnitCapability, type UnitCommands } from "./probe-libs";
3
+ export type { UnitCapability, UnitCommands, UnitLibState, UnitPluginState } from "./probe-libs";
4
+ export declare function parsePluginList(stdout: string): unknown[] | undefined;
5
+ export declare function probeUnitCommands(options: {
6
+ shell: DshShell;
7
+ command?: string;
8
+ unit: UnitCommands;
9
+ force?: boolean;
10
+ }): Promise<UnitCapability>;
11
+ export declare function probeUnits(options: {
12
+ shell: DshShell;
13
+ command?: string;
14
+ units: readonly UnitCommands[];
15
+ force?: boolean;
16
+ }): Promise<UnitCapability[]>;
17
+ export declare const resetProbeCache: () => void;
@@ -0,0 +1,102 @@
1
+ import { OPENCLI_TIMEOUT_MS, PROBE_TTL_MS } from '@dsh-workbench/shared';
2
+ import { createTimedMemo } from "./probe-cache";
3
+ import { parseJsonLoose } from '@dsh-workbench/shared';
4
+ import { npmRegistryOf } from "./plugin-source";
5
+ import { probeRelease } from "./release";
6
+ import { probeLibs } from "./probe-libs";
7
+ import { evaluateUnit } from "./probe-verdict";
8
+ import { runShell } from "./shell";
9
+ export function parsePluginList(stdout) {
10
+ const parsed = parseJsonLoose(stdout);
11
+ if (Array.isArray(parsed))
12
+ return parsed;
13
+ return /no plugins? installed/i.test(stdout) ? [] : undefined;
14
+ }
15
+ async function probeReleases(units, options) {
16
+ const needs = new Map();
17
+ for (const unit of units) {
18
+ for (const need of unit.plugins ?? []) {
19
+ const source = need.installSource;
20
+ if (source?.kind === "npm")
21
+ needs.set(need.name, source);
22
+ }
23
+ }
24
+ const found = new Map();
25
+ for (const [plugin, source] of needs) {
26
+ const registry = npmRegistryOf(source, process.env);
27
+ const signal = await probeRelease({
28
+ shell: options.shell,
29
+ package: source.package,
30
+ ...(registry.ok ? { registry: registry.value } : {}),
31
+ force: options.force,
32
+ });
33
+ if (signal !== undefined)
34
+ found.set(plugin, signal);
35
+ }
36
+ return found;
37
+ }
38
+ export async function probeUnitCommands(options) {
39
+ const all = await probeUnits({ ...options, units: [options.unit] });
40
+ return (all[0] ??
41
+ evaluateUnit(undefined, options.unit, "unchecked", options.command ?? "opencli"));
42
+ }
43
+ export async function probeUnits(options) {
44
+ const { shell, command = "opencli", units, force = false } = options;
45
+ const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
46
+ if (!force && hits.every((hit) => hit !== undefined)) {
47
+ return hits;
48
+ }
49
+ const result = await runShell(shell, `${command} plugin list -f json`, {
50
+ timeoutMs: OPENCLI_TIMEOUT_MS,
51
+ }).then((value) => ({
52
+ kind: "ok",
53
+ stdout: value.exitCode === 0 ? value.stdout : "",
54
+ stderr: value.stderr,
55
+ }), (error) => ({
56
+ kind: "error",
57
+ stdout: "",
58
+ stderr: String(error),
59
+ }));
60
+ const list = result.kind === "ok" ? parsePluginList(result.stdout) : undefined;
61
+ const failure = result.kind === "error"
62
+ ? result.stderr
63
+ : list === undefined
64
+ ? "plugin list 的输出读不懂(不是 JSON 数组)"
65
+ : undefined;
66
+ if (failure !== undefined) {
67
+ return units.map((unit) => {
68
+ const capability = {
69
+ unit: unit.unit,
70
+ ...(unit.plugins?.[0] === undefined
71
+ ? {}
72
+ : { plugin: unit.plugins[0].name }),
73
+ status: "unchecked",
74
+ required: (unit.plugins ?? []).flatMap((need) => need.commands ?? []),
75
+ missing: [],
76
+ detail: failure,
77
+ };
78
+ unitCache.set(`${command}:${unit.unit}`, capability);
79
+ return capability;
80
+ });
81
+ }
82
+ const [releases, libs] = await Promise.all([
83
+ probeReleases(units, { shell, force }),
84
+ probeLibs({
85
+ needs: units.flatMap((unit) => unit.libs ?? []),
86
+ shell,
87
+ force,
88
+ }),
89
+ ]);
90
+ return units.map((unit) => {
91
+ const capability = evaluateUnit(list, unit, undefined, command, releases, libsOfUnit(unit, libs));
92
+ unitCache.set(`${command}:${unit.unit}`, capability);
93
+ return capability;
94
+ });
95
+ }
96
+ function libsOfUnit(unit, libs) {
97
+ return (unit.libs ?? [])
98
+ .map((need) => libs.get(need.name))
99
+ .filter((state) => state !== undefined);
100
+ }
101
+ const unitCache = createTimedMemo(PROBE_TTL_MS);
102
+ export const resetProbeCache = () => void unitCache.clear();
@@ -0,0 +1,7 @@
1
+ import type { PluginRelease, UnitPluginNeed } from '@dsh-workbench/shared';
2
+ import type { UnitCapability, UnitCommands, UnitLibState } from "./probe-libs";
3
+ export declare function evaluateUnit(list: readonly unknown[] | undefined, unit: UnitCommands, failure: string | undefined, command?: string, releases?: ReadonlyMap<string, PluginRelease>, libs?: readonly UnitLibState[]): UnitCapability;
4
+ export declare function gapsText(gaps: readonly string[]): string;
5
+ export declare function pluginUpdateCommand(plugin: string | undefined): string | undefined;
6
+ export declare function pluginInstallCommand(plugin: string | undefined): string;
7
+ export declare function upgradeLine(need: UnitPluginNeed, command: string, latest: string): string | undefined;
@@ -0,0 +1,143 @@
1
+ import { resolvePluginSource } from "./plugin-source";
2
+ import { isNewerVersion } from "./release";
3
+ export function evaluateUnit(list, unit, failure, command = "opencli", releases = new Map(), libs = []) {
4
+ const isPlugin = (item) => typeof item === "object" && item !== null;
5
+ const needs = unit.plugins ?? [];
6
+ const required = needs.flatMap((need) => need.commands ?? []);
7
+ const primary = needs[0]?.name;
8
+ if (failure !== undefined || list === undefined) {
9
+ return {
10
+ unit: unit.unit,
11
+ ...(primary === undefined ? {} : { plugin: primary }),
12
+ status: "unchecked",
13
+ required,
14
+ missing: [],
15
+ detail: failure ?? "探不动",
16
+ ...(libs.length === 0 ? {} : { libs }),
17
+ };
18
+ }
19
+ const plugins = list.filter(isPlugin);
20
+ const installed = new Map(plugins.map((item) => [String(item.name), item]));
21
+ const states = needs.map((need) => {
22
+ const hit = installed.get(need.name);
23
+ if (hit === undefined) {
24
+ const source = resolvePluginSource(need.installSource, process.env, {
25
+ command,
26
+ plugin: need.name,
27
+ });
28
+ return {
29
+ name: need.name,
30
+ status: "missing",
31
+ missing: [],
32
+ install: source.ok ? "ready" : source.problem,
33
+ command: source.ok ? source.line : pluginInstallCommand(need.name),
34
+ };
35
+ }
36
+ const available = new Set((Array.isArray(hit.commands) ? hit.commands : []).map(String));
37
+ const missing = (need.commands ?? []).filter((name) => !available.has(name));
38
+ const release = releases.get(need.name);
39
+ const behind = release?.latest !== undefined &&
40
+ isNewerVersion(release.latest, release.installed);
41
+ const commandsBehind = missing.length > 0;
42
+ const upgrade = behind && release?.latest !== undefined
43
+ ? upgradeLine(need, command, release.latest)
44
+ : undefined;
45
+ return {
46
+ name: need.name,
47
+ status: commandsBehind || behind ? "outdated" : "ok",
48
+ missing,
49
+ ...(release === undefined ? {} : { release }),
50
+ ...(commandsBehind || behind
51
+ ? {
52
+ command: upgrade ??
53
+ pluginUpdateCommand(need.name) ??
54
+ pluginInstallCommand(need.name),
55
+ }
56
+ : {}),
57
+ };
58
+ });
59
+ const absent = states.filter((state) => state.status === "missing");
60
+ const outdated = states.filter((state) => state.status === "outdated");
61
+ const missing = states.flatMap((state) => state.missing);
62
+ const libMissing = libs.filter((state) => state.status === "missing");
63
+ const libMismatch = libs.filter((state) => state.status === "unsatisfied");
64
+ const libBehind = libs.filter((state) => state.status === "outdated");
65
+ const status = absent.length > 0 || libMissing.length > 0 || libMismatch.length > 0
66
+ ? "missing-plugin"
67
+ : missing.length > 0
68
+ ? "missing-commands"
69
+ : "ok";
70
+ const blocked = absent.find((state) => state.install !== "ready");
71
+ const install = absent.length === 0
72
+ ?
73
+ libMissing.length > 0 || libMismatch.length > 0
74
+ ? "ready"
75
+ : undefined
76
+ : blocked === undefined
77
+ ? "ready"
78
+ : blocked.install;
79
+ const fix = status === "missing-plugin"
80
+ ? install === "ready"
81
+ ? "install"
82
+ : undefined
83
+ : outdated.length > 0 || libBehind.length > 0
84
+ ? "update"
85
+ : undefined;
86
+ const first = absent[0] ?? outdated[0] ?? libMissing[0] ?? libMismatch[0] ?? libBehind[0];
87
+ const gaps = [...(missing.length === 0 ? [] : [`缺必需:${missing.join("、")}`])];
88
+ const newer = states.filter((state) => state.release?.latest !== undefined &&
89
+ isNewerVersion(state.release.latest, state.release.installed));
90
+ const versionText = newer.length === 0
91
+ ? ""
92
+ : `(源上有新版:${newer.map((state) => `${state.release?.installed ?? ""} → ${state.release?.latest ?? ""}`).join("、")})`;
93
+ const absentNames = [
94
+ ...absent.map((state) => state.name),
95
+ ...libMissing.map((state) => state.name),
96
+ ];
97
+ const mismatchText = libMismatch.map((state) => {
98
+ const declared = unit.libs?.find((need) => need.name === state.name)?.version?.trim();
99
+ return `${state.name}(装的是 ${state.installed ?? "未知"},声明要 ${declared === undefined || declared === "" ? "任意版本" : declared})`;
100
+ });
101
+ return {
102
+ unit: unit.unit,
103
+ ...(primary === undefined ? {} : { plugin: primary }),
104
+ status,
105
+ required,
106
+ missing,
107
+ detail: absentNames.length > 0 || mismatchText.length > 0
108
+ ? [
109
+ ...(absentNames.length === 0
110
+ ? []
111
+ : [`${plugins.length} 个已装插件里没有 ${absentNames.join("、")}`]),
112
+ ...mismatchText,
113
+ ].join(";")
114
+ : `${needs.length} 份安装都在${gapsText(gaps)}${versionText}${libs.length === 0
115
+ ? ""
116
+ : `;交叉依赖:${libs.map((state) => `${state.name}${state.installed === undefined ? " 未装" : ` ${state.installed}`}`).join("、")}${libBehind.length === 0 ? "" : "(源上有新版)"}`}`,
117
+ ...(fix === undefined && first === undefined
118
+ ? {}
119
+ : { command: fix === "install" ? (absent[0] ?? first)?.command : first?.command }),
120
+ ...(fix === undefined ? {} : { fix }),
121
+ ...(install === undefined ? {} : { install }),
122
+ plugins: states,
123
+ ...(libs.length === 0 ? {} : { libs }),
124
+ };
125
+ }
126
+ export function gapsText(gaps) {
127
+ return gaps.length === 0 ? "" : `(${gaps.join(";")})`;
128
+ }
129
+ export function pluginUpdateCommand(plugin) {
130
+ return plugin === undefined ? undefined : `opencli plugin update ${plugin}`;
131
+ }
132
+ export function pluginInstallCommand(plugin) {
133
+ return `opencli plugin install <${plugin ?? "插件"}源>`;
134
+ }
135
+ export function upgradeLine(need, command, latest) {
136
+ const source = resolvePluginSource(need.installSource, process.env, {
137
+ command,
138
+ plugin: need.name,
139
+ version: latest,
140
+ mode: "update",
141
+ });
142
+ return source.ok ? source.line : undefined;
143
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProvisionAction } from '../api/workbench-contract.js';
2
+ export type { ProvisionAction };
3
+ export declare const PROVISION_ACTIONS: readonly ["install", "update", "uninstall", "update-dependency"];
4
+ export declare const DEPENDENCY_ACTIONS: readonly ["update-dependency"];
5
+ export declare function isDependencyAction(action: ProvisionAction): action is (typeof DEPENDENCY_ACTIONS)[number];
6
+ export interface ProvisionRequest {
7
+ readonly action: ProvisionAction;
8
+ readonly unit?: string;
9
+ readonly id?: string;
10
+ }
11
+ export interface ProvisionHandler {
12
+ handle(request: ProvisionRequest): Promise<unknown> | unknown;
13
+ affectedUnits?(request: ProvisionRequest): readonly string[];
14
+ }
@@ -0,0 +1,12 @@
1
+ export const PROVISION_ACTIONS = [
2
+ 'install',
3
+ 'update',
4
+ 'uninstall',
5
+ 'update-dependency',
6
+ ];
7
+ export const DEPENDENCY_ACTIONS = ['update-dependency'];
8
+ export function isDependencyAction(action) {
9
+ return DEPENDENCY_ACTIONS.includes(action);
10
+ }
11
+ const ALL_ACTIONS_COVERED = true;
12
+ void ALL_ACTIONS_COVERED;
@@ -0,0 +1,14 @@
1
+ import type { DshShell } from '../api/dsh.js';
2
+ export interface ShellResult {
3
+ readonly exitCode: number | null;
4
+ readonly timedOut: boolean;
5
+ readonly aborted: boolean;
6
+ readonly stdout: string;
7
+ readonly stderr: string;
8
+ readonly truncated: boolean;
9
+ }
10
+ export declare function runShell(shell: DshShell, command: string, options?: {
11
+ workdir?: string;
12
+ timeoutMs?: number;
13
+ signal?: AbortSignal;
14
+ }): Promise<ShellResult>;
@@ -0,0 +1,23 @@
1
+ import { GIT_TIMEOUT_MS } from '../config/constants.js';
2
+ const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
3
+ export async function runShell(shell, command, options = {}) {
4
+ const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
5
+ const spec = shell.resolve({
6
+ command,
7
+ ...(workdir === undefined ? {} : { workdir }),
8
+ timeoutMs,
9
+ onExpiry: 'kill',
10
+ stdoutMaxBytes: DEFAULT_MAX_BYTES,
11
+ ...(signal === undefined ? {} : { signal }),
12
+ });
13
+ const execution = await shell.execute(spec);
14
+ const result = await execution.result();
15
+ return {
16
+ exitCode: result.exitCode,
17
+ timedOut: result.timedOut === true,
18
+ aborted: result.aborted === true,
19
+ stdout: result.stdout.text,
20
+ stderr: result.stderr.text,
21
+ truncated: result.stdout.truncated || result.stderr.truncated,
22
+ };
23
+ }
@@ -0,0 +1,29 @@
1
+ export type AskDecision = {
2
+ readonly kind: 'ask';
3
+ readonly reason: string;
4
+ readonly displayReason: {
5
+ readonly en: string;
6
+ readonly zh: string;
7
+ };
8
+ } | {
9
+ readonly kind: 'deny';
10
+ readonly reason: string;
11
+ };
12
+ export interface ApprovalDetail {
13
+ readonly en: string;
14
+ readonly zh: string;
15
+ }
16
+ export type ApprovalFacts = ApprovalDetail | {
17
+ readonly blocked: string;
18
+ };
19
+ export interface ApprovalCall {
20
+ readonly name?: string | undefined;
21
+ readonly arguments?: unknown;
22
+ readonly workspace?: string | undefined;
23
+ }
24
+ export interface ApprovalContext {
25
+ readonly titleOf?: ((id: string) => string | undefined) | undefined;
26
+ readonly writes?: boolean | undefined;
27
+ readonly detailOf?: ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>) | undefined;
28
+ }
29
+ export declare function approvalFor(call: ApprovalCall, context?: ApprovalContext): Promise<AskDecision | undefined>;
@@ -0,0 +1,190 @@
1
+ function compose(detail, texts) {
2
+ if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '')
3
+ return texts.generic;
4
+ return { en: `${detail.en} — ${texts.question.en}`, zh: `${detail.zh} —— ${texts.question.zh}` };
5
+ }
6
+ function actionOf(call) {
7
+ const raw = call.arguments;
8
+ if (typeof raw !== 'object' || raw === null)
9
+ return {};
10
+ const value = raw;
11
+ return {
12
+ ...(typeof value.action === 'string' ? { action: value.action } : {}),
13
+ ...(typeof value.id === 'string' ? { id: value.id } : typeof value.unit === 'string' ? { id: value.unit } : {}),
14
+ };
15
+ }
16
+ const READ_ONLY_ACTIONS = new Set(['list', '']);
17
+ const READ = { kind: 'read' };
18
+ const UNKNOWN = { kind: 'unknown' };
19
+ function ask(spec) {
20
+ return { kind: 'ask', spec };
21
+ }
22
+ function confirmed(call) {
23
+ const raw = call.arguments;
24
+ if (typeof raw !== 'object' || raw === null)
25
+ return undefined;
26
+ const value = raw.confirm;
27
+ return typeof value === 'boolean' ? value : undefined;
28
+ }
29
+ function effectOf(call, context) {
30
+ const name = call.name ?? '';
31
+ const { action, id } = actionOf(call);
32
+ const readAction = action !== undefined && action !== '' && READ_ONLY_ACTIONS.has(action);
33
+ const what = () => {
34
+ if (id === undefined || id === '')
35
+ return '一个模块';
36
+ return `「${context.titleOf?.(id) ?? id}」`;
37
+ };
38
+ if (name === 'workbench_cmdb_release') {
39
+ if (confirmed(call) !== true)
40
+ return READ;
41
+ return ask({
42
+ reason: 'confirm=true releases a version to a real environment',
43
+ generic: {
44
+ en: 'The agent wants to deploy a version for real (confirm: true) — it deploys on the platform (non-production). Allow?',
45
+ zh: 'agent 想在非生产环境上**真的部署一个版本**(confirm: true)—— 会在平台上部署。允许吗?',
46
+ },
47
+ question: {
48
+ en: 'it deploys on the platform for real (non-production). Allow?',
49
+ zh: '会在平台上真的部署一次(非生产环境)。允许吗?',
50
+ },
51
+ });
52
+ }
53
+ if (name === 'workbench_coding_issue_comment' || name === 'workbench_coding_issue_transfer') {
54
+ if (confirmed(call) !== true)
55
+ return READ;
56
+ const comment = name === 'workbench_coding_issue_comment';
57
+ return ask({
58
+ reason: comment
59
+ ? 'confirm=true posts a comment on the platform'
60
+ : 'confirm=true moves an issue to another status on the platform',
61
+ generic: comment
62
+ ? {
63
+ en: 'The agent wants to post a comment on the platform for real (confirm: true) — it writes on the platform. Allow?',
64
+ zh: 'agent 想**真的给一条缺陷加评论**(confirm: true)—— 会写平台数据。允许吗?',
65
+ }
66
+ : {
67
+ en: 'The agent wants to move an issue to another status for real (confirm: true) — it writes on the platform. Allow?',
68
+ zh: 'agent 想**真的把一条缺陷流转到别的状态**(confirm: true)—— 会写平台数据。允许吗?',
69
+ },
70
+ question: comment
71
+ ? {
72
+ en: 'it posts that comment on the platform for real. Allow?',
73
+ zh: '会在平台上真的发表这条评论。允许吗?',
74
+ }
75
+ : {
76
+ en: 'it changes that issue status on the platform for real. Allow?',
77
+ zh: '会在平台上真的改这条缺陷的状态。允许吗?',
78
+ },
79
+ });
80
+ }
81
+ if (name === 'workbench_modules') {
82
+ if (readAction)
83
+ return READ;
84
+ if (action === 'update-dependency') {
85
+ return ask({
86
+ reason: `workbench_modules update-dependency ${id ?? ''} upgrades a shared dependency on this machine`,
87
+ generic: {
88
+ en: `The agent wants to upgrade the shared dependency ${what()} (it changes what is installed on this machine). Allow?`,
89
+ zh: `agent 想升级共用依赖 ${what()}(会改这台机器上装的东西)。允许吗?`,
90
+ },
91
+ question: {
92
+ en: 'it upgrades that shared dependency on this machine. Allow?',
93
+ zh: `会升级共用依赖 ${what()}。允许吗?`,
94
+ },
95
+ });
96
+ }
97
+ const verb = action === 'install' ? '装' : action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '装 / 更新 / 卸载';
98
+ return ask({
99
+ reason: `workbench_modules ${action ?? '(unknown action)'} ${id ?? ''} changes plugins on this machine`.trim(),
100
+ generic: {
101
+ en: `The agent wants to ${verb === '装' ? 'install' : verb === '卸载' ? 'remove' : verb === '更新' ? 'update' : 'install/update/remove'} the module ${what()} (it changes plugins on this machine). Allow?`,
102
+ zh: `agent 想${verb}模块 ${what()}(会改这台机器上的插件)。允许吗?`,
103
+ },
104
+ question: {
105
+ en: 'it changes plugins on this machine. Allow?',
106
+ zh: '会改这台机器上的插件。允许吗?',
107
+ },
108
+ });
109
+ }
110
+ if (name !== 'workbench_panels')
111
+ return UNKNOWN;
112
+ if (readAction)
113
+ return READ;
114
+ if (action === 'hide') {
115
+ return ask({
116
+ reason: 'workbench_panels hide removes a module from the panel',
117
+ generic: {
118
+ en: `The agent wants to remove ${what()} from this project's panel (its agent tools go offline here too). Allow?`,
119
+ zh: `agent 想把 ${what()} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
120
+ },
121
+ question: {
122
+ en: 'it takes that module off this panel (its agent tools go offline here too). Allow?',
123
+ zh: `${what()}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
124
+ },
125
+ });
126
+ }
127
+ if (action === 'move') {
128
+ return ask({
129
+ reason: 'workbench_panels move reorders the panel',
130
+ generic: {
131
+ en: `The agent wants to reorder this project's panel (put ${what()} first). Allow?`,
132
+ zh: `agent 想调整这个项目面板的顺序(把 ${what()} 放前面)。允许吗?`,
133
+ },
134
+ question: {
135
+ en: `it reorders this panel (${what()} goes first). Allow?`,
136
+ zh: `会调整这个项目面板的顺序(${what()}放前面)。允许吗?`,
137
+ },
138
+ });
139
+ }
140
+ if (action === 'reset') {
141
+ return ask({
142
+ reason: 'workbench_panels reset clears the panel',
143
+ generic: {
144
+ en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
145
+ zh: 'agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
146
+ },
147
+ question: {
148
+ en: 'it clears this panel (every module goes offline here). Allow?',
149
+ zh: '会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
150
+ },
151
+ });
152
+ }
153
+ return ask({
154
+ reason: `workbench_panels ${action ?? '(unknown action)'} changes the user's panel`,
155
+ generic: {
156
+ en: `The agent wants to add ${what()} to this project's panel. Allow?`,
157
+ zh: `agent 想把 ${what()} 加到**这个项目**的面板上。允许吗?`,
158
+ },
159
+ question: {
160
+ en: `it adds ${what()} to this project's panel. Allow?`,
161
+ zh: `会把 ${what()} 加到**这个项目**的面板上。允许吗?`,
162
+ },
163
+ });
164
+ }
165
+ export async function approvalFor(call, context = {}) {
166
+ const effect = effectOf(call, context);
167
+ if (effect.kind === 'read')
168
+ return undefined;
169
+ if (effect.kind === 'unknown') {
170
+ if (context.writes !== true)
171
+ return undefined;
172
+ return {
173
+ kind: 'deny',
174
+ reason: `${call.name ?? '这把工具'} 在清单里声明会写东西,但审批表没认领这次调用 —— 停下:` +
175
+ '给它在 tool-approval.ts 里补一条审批(含"这次会发生什么"),别让写静默过去',
176
+ };
177
+ }
178
+ const { spec } = effect;
179
+ let facts;
180
+ try {
181
+ facts = await context.detailOf?.(call);
182
+ }
183
+ catch (error) {
184
+ const why = error instanceof Error ? error.message : String(error);
185
+ return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${why}` };
186
+ }
187
+ if (facts !== undefined && 'blocked' in facts)
188
+ return { kind: 'deny', reason: facts.blocked };
189
+ return { kind: 'ask', reason: spec.reason, displayReason: compose(facts, spec) };
190
+ }
@@ -0,0 +1,19 @@
1
+ import type { DshToolRunContext } from '../api/dsh.js';
2
+ export declare function sessionWorkspace(exec: DshToolRunContext | undefined): string | null;
3
+ export declare function textOutput(text: string): Array<Record<string, unknown>>;
4
+ export declare function textRender(_args: unknown, value: unknown): Array<Record<string, unknown>>;
5
+ export declare const TEXT_OUTPUT: {
6
+ schema: {
7
+ type: string;
8
+ properties: {};
9
+ additionalProperties: boolean;
10
+ };
11
+ render: typeof textRender;
12
+ };
13
+ export declare function jsonText(value: unknown): string;
14
+ export interface CommandHintNames {
15
+ readonly plugin: string;
16
+ readonly suiteId: string;
17
+ }
18
+ export type CommandHint = <T>(command: string, run: () => Promise<T>) => Promise<T>;
19
+ export declare function commandHintFor(names: CommandHintNames): CommandHint;