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

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 (140) 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 +62 -0
  22. package/dist/host/fs-write.js +112 -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 +85 -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 +421 -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.d.ts +12 -0
  72. package/dist/host/shell.js +3 -1
  73. package/dist/host/time.d.ts +10 -0
  74. package/dist/host/time.js +14 -0
  75. package/dist/host/value-handoff.js +3 -2
  76. package/dist/host/workspace-store.js +2 -8
  77. package/dist/host.d.ts +11 -0
  78. package/dist/host.js +11 -0
  79. package/dist/i18n/en.js +3 -3
  80. package/dist/i18n/index.d.ts +4 -4
  81. package/dist/i18n/zh.d.ts +3 -3
  82. package/dist/i18n/zh.js +3 -3
  83. package/dist/lib/version.d.ts +4 -0
  84. package/dist/lib/version.js +21 -0
  85. package/dist/testing/fake-adapter-set.js +0 -1
  86. package/dist/testing/git.d.ts +3 -0
  87. package/dist/testing/git.js +36 -0
  88. package/dist/testing/host-fakes.d.ts +30 -0
  89. package/dist/testing/host-fakes.js +104 -0
  90. package/package.json +5 -2
  91. package/src/api/dsh.ts +8 -1
  92. package/src/contract/unit-declaration.ts +20 -1
  93. package/src/contract/unit.ts +3 -0
  94. package/src/host/__tests__/env-policy.test.ts +114 -0
  95. package/src/host/__tests__/fs-write.test.ts +96 -0
  96. package/src/host/__tests__/preflight-route.test.ts +29 -0
  97. package/src/host/__tests__/seam.test.ts +296 -0
  98. package/src/host/__tests__/shell.test.ts +24 -0
  99. package/src/host/__tests__/time.test.ts +20 -0
  100. package/src/host/adapter-set.ts +0 -1
  101. package/src/host/changelog.ts +2 -1
  102. package/src/host/config-sync.ts +3 -90
  103. package/src/host/env-policy.ts +192 -0
  104. package/src/host/extension.ts +2 -1
  105. package/src/host/flow/run.ts +2 -1
  106. package/src/host/fs-write.ts +137 -0
  107. package/src/host/host-kit.ts +7 -2
  108. package/src/host/http.ts +11 -5
  109. package/src/host/installed-sources.ts +4 -5
  110. package/src/host/long-task.ts +3 -3
  111. package/src/host/package-content.ts +23 -13
  112. package/src/host/package-route.ts +10 -9
  113. package/src/host/panel-state.ts +2 -1
  114. package/src/host/patch-blocks.ts +67 -0
  115. package/src/host/patch.ts +5 -89
  116. package/src/host/port-types.ts +9 -9
  117. package/src/host/profile-registries.ts +2 -3
  118. package/src/host/registry-declaration.ts +17 -12
  119. package/src/host/release-probe.ts +47 -34
  120. package/src/host/seam/denial.ts +26 -0
  121. package/src/host/seam/env.ts +23 -0
  122. package/src/host/seam/exec.ts +94 -0
  123. package/src/host/seam/host-env.ts +20 -0
  124. package/src/host/seam/intent.ts +101 -0
  125. package/src/host/seam/policy.ts +89 -0
  126. package/src/host/seam/process.ts +90 -0
  127. package/src/host/seam/run.ts +484 -0
  128. package/src/host/seam/service.ts +8 -0
  129. package/src/host/seam/unit-seam.ts +62 -0
  130. package/src/host/shell.ts +17 -1
  131. package/src/host/time.ts +16 -0
  132. package/src/host/value-handoff.ts +3 -2
  133. package/src/host/workspace-store.ts +2 -8
  134. package/src/host.ts +11 -0
  135. package/src/i18n/en.ts +3 -3
  136. package/src/i18n/zh.ts +3 -3
  137. package/src/testing/fake-adapter-set.ts +0 -1
  138. package/src/testing/host-fakes.ts +126 -0
  139. package/dist/lib/unit-plugin.d.ts +0 -28
  140. package/dist/lib/unit-plugin.js +0 -23
@@ -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
 
@@ -1,4 +1,6 @@
1
- import type { DshShell } from '../api/dsh.js';
1
+ import { now } from './time.js';
2
+ import type { Seam } from './seam/run.js';
3
+ import { productWrite } from './seam/intent.js';
2
4
  import type { PluginRelease } from '../contract/unit.js';
3
5
 
4
6
  import { RELEASE_PACKAGES } from './release-packages.generated.js';
@@ -6,7 +8,6 @@ import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/consta
6
8
  import { isValidVersion } from './version.js';
7
9
  import { parseJsonLoose } from '../lib/text.js';
8
10
  import { quoteShellArg } from './shell-text.js';
9
- import { runShell } from './shell.js';
10
11
 
11
12
  function cacheFlag(cacheDir: string | undefined): string {
12
13
  return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
@@ -61,7 +62,7 @@ export function createReleaseProbe(): ReleaseProbe {
61
62
  }
62
63
 
63
64
  interface LatestVersionOptions {
64
- readonly shell: DshShell;
65
+ readonly seam: Seam;
65
66
  readonly name: string;
66
67
  readonly registry: string;
67
68
  readonly force?: boolean;
@@ -69,7 +70,7 @@ interface LatestVersionOptions {
69
70
  }
70
71
 
71
72
  interface MissingPartsOptions {
72
- readonly shell: DshShell;
73
+ readonly seam: Seam;
73
74
  readonly name: string;
74
75
  readonly registry: string;
75
76
  readonly version: string;
@@ -78,7 +79,7 @@ interface MissingPartsOptions {
78
79
  }
79
80
 
80
81
  interface ProbeReleaseOptions {
81
- readonly shell: DshShell;
82
+ readonly seam: Seam;
82
83
  readonly package: string;
83
84
  readonly registry?: string | undefined;
84
85
  readonly force?: boolean;
@@ -88,25 +89,29 @@ interface ProbeReleaseOptions {
88
89
  }
89
90
 
90
91
  async function latestVersionOf(latestCache: Map<string, CacheEntry>, options: {
91
- shell: DshShell;
92
+ seam: Seam;
92
93
  name: string;
93
94
  registry: string;
94
95
  force?: boolean;
95
96
  cacheDir?: string | undefined;
96
97
  }): Promise<string | undefined> {
97
- const { shell, name, registry, force = false, cacheDir } = options;
98
+ const { seam, name, registry, force = false, cacheDir } = options;
98
99
  const key = `${registry}|${name}`;
99
100
  const hit = latestCache.get(key);
100
- if (!force && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS)
101
+ if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS)
101
102
  return hit.value;
102
103
 
103
- const result = await runShell(
104
- shell,
105
- `npm view ${quoteShellArg(name)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
106
- { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
107
- ).catch(() => undefined);
104
+ const result = await seam
105
+ .run({
106
+ command: `npm view ${quoteShellArg(name)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
107
+ intent: productWrite(),
108
+ retry: 'safe',
109
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
110
+ bestEffort: true,
111
+ })
112
+ .catch(() => undefined);
108
113
  const read = result?.exitCode === 0 ? parseVersion(result.stdout) : undefined;
109
- latestCache.set(key, { at: Date.now(), value: read });
114
+ latestCache.set(key, { at: now(), value: read });
110
115
  return read;
111
116
  }
112
117
 
@@ -119,26 +124,30 @@ interface ExistsEntry {
119
124
  async function versionExists(
120
125
  existsCache: Map<string, ExistsEntry>,
121
126
  options: {
122
- shell: DshShell;
127
+ seam: Seam;
123
128
  name: string;
124
129
  version: string;
125
130
  registry: string;
126
131
  force?: boolean;
127
132
  cacheDir?: string | undefined;
128
133
  }): Promise<boolean> {
129
- const { shell, name, version, registry, force = false, cacheDir } = options;
134
+ const { seam, name, version, registry, force = false, cacheDir } = options;
130
135
  const key = `${registry}|${name}@${version}`;
131
136
  const hit = existsCache.get(key);
132
- if (!force && hit !== undefined && (hit.value || Date.now() - hit.at < VERSION_CHECK_TTL_MS))
137
+ if (!force && hit !== undefined && (hit.value || now() - hit.at < VERSION_CHECK_TTL_MS))
133
138
  return hit.value;
134
139
 
135
- const result = await runShell(
136
- shell,
137
- `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
138
- { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
139
- ).catch(() => undefined);
140
+ const result = await seam
141
+ .run({
142
+ command: `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
143
+ intent: productWrite(),
144
+ retry: 'safe',
145
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
146
+ bestEffort: true,
147
+ })
148
+ .catch(() => undefined);
140
149
  const value = result?.exitCode === 0 && parseVersion(result.stdout) !== undefined;
141
- existsCache.set(key, { at: Date.now(), value });
150
+ existsCache.set(key, { at: now(), value });
142
151
  return value;
143
152
  }
144
153
 
@@ -146,18 +155,18 @@ async function missingPartsOf(
146
155
  latestCache: Map<string, CacheEntry>,
147
156
  existsCache: Map<string, ExistsEntry>,
148
157
  options: {
149
- shell: DshShell;
158
+ seam: Seam;
150
159
  name: string;
151
160
  registry: string;
152
161
  version: string;
153
162
  force?: boolean;
154
163
  cacheDir?: string | undefined;
155
164
  }): Promise<readonly string[]> {
156
- const { shell, name, registry, version, force = false, cacheDir } = options;
165
+ const { seam, name, registry, version, force = false, cacheDir } = options;
157
166
  const checked = await Promise.all(
158
167
  RELEASE_PACKAGES.filter((part) => part !== name).map(async (part) => ({
159
168
  part,
160
- present: await versionExists(existsCache, { shell, name: part, version, registry, force, cacheDir }),
169
+ present: await versionExists(existsCache, { seam, name: part, version, registry, force, cacheDir }),
161
170
  })),
162
171
  );
163
172
  return checked.filter((entry) => !entry.present).map((entry) => entry.part);
@@ -166,7 +175,7 @@ async function missingPartsOf(
166
175
  async function probeOf(
167
176
  latestCache: Map<string, CacheEntry>,
168
177
  options: {
169
- shell: DshShell;
178
+ seam: Seam;
170
179
  package: string;
171
180
  registry?: string | undefined;
172
181
  force?: boolean;
@@ -174,14 +183,18 @@ async function probeOf(
174
183
  cacheDir?: string | undefined;
175
184
  prefix?: string | undefined;
176
185
  }): Promise<PluginRelease | undefined> {
177
- const { shell, package: pkg, registry, force = false, check = true, cacheDir, prefix } = options;
186
+ const { seam, package: pkg, registry, force = false, check = true, cacheDir, prefix } = options;
178
187
  if (pkg === "") return undefined;
179
188
 
180
- const ledger = await runShell(
181
- shell,
182
- `npm ls --prefix ${quoteShellArg(prefix ?? '')} ${quoteShellArg(pkg)} --json --depth=0${cacheFlag(cacheDir)}`,
183
- { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
184
- ).catch(() => undefined);
189
+ const ledger = await seam
190
+ .run({
191
+ command: `npm ls --prefix ${quoteShellArg(prefix ?? '')} ${quoteShellArg(pkg)} --json --depth=0${cacheFlag(cacheDir)}`,
192
+ intent: productWrite(),
193
+ retry: 'safe',
194
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
195
+ bestEffort: true,
196
+ })
197
+ .catch(() => undefined);
185
198
  const installed =
186
199
  ledger === undefined ? undefined : parseLedgerVersion(ledger.stdout, pkg);
187
200
  if (installed === undefined) return undefined;
@@ -193,7 +206,7 @@ async function probeOf(
193
206
  const latest =
194
207
  source === undefined || !check
195
208
  ? undefined
196
- : await latestVersionOf(latestCache, { shell, name: pkg, registry: source, force, cacheDir });
209
+ : await latestVersionOf(latestCache, { seam, name: pkg, registry: source, force, cacheDir });
197
210
 
198
211
  return {
199
212
  installed,
@@ -0,0 +1,26 @@
1
+ const PATTERNS: readonly RegExp[] = [
2
+ /npm (?:error|ERR!) path (.+)$/m,
3
+ /(?:EPERM|EACCES):[^,\n]*, (?:open|unlink|mkdir|scandir|rename|access|stat|lstat|read|write|copyfile|rmdir|opendir|utimes|chmod|chown) '([^']+)'/i,
4
+ /(?:Operation not permitted|Permission denied)[^\n]*?'([^']+)'/i,
5
+ /fatal: could not create work tree dir '([^']+)'/i,
6
+ /mkdir: cannot create directory '([^']+)'/i,
7
+ /cannot (?:create|mkdir) (?:directory )?'([^']+)'/i,
8
+ /(?:Operation not permitted|Permission denied):\s*([^\s'\n]+)/i,
9
+ ];
10
+
11
+ function firstMatch(text: string): string | undefined {
12
+ for (const pattern of PATTERNS) {
13
+ const found = pattern.exec(text);
14
+ const value = found?.[1]?.trim();
15
+ if (value !== undefined && value !== '') return value;
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ export function deniedPathOf(stderr: string, stdout = ''): string | undefined {
21
+ return firstMatch(stderr) ?? firstMatch(stdout);
22
+ }
23
+
24
+ export function sandboxDeniedOf(facts: { readonly denied?: boolean | undefined } | undefined): boolean {
25
+ return facts?.denied === true;
26
+ }
@@ -0,0 +1,23 @@
1
+ import { productEnvOf, unitEnvOf, type ProductEnvInput, type UnitEnvInput } from '../env-policy.js';
2
+
3
+ export type SeamEnvInput = ProductEnvInput;
4
+
5
+ export type SeamUnitEnvInput = UnitEnvInput;
6
+
7
+ /**
8
+ * 产品域(运行时根)的受控 env:隔离 HOME、npm/pnpm 缓存钉在运行时根、真 npmrc/gitconfig 只读指过去。
9
+ * @param input 运行时根、可选的 HOME 与真实用户目录。
10
+ * @returns 产品域 env。
11
+ */
12
+ export function seamProductEnv(input: SeamEnvInput): Record<string, string> {
13
+ return productEnvOf(input);
14
+ }
15
+
16
+ /**
17
+ * 模块单元的受控 env:HOME 与 npm/pnpm 目录钉到单元数据目录。
18
+ * @param input 运行时根、单元数据目录、可选的 TRUE 用户目录。
19
+ * @returns 单元 env。
20
+ */
21
+ export function seamUnitEnv(input: SeamUnitEnvInput): Record<string, string> {
22
+ return unitEnvOf(input);
23
+ }