@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
@@ -0,0 +1,94 @@
1
+ import type { ShellResult } from '../shell.js';
2
+ import type { ShellKind } from '../shell-text.js';
3
+ import type { WriteIntent } from './intent.js';
4
+
5
+ export type SeamShellKind = ShellKind;
6
+
7
+ export interface HostWriteIntent {
8
+ readonly kind: 'host';
9
+ }
10
+
11
+ export type ExecIntent = WriteIntent | HostWriteIntent;
12
+
13
+ export type SeamTarget =
14
+ | { readonly argv: readonly string[] }
15
+ | { readonly line: string; readonly shellKind?: SeamShellKind | undefined };
16
+
17
+ export interface SeamOutcomeWithTarget extends ShellResult {
18
+ readonly outcome?: 'exited' | 'timed-out' | 'spawn-failed';
19
+ }
20
+
21
+ export interface SpawnRequest {
22
+ readonly target: SeamTarget;
23
+ readonly intent: ExecIntent;
24
+ readonly lifetime: 'host';
25
+ readonly cwd?: string | undefined;
26
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
27
+ readonly logFile?: string | undefined;
28
+ readonly label?: string | undefined;
29
+ }
30
+
31
+ export interface SpawnExit {
32
+ readonly code: number | null;
33
+ readonly signal: string | null;
34
+ }
35
+
36
+ export interface SpawnHandle {
37
+ readonly pid: number | undefined;
38
+ readonly logFile: string | undefined;
39
+ exit(): Promise<SpawnExit>;
40
+ onError(listener: (error: Error) => void): () => void;
41
+ kill(signal?: string): void;
42
+ }
43
+
44
+ export interface OpenRequest {
45
+ readonly target: SeamTarget;
46
+ readonly intent: ExecIntent;
47
+ readonly cwd?: string | undefined;
48
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
49
+ readonly label?: string | undefined;
50
+ }
51
+
52
+ export interface ChannelHandle {
53
+ readonly pid: number | undefined;
54
+ write(chunk: string): void;
55
+ onData(listener: (chunk: string) => void): () => void;
56
+ onExit(listener: (event: SpawnExit) => void): () => void;
57
+ onError(listener: (error: Error) => void): () => void;
58
+ kill(signal?: string): void;
59
+ }
60
+
61
+ export interface ProcessSpawnOptions {
62
+ readonly file: string;
63
+ readonly args: readonly string[];
64
+ readonly env: Readonly<Record<string, string | undefined>>;
65
+ readonly cwd?: string | undefined;
66
+ readonly detached: boolean;
67
+ readonly stdio: 'ignore' | 'pipe' | 'log';
68
+ readonly logFile?: string | undefined;
69
+ readonly windowsHide: boolean;
70
+ }
71
+
72
+ export interface ProcessExitEvent {
73
+ readonly code: number | null;
74
+ readonly signal: string | null;
75
+ }
76
+
77
+ export interface ProcessStream {
78
+ onData(listener: (chunk: string) => void): () => void;
79
+ }
80
+
81
+ export interface ProcessChild {
82
+ readonly pid: number | undefined;
83
+ readonly stdin: { write(chunk: string): void } | null;
84
+ readonly stdout: ProcessStream | null;
85
+ readonly stderr: ProcessStream | null;
86
+ onExit(listener: (event: ProcessExitEvent) => void): () => void;
87
+ onError(listener: (error: Error) => void): () => void;
88
+ unref(): void;
89
+ kill(signal?: string): void;
90
+ }
91
+
92
+ export interface ProcessHost {
93
+ spawn(options: ProcessSpawnOptions): ProcessChild;
94
+ }
@@ -0,0 +1,20 @@
1
+ export function hostEnvOf(
2
+ env: Readonly<Record<string, string | undefined>> | undefined,
3
+ ): Record<string, string | undefined> {
4
+ const merged: Record<string, string | undefined> = { ...process.env };
5
+ for (const [key, value] of Object.entries(env ?? {})) {
6
+ if (value === undefined) delete merged[key];
7
+ else merged[key] = value;
8
+ }
9
+ return merged;
10
+ }
11
+
12
+ export function definedEnvOf(
13
+ env: Readonly<Record<string, string | undefined>> | undefined,
14
+ ): Record<string, string> {
15
+ const out: Record<string, string> = {};
16
+ for (const [key, value] of Object.entries(env ?? {})) {
17
+ if (value !== undefined) out[key] = value;
18
+ }
19
+ return out;
20
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 执行缝的语义写入域:调用点只声明自己写哪一类树,不传裸根。
3
+ * 策略合成、环境注入与拒绝处置都住在执行缝模块里。
4
+ */
5
+ export type SeamMode = 'read-only' | 'workspace-write' | 'danger-full-access';
6
+
7
+ export interface ProductWriteIntent {
8
+ readonly kind: 'product';
9
+ }
10
+
11
+ export interface WorkspaceWriteIntent {
12
+ readonly kind: 'workspace';
13
+ }
14
+
15
+ export interface PathsWriteIntent {
16
+ readonly kind: 'paths';
17
+ readonly paths: readonly string[];
18
+ }
19
+
20
+ export interface NoWriteIntent {
21
+ readonly kind: 'none';
22
+ }
23
+
24
+ export type WriteIntent = ProductWriteIntent | WorkspaceWriteIntent | PathsWriteIntent | NoWriteIntent;
25
+
26
+ export type DenialScope = 'product' | 'workspace' | 'external' | 'unknown';
27
+
28
+ export interface SeamPolicy {
29
+ readonly mode: SeamMode;
30
+ readonly workspaceRoot: string;
31
+ }
32
+
33
+ export interface SeamDenial {
34
+ readonly path?: string | undefined;
35
+ readonly scope: DenialScope;
36
+ readonly healed?: boolean | undefined;
37
+ }
38
+
39
+ export interface SeamFact {
40
+ readonly at: number;
41
+ readonly verb: 'run' | 'spawn' | 'open';
42
+ readonly command: string;
43
+ readonly intent: WriteIntent['kind'] | 'host';
44
+ readonly mode: SeamMode | 'host';
45
+ readonly workspaceRoot: string;
46
+ readonly exitCode: number | null;
47
+ readonly timedOut: boolean;
48
+ readonly stalled: boolean;
49
+ readonly denial?: SeamDenial | undefined;
50
+ }
51
+
52
+ export type SeamErrorCode =
53
+ | 'workspace-unknown'
54
+ | 'read-only'
55
+ | 'paths-span-trees'
56
+ | 'scope-not-declared'
57
+ | 'sandbox-denied'
58
+ | 'sandbox-unavailable';
59
+
60
+ export class SeamError extends Error {
61
+ readonly code: SeamErrorCode;
62
+ readonly path?: string | undefined;
63
+ readonly scope?: DenialScope | undefined;
64
+
65
+ constructor(code: SeamErrorCode, message: string, extra: { readonly path?: string | undefined; readonly scope?: DenialScope | undefined } = {}) {
66
+ super(message);
67
+ this.name = 'SeamError';
68
+ this.code = code;
69
+ this.path = extra.path;
70
+ this.scope = extra.scope;
71
+ }
72
+ }
73
+
74
+ export type UnitWriteScope = 'unit' | 'workspace';
75
+
76
+ export interface UnitSeamRequest {
77
+ readonly command: string;
78
+ readonly scope: UnitWriteScope;
79
+ readonly workdir?: string | undefined;
80
+ readonly timeoutMs?: number | undefined;
81
+ readonly env?: Readonly<Record<string, string>> | undefined;
82
+ readonly session?: unknown;
83
+ readonly signal?: AbortSignal | undefined;
84
+ readonly retry?: 'safe' | 'never' | undefined;
85
+ }
86
+
87
+ export function productWrite(): WriteIntent {
88
+ return { kind: 'product' };
89
+ }
90
+
91
+ export function workspaceWrite(): WriteIntent {
92
+ return { kind: 'workspace' };
93
+ }
94
+
95
+ export function pathsWrite(paths: readonly string[]): WriteIntent {
96
+ return { kind: 'paths', paths };
97
+ }
98
+
99
+ export function noWrite(): WriteIntent {
100
+ return { kind: 'none' };
101
+ }
@@ -0,0 +1,89 @@
1
+ import { dirname, isAbsolute, resolve, sep } from 'node:path';
2
+ import type { DenialScope, SeamMode, SeamPolicy, WriteIntent } from './intent.js';
3
+ import { SeamError } from './intent.js';
4
+
5
+ export const SEAM_MODES: readonly SeamMode[] = ['read-only', 'workspace-write', 'danger-full-access'];
6
+
7
+ export function seamModeOf(value: string | undefined): SeamMode {
8
+ const text = value?.trim();
9
+ return text === 'read-only' || text === 'workspace-write' || text === 'danger-full-access' ? text : 'workspace-write';
10
+ }
11
+
12
+ function rank(mode: SeamMode): number {
13
+ return mode === 'read-only' ? 0 : mode === 'workspace-write' ? 1 : 2;
14
+ }
15
+
16
+ export function narrowerMode(left: SeamMode, right: SeamMode): SeamMode {
17
+ return rank(left) <= rank(right) ? left : right;
18
+ }
19
+
20
+ export function insideOf(root: string, path: string): boolean {
21
+ const confined = resolve(root);
22
+ const target = resolve(path);
23
+ if (target === confined) return true;
24
+ return target.startsWith(confined.endsWith(sep) ? confined : `${confined}${sep}`);
25
+ }
26
+
27
+ export function productPolicy(ceiling: SeamMode, productRoot: string): SeamPolicy {
28
+ return {
29
+ mode: ceiling === 'read-only' ? 'read-only' : 'workspace-write',
30
+ workspaceRoot: resolve(productRoot),
31
+ };
32
+ }
33
+
34
+ export function workspacePolicy(ceiling: SeamMode, grant: SeamPolicy | undefined): SeamPolicy {
35
+ if (grant === undefined) {
36
+ throw new SeamError('workspace-unknown', '这条会话解析不到工作区:先在左栏选一个工作区再跑需要写工作区的命令');
37
+ }
38
+ const mode = narrowerMode(ceiling, grant.mode);
39
+ if (mode === 'read-only') {
40
+ throw new SeamError('read-only', '当前会话是只读模式:要写工作区,先在权限设置里切换模式');
41
+ }
42
+ return { mode, workspaceRoot: resolve(grant.workspaceRoot) };
43
+ }
44
+
45
+ function commonDirectory(paths: readonly string[]): string | undefined {
46
+ const dirs = paths.map((path) => dirname(resolve(path)));
47
+ let shared = dirs[0];
48
+ if (shared === undefined) return undefined;
49
+ for (const dir of dirs.slice(1)) {
50
+ while (shared !== dirname(shared) && !insideOf(shared, dir)) {
51
+ shared = dirname(shared);
52
+ }
53
+ }
54
+ return shared;
55
+ }
56
+
57
+ export function pathsPolicy(ceiling: SeamMode, paths: readonly string[]): SeamPolicy {
58
+ if (paths.length === 0) {
59
+ throw new SeamError('paths-span-trees', '这次写入没有给出任何路径');
60
+ }
61
+ if (!paths.every((path) => isAbsolute(path))) {
62
+ throw new SeamError('paths-span-trees', '外部路径必须是绝对路径');
63
+ }
64
+ if (ceiling === 'read-only') {
65
+ throw new SeamError('read-only', '当前部署是只读模式:这条命令不能写外部路径');
66
+ }
67
+ const root = commonDirectory(paths);
68
+ if (root === undefined || (root === sep && paths.length > 1)) {
69
+ throw new SeamError('paths-span-trees', '这次写入跨了多个目录:请分开做,或把产物改到工作台目录');
70
+ }
71
+ if (root === sep) {
72
+ throw new SeamError('paths-span-trees', '不能把整个文件系统声明成写入域');
73
+ }
74
+ return { mode: 'workspace-write', workspaceRoot: root };
75
+ }
76
+
77
+ export function denialScopeOf(path: string | undefined, policy: SeamPolicy, productRoot: string, grant: SeamPolicy | undefined): DenialScope {
78
+ if (path === undefined) return 'unknown';
79
+ if (insideOf(productRoot, path)) return 'product';
80
+ if (grant !== undefined && insideOf(grant.workspaceRoot, path)) return 'workspace';
81
+ if (insideOf(policy.workspaceRoot, path)) return policy.workspaceRoot === resolve(productRoot) ? 'product' : 'workspace';
82
+ return 'external';
83
+ }
84
+
85
+ export function policyOf(intent: WriteIntent, ceiling: SeamMode, productRoot: string, grant: SeamPolicy | undefined): SeamPolicy {
86
+ if (intent.kind === 'product' || intent.kind === 'none') return productPolicy(ceiling, productRoot);
87
+ if (intent.kind === 'workspace') return workspacePolicy(ceiling, grant);
88
+ return pathsPolicy(ceiling, intent.paths);
89
+ }
@@ -0,0 +1,90 @@
1
+ import { closeSync, openSync } from 'node:fs';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import { execa, type Options, type ResultPromise } from 'execa';
4
+ import type {
5
+ ProcessChild,
6
+ ProcessExitEvent,
7
+ ProcessHost,
8
+ ProcessSpawnOptions,
9
+ ProcessStream,
10
+ } from './exec.js';
11
+
12
+ function streamOf(readable: Readable | null | undefined): ProcessStream | null {
13
+ if (readable === null || readable === undefined) return null;
14
+ return {
15
+ onData(listener) {
16
+ const handler = (chunk: unknown): void => listener(String(chunk));
17
+ readable.on('data', handler);
18
+ return () => {
19
+ readable.off('data', handler);
20
+ };
21
+ },
22
+ };
23
+ }
24
+
25
+ export function createNodeProcessHost(): ProcessHost {
26
+ return {
27
+ spawn(options: ProcessSpawnOptions): ProcessChild {
28
+ const logFd =
29
+ options.stdio === 'log' && options.logFile !== undefined ? openSync(options.logFile, 'a') : undefined;
30
+ const stdio: Options['stdio'] =
31
+ options.stdio === 'pipe'
32
+ ? 'pipe'
33
+ : logFd === undefined
34
+ ? 'ignore'
35
+ : (['ignore', logFd, logFd] as unknown as Options['stdio']);
36
+ let logClosed = false;
37
+ const closeLog = (): void => {
38
+ if (logClosed || logFd === undefined) return;
39
+ logClosed = true;
40
+ closeSync(logFd);
41
+ };
42
+ let child: ResultPromise;
43
+ try {
44
+ child = execa(options.file, [...options.args], {
45
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
46
+ env: { ...options.env },
47
+ detached: options.detached,
48
+ stdio,
49
+ windowsHide: options.windowsHide,
50
+ reject: false,
51
+ buffer: false,
52
+ } as Options);
53
+ } catch (error) {
54
+ closeLog();
55
+ throw error;
56
+ }
57
+ if (logFd !== undefined) {
58
+ child.nodeChildProcess.once('spawn', closeLog);
59
+ child.nodeChildProcess.once('error', closeLog);
60
+ }
61
+ const stdin = child.stdin as Writable | null | undefined;
62
+ return {
63
+ pid: child.pid,
64
+ stdin: stdin === null || stdin === undefined ? null : { write: (chunk: string) => stdin.write(chunk) },
65
+ stdout: streamOf(child.stdout as Readable | null | undefined),
66
+ stderr: streamOf(child.stderr as Readable | null | undefined),
67
+ onExit(listener) {
68
+ const handler = (code: number | null, signal: string | null): void =>
69
+ listener({ code, signal } satisfies ProcessExitEvent);
70
+ child.nodeChildProcess.on('exit', handler);
71
+ return () => {
72
+ child.nodeChildProcess.off('exit', handler);
73
+ };
74
+ },
75
+ onError(listener) {
76
+ child.nodeChildProcess.on('error', listener);
77
+ return () => {
78
+ child.nodeChildProcess.off('error', listener);
79
+ };
80
+ },
81
+ unref() {
82
+ child.nodeChildProcess.unref();
83
+ },
84
+ kill(signal) {
85
+ child.kill(signal as NodeJS.Signals | undefined);
86
+ },
87
+ };
88
+ },
89
+ };
90
+ }