@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
package/src/host/http.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { now, nowIso } from './time.js';
1
2
  import type { DshHostContext, DshRequest, DshResponse } from '../api/dsh.js';
2
3
  import { unitFactScope } from './facts.js';
3
4
  import { parseCardsReport, parseCrashReport, type PanelStateStore } from './panel-state.js';
@@ -234,8 +235,13 @@ export function preflightHandler(preflight: {
234
235
  snapshot(): unknown;
235
236
  }): RouteHandler {
236
237
  return (search) => {
237
- void preflight.run({ hard: flag(search, 'hard') });
238
- return { ok: true, ...(preflight.snapshot() as Record<string, unknown>) };
238
+ if (flag(search, 'hard')) {
239
+ void preflight.run({ hard: true });
240
+ return { ok: true, ...(preflight.snapshot() as Record<string, unknown>) };
241
+ }
242
+ const snapshot = preflight.snapshot() as Record<string, unknown>;
243
+ void preflight.run({ hard: false });
244
+ return { ok: true, ...snapshot };
239
245
  };
240
246
  }
241
247
 
@@ -265,7 +271,7 @@ export function healthHandler(
265
271
  profile: dir === undefined ? null : dir.split('/').pop(),
266
272
  profileDir: dir ?? null,
267
273
  pid: process.pid,
268
- now: new Date().toISOString(),
274
+ now: nowIso(),
269
275
  ...(runtimes === undefined ? {} : { runtimes }),
270
276
  };
271
277
  };
@@ -310,7 +316,7 @@ export function registerPanelState(ctx: DshHostContext, store: PanelStateStore):
310
316
  const parsed = parseCardsReport(payload);
311
317
  if (!parsed.ok) return { ok: false, text: parsed.message };
312
318
  store.cards(parsed.value);
313
- return { ok: true, at: Date.now() };
319
+ return { ok: true, at: now() };
314
320
  },
315
321
  },
316
322
  {
@@ -319,7 +325,7 @@ export function registerPanelState(ctx: DshHostContext, store: PanelStateStore):
319
325
  const parsed = parseCrashReport(payload);
320
326
  if (!parsed.ok) return { ok: false, text: parsed.message };
321
327
  store.crash(parsed.value);
322
- return { ok: true, at: Date.now() };
328
+ return { ok: true, at: now() };
323
329
  },
324
330
  },
325
331
  ],
@@ -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
 
4
5
  export interface InstalledSource {
5
6
  readonly version: string;
@@ -50,11 +51,9 @@ export function packageSourceOf(profileDir: string | null, pkg: string): Install
50
51
  }
51
52
 
52
53
  function writeInstalledSources(profileDir: string, sources: InstalledSourceMap): void {
53
- const file = installedSourcesFile(profileDir);
54
- const temp = `${file}.tmp`;
55
- const text = `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`;
56
- fs.writeFileSync(temp, text, { mode: 0o600 });
57
- fs.renameSync(temp, file);
54
+ writeTextFile(installedSourcesFile(profileDir), `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`, {
55
+ mode: 0o600,
56
+ });
58
57
  }
59
58
 
60
59
  export function rememberPackageSource(profileDir: string, pkg: string, source: InstalledSource): void {
@@ -1,4 +1,4 @@
1
- import { epochOf } from './time.js';
1
+ import { epochOf, now as clockNow, nowIso } from './time.js';
2
2
  import { runsRouteOf, type LongTaskStatus, type LongTaskStep } from '../contract/long-task.js';
3
3
 
4
4
  const LIVE: ReadonlySet<string> = new Set<LongTaskStatus>(['running', 'needs-login']);
@@ -66,9 +66,9 @@ export interface LongTaskRegistry<S extends LongTaskBase> {
66
66
  export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDeps<S>): LongTaskRegistry<S> {
67
67
  const runs = new Map<string, LongTaskBase>();
68
68
  const controls = new Map<string, AbortController>();
69
- const now = deps.now ?? (() => new Date().toISOString());
69
+ const now = deps.now ?? (() => nowIso());
70
70
  let counter = 0;
71
- const nextId = deps.nextId ?? (() => `run-${Date.now()}-${(counter += 1)}`);
71
+ const nextId = deps.nextId ?? (() => `run-${clockNow()}-${(counter += 1)}`);
72
72
  const recentMs = deps.recentMs ?? RECENT_MS;
73
73
  const listeners = new Set<(change: LongTaskChange<S>) => void>();
74
74
 
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import type { DshShell } from '../api/dsh.js';
4
+ import { productWrite } from './seam/intent.js';
5
+ import type { Seam } from './seam/run.js';
5
6
  import { INSTALL_TIMEOUT_MS } from '../config/constants.js';
6
7
  import { parseJsonLoose } from '../lib/text.js';
7
8
  import { quoteShellArg } from './shell-text.js';
8
- import { runShell } from './shell.js';
9
9
  import { splitNpmSpec } from './package-kind.js';
10
10
  import { localDirOf } from './local-declaration.js';
11
11
 
@@ -34,7 +34,7 @@ function packedFileFrom(stdout: string, root: string): string | undefined {
34
34
 
35
35
  export async function withPackageDir<T>(
36
36
  options: {
37
- readonly shell?: DshShell | undefined;
37
+ readonly seam?: Seam | undefined;
38
38
  readonly spec: string;
39
39
  readonly registry?: string | undefined;
40
40
  readonly cacheDir?: string | undefined;
@@ -44,8 +44,8 @@ export async function withPackageDir<T>(
44
44
  const local = localDirOf(options.spec);
45
45
  if (local !== undefined) return visit(local);
46
46
  if (splitNpmSpec(options.spec) === undefined) return undefined;
47
- const shell = options.shell;
48
- if (shell === undefined) return undefined;
47
+ const seam = options.seam;
48
+ if (seam === undefined) return undefined;
49
49
 
50
50
  let root: string;
51
51
  try {
@@ -55,17 +55,27 @@ export async function withPackageDir<T>(
55
55
  }
56
56
  try {
57
57
  const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
58
- const packed = await runShell(
59
- shell,
60
- `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
61
- { timeoutMs: INSTALL_TIMEOUT_MS },
62
- ).catch(() => undefined);
58
+ const packed = await seam
59
+ .run({
60
+ command: `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
61
+ intent: productWrite(),
62
+ retry: 'safe',
63
+ timeoutMs: INSTALL_TIMEOUT_MS,
64
+ bestEffort: true,
65
+ })
66
+ .catch(() => undefined);
63
67
  if (packed === undefined || packed.exitCode !== 0) return undefined;
64
68
  const file = packedFileFrom(packed.stdout, root);
65
69
  if (file === undefined) return undefined;
66
- const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, {
67
- timeoutMs: INSTALL_TIMEOUT_MS,
68
- }).catch(() => undefined);
70
+ const unpacked = await seam
71
+ .run({
72
+ command: `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`,
73
+ intent: productWrite(),
74
+ retry: 'safe',
75
+ timeoutMs: INSTALL_TIMEOUT_MS,
76
+ bestEffort: true,
77
+ })
78
+ .catch(() => undefined);
69
79
  if (unpacked === undefined || unpacked.exitCode !== 0) return undefined;
70
80
  const dir = path.join(root, 'package');
71
81
  if (!fs.existsSync(dir)) return undefined;
@@ -1,4 +1,5 @@
1
- import type { DshShell } from '../api/dsh.js';
1
+ import { now } from './time.js';
2
+ import type { Seam } from './seam/run.js';
2
3
  import { VERSION_CHECK_TTL_MS } from '../config/constants.js';
3
4
  import { dshMarkersOf, splitNpmSpec, verdictOf, type PackageVerdict } from './package-kind.js';
4
5
  import { withPackageDir } from './package-content.js';
@@ -30,7 +31,7 @@ export interface PackageRouter {
30
31
 
31
32
  interface RouteOptions {
32
33
  readonly spec: string;
33
- readonly shell?: DshShell | undefined;
34
+ readonly seam?: Seam | undefined;
34
35
  readonly registry?: string | undefined;
35
36
  readonly force?: boolean;
36
37
  }
@@ -38,7 +39,7 @@ interface RouteOptions {
38
39
  export interface PackageRouterOptions {
39
40
  readonly manifestFile: string;
40
41
  readonly cacheDir?: string | undefined;
41
- readonly dshLookup?: ((options: { shell: DshShell; name: string; registry: string }) => Promise<{ readable: boolean; raw: unknown }>) | undefined;
42
+ readonly dshLookup?: ((options: { seam: Seam; name: string; registry: string }) => Promise<{ readable: boolean; raw: unknown }>) | undefined;
42
43
  }
43
44
 
44
45
  export function createPackageRouter(options: PackageRouterOptions): PackageRouter {
@@ -64,30 +65,30 @@ async function routeOf(
64
65
 
65
66
  const ref = splitNpmSpec(routeOptions.spec);
66
67
  if (ref === undefined) return undefined;
67
- const shell = routeOptions.shell;
68
+ const seam = routeOptions.seam;
68
69
  const registry = routeOptions.registry;
69
- if (shell === undefined || registry === undefined) return undefined;
70
+ if (seam === undefined || registry === undefined) return undefined;
70
71
 
71
72
  const key = `${registry}|${routeOptions.spec}`;
72
73
  const hit = cache.get(key);
73
- if (routeOptions.force !== true && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
74
+ if (routeOptions.force !== true && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
74
75
  return hit.route ?? undefined;
75
76
  }
76
77
 
77
78
  const found =
78
79
  options.dshLookup === undefined
79
80
  ? { readable: false, raw: undefined }
80
- : await options.dshLookup({ shell, name: routeOptions.spec, registry });
81
+ : await options.dshLookup({ seam, name: routeOptions.spec, registry });
81
82
  let route: PackageRoute | undefined;
82
83
  if (found.readable && dshMarkersOf(found.raw)) {
83
84
  route = { kind: 'dsh-bundle' };
84
85
  } else {
85
86
  const verdict = await withPackageDir(
86
- { shell, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) },
87
+ { seam, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) },
87
88
  (dir) => verdictOf(dir, options.manifestFile),
88
89
  );
89
90
  route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
90
91
  }
91
- cache.set(key, { at: Date.now(), route: route ?? null });
92
+ cache.set(key, { at: now(), route: route ?? null });
92
93
  return route;
93
94
  }
@@ -1,3 +1,4 @@
1
+ import { now } from './time.js';
1
2
  /**
2
3
  * 一张面板卡片取数走到哪一步:`idle` 还没取、`loading` 取着、`ready` 拿到了、`error` 失败了。
3
4
  */
@@ -188,7 +189,7 @@ export function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport>
188
189
  if (workspace === '') return { ok: false, message: 'workspace 不能是空的' };
189
190
  if (unit === '') return { ok: false, message: 'unit 不能是空的' };
190
191
  if (reason === '') return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
191
- return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
192
+ return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? now() } };
192
193
  }
193
194
 
194
195
  /**
@@ -0,0 +1,67 @@
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
+
9
+ export interface SpannedBlock extends PatchBlock {
10
+ readonly from: number;
11
+ readonly to: number;
12
+ }
13
+
14
+ const ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
15
+ const INSERT_LINE = /^- insert:[ \t]*$/;
16
+ const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
17
+ const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
18
+
19
+ function trimmedTail(lines: readonly string[]): readonly string[] {
20
+ let end = lines.length;
21
+ while (end > 0 && (lines[end - 1] ?? '').trim() === '') end -= 1;
22
+ return lines.slice(0, end);
23
+ }
24
+
25
+ function matched(text: string, pattern: RegExp): string | undefined {
26
+ const found = pattern.exec(text);
27
+ return found === null ? undefined : found[1];
28
+ }
29
+
30
+ function blockOf(body: string, index: number): PatchBlock {
31
+ const lines = body.split('\n');
32
+ const first = lines[0] ?? '';
33
+ const id = matched(first, ID_LINE);
34
+ const nested = lines.flatMap((line) => {
35
+ const one = matched(line, NESTED_ID);
36
+ return one === undefined ? [] : [one];
37
+ });
38
+ const insert = id === undefined && INSERT_LINE.test(first);
39
+ const label = id ?? nested.join(', ');
40
+ const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
41
+ const names = lines.flatMap((line) => {
42
+ const one = matched(line, NAME_LINE);
43
+ return one === undefined ? [] : [one];
44
+ });
45
+ return { key, id: label === '' ? key : label, insert, body, names };
46
+ }
47
+
48
+ export function spannedBlocks(text: string): SpannedBlock[] {
49
+ const lines = text.split('\n');
50
+ const starts: number[] = [];
51
+ lines.forEach((line, index) => {
52
+ if (/^- /.test(line) || line === '-') starts.push(index);
53
+ });
54
+ const blocks: SpannedBlock[] = [];
55
+ for (let i = 0; i < starts.length; i += 1) {
56
+ const from = starts[i] ?? 0;
57
+ const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
58
+ const body = trimmedTail(lines.slice(from, to)).join('\n');
59
+ if (body.trim() === '') continue;
60
+ blocks.push({ ...blockOf(body, blocks.length), from, to });
61
+ }
62
+ return blocks;
63
+ }
64
+
65
+ export function patchBlocks(text: string): PatchBlock[] {
66
+ return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
67
+ }
package/src/host/patch.ts CHANGED
@@ -1,93 +1,9 @@
1
- import { renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { writeTextFile } from './fs-write.js';
2
+ import { patchBlocks, type PatchBlock } from './patch-blocks.js';
2
3
 
3
- export interface PatchBlock {
4
- readonly key: string;
5
- readonly id: string;
6
- readonly insert: boolean;
7
- readonly body: string;
8
- readonly names: readonly string[];
9
- }
10
-
11
- interface SpannedBlock extends PatchBlock {
12
- readonly from: number;
13
- readonly to: number;
14
- }
15
-
16
- const ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
17
- const INSERT_LINE = /^- insert:[ \t]*$/;
18
- const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
19
- const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
20
-
21
- function trimmedTail(lines: readonly string[]): readonly string[] {
22
- let end = lines.length;
23
- while (end > 0 && (lines[end - 1] ?? '').trim() === '') end -= 1;
24
- return lines.slice(0, end);
25
- }
26
-
27
- function matched(text: string, pattern: RegExp): string | undefined {
28
- const found = pattern.exec(text);
29
- return found === null ? undefined : found[1];
30
- }
31
-
32
- function blockOf(body: string, index: number): PatchBlock {
33
- const lines = body.split('\n');
34
- const first = lines[0] ?? '';
35
- const id = matched(first, ID_LINE);
36
- const nested = lines.flatMap((line) => {
37
- const one = matched(line, NESTED_ID);
38
- return one === undefined ? [] : [one];
39
- });
40
- const insert = id === undefined && INSERT_LINE.test(first);
41
- const label = id ?? nested.join(', ');
42
- const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
43
- const names = lines.flatMap((line) => {
44
- const one = matched(line, NAME_LINE);
45
- return one === undefined ? [] : [one];
46
- });
47
- return { key, id: label === '' ? key : label, insert, body, names };
48
- }
49
-
50
- function spannedBlocks(text: string): SpannedBlock[] {
51
- const lines = text.split('\n');
52
- const starts: number[] = [];
53
- lines.forEach((line, index) => {
54
- if (/^- /.test(line) || line === '-') starts.push(index);
55
- });
56
- const blocks: SpannedBlock[] = [];
57
- for (let i = 0; i < starts.length; i += 1) {
58
- const from = starts[i] ?? 0;
59
- const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
60
- const body = trimmedTail(lines.slice(from, to)).join('\n');
61
- if (body.trim() === '') continue;
62
- blocks.push({ ...blockOf(body, blocks.length), from, to });
63
- }
64
- return blocks;
65
- }
66
-
67
- export function patchBlocks(text: string): PatchBlock[] {
68
- return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
69
- }
70
-
71
- function patchMode(path: string): number {
72
- try {
73
- return statSync(path).mode & 0o777;
74
- } catch {
75
- return 0o644;
76
- }
77
- }
4
+ export { patchBlocks, type PatchBlock };
78
5
 
6
+ /** 把 patch 文本写回原文件:原子写,保留已有权限位(新文件 0644)。 */
79
7
  export function writePatchFile(path: string, text: string): void {
80
- const mode = patchMode(path);
81
- const tmp = `${path}.wb-sync-${String(process.pid)}`;
82
- writeFileSync(tmp, text, { mode });
83
- try {
84
- renameSync(tmp, path);
85
- } catch (error) {
86
- try {
87
- unlinkSync(tmp);
88
- } catch {
89
- throw error;
90
- }
91
- throw error;
92
- }
8
+ writeTextFile(path, text, { mode: 'keep' });
93
9
  }
@@ -1,4 +1,5 @@
1
- import type { DshHostContext, DshShell } from '../api/dsh.js';
1
+ import type { DshHostContext } from '../api/dsh.js';
2
+ import type { Seam } from './seam/run.js';
2
3
  import type { RuntimeCapabilities, RuntimeProfile } from '../contract/capabilities.js';
3
4
  import type { PluginPackageKind } from '../contract/protocol/bundle-install.js';
4
5
  import type { LibNeed, SupplySource } from '../contract/needs.js';
@@ -57,14 +58,14 @@ export interface SupplyPluginInstallOptions {
57
58
  }
58
59
 
59
60
  export interface SupplyLatestOptions {
60
- readonly shell: DshShell;
61
+ readonly seam: Seam;
61
62
  readonly name: string;
62
63
  readonly registry: string;
63
64
  readonly force?: boolean;
64
65
  }
65
66
 
66
67
  export interface SupplyMissingPartsOptions {
67
- readonly shell: DshShell;
68
+ readonly seam: Seam;
68
69
  readonly name: string;
69
70
  readonly registry: string;
70
71
  readonly version: string;
@@ -72,7 +73,7 @@ export interface SupplyMissingPartsOptions {
72
73
  }
73
74
 
74
75
  export interface SupplyProbeReleaseOptions {
75
- readonly shell: DshShell;
76
+ readonly seam: Seam;
76
77
  readonly package: string;
77
78
  readonly registry?: string | undefined;
78
79
  readonly force?: boolean;
@@ -116,7 +117,7 @@ export interface ModulePort {
116
117
  declarationGaps(dir: string | null): readonly { readonly pkg: string }[] | undefined;
117
118
  moduleDeclarationOf(dir: string | null, pkg: string): unknown;
118
119
  registryDeclarationOf(options: {
119
- readonly shell: DshShell;
120
+ readonly seam: Seam;
120
121
  readonly name: string;
121
122
  readonly registry: string;
122
123
  readonly force?: boolean;
@@ -126,7 +127,7 @@ export interface ModulePort {
126
127
  refusalReason(): string;
127
128
  packageRouteOf(options: {
128
129
  readonly spec: string;
129
- readonly shell?: DshShell | undefined;
130
+ readonly seam?: Seam | undefined;
130
131
  readonly registry?: string | undefined;
131
132
  readonly force?: boolean;
132
133
  }): Promise<SupplyPackageRoute | undefined>;
@@ -140,7 +141,7 @@ export interface SupplyPort extends EnvSupplyPort, ModulePort {
140
141
  }
141
142
 
142
143
  export interface ProbeUnitsOptions {
143
- readonly shell: DshShell;
144
+ readonly seam: Seam;
144
145
  readonly runtimeCommands: Readonly<Record<string, string>>;
145
146
  readonly units: readonly NeedProbeInput[];
146
147
  readonly force?: boolean;
@@ -149,7 +150,7 @@ export interface ProbeUnitsOptions {
149
150
  export interface ProbePreflightInput {
150
151
  readonly routes: RouteTable;
151
152
  readonly ctx: DshHostContext;
152
- readonly shell: DshShell;
153
+ readonly seam: Seam;
153
154
  readonly runtimeCommands: Readonly<Record<string, string>>;
154
155
  readonly runtimeCapabilities: RuntimeCapabilities;
155
156
  readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
@@ -195,7 +196,6 @@ export interface RuntimePort {
195
196
  runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
196
197
  health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
197
198
  seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
198
- warnUnwritableRoot(ctx: DshHostContext): void;
199
199
  }
200
200
 
201
201
  export interface PluginDeclarationSource {
@@ -2,6 +2,7 @@ import type { RegistryEntry, RegistryNotebook } from './adapter-types.js';
2
2
  import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { writeTextFile } from './fs-write.js';
5
6
  export const REGISTRY_MARKER =
6
7
  '# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
7
8
 
@@ -93,9 +94,7 @@ function readNpmrc(file: string): string {
93
94
  }
94
95
 
95
96
  function writeNpmrc(file: string, text: string): void {
96
- const temp = `${file}.dsh-workbench.tmp`;
97
- fs.writeFileSync(temp, text, { mode: 0o600 });
98
- fs.renameSync(temp, file);
97
+ writeTextFile(file, text, { mode: 0o600 });
99
98
  }
100
99
 
101
100
  export function createRegistryNotebook(dir: string | null): RegistryNotebook {
@@ -1,5 +1,6 @@
1
- import type { DshShell } from '../api/dsh.js';
2
- import { runShell } from './shell.js';
1
+ import { now } from './time.js';
2
+ import { productWrite } from './seam/intent.js';
3
+ import type { Seam } from './seam/run.js';
3
4
  import { quoteShellArg } from './shell-text.js';
4
5
  import { parseJsonLoose } from '../lib/text.js';
5
6
  import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
@@ -19,7 +20,7 @@ export interface RegistryLookup {
19
20
  }
20
21
 
21
22
  interface RegistryDshOptions {
22
- readonly shell: DshShell;
23
+ readonly seam: Seam;
23
24
  readonly name: string;
24
25
  readonly registry?: string | undefined;
25
26
  readonly force?: boolean;
@@ -42,29 +43,33 @@ export function createRegistryLookup(): RegistryLookup {
42
43
  }
43
44
 
44
45
  async function dshOf(cache: Map<string, { at: number; raw: unknown }>, options: {
45
- shell: DshShell;
46
+ seam: Seam;
46
47
  name: string;
47
48
  registry?: string | undefined;
48
49
  force?: boolean;
49
50
  cacheDir?: string | undefined;
50
51
  }): Promise<RegistryDshLookup> {
51
- const { shell, name, registry, force = false, cacheDir } = options;
52
+ const { seam, name, registry, force = false, cacheDir } = options;
52
53
  const key = `${registry ?? ''}|${name}`;
53
54
  const hit = cache.get(key);
54
- if (!force && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
55
+ if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
55
56
  return { readable: true, raw: hit.raw };
56
57
  }
57
58
 
58
59
  const at = registry === undefined ? '' : ` --registry ${quoteShellArg(registry)}`;
59
60
  const cached = cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
60
- const result = await runShell(
61
- shell,
62
- `npm view ${quoteShellArg(name)} dsh --json${at}${cached}`,
63
- { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
64
- ).catch(() => undefined);
61
+ const result = await seam
62
+ .run({
63
+ command: `npm view ${quoteShellArg(name)} dsh --json${at}${cached}`,
64
+ intent: productWrite(),
65
+ retry: 'safe',
66
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
67
+ bestEffort: true,
68
+ })
69
+ .catch(() => undefined);
65
70
  const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : undefined;
66
71
  if (parsed === undefined) return { readable: false, raw: undefined };
67
- cache.set(key, { at: Date.now(), raw: parsed });
72
+ cache.set(key, { at: now(), raw: parsed });
68
73
  return { readable: true, raw: parsed };
69
74
  }
70
75