@dsh-workbench/shared 1.1.14-beta.2 → 1.1.15

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 (144) hide show
  1. package/README.md +1 -1
  2. package/dist/api/dsh.d.ts +12 -0
  3. package/dist/components/SelectFieldWheel.d.ts +9 -0
  4. package/dist/components/SelectFieldWheel.js +35 -0
  5. package/dist/contract/unit-declaration.js +15 -1
  6. package/dist/contract/unit.d.ts +3 -0
  7. package/dist/host/__scratch-guard.d.ts +1 -0
  8. package/dist/host/__scratch-guard.js +1 -0
  9. package/dist/host/adapter-set.js +0 -1
  10. package/dist/host/changelog.js +2 -1
  11. package/dist/host/channel-plugin.d.ts +15 -0
  12. package/dist/host/channel-plugin.js +42 -0
  13. package/dist/host/command-name.d.ts +1 -0
  14. package/dist/host/command-name.js +14 -0
  15. package/dist/host/config-sync.d.ts +1 -9
  16. package/dist/host/config-sync.js +3 -78
  17. package/dist/host/env-policy.d.ts +75 -0
  18. package/dist/host/env-policy.js +161 -0
  19. package/dist/host/extension.js +2 -1
  20. package/dist/host/flow/run.js +2 -1
  21. package/dist/host/fs-write.d.ts +68 -0
  22. package/dist/host/fs-write.js +121 -0
  23. package/dist/host/host-kit.d.ts +6 -2
  24. package/dist/host/host-kit.js +3 -1
  25. package/dist/host/http.js +11 -5
  26. package/dist/host/installed-sources.js +4 -5
  27. package/dist/host/long-task.js +3 -3
  28. package/dist/host/opencli-runner.d.ts +26 -0
  29. package/dist/host/opencli-runner.js +50 -0
  30. package/dist/host/package-content.d.ts +2 -2
  31. package/dist/host/package-content.js +20 -6
  32. package/dist/host/package-route.d.ts +3 -3
  33. package/dist/host/package-route.js +7 -6
  34. package/dist/host/panel-state.js +2 -1
  35. package/dist/host/patch-blocks.d.ts +13 -0
  36. package/dist/host/patch-blocks.js +52 -0
  37. package/dist/host/patch.d.ts +3 -8
  38. package/dist/host/patch.js +5 -76
  39. package/dist/host/plugin-source.d.ts +33 -0
  40. package/dist/host/plugin-source.js +91 -0
  41. package/dist/host/port-types.d.ts +9 -9
  42. package/dist/host/profile-registries.js +2 -3
  43. package/dist/host/registry-declaration.d.ts +2 -2
  44. package/dist/host/registry-declaration.js +14 -5
  45. package/dist/host/release-probe.d.ts +4 -4
  46. package/dist/host/release-probe.js +39 -14
  47. package/dist/host/runner.d.ts +26 -0
  48. package/dist/host/runner.js +50 -0
  49. package/dist/host/runtime-links.d.ts +7 -0
  50. package/dist/host/runtime-links.js +25 -0
  51. package/dist/host/seam/denial.d.ts +4 -0
  52. package/dist/host/seam/denial.js +24 -0
  53. package/dist/host/seam/env.d.ts +15 -0
  54. package/dist/host/seam/env.js +17 -0
  55. package/dist/host/seam/exec.d.ts +84 -0
  56. package/dist/host/seam/exec.js +1 -0
  57. package/dist/host/seam/host-env.d.ts +2 -0
  58. package/dist/host/seam/host-env.js +18 -0
  59. package/dist/host/seam/intent.d.ts +66 -0
  60. package/dist/host/seam/intent.js +24 -0
  61. package/dist/host/seam/policy.d.ts +10 -0
  62. package/dist/host/seam/policy.js +88 -0
  63. package/dist/host/seam/process.d.ts +2 -0
  64. package/dist/host/seam/process.js +80 -0
  65. package/dist/host/seam/run.d.ts +44 -0
  66. package/dist/host/seam/run.js +415 -0
  67. package/dist/host/seam/service.d.ts +4 -0
  68. package/dist/host/seam/service.js +4 -0
  69. package/dist/host/seam/unit-seam.d.ts +14 -0
  70. package/dist/host/seam/unit-seam.js +43 -0
  71. package/dist/host/shell-render.d.ts +9 -0
  72. package/dist/host/shell-render.js +18 -0
  73. package/dist/host/shell.d.ts +12 -0
  74. package/dist/host/shell.js +3 -1
  75. package/dist/host/time.d.ts +10 -0
  76. package/dist/host/time.js +14 -0
  77. package/dist/host/value-handoff.js +3 -2
  78. package/dist/host/workspace-store.js +2 -8
  79. package/dist/host.d.ts +12 -0
  80. package/dist/host.js +12 -0
  81. package/dist/i18n/en.js +3 -3
  82. package/dist/i18n/index.d.ts +4 -4
  83. package/dist/i18n/zh.d.ts +3 -3
  84. package/dist/i18n/zh.js +3 -3
  85. package/dist/lib/version.d.ts +4 -0
  86. package/dist/lib/version.js +21 -0
  87. package/dist/testing/fake-adapter-set.js +0 -1
  88. package/dist/testing/git.d.ts +3 -0
  89. package/dist/testing/git.js +36 -0
  90. package/dist/testing/host-fakes.d.ts +30 -0
  91. package/dist/testing/host-fakes.js +104 -0
  92. package/package.json +5 -2
  93. package/src/api/dsh.ts +8 -1
  94. package/src/contract/unit-declaration.ts +20 -1
  95. package/src/contract/unit.ts +3 -0
  96. package/src/host/__tests__/env-policy.test.ts +114 -0
  97. package/src/host/__tests__/fs-write.test.ts +96 -0
  98. package/src/host/__tests__/preflight-route.test.ts +29 -0
  99. package/src/host/__tests__/seam.test.ts +354 -0
  100. package/src/host/__tests__/shell-render.test.ts +58 -0
  101. package/src/host/__tests__/shell.test.ts +24 -0
  102. package/src/host/__tests__/time.test.ts +20 -0
  103. package/src/host/adapter-set.ts +0 -1
  104. package/src/host/changelog.ts +2 -1
  105. package/src/host/config-sync.ts +3 -90
  106. package/src/host/env-policy.ts +192 -0
  107. package/src/host/extension.ts +2 -1
  108. package/src/host/flow/run.ts +2 -1
  109. package/src/host/fs-write.ts +147 -0
  110. package/src/host/host-kit.ts +7 -2
  111. package/src/host/http.ts +11 -5
  112. package/src/host/installed-sources.ts +4 -5
  113. package/src/host/long-task.ts +3 -3
  114. package/src/host/package-content.ts +23 -13
  115. package/src/host/package-route.ts +10 -9
  116. package/src/host/panel-state.ts +2 -1
  117. package/src/host/patch-blocks.ts +67 -0
  118. package/src/host/patch.ts +5 -89
  119. package/src/host/port-types.ts +9 -9
  120. package/src/host/profile-registries.ts +2 -3
  121. package/src/host/registry-declaration.ts +17 -12
  122. package/src/host/release-probe.ts +47 -34
  123. package/src/host/seam/denial.ts +26 -0
  124. package/src/host/seam/env.ts +23 -0
  125. package/src/host/seam/exec.ts +94 -0
  126. package/src/host/seam/host-env.ts +20 -0
  127. package/src/host/seam/intent.ts +101 -0
  128. package/src/host/seam/policy.ts +96 -0
  129. package/src/host/seam/process.ts +90 -0
  130. package/src/host/seam/run.ts +478 -0
  131. package/src/host/seam/service.ts +8 -0
  132. package/src/host/seam/unit-seam.ts +62 -0
  133. package/src/host/shell-render.ts +28 -0
  134. package/src/host/shell.ts +17 -1
  135. package/src/host/time.ts +16 -0
  136. package/src/host/value-handoff.ts +3 -2
  137. package/src/host/workspace-store.ts +2 -8
  138. package/src/host.ts +12 -0
  139. package/src/i18n/en.ts +3 -3
  140. package/src/i18n/zh.ts +3 -3
  141. package/src/testing/fake-adapter-set.ts +0 -1
  142. package/src/testing/host-fakes.ts +126 -0
  143. package/dist/lib/unit-plugin.d.ts +0 -28
  144. package/dist/lib/unit-plugin.js +0 -23
@@ -0,0 +1,68 @@
1
+ export type WriteMode = number | 'keep';
2
+ export interface WriteFileOptions {
3
+ readonly atomic?: boolean | undefined;
4
+ readonly mkdir?: boolean | undefined;
5
+ readonly fsync?: boolean | undefined;
6
+ readonly mode?: WriteMode | undefined;
7
+ }
8
+ /**
9
+ * 把文本写进文件;默认原子写(temp + rename)、目录不存在就建、已有文件保留原权限。
10
+ * @param path 目标文件;父目录按 `mkdir` 决定是否创建。
11
+ * @param text 文本内容。
12
+ * @param options 原子/目录/权限/fsync;`mode` 默认 `'keep'`(新文件 0644)。
13
+ */
14
+ export declare function writeTextFile(path: string, text: string, options?: WriteFileOptions): void;
15
+ /**
16
+ * 往文件末尾追加文本;文件或目录不存在就建。追加不是原子写,只给「日志/采集」这类只增不改的流。
17
+ * @param path 目标文件。
18
+ * @param text 追加内容。
19
+ */
20
+ export declare function appendTextFile(path: string, text: string): void;
21
+ /**
22
+ * `writeTextFile` 的异步版;语义与默认值相同。
23
+ * @param path 目标文件。
24
+ * @param text 文本内容。
25
+ * @param options 原子/目录/权限/fsync。
26
+ * @returns 落盘完成。
27
+ */
28
+ export declare function writeTextFileAsync(path: string, text: string, options?: WriteFileOptions): Promise<void>;
29
+ /**
30
+ * 把二进制写进文件;语义与默认值同 `writeTextFile`。
31
+ * @param path 目标文件。
32
+ * @param bytes 二进制内容。
33
+ * @param options 原子/目录/权限/fsync。
34
+ */
35
+ export declare function writeBytesFile(path: string, bytes: Uint8Array, options?: WriteFileOptions): void;
36
+ /**
37
+ * `writeBytesFile` 的异步版;语义与默认值相同。
38
+ * @param path 目标文件。
39
+ * @param bytes 二进制内容。
40
+ * @param options 原子/目录/权限/fsync。
41
+ * @returns 落盘完成。
42
+ */
43
+ export declare function writeBytesFileAsync(path: string, bytes: Uint8Array, options?: WriteFileOptions): Promise<void>;
44
+ /**
45
+ * 把 JSON 写进文件:`JSON.stringify(value, null, 2)` 加末尾换行;语义与默认值同 `writeTextFile`。
46
+ * @param path 目标文件。
47
+ * @param value 可序列化的值。
48
+ * @param options 原子/目录/权限/fsync。
49
+ */
50
+ export declare function writeJsonFile(path: string, value: unknown, options?: WriteFileOptions): void;
51
+ /**
52
+ * `writeJsonFile` 的异步版;语义与默认值相同。
53
+ * @param path 目标文件。
54
+ * @param value 可序列化的值。
55
+ * @param options 原子/目录/权限/fsync。
56
+ * @returns 落盘完成。
57
+ */
58
+ export declare function writeJsonFileAsync(path: string, value: unknown, options?: WriteFileOptions): Promise<void>;
59
+ /**
60
+ * 确保目录存在(`mkdir -p`)。
61
+ * @param dir 目录路径。
62
+ */
63
+ export declare function ensureDir(dir: string): void;
64
+ /**
65
+ * 把已存在的文件设成可执行(0755)。
66
+ * @param path 文件路径。
67
+ */
68
+ export declare function makeExecutable(path: string): void;
@@ -0,0 +1,121 @@
1
+ import { appendFileSync, chmodSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import writeFileAtomic from 'write-file-atomic';
4
+ function modeOf(path, mode) {
5
+ if (typeof mode === 'number')
6
+ return mode;
7
+ try {
8
+ return statSync(path).mode & 0o777;
9
+ }
10
+ catch {
11
+ return 0o644;
12
+ }
13
+ }
14
+ function prepareOf(path, options) {
15
+ if (options.mkdir !== false)
16
+ mkdirSync(dirname(path), { recursive: true });
17
+ return modeOf(path, options.mode);
18
+ }
19
+ function atomicOptionsOf(mode, options) {
20
+ return options.fsync === true ? { mode, fsync: true } : { mode };
21
+ }
22
+ /**
23
+ * 把文本写进文件;默认原子写(temp + rename)、目录不存在就建、已有文件保留原权限。
24
+ * @param path 目标文件;父目录按 `mkdir` 决定是否创建。
25
+ * @param text 文本内容。
26
+ * @param options 原子/目录/权限/fsync;`mode` 默认 `'keep'`(新文件 0644)。
27
+ */
28
+ export function writeTextFile(path, text, options = {}) {
29
+ const mode = prepareOf(path, options);
30
+ if (options.atomic === false) {
31
+ writeFileSync(path, text, { mode });
32
+ return;
33
+ }
34
+ writeFileAtomic.sync(path, text, atomicOptionsOf(mode, options));
35
+ }
36
+ /**
37
+ * 往文件末尾追加文本;文件或目录不存在就建。追加不是原子写,只给「日志/采集」这类只增不改的流。
38
+ * @param path 目标文件。
39
+ * @param text 追加内容。
40
+ */
41
+ export function appendTextFile(path, text) {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ appendFileSync(path, text, { mode: modeOf(path, 'keep') });
44
+ }
45
+ /**
46
+ * `writeTextFile` 的异步版;语义与默认值相同。
47
+ * @param path 目标文件。
48
+ * @param text 文本内容。
49
+ * @param options 原子/目录/权限/fsync。
50
+ * @returns 落盘完成。
51
+ */
52
+ export async function writeTextFileAsync(path, text, options = {}) {
53
+ const mode = prepareOf(path, options);
54
+ if (options.atomic === false) {
55
+ writeFileSync(path, text, { mode });
56
+ return;
57
+ }
58
+ await writeFileAtomic(path, text, atomicOptionsOf(mode, options));
59
+ }
60
+ /**
61
+ * 把二进制写进文件;语义与默认值同 `writeTextFile`。
62
+ * @param path 目标文件。
63
+ * @param bytes 二进制内容。
64
+ * @param options 原子/目录/权限/fsync。
65
+ */
66
+ export function writeBytesFile(path, bytes, options = {}) {
67
+ const mode = prepareOf(path, options);
68
+ if (options.atomic === false) {
69
+ writeFileSync(path, bytes, { mode });
70
+ return;
71
+ }
72
+ writeFileAtomic.sync(path, Buffer.from(bytes), atomicOptionsOf(mode, options));
73
+ }
74
+ /**
75
+ * `writeBytesFile` 的异步版;语义与默认值相同。
76
+ * @param path 目标文件。
77
+ * @param bytes 二进制内容。
78
+ * @param options 原子/目录/权限/fsync。
79
+ * @returns 落盘完成。
80
+ */
81
+ export async function writeBytesFileAsync(path, bytes, options = {}) {
82
+ const mode = prepareOf(path, options);
83
+ if (options.atomic === false) {
84
+ writeFileSync(path, bytes, { mode });
85
+ return;
86
+ }
87
+ await writeFileAtomic(path, Buffer.from(bytes), atomicOptionsOf(mode, options));
88
+ }
89
+ /**
90
+ * 把 JSON 写进文件:`JSON.stringify(value, null, 2)` 加末尾换行;语义与默认值同 `writeTextFile`。
91
+ * @param path 目标文件。
92
+ * @param value 可序列化的值。
93
+ * @param options 原子/目录/权限/fsync。
94
+ */
95
+ export function writeJsonFile(path, value, options = {}) {
96
+ writeTextFile(path, `${JSON.stringify(value, null, 2)}\n`, options);
97
+ }
98
+ /**
99
+ * `writeJsonFile` 的异步版;语义与默认值相同。
100
+ * @param path 目标文件。
101
+ * @param value 可序列化的值。
102
+ * @param options 原子/目录/权限/fsync。
103
+ * @returns 落盘完成。
104
+ */
105
+ export async function writeJsonFileAsync(path, value, options = {}) {
106
+ await writeTextFileAsync(path, `${JSON.stringify(value, null, 2)}\n`, options);
107
+ }
108
+ /**
109
+ * 确保目录存在(`mkdir -p`)。
110
+ * @param dir 目录路径。
111
+ */
112
+ export function ensureDir(dir) {
113
+ mkdirSync(dir, { recursive: true });
114
+ }
115
+ /**
116
+ * 把已存在的文件设成可执行(0755)。
117
+ * @param path 文件路径。
118
+ */
119
+ export function makeExecutable(path) {
120
+ chmodSync(path, 0o755);
121
+ }
@@ -1,9 +1,10 @@
1
1
  import type { HostSurface } from '../contract/protocol/platform.js';
2
2
  import type { EnvSupplyPort } from './port-types.js';
3
+ import type { UnitSeam } from './seam/unit-seam.js';
3
4
  import { type FlowHost } from './flow/kit.js';
4
5
  import { sessionIdOf, sessionWorkspace } from './tool-kit.js';
5
6
  /**
6
- * 宿主 kit 的公共部分:flow、工具侧 session 读法、外部供给装法、宿主平台事实。
7
+ * 宿主 kit 的公共部分:flow、工具侧 session 读法、外部供给装法、宿主平台事实、按 unit 绑定的执行缝门面。
7
8
  */
8
9
  export interface HostKit {
9
10
  readonly flow: FlowHost;
@@ -11,14 +12,17 @@ export interface HostKit {
11
12
  readonly sessionWorkspace: typeof sessionWorkspace;
12
13
  readonly libInstallCommand: EnvSupplyPort['libInstallCommand'];
13
14
  readonly surface: HostSurface;
15
+ readonly seam?: UnitSeam | undefined;
14
16
  }
15
17
  /**
16
18
  * 建一份宿主 kit;每个挂载点一份,没有模块级状态。
17
19
  * @param parts.libInstallCommand 拼「装这个共用依赖」的那条命令。
18
20
  * @param parts.surface 宿主平台事实;由组合根按 profile 与宿主进程算好再交进来。
19
- * @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface。
21
+ * @param parts.seam 按 unit 绑定好的执行缝门面;模块清单没声明写入域时缺省不给。
22
+ * @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface,有门面时带上 seam。
20
23
  */
21
24
  export declare function createHostKit(parts: {
22
25
  readonly libInstallCommand: EnvSupplyPort['libInstallCommand'];
23
26
  readonly surface: HostSurface;
27
+ readonly seam?: UnitSeam | undefined;
24
28
  }): HostKit;
@@ -4,7 +4,8 @@ import { sessionIdOf, sessionWorkspace } from './tool-kit.js';
4
4
  * 建一份宿主 kit;每个挂载点一份,没有模块级状态。
5
5
  * @param parts.libInstallCommand 拼「装这个共用依赖」的那条命令。
6
6
  * @param parts.surface 宿主平台事实;由组合根按 profile 与宿主进程算好再交进来。
7
- * @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface。
7
+ * @param parts.seam 按 unit 绑定好的执行缝门面;模块清单没声明写入域时缺省不给。
8
+ * @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface,有门面时带上 seam。
8
9
  */
9
10
  export function createHostKit(parts) {
10
11
  return {
@@ -13,5 +14,6 @@ export function createHostKit(parts) {
13
14
  sessionWorkspace,
14
15
  libInstallCommand: parts.libInstallCommand,
15
16
  surface: parts.surface,
17
+ ...(parts.seam === undefined ? {} : { seam: parts.seam }),
16
18
  };
17
19
  }
package/dist/host/http.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { now, nowIso } from './time.js';
1
2
  import { unitFactScope } from './facts.js';
2
3
  import { parseCardsReport, parseCrashReport } from './panel-state.js';
3
4
  import { ACTIONS_PREFIX, API_PREFIX, DIY_DEBUG_PATH, DIY_RECORD_PATH, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
@@ -180,8 +181,13 @@ export function changesHandler(log) {
180
181
  }
181
182
  export function preflightHandler(preflight) {
182
183
  return (search) => {
183
- void preflight.run({ hard: flag(search, 'hard') });
184
- return { ok: true, ...preflight.snapshot() };
184
+ if (flag(search, 'hard')) {
185
+ void preflight.run({ hard: true });
186
+ return { ok: true, ...preflight.snapshot() };
187
+ }
188
+ const snapshot = preflight.snapshot();
189
+ void preflight.run({ hard: false });
190
+ return { ok: true, ...snapshot };
185
191
  };
186
192
  }
187
193
  export function capabilityHandler(probe) {
@@ -204,7 +210,7 @@ export function healthHandler(ctx, runtimes) {
204
210
  profile: dir === undefined ? null : dir.split('/').pop(),
205
211
  profileDir: dir ?? null,
206
212
  pid: process.pid,
207
- now: new Date().toISOString(),
213
+ now: nowIso(),
208
214
  ...(runtimes === undefined ? {} : { runtimes }),
209
215
  };
210
216
  };
@@ -240,7 +246,7 @@ export function registerPanelState(ctx, store) {
240
246
  if (!parsed.ok)
241
247
  return { ok: false, text: parsed.message };
242
248
  store.cards(parsed.value);
243
- return { ok: true, at: Date.now() };
249
+ return { ok: true, at: now() };
244
250
  },
245
251
  },
246
252
  {
@@ -250,7 +256,7 @@ export function registerPanelState(ctx, store) {
250
256
  if (!parsed.ok)
251
257
  return { ok: false, text: parsed.message };
252
258
  store.crash(parsed.value);
253
- return { ok: true, at: Date.now() };
259
+ return { ok: true, at: now() };
254
260
  },
255
261
  },
256
262
  ],
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { writeTextFile } from './fs-write.js';
3
4
  const FILE_NAME = '.dsh-workbench-sources.json';
4
5
  const FILE_VERSION = 1;
5
6
  function asRecord(value) {
@@ -41,11 +42,9 @@ export function packageSourceOf(profileDir, pkg) {
41
42
  return readInstalledSources(profileDir)[pkg];
42
43
  }
43
44
  function writeInstalledSources(profileDir, sources) {
44
- const file = installedSourcesFile(profileDir);
45
- const temp = `${file}.tmp`;
46
- const text = `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`;
47
- fs.writeFileSync(temp, text, { mode: 0o600 });
48
- fs.renameSync(temp, file);
45
+ writeTextFile(installedSourcesFile(profileDir), `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`, {
46
+ mode: 0o600,
47
+ });
49
48
  }
50
49
  export function rememberPackageSource(profileDir, pkg, source) {
51
50
  const trimmed = pkg.trim();
@@ -1,13 +1,13 @@
1
- import { epochOf } from './time.js';
1
+ import { epochOf, now as clockNow, nowIso } from './time.js';
2
2
  import { runsRouteOf } from '../contract/long-task.js';
3
3
  const LIVE = new Set(['running', 'needs-login']);
4
4
  const RECENT_MS = 10 * 60 * 1000;
5
5
  export function createLongTaskRegistry(deps) {
6
6
  const runs = new Map();
7
7
  const controls = new Map();
8
- const now = deps.now ?? (() => new Date().toISOString());
8
+ const now = deps.now ?? (() => nowIso());
9
9
  let counter = 0;
10
- const nextId = deps.nextId ?? (() => `run-${Date.now()}-${(counter += 1)}`);
10
+ const nextId = deps.nextId ?? (() => `run-${clockNow()}-${(counter += 1)}`);
11
11
  const recentMs = deps.recentMs ?? RECENT_MS;
12
12
  const listeners = new Set();
13
13
  const live = (id) => {
@@ -0,0 +1,26 @@
1
+ import type { DataRequest, DataResult } from '../contract/data.js';
2
+ /**
3
+ * 一条 opencli 命令的执行器:同一形状的 `execFile('opencli', args)`,结果只留 stdout。
4
+ */
5
+ export type LibRunner = (args: readonly string[]) => Promise<{
6
+ readonly stdout: string;
7
+ }>;
8
+ export type OpencliRunner = LibRunner;
9
+ /**
10
+ * 读缝(`DataReader.read`)的形状;模块直接把自己的 `data.read` 传进来即可。
11
+ */
12
+ export type OpencliRead = (request: DataRequest) => Promise<DataResult<unknown>>;
13
+ export interface OpencliRunnerOptions {
14
+ readonly timeoutMs?: number;
15
+ }
16
+ /**
17
+ * 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
18
+ * 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
19
+ *
20
+ * 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
21
+ * 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
22
+ * @param read 模块手里的读缝(`data.read`)。
23
+ * @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
24
+ * @returns 执行器;直接交给编排库的 `setOpencliRunner`。
25
+ */
26
+ export declare function opencliRunnerOf(read: OpencliRead, options?: OpencliRunnerOptions): OpencliRunner;
@@ -0,0 +1,50 @@
1
+ const AUTH_REQUIRED_EXIT_CODE = 77;
2
+ function textOf(value) {
3
+ return typeof value === 'string' ? value : '';
4
+ }
5
+ function failedOf(args, error) {
6
+ const head = `Command failed: opencli ${args.join(' ')}`;
7
+ const auth = error?.pageCode === 'auth-required';
8
+ const helpUrl = textOf(error?.helpUrl);
9
+ const detail = textOf(error?.detail);
10
+ const hint = textOf(error?.hint);
11
+ const failed = new Error([
12
+ head,
13
+ ...(auth
14
+ ? ['code: AUTH_REQUIRED', `message: ${textOf(error?.message)}`]
15
+ : [textOf(error?.message) || 'opencli 这条命令没跑通']),
16
+ ...(detail === '' ? [] : [detail]),
17
+ ...(hint === '' ? [] : [hint]),
18
+ ...(auth && helpUrl !== ''
19
+ ? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`]
20
+ : []),
21
+ ].join('\n'));
22
+ if (auth)
23
+ failed.code = AUTH_REQUIRED_EXIT_CODE;
24
+ return failed;
25
+ }
26
+ /**
27
+ * 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
28
+ * 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
29
+ *
30
+ * 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
31
+ * 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
32
+ * @param read 模块手里的读缝(`data.read`)。
33
+ * @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
34
+ * @returns 执行器;直接交给编排库的 `setOpencliRunner`。
35
+ */
36
+ export function opencliRunnerOf(read, options = {}) {
37
+ return async (args) => {
38
+ const result = await read({
39
+ kind: 'command',
40
+ adapter: 'opencli',
41
+ argv: [...args],
42
+ ...(options.timeoutMs === undefined
43
+ ? {}
44
+ : { timeoutMs: options.timeoutMs }),
45
+ });
46
+ if (result.ok)
47
+ return { stdout: JSON.stringify(result.data) };
48
+ throw failedOf(args, result.error);
49
+ };
50
+ }
@@ -1,6 +1,6 @@
1
- import type { DshShell } from '../api/dsh.js';
1
+ import type { Seam } from './seam/run.js';
2
2
  export declare function withPackageDir<T>(options: {
3
- readonly shell?: DshShell | undefined;
3
+ readonly seam?: Seam | undefined;
4
4
  readonly spec: string;
5
5
  readonly registry?: string | undefined;
6
6
  readonly cacheDir?: string | undefined;
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { productWrite } from './seam/intent.js';
4
5
  import { INSTALL_TIMEOUT_MS } from '../config/constants.js';
5
6
  import { parseJsonLoose } from '../lib/text.js';
6
7
  import { quoteShellArg } from './shell-text.js';
7
- import { runShell } from './shell.js';
8
8
  import { splitNpmSpec } from './package-kind.js';
9
9
  import { localDirOf } from './local-declaration.js';
10
10
  function cacheFlag(cacheDir) {
@@ -35,8 +35,8 @@ export async function withPackageDir(options, visit) {
35
35
  return visit(local);
36
36
  if (splitNpmSpec(options.spec) === undefined)
37
37
  return undefined;
38
- const shell = options.shell;
39
- if (shell === undefined)
38
+ const seam = options.seam;
39
+ if (seam === undefined)
40
40
  return undefined;
41
41
  let root;
42
42
  try {
@@ -47,15 +47,29 @@ export async function withPackageDir(options, visit) {
47
47
  }
48
48
  try {
49
49
  const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
50
- const packed = await runShell(shell, `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`, { timeoutMs: INSTALL_TIMEOUT_MS }).catch(() => undefined);
50
+ const packed = await seam
51
+ .run({
52
+ command: `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
53
+ intent: productWrite(),
54
+ retry: 'safe',
55
+ timeoutMs: INSTALL_TIMEOUT_MS,
56
+ bestEffort: true,
57
+ })
58
+ .catch(() => undefined);
51
59
  if (packed === undefined || packed.exitCode !== 0)
52
60
  return undefined;
53
61
  const file = packedFileFrom(packed.stdout, root);
54
62
  if (file === undefined)
55
63
  return undefined;
56
- const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, {
64
+ const unpacked = await seam
65
+ .run({
66
+ command: `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`,
67
+ intent: productWrite(),
68
+ retry: 'safe',
57
69
  timeoutMs: INSTALL_TIMEOUT_MS,
58
- }).catch(() => undefined);
70
+ bestEffort: true,
71
+ })
72
+ .catch(() => undefined);
59
73
  if (unpacked === undefined || unpacked.exitCode !== 0)
60
74
  return undefined;
61
75
  const dir = path.join(root, 'package');
@@ -1,4 +1,4 @@
1
- import type { DshShell } from '../api/dsh.js';
1
+ import type { Seam } from './seam/run.js';
2
2
  export type PackageRouteKind = 'runtime-plugin' | 'dsh-bundle' | 'neither';
3
3
  export interface PackageRoute {
4
4
  readonly kind: PackageRouteKind;
@@ -11,7 +11,7 @@ export interface PackageRouter {
11
11
  }
12
12
  interface RouteOptions {
13
13
  readonly spec: string;
14
- readonly shell?: DshShell | undefined;
14
+ readonly seam?: Seam | undefined;
15
15
  readonly registry?: string | undefined;
16
16
  readonly force?: boolean;
17
17
  }
@@ -19,7 +19,7 @@ export interface PackageRouterOptions {
19
19
  readonly manifestFile: string;
20
20
  readonly cacheDir?: string | undefined;
21
21
  readonly dshLookup?: ((options: {
22
- shell: DshShell;
22
+ seam: Seam;
23
23
  name: string;
24
24
  registry: string;
25
25
  }) => Promise<{
@@ -1,3 +1,4 @@
1
+ import { now } from './time.js';
1
2
  import { VERSION_CHECK_TTL_MS } from '../config/constants.js';
2
3
  import { dshMarkersOf, splitNpmSpec, verdictOf } from './package-kind.js';
3
4
  import { withPackageDir } from './package-content.js';
@@ -32,26 +33,26 @@ async function routeOf(cache, options, routeOptions) {
32
33
  const ref = splitNpmSpec(routeOptions.spec);
33
34
  if (ref === undefined)
34
35
  return undefined;
35
- const shell = routeOptions.shell;
36
+ const seam = routeOptions.seam;
36
37
  const registry = routeOptions.registry;
37
- if (shell === undefined || registry === undefined)
38
+ if (seam === undefined || registry === undefined)
38
39
  return undefined;
39
40
  const key = `${registry}|${routeOptions.spec}`;
40
41
  const hit = cache.get(key);
41
- if (routeOptions.force !== true && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
42
+ if (routeOptions.force !== true && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
42
43
  return hit.route ?? undefined;
43
44
  }
44
45
  const found = options.dshLookup === undefined
45
46
  ? { readable: false, raw: undefined }
46
- : await options.dshLookup({ shell, name: routeOptions.spec, registry });
47
+ : await options.dshLookup({ seam, name: routeOptions.spec, registry });
47
48
  let route;
48
49
  if (found.readable && dshMarkersOf(found.raw)) {
49
50
  route = { kind: 'dsh-bundle' };
50
51
  }
51
52
  else {
52
- const verdict = await withPackageDir({ shell, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) }, (dir) => verdictOf(dir, options.manifestFile));
53
+ const verdict = await withPackageDir({ seam, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) }, (dir) => verdictOf(dir, options.manifestFile));
53
54
  route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
54
55
  }
55
- cache.set(key, { at: Date.now(), route: route ?? null });
56
+ cache.set(key, { at: now(), route: route ?? null });
56
57
  return route;
57
58
  }
@@ -1,3 +1,4 @@
1
+ import { now } from './time.js';
1
2
  /**
2
3
  * 留存的工作区上限(LRU,满 64 个丢最旧的)。
3
4
  */
@@ -97,7 +98,7 @@ export function parseCrashReport(payload) {
97
98
  return { ok: false, message: 'unit 不能是空的' };
98
99
  if (reason === '')
99
100
  return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
100
- return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
101
+ return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? now() } };
101
102
  }
102
103
  /**
103
104
  * 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
@@ -0,0 +1,13 @@
1
+ export interface PatchBlock {
2
+ readonly key: string;
3
+ readonly id: string;
4
+ readonly insert: boolean;
5
+ readonly body: string;
6
+ readonly names: readonly string[];
7
+ }
8
+ export interface SpannedBlock extends PatchBlock {
9
+ readonly from: number;
10
+ readonly to: number;
11
+ }
12
+ export declare function spannedBlocks(text: string): SpannedBlock[];
13
+ export declare function patchBlocks(text: string): PatchBlock[];
@@ -0,0 +1,52 @@
1
+ const ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
2
+ const INSERT_LINE = /^- insert:[ \t]*$/;
3
+ const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
4
+ const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
5
+ function trimmedTail(lines) {
6
+ let end = lines.length;
7
+ while (end > 0 && (lines[end - 1] ?? '').trim() === '')
8
+ end -= 1;
9
+ return lines.slice(0, end);
10
+ }
11
+ function matched(text, pattern) {
12
+ const found = pattern.exec(text);
13
+ return found === null ? undefined : found[1];
14
+ }
15
+ function blockOf(body, index) {
16
+ const lines = body.split('\n');
17
+ const first = lines[0] ?? '';
18
+ const id = matched(first, ID_LINE);
19
+ const nested = lines.flatMap((line) => {
20
+ const one = matched(line, NESTED_ID);
21
+ return one === undefined ? [] : [one];
22
+ });
23
+ const insert = id === undefined && INSERT_LINE.test(first);
24
+ const label = id ?? nested.join(', ');
25
+ const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
26
+ const names = lines.flatMap((line) => {
27
+ const one = matched(line, NAME_LINE);
28
+ return one === undefined ? [] : [one];
29
+ });
30
+ return { key, id: label === '' ? key : label, insert, body, names };
31
+ }
32
+ export function spannedBlocks(text) {
33
+ const lines = text.split('\n');
34
+ const starts = [];
35
+ lines.forEach((line, index) => {
36
+ if (/^- /.test(line) || line === '-')
37
+ starts.push(index);
38
+ });
39
+ const blocks = [];
40
+ for (let i = 0; i < starts.length; i += 1) {
41
+ const from = starts[i] ?? 0;
42
+ const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
43
+ const body = trimmedTail(lines.slice(from, to)).join('\n');
44
+ if (body.trim() === '')
45
+ continue;
46
+ blocks.push({ ...blockOf(body, blocks.length), from, to });
47
+ }
48
+ return blocks;
49
+ }
50
+ export function patchBlocks(text) {
51
+ return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
52
+ }
@@ -1,9 +1,4 @@
1
- export interface PatchBlock {
2
- readonly key: string;
3
- readonly id: string;
4
- readonly insert: boolean;
5
- readonly body: string;
6
- readonly names: readonly string[];
7
- }
8
- export declare function patchBlocks(text: string): PatchBlock[];
1
+ import { patchBlocks, type PatchBlock } from './patch-blocks.js';
2
+ export { patchBlocks, type PatchBlock };
3
+ /** 把 patch 文本写回原文件:原子写,保留已有权限位(新文件 0644)。 */
9
4
  export declare function writePatchFile(path: string, text: string): void;