@dsh-workbench/shared 0.3.6 → 0.3.7

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 (136) hide show
  1. package/dist/api/context-ref.d.ts +0 -12
  2. package/dist/api/context-ref.js +0 -40
  3. package/dist/api/facts.d.ts +0 -6
  4. package/dist/api/facts.js +0 -8
  5. package/dist/components/ConfirmBar.css +26 -0
  6. package/dist/components/ConfirmBar.d.ts +36 -0
  7. package/dist/components/ConfirmBar.js +26 -0
  8. package/dist/components/Form.d.ts +7 -5
  9. package/dist/components/Form.js +5 -3
  10. package/dist/components/FormNodes.d.ts +17 -13
  11. package/dist/components/FormNodes.js +13 -5
  12. package/dist/components/FormTypes.d.ts +39 -14
  13. package/dist/components/MockStrip.css +11 -1
  14. package/dist/components/StateDot.d.ts +5 -0
  15. package/dist/components/StateDot.js +10 -0
  16. package/dist/config/constants.d.ts +2 -0
  17. package/dist/config/constants.js +2 -0
  18. package/dist/host/__tests__/fakes.d.ts +41 -0
  19. package/dist/host/__tests__/fakes.js +76 -0
  20. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  21. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  22. package/dist/host/board-fact.d.ts +8 -0
  23. package/dist/host/board-fact.js +16 -0
  24. package/dist/host/browser-read.js +2 -1
  25. package/dist/host/context-inject.d.ts +24 -0
  26. package/dist/host/context-inject.js +74 -0
  27. package/dist/host/context-ref.d.ts +13 -0
  28. package/dist/host/context-ref.js +41 -0
  29. package/dist/host/diy-debug.d.ts +80 -0
  30. package/dist/host/diy-debug.js +88 -0
  31. package/dist/host/diy-record.d.ts +135 -0
  32. package/dist/host/diy-record.js +162 -0
  33. package/dist/host/events.d.ts +9 -0
  34. package/dist/host/events.js +70 -0
  35. package/dist/host/facts.d.ts +6 -0
  36. package/dist/host/facts.js +9 -0
  37. package/dist/host/git.js +1 -1
  38. package/dist/host/http.d.ts +2 -2
  39. package/dist/host/http.js +3 -3
  40. package/dist/host/long-task.d.ts +5 -0
  41. package/dist/host/long-task.js +3 -0
  42. package/dist/host/mount-units.d.ts +52 -0
  43. package/dist/host/mount-units.js +186 -0
  44. package/dist/host/opencli-runtime.js +1 -1
  45. package/dist/host/opencli-window.js +1 -1
  46. package/dist/host/opencli.js +2 -1
  47. package/dist/host/panel-state.d.ts +120 -0
  48. package/dist/host/panel-state.js +176 -0
  49. package/dist/host/plugin-source.js +1 -1
  50. package/dist/host/preflight.d.ts +58 -0
  51. package/dist/host/preflight.js +233 -0
  52. package/dist/host/probe-cache.d.ts +6 -0
  53. package/dist/host/probe-cache.js +19 -0
  54. package/dist/host/probe-libs.d.ts +45 -0
  55. package/dist/host/probe-libs.js +61 -0
  56. package/dist/host/probe-units.d.ts +17 -0
  57. package/dist/host/probe-units.js +102 -0
  58. package/dist/host/probe-verdict.d.ts +7 -0
  59. package/dist/host/probe-verdict.js +143 -0
  60. package/dist/host/shell-text.d.ts +13 -0
  61. package/dist/host/shell-text.js +20 -0
  62. package/dist/host/tool-scope.d.ts +32 -0
  63. package/dist/host/tool-scope.js +84 -0
  64. package/dist/host/unit-declaration.d.ts +65 -0
  65. package/dist/host/unit-declaration.js +213 -0
  66. package/dist/host/unit-readiness.d.ts +7 -0
  67. package/dist/host/unit-readiness.js +5 -0
  68. package/dist/host/unit-registry.d.ts +9 -0
  69. package/dist/host/unit-registry.js +33 -0
  70. package/dist/host/version-range.d.ts +2 -0
  71. package/dist/host/version-range.js +73 -0
  72. package/dist/host.d.ts +7 -4
  73. package/dist/host.js +7 -4
  74. package/dist/i18n/en.js +4 -0
  75. package/dist/i18n/index.d.ts +5 -1
  76. package/dist/i18n/zh.d.ts +4 -0
  77. package/dist/i18n/zh.js +4 -0
  78. package/dist/index.d.ts +1 -4
  79. package/dist/index.js +1 -4
  80. package/dist/lib/board-fact.d.ts +0 -7
  81. package/dist/lib/board-fact.js +0 -15
  82. package/dist/lib/diy-record.d.ts +38 -1
  83. package/dist/lib/diy-record.js +66 -0
  84. package/dist/lib/text.d.ts +0 -13
  85. package/dist/lib/text.js +0 -20
  86. package/dist/testing/declaration.js +1 -1
  87. package/package.json +1 -1
  88. package/src/api/__tests__/facts.test.ts +0 -13
  89. package/src/api/context-ref.ts +0 -39
  90. package/src/api/facts.ts +0 -9
  91. package/src/components/ConfirmBar.css +26 -0
  92. package/src/components/ConfirmBar.test.tsx +130 -0
  93. package/src/components/ConfirmBar.tsx +63 -0
  94. package/src/components/Form.test.tsx +15 -0
  95. package/src/components/Form.tsx +7 -5
  96. package/src/components/FormNodes.tsx +50 -26
  97. package/src/components/FormTypes.test.ts +31 -0
  98. package/src/components/FormTypes.ts +46 -15
  99. package/src/components/FormWiring.test.tsx +1 -0
  100. package/src/config/constants.ts +3 -0
  101. package/src/host/__tests__/board-fact.test.ts +47 -0
  102. package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
  103. package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
  104. package/src/host/__tests__/facts.test.ts +13 -0
  105. package/src/host/__tests__/long-task.test.ts +42 -0
  106. package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
  107. package/src/host/__tests__/preflight-route.test.ts +33 -0
  108. package/src/host/__tests__/shell-text.test.ts +44 -0
  109. package/src/host/board-fact.ts +14 -0
  110. package/src/host/browser-read.ts +2 -1
  111. package/src/host/context-ref.ts +40 -0
  112. package/src/{lib → host}/diy-record.ts +95 -1
  113. package/src/host/facts.ts +10 -0
  114. package/src/host/git.ts +1 -1
  115. package/src/host/http.ts +4 -4
  116. package/src/host/long-task.ts +9 -0
  117. package/src/host/opencli-runtime.ts +1 -1
  118. package/src/host/opencli-window.ts +1 -1
  119. package/src/host/opencli.ts +2 -1
  120. package/src/host/plugin-source.ts +1 -1
  121. package/src/host/shell-text.ts +21 -0
  122. package/src/{api → host}/unit-declaration.ts +1 -1
  123. package/src/host.ts +7 -4
  124. package/src/i18n/en.ts +4 -0
  125. package/src/i18n/zh.ts +4 -0
  126. package/src/index.ts +1 -4
  127. package/src/lib/board-fact.test.ts +1 -33
  128. package/src/lib/board-fact.ts +0 -13
  129. package/src/lib/text.test.ts +1 -43
  130. package/src/lib/text.ts +0 -22
  131. package/src/testing/declaration.ts +1 -1
  132. package/dist/lib/lint-probe.d.ts +0 -1
  133. package/dist/lib/lint-probe.js +0 -6
  134. /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
  135. /package/src/{lib → host}/diy-debug.ts +0 -0
  136. /package/src/{lib → host}/panel-state.ts +0 -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,13 @@
1
+ /**
2
+ * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
3
+ * @param value - 任意值;不是字符串的先 `String()`。
4
+ * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
5
+ */
6
+ export declare function quoteShellArg(value: unknown): string;
7
+ /**
8
+ * 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
9
+ * @param text - 原文;`null` / `undefined` 当空串。
10
+ * @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
11
+ * @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
12
+ */
13
+ export declare function tailLines(text: string | null | undefined, lines?: number): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
3
+ * @param value - 任意值;不是字符串的先 `String()`。
4
+ * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
5
+ */
6
+ export function quoteShellArg(value) {
7
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
8
+ }
9
+ /**
10
+ * 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
11
+ * @param text - 原文;`null` / `undefined` 当空串。
12
+ * @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
13
+ * @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
14
+ */
15
+ export function tailLines(text, lines = 6) {
16
+ const all = String(text ?? '')
17
+ .trim()
18
+ .split('\n');
19
+ return all.slice(-lines).join('\n');
20
+ }
@@ -0,0 +1,32 @@
1
+ import type { DshPromptAssembly } from '@dsh-workbench/shared';
2
+ export interface ScopedToolRegistry {
3
+ restrict(filter: {
4
+ readonly allow?: readonly string[];
5
+ readonly deny?: readonly string[];
6
+ }): () => void;
7
+ }
8
+ export interface ToolScopeAgent {
9
+ readonly ctx?: {
10
+ readonly tools?: ScopedToolRegistry | undefined;
11
+ get?<T = unknown>(name: string): T | undefined;
12
+ } | undefined;
13
+ readonly session?: {
14
+ readonly header?: {
15
+ readonly cwd?: string;
16
+ } | undefined;
17
+ } | undefined;
18
+ }
19
+ export interface ToolScopeOptions {
20
+ deniedTools(workspace: string | null): readonly string[];
21
+ logger?: {
22
+ info?(message: string): void;
23
+ warn?(message: string): void;
24
+ } | undefined;
25
+ }
26
+ export interface ToolScope {
27
+ apply(agent: unknown): void;
28
+ narrow(assembly: DshPromptAssembly, agent: unknown): DshPromptAssembly;
29
+ reapply(): void;
30
+ disposeAll(): void;
31
+ }
32
+ export declare function createToolScope(options: ToolScopeOptions): ToolScope;
@@ -0,0 +1,84 @@
1
+ function agentOf(value) {
2
+ return typeof value === 'object' && value !== null ? value : undefined;
3
+ }
4
+ function workspaceOf(handle) {
5
+ const cwd = handle.session?.header?.cwd;
6
+ return typeof cwd === 'string' && cwd !== '' ? cwd : null;
7
+ }
8
+ export function createToolScope(options) {
9
+ const applied = new WeakMap();
10
+ const seen = new Set();
11
+ function toolsOf(handle) {
12
+ const byProperty = handle.ctx?.tools;
13
+ if (byProperty?.restrict !== undefined)
14
+ return byProperty;
15
+ const byGet = handle.ctx?.get?.('tools');
16
+ return byGet?.restrict === undefined ? undefined : byGet;
17
+ }
18
+ function sync(handle) {
19
+ const tools = toolsOf(handle);
20
+ if (tools === undefined) {
21
+ options.logger?.warn?.('[dsh-workbench] 拿不到 agent 作用域的工具面:这次不收窄(下次再试)');
22
+ return;
23
+ }
24
+ const workspace = workspaceOf(handle);
25
+ const deny = [...options.deniedTools(workspace)];
26
+ const signature = `${workspace ?? ''}\u0000${deny.join(',')}`;
27
+ const previous = applied.get(handle);
28
+ if (previous !== undefined && previous.signature === signature)
29
+ return;
30
+ if (previous === undefined)
31
+ seen.add(new WeakRef(handle));
32
+ previous?.dispose();
33
+ if (deny.length === 0) {
34
+ options.logger?.info?.('[dsh-workbench] 这个工作区启用了全部套件:不收窄工具面');
35
+ applied.delete(handle);
36
+ return;
37
+ }
38
+ try {
39
+ applied.set(handle, { signature, dispose: tools.restrict({ deny }) });
40
+ options.logger?.info?.(`[dsh-workbench] 按工作区收窄工具面:${workspace ?? '(未知工作区)'} 里收起 ${deny.length} 个工具(${deny.join(', ')})`);
41
+ }
42
+ catch (error) {
43
+ options.logger?.warn?.(`[dsh-workbench] 收窄工具面失败:${String(error)}`);
44
+ }
45
+ }
46
+ return {
47
+ apply(agent) {
48
+ const handle = agentOf(agent);
49
+ if (handle === undefined)
50
+ return;
51
+ sync(handle);
52
+ },
53
+ narrow(assembly, agent) {
54
+ const handle = agentOf(agent);
55
+ const tools = assembly?.tools;
56
+ if (handle === undefined || tools === undefined)
57
+ return assembly;
58
+ const deny = new Set(options.deniedTools(workspaceOf(handle)));
59
+ if (deny.size === 0)
60
+ return assembly;
61
+ const kept = tools.filter((tool) => !deny.has(tool.name));
62
+ return kept.length === tools.length ? assembly : { ...assembly, tools: kept };
63
+ },
64
+ reapply() {
65
+ for (const ref of [...seen]) {
66
+ const handle = ref.deref();
67
+ if (handle === undefined)
68
+ seen.delete(ref);
69
+ else
70
+ sync(handle);
71
+ }
72
+ },
73
+ disposeAll() {
74
+ for (const ref of [...seen]) {
75
+ const handle = ref.deref();
76
+ if (handle === undefined)
77
+ continue;
78
+ applied.get(handle)?.dispose();
79
+ applied.delete(handle);
80
+ }
81
+ seen.clear();
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,65 @@
1
+ import type { UnitEnvNeeds, UnitHostNeeds, UnitManifest } from '../api/unit.js';
2
+ /**
3
+ * 声明协议当前版本;模块清单的 dsh.workbench.apiVersion 要写这个整数。
4
+ */
5
+ export declare const WORKBENCH_API_VERSION = 1;
6
+ /**
7
+ * 外壳愿意认的协议版本;不在表里的模块当场被拒,不猜语义。
8
+ */
9
+ export declare const SUPPORTED_API_VERSIONS: readonly number[];
10
+ /**
11
+ * 清单里那半张卡片声明:文案键加排序位;卡片默认显不显示不在这里(那是运行期面板配置的事)。
12
+ */
13
+ export interface UnitCardDeclaration {
14
+ readonly titleKey: string;
15
+ readonly order: number;
16
+ }
17
+ /**
18
+ * 清单里的一条工具声明:进 dsh 工具表的全名、会不会写,以及生成物里的键名。
19
+ */
20
+ export interface UnitToolDeclaration {
21
+ readonly name: string;
22
+ readonly writes: boolean;
23
+ readonly key?: string;
24
+ }
25
+ /**
26
+ * 模块清单里 dsh.workbench 那一段的完整形状(协议版本、单元 id、卡片、工具、宿主与环境需要)。
27
+ */
28
+ export interface UnitDeclaration {
29
+ readonly apiVersion: number;
30
+ readonly unit: string;
31
+ readonly card: UnitCardDeclaration | null;
32
+ readonly tools: readonly UnitToolDeclaration[];
33
+ readonly host: UnitHostNeeds;
34
+ readonly env?: UnitEnvNeeds;
35
+ }
36
+ /**
37
+ * 协议认得的全部键;写了别的键不会被采纳(外壳静默忽略),所以字段名拼错不会有提示。
38
+ */
39
+ export declare const DECLARATION_KEYS: readonly string[];
40
+ /**
41
+ * 协议要求必写的键;env 不在其中(没有环境需要就不写)。
42
+ */
43
+ export declare const DECLARATION_REQUIRED_KEYS: readonly string[];
44
+ /**
45
+ * 查清单里 dsh.workbench 那一段的形状,回一串给人看的问题(中文,带字段路径)。
46
+ * @param subject 报错时用的主语,一般是包名。
47
+ * @param raw 清单里那段原始值,可能什么都不是。
48
+ * @returns 空数组表示形状过得去;版本不对就当场返回(后面的字段无从判起)。
49
+ */
50
+ export declare function declarationProblemsOf(subject: string, raw: unknown): readonly string[];
51
+ /**
52
+ * 读一份清单的结果:要么带着规整过的声明与 manifest,要么只带一串问题。
53
+ */
54
+ export interface DeclarationRead {
55
+ readonly declaration?: UnitDeclaration;
56
+ readonly manifest?: UnitManifest;
57
+ readonly problems: readonly string[];
58
+ }
59
+ /**
60
+ * 读一份清单:先查形状,过了再把声明规整成 manifest。
61
+ * @param subject 报错时用的主语。
62
+ * @param raw 清单里 dsh.workbench 那段原始值。
63
+ * @returns 有问题时只有 problems;没问题时 declaration 与 manifest 都给,manifest 里的字符串已 trim。
64
+ */
65
+ export declare function readDeclaration(subject: string, raw: unknown): DeclarationRead;