@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.
- package/README.md +1 -1
- package/dist/api/dsh.d.ts +12 -0
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/contract/unit-declaration.js +15 -1
- package/dist/contract/unit.d.ts +3 -0
- package/dist/host/__scratch-guard.d.ts +1 -0
- package/dist/host/__scratch-guard.js +1 -0
- package/dist/host/adapter-set.js +0 -1
- package/dist/host/changelog.js +2 -1
- package/dist/host/channel-plugin.d.ts +15 -0
- package/dist/host/channel-plugin.js +42 -0
- package/dist/host/command-name.d.ts +1 -0
- package/dist/host/command-name.js +14 -0
- package/dist/host/config-sync.d.ts +1 -9
- package/dist/host/config-sync.js +3 -78
- package/dist/host/env-policy.d.ts +75 -0
- package/dist/host/env-policy.js +161 -0
- package/dist/host/extension.js +2 -1
- package/dist/host/flow/run.js +2 -1
- package/dist/host/fs-write.d.ts +62 -0
- package/dist/host/fs-write.js +112 -0
- package/dist/host/host-kit.d.ts +6 -2
- package/dist/host/host-kit.js +3 -1
- package/dist/host/http.js +11 -5
- package/dist/host/installed-sources.js +4 -5
- package/dist/host/long-task.js +3 -3
- package/dist/host/opencli-runner.d.ts +26 -0
- package/dist/host/opencli-runner.js +50 -0
- package/dist/host/package-content.d.ts +2 -2
- package/dist/host/package-content.js +20 -6
- package/dist/host/package-route.d.ts +3 -3
- package/dist/host/package-route.js +7 -6
- package/dist/host/panel-state.js +2 -1
- package/dist/host/patch-blocks.d.ts +13 -0
- package/dist/host/patch-blocks.js +52 -0
- package/dist/host/patch.d.ts +3 -8
- package/dist/host/patch.js +5 -76
- package/dist/host/plugin-source.d.ts +33 -0
- package/dist/host/plugin-source.js +91 -0
- package/dist/host/port-types.d.ts +9 -9
- package/dist/host/profile-registries.js +2 -3
- package/dist/host/registry-declaration.d.ts +2 -2
- package/dist/host/registry-declaration.js +14 -5
- package/dist/host/release-probe.d.ts +4 -4
- package/dist/host/release-probe.js +39 -14
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/runtime-links.d.ts +7 -0
- package/dist/host/runtime-links.js +25 -0
- package/dist/host/seam/denial.d.ts +4 -0
- package/dist/host/seam/denial.js +24 -0
- package/dist/host/seam/env.d.ts +15 -0
- package/dist/host/seam/env.js +17 -0
- package/dist/host/seam/exec.d.ts +84 -0
- package/dist/host/seam/exec.js +1 -0
- package/dist/host/seam/host-env.d.ts +2 -0
- package/dist/host/seam/host-env.js +18 -0
- package/dist/host/seam/intent.d.ts +66 -0
- package/dist/host/seam/intent.js +24 -0
- package/dist/host/seam/policy.d.ts +10 -0
- package/dist/host/seam/policy.js +85 -0
- package/dist/host/seam/process.d.ts +2 -0
- package/dist/host/seam/process.js +80 -0
- package/dist/host/seam/run.d.ts +44 -0
- package/dist/host/seam/run.js +421 -0
- package/dist/host/seam/service.d.ts +4 -0
- package/dist/host/seam/service.js +4 -0
- package/dist/host/seam/unit-seam.d.ts +14 -0
- package/dist/host/seam/unit-seam.js +43 -0
- package/dist/host/shell.d.ts +12 -0
- package/dist/host/shell.js +3 -1
- package/dist/host/time.d.ts +10 -0
- package/dist/host/time.js +14 -0
- package/dist/host/value-handoff.js +3 -2
- package/dist/host/workspace-store.js +2 -8
- package/dist/host.d.ts +11 -0
- package/dist/host.js +11 -0
- package/dist/i18n/en.js +3 -3
- package/dist/i18n/index.d.ts +4 -4
- package/dist/i18n/zh.d.ts +3 -3
- package/dist/i18n/zh.js +3 -3
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.js +21 -0
- package/dist/testing/fake-adapter-set.js +0 -1
- package/dist/testing/git.d.ts +3 -0
- package/dist/testing/git.js +36 -0
- package/dist/testing/host-fakes.d.ts +30 -0
- package/dist/testing/host-fakes.js +104 -0
- package/package.json +5 -2
- package/src/api/dsh.ts +8 -1
- package/src/contract/unit-declaration.ts +20 -1
- package/src/contract/unit.ts +3 -0
- package/src/host/__tests__/env-policy.test.ts +114 -0
- package/src/host/__tests__/fs-write.test.ts +96 -0
- package/src/host/__tests__/preflight-route.test.ts +29 -0
- package/src/host/__tests__/seam.test.ts +296 -0
- package/src/host/__tests__/shell.test.ts +24 -0
- package/src/host/__tests__/time.test.ts +20 -0
- package/src/host/adapter-set.ts +0 -1
- package/src/host/changelog.ts +2 -1
- package/src/host/config-sync.ts +3 -90
- package/src/host/env-policy.ts +192 -0
- package/src/host/extension.ts +2 -1
- package/src/host/flow/run.ts +2 -1
- package/src/host/fs-write.ts +137 -0
- package/src/host/host-kit.ts +7 -2
- package/src/host/http.ts +11 -5
- package/src/host/installed-sources.ts +4 -5
- package/src/host/long-task.ts +3 -3
- package/src/host/package-content.ts +23 -13
- package/src/host/package-route.ts +10 -9
- package/src/host/panel-state.ts +2 -1
- package/src/host/patch-blocks.ts +67 -0
- package/src/host/patch.ts +5 -89
- package/src/host/port-types.ts +9 -9
- package/src/host/profile-registries.ts +2 -3
- package/src/host/registry-declaration.ts +17 -12
- package/src/host/release-probe.ts +47 -34
- package/src/host/seam/denial.ts +26 -0
- package/src/host/seam/env.ts +23 -0
- package/src/host/seam/exec.ts +94 -0
- package/src/host/seam/host-env.ts +20 -0
- package/src/host/seam/intent.ts +101 -0
- package/src/host/seam/policy.ts +89 -0
- package/src/host/seam/process.ts +90 -0
- package/src/host/seam/run.ts +484 -0
- package/src/host/seam/service.ts +8 -0
- package/src/host/seam/unit-seam.ts +62 -0
- package/src/host/shell.ts +17 -1
- package/src/host/time.ts +16 -0
- package/src/host/value-handoff.ts +3 -2
- package/src/host/workspace-store.ts +2 -8
- package/src/host.ts +11 -0
- package/src/i18n/en.ts +3 -3
- package/src/i18n/zh.ts +3 -3
- package/src/testing/fake-adapter-set.ts +0 -1
- package/src/testing/host-fakes.ts +126 -0
- package/dist/lib/unit-plugin.d.ts +0 -28
- package/dist/lib/unit-plugin.js +0 -23
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ CSS 处理。
|
|
|
47
47
|
| `src/config/` | 常量与存储键 |
|
|
48
48
|
| `src/i18n/` | zh / en 字典、合并与设置 |
|
|
49
49
|
| `src/lib/` | 纯函数:文本、忙碌态、表单问题、看板事实 |
|
|
50
|
-
| `src/host/` | 宿主侧内核与领域:http、facts、面板状态、workspace
|
|
50
|
+
| `src/host/` | 宿主侧内核与领域:http、facts、面板状态、workspace、单元声明、执行缝、文件写(`fs-write`:原子写 / JSON / 权限)、受控 env(`env-policy`:product / unit / bootstrap / promptless) |
|
|
51
51
|
| `src/testing/` | 测试假件与声明读取 |
|
|
52
52
|
|
|
53
53
|
`src/index.ts` 与 `src/host.ts` 是**生成的 barrel**:新增文件后跑 `pnpm run barrels`,别手抄。
|
package/dist/api/dsh.d.ts
CHANGED
|
@@ -140,6 +140,12 @@ export interface DshShellExecution {
|
|
|
140
140
|
text: string;
|
|
141
141
|
truncated: boolean;
|
|
142
142
|
};
|
|
143
|
+
sandbox?: {
|
|
144
|
+
mode?: string;
|
|
145
|
+
denied?: boolean;
|
|
146
|
+
enforcement?: string;
|
|
147
|
+
runnerFailed?: boolean;
|
|
148
|
+
} | undefined;
|
|
143
149
|
}>;
|
|
144
150
|
}
|
|
145
151
|
/**
|
|
@@ -155,6 +161,10 @@ export interface DshShell {
|
|
|
155
161
|
stdoutMaxBytes?: number;
|
|
156
162
|
signal?: AbortSignal;
|
|
157
163
|
env?: Readonly<Record<string, string>>;
|
|
164
|
+
sandboxPolicy?: {
|
|
165
|
+
mode: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
166
|
+
workspaceRoot: string;
|
|
167
|
+
};
|
|
158
168
|
}): unknown;
|
|
159
169
|
execute(spec: unknown): Promise<DshShellExecution>;
|
|
160
170
|
}
|
|
@@ -238,8 +248,10 @@ export interface DshApprovalService {
|
|
|
238
248
|
export interface DshSandboxPolicy {
|
|
239
249
|
resolve?(request?: {
|
|
240
250
|
readonly session?: unknown;
|
|
251
|
+
readonly mode?: string;
|
|
241
252
|
}): {
|
|
242
253
|
readonly mode?: string;
|
|
254
|
+
readonly workspaceRoot?: string;
|
|
243
255
|
} | undefined;
|
|
244
256
|
}
|
|
245
257
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 菜单开着时,滚轮落在「头条」(搜索框 / 触发钮)上也滚列表:点开菜单后指针还停在原来那个点上,
|
|
4
|
+
* 第一下滚动不该没有反应。列表自己那一格照旧由 dsh 的 `Menu` 滚,这里只补头条这段死区;
|
|
5
|
+
* 列表短到不用滚时不接管,滚轮该给谁还给谁。
|
|
6
|
+
* @param open - 菜单是否开着;关着时不接管滚轮。
|
|
7
|
+
* @param head - 头条那个节点的 ref:`search` 那档是搜索框,窄栏(`iconOnly`)那档是触发钮。
|
|
8
|
+
*/
|
|
9
|
+
export declare function useHeadWheel(open: boolean, head: RefObject<HTMLElement | null>): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from '../lib/react.js';
|
|
2
|
+
const MENU_CARD = '[role="menu"].wb-selectMenu';
|
|
3
|
+
const LINE_HEIGHT_PX = 16;
|
|
4
|
+
function menuViewport() {
|
|
5
|
+
const cards = document.querySelectorAll(MENU_CARD);
|
|
6
|
+
const card = cards[cards.length - 1];
|
|
7
|
+
const viewport = card?.querySelector('[role="presentation"]');
|
|
8
|
+
return viewport instanceof HTMLElement ? viewport : null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 菜单开着时,滚轮落在「头条」(搜索框 / 触发钮)上也滚列表:点开菜单后指针还停在原来那个点上,
|
|
12
|
+
* 第一下滚动不该没有反应。列表自己那一格照旧由 dsh 的 `Menu` 滚,这里只补头条这段死区;
|
|
13
|
+
* 列表短到不用滚时不接管,滚轮该给谁还给谁。
|
|
14
|
+
* @param open - 菜单是否开着;关着时不接管滚轮。
|
|
15
|
+
* @param head - 头条那个节点的 ref:`search` 那档是搜索框,窄栏(`iconOnly`)那档是触发钮。
|
|
16
|
+
*/
|
|
17
|
+
export function useHeadWheel(open, head) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const node = head.current;
|
|
20
|
+
if (!open || node === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
const onWheel = (event) => {
|
|
23
|
+
const viewport = menuViewport();
|
|
24
|
+
if (viewport === null || viewport.scrollHeight <= viewport.clientHeight)
|
|
25
|
+
return;
|
|
26
|
+
const step = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? event.deltaY * LINE_HEIGHT_PX : event.deltaY;
|
|
27
|
+
viewport.scrollTop += step;
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
};
|
|
30
|
+
node.addEventListener('wheel', onWheel, { passive: false });
|
|
31
|
+
return () => {
|
|
32
|
+
node.removeEventListener('wheel', onWheel);
|
|
33
|
+
};
|
|
34
|
+
}, [open, head]);
|
|
35
|
+
}
|
|
@@ -35,7 +35,8 @@ const CARD_KEYS = ['order'];
|
|
|
35
35
|
const TITLE_KEYS = ['zh', 'en'];
|
|
36
36
|
const TOOL_KEYS = ['name', 'writes', 'key'];
|
|
37
37
|
const TOOL_KEY_SHAPE = /^[a-z][a-zA-Z0-9]*$/;
|
|
38
|
-
const HOST_KEYS = ['inject', 'optional'];
|
|
38
|
+
const HOST_KEYS = ['inject', 'optional', 'writes'];
|
|
39
|
+
const UNIT_WRITE_SCOPES = ['unit', 'workspace'];
|
|
39
40
|
function text(value) {
|
|
40
41
|
return typeof value === 'string' ? value.trim() : '';
|
|
41
42
|
}
|
|
@@ -203,6 +204,18 @@ export function declarationProblemsOf(subject, raw) {
|
|
|
203
204
|
if (host.optional !== undefined) {
|
|
204
205
|
problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.optional`, host.optional));
|
|
205
206
|
}
|
|
207
|
+
if (host.writes !== undefined) {
|
|
208
|
+
const shape = stringArrayProblems(`${subject}: dsh.workbench.host.writes`, host.writes);
|
|
209
|
+
problems.push(...shape);
|
|
210
|
+
if (shape.length === 0) {
|
|
211
|
+
const outsideScopes = host.writes.filter((scope) => !UNIT_WRITE_SCOPES.includes(scope));
|
|
212
|
+
if (outsideScopes.length > 0) {
|
|
213
|
+
problems.push(`${subject}: dsh.workbench.host.writes 里有不认的写入域 ${listOf(outsideScopes)} —— ` +
|
|
214
|
+
`只认 "unit"(模块自己的数据目录)与 "workspace"(当前会话工作区)两档;` +
|
|
215
|
+
`模块拿不到产品域与任意路径,别写别的名字(缺省 [] = 不给执行缝门面)`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
return problems;
|
|
@@ -238,6 +251,7 @@ export function readDeclaration(subject, raw) {
|
|
|
238
251
|
host: {
|
|
239
252
|
inject: [...(declaration.host.inject ?? [])],
|
|
240
253
|
...(declaration.host.optional === undefined ? {} : { optional: [...declaration.host.optional] }),
|
|
254
|
+
writes: [...(declaration.host.writes ?? [])],
|
|
241
255
|
},
|
|
242
256
|
...(declaration.needs === undefined ? {} : { needs: declaration.needs }),
|
|
243
257
|
},
|
package/dist/contract/unit.d.ts
CHANGED
|
@@ -37,10 +37,13 @@ export interface UnitTool {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* 模块要外壳的哪些服务:inject 缺一个就挂不上,optional 缺了照挂。
|
|
40
|
+
* writes 声明模块要哪几档执行缝写入域:unit 是模块自己的数据目录,workspace 是当前会话工作区;
|
|
41
|
+
* 缺省 [] = 不给执行缝门面(deps.seam 是 undefined)。
|
|
40
42
|
*/
|
|
41
43
|
export interface UnitHostNeeds {
|
|
42
44
|
readonly inject: readonly string[];
|
|
43
45
|
readonly optional?: readonly string[];
|
|
46
|
+
readonly writes?: readonly ('unit' | 'workspace')[];
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
49
|
* 已装插件与源上最新版那一对,用来判该不该提示升级;latest 缺省表示还没查到。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const probe: string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const probe = process.env.HOME;
|
package/dist/host/adapter-set.js
CHANGED
package/dist/host/changelog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now as clockNow } from './time.js';
|
|
1
2
|
import dayjs from 'dayjs';
|
|
2
3
|
export const CHANGELOG_FILE = 'CHANGELOG.md';
|
|
3
4
|
export const CHANGELOG_KINDS = ['added', 'changed', 'fixed', 'removed', 'security', 'deprecated'];
|
|
@@ -17,7 +18,7 @@ export function changelogVersion(value) {
|
|
|
17
18
|
.trim()
|
|
18
19
|
.replace(/^v/, '');
|
|
19
20
|
}
|
|
20
|
-
export function changelogDate(now = new Date()) {
|
|
21
|
+
export function changelogDate(now = new Date(clockNow())) {
|
|
21
22
|
return dayjs(now).format('YYYY-MM-DD');
|
|
22
23
|
}
|
|
23
24
|
export function normalizeChangelogText(text) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const CHANNEL_PLUGIN_NAME = "opencli-plugin-workbench";
|
|
2
|
+
export declare const CHANNEL_PLUGIN_SITE = "workbench";
|
|
3
|
+
export interface ChannelPluginPlan {
|
|
4
|
+
readonly dest: string;
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly commands: readonly string[];
|
|
7
|
+
readonly line: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function channelVersionOf(dir: string): string | undefined;
|
|
10
|
+
export declare function channelPluginPlan(options: {
|
|
11
|
+
readonly channelDir: string;
|
|
12
|
+
readonly command?: string;
|
|
13
|
+
}): ChannelPluginPlan | undefined;
|
|
14
|
+
export declare function channelInstalledVersion(): string | undefined;
|
|
15
|
+
export declare function channelSiteInstalled(): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isolatedHome, toolchainOpencliDir, vendorPeerLink, vendorPluginsDir } from './paths.js';
|
|
4
|
+
import { quoteShellArg } from './shell-text.js';
|
|
5
|
+
export const CHANNEL_PLUGIN_NAME = 'opencli-plugin-workbench';
|
|
6
|
+
export const CHANNEL_PLUGIN_SITE = 'workbench';
|
|
7
|
+
function textOf(value) {
|
|
8
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
9
|
+
}
|
|
10
|
+
export function channelVersionOf(dir) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(join(dir, 'opencli-plugin.json'), 'utf8'));
|
|
13
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
const version = textOf(parsed['version']);
|
|
16
|
+
return version === '' ? undefined : version;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function channelPluginPlan(options) {
|
|
23
|
+
const version = channelVersionOf(options.channelDir);
|
|
24
|
+
if (version === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const command = options.command ?? 'opencli';
|
|
27
|
+
const dest = join(vendorPluginsDir(), CHANNEL_PLUGIN_NAME);
|
|
28
|
+
const peer = vendorPeerLink();
|
|
29
|
+
const commands = [
|
|
30
|
+
`rm -rf ${quoteShellArg(dest)} && mkdir -p ${quoteShellArg(dest)} && cp -R ${quoteShellArg(options.channelDir)}/. ${quoteShellArg(dest)}/`,
|
|
31
|
+
`rm -rf ${quoteShellArg(peer.dir)} && mkdir -p ${quoteShellArg(peer.parent)} && ln -s ${quoteShellArg(toolchainOpencliDir())} ${quoteShellArg(peer.dir)}`,
|
|
32
|
+
`(${quoteShellArg(command)} plugin uninstall ${quoteShellArg(CHANNEL_PLUGIN_SITE)} || true)`,
|
|
33
|
+
`${quoteShellArg(command)} plugin install ${quoteShellArg(dest)}`,
|
|
34
|
+
];
|
|
35
|
+
return { dest, version, commands, line: commands.join(' && ') };
|
|
36
|
+
}
|
|
37
|
+
export function channelInstalledVersion() {
|
|
38
|
+
return channelVersionOf(join(vendorPluginsDir(), CHANNEL_PLUGIN_NAME));
|
|
39
|
+
}
|
|
40
|
+
export function channelSiteInstalled() {
|
|
41
|
+
return existsSync(join(isolatedHome(), '.opencli', 'plugins', CHANNEL_PLUGIN_SITE));
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function commandNamesMatch(declared: string, resolved: string): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function baseName(value) {
|
|
2
|
+
const parts = value.trim().split(/[\\/]/);
|
|
3
|
+
return parts[parts.length - 1] ?? '';
|
|
4
|
+
}
|
|
5
|
+
function isOpencli(name) {
|
|
6
|
+
return name === 'opencli' || name === 'opencli.cmd' || name === 'opencli.exe';
|
|
7
|
+
}
|
|
8
|
+
export function commandNamesMatch(declared, resolved) {
|
|
9
|
+
const left = baseName(declared);
|
|
10
|
+
const right = baseName(resolved);
|
|
11
|
+
if (left === '' || right === '')
|
|
12
|
+
return false;
|
|
13
|
+
return left === right || (isOpencli(left) && isOpencli(right));
|
|
14
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { PostResource, RouteHandler } from './http.js';
|
|
2
2
|
import type { ConfigSyncRow, ConfigSyncSnapshot } from '../contract/protocol/config-sync.js';
|
|
3
|
-
|
|
4
|
-
readonly key: string;
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly insert: boolean;
|
|
7
|
-
readonly body: string;
|
|
8
|
-
readonly names: readonly string[];
|
|
9
|
-
}
|
|
10
|
-
export declare function patchBlocks(text: string): PatchBlock[];
|
|
3
|
+
import { type PatchBlock } from './patch-blocks.js';
|
|
11
4
|
export declare function normalizeBlock(body: string): string;
|
|
12
5
|
export declare function syncRows(source: readonly PatchBlock[], own: readonly PatchBlock[], missing: (block: PatchBlock) => readonly string[]): ConfigSyncRow[];
|
|
13
6
|
export declare function mergeBlocks(ownText: string, incoming: readonly PatchBlock[], overwrite: ReadonlySet<string>): {
|
|
@@ -27,7 +20,6 @@ export declare function readPatchBlocks(path: string): {
|
|
|
27
20
|
ok: false;
|
|
28
21
|
reason: string;
|
|
29
22
|
};
|
|
30
|
-
export declare function writePatchFile(path: string, text: string): void;
|
|
31
23
|
export interface ConfigSyncDeps {
|
|
32
24
|
readonly home: string;
|
|
33
25
|
readonly self: string;
|
package/dist/host/config-sync.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { CONFIG_SYNC_RESOURCE } from '../contract/protocol/config-sync.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
|
|
7
|
-
const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
|
|
4
|
+
import { writePatchFile } from './patch.js';
|
|
5
|
+
import { patchBlocks, spannedBlocks } from './patch-blocks.js';
|
|
8
6
|
const COMMENT_LINE = /^[ \t]*#/;
|
|
9
7
|
const ISOLATION_IDS = new Set([
|
|
10
8
|
'session-persistence-jsonl',
|
|
@@ -12,54 +10,6 @@ const ISOLATION_IDS = new Set([
|
|
|
12
10
|
'attachment-local',
|
|
13
11
|
'sandbox-policy',
|
|
14
12
|
]);
|
|
15
|
-
function trimmedTail(lines) {
|
|
16
|
-
let end = lines.length;
|
|
17
|
-
while (end > 0 && (lines[end - 1] ?? '').trim() === '')
|
|
18
|
-
end -= 1;
|
|
19
|
-
return lines.slice(0, end);
|
|
20
|
-
}
|
|
21
|
-
function matched(text, pattern) {
|
|
22
|
-
const found = pattern.exec(text);
|
|
23
|
-
return found === null ? undefined : found[1];
|
|
24
|
-
}
|
|
25
|
-
function blockOf(body, index) {
|
|
26
|
-
const lines = body.split('\n');
|
|
27
|
-
const first = lines[0] ?? '';
|
|
28
|
-
const id = matched(first, ID_LINE);
|
|
29
|
-
const nested = lines.flatMap((line) => {
|
|
30
|
-
const one = matched(line, NESTED_ID);
|
|
31
|
-
return one === undefined ? [] : [one];
|
|
32
|
-
});
|
|
33
|
-
const insert = id === undefined && INSERT_LINE.test(first);
|
|
34
|
-
const label = id ?? nested.join(', ');
|
|
35
|
-
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
|
|
36
|
-
const names = lines.flatMap((line) => {
|
|
37
|
-
const one = matched(line, NAME_LINE);
|
|
38
|
-
return one === undefined ? [] : [one];
|
|
39
|
-
});
|
|
40
|
-
return { key, id: label === '' ? key : label, insert, body, names };
|
|
41
|
-
}
|
|
42
|
-
function spannedBlocks(text) {
|
|
43
|
-
const lines = text.split('\n');
|
|
44
|
-
const starts = [];
|
|
45
|
-
lines.forEach((line, index) => {
|
|
46
|
-
if (/^- /.test(line) || line === '-')
|
|
47
|
-
starts.push(index);
|
|
48
|
-
});
|
|
49
|
-
const blocks = [];
|
|
50
|
-
for (let i = 0; i < starts.length; i += 1) {
|
|
51
|
-
const from = starts[i] ?? 0;
|
|
52
|
-
const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
|
|
53
|
-
const body = trimmedTail(lines.slice(from, to)).join('\n');
|
|
54
|
-
if (body.trim() === '')
|
|
55
|
-
continue;
|
|
56
|
-
blocks.push({ ...blockOf(body, blocks.length), from, to });
|
|
57
|
-
}
|
|
58
|
-
return blocks;
|
|
59
|
-
}
|
|
60
|
-
export function patchBlocks(text) {
|
|
61
|
-
return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
|
|
62
|
-
}
|
|
63
13
|
export function normalizeBlock(body) {
|
|
64
14
|
return body
|
|
65
15
|
.split('\n')
|
|
@@ -150,31 +100,6 @@ export function readPatchBlocks(path) {
|
|
|
150
100
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
151
101
|
}
|
|
152
102
|
}
|
|
153
|
-
function patchMode(path) {
|
|
154
|
-
try {
|
|
155
|
-
return statSync(path).mode & 0o777;
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
return 0o644;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
export function writePatchFile(path, text) {
|
|
162
|
-
const mode = patchMode(path);
|
|
163
|
-
const tmp = `${path}.wb-sync-${String(process.pid)}`;
|
|
164
|
-
writeFileSync(tmp, text, { mode });
|
|
165
|
-
try {
|
|
166
|
-
renameSync(tmp, path);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
try {
|
|
170
|
-
unlinkSync(tmp);
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
103
|
export function configSyncHandler(deps) {
|
|
179
104
|
return (search) => ({ ok: true, ...configSyncSnapshot(deps, search.get('source') ?? undefined) });
|
|
180
105
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ShellKind } from './shell-text.js';
|
|
2
|
+
export type EnvLayer = Readonly<Record<string, string | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* 把父进程环境里的字符串值拷成一层 env(丢掉 undefined)。
|
|
5
|
+
* @param parent 父环境(通常是 `process.env`)。
|
|
6
|
+
* @returns 可继续叠加的 env 层。
|
|
7
|
+
*/
|
|
8
|
+
export declare function inheritedEnvOf(parent: NodeJS.ProcessEnv | EnvLayer): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* 在一层 env 上叠另一层;`undefined` 表示删掉这个键。
|
|
11
|
+
* @param base 底层 env。
|
|
12
|
+
* @param layer 上层 env;值为 undefined 的键被删除。
|
|
13
|
+
* @returns 叠加后的新对象。
|
|
14
|
+
*/
|
|
15
|
+
export declare function overlayEnvOf(base: EnvLayer, layer: EnvLayer | undefined): Record<string, string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* 把 env 层收成只含字符串的 env(用于执行缝 `env` 参数)。
|
|
18
|
+
* @param layer env 层。
|
|
19
|
+
* @returns 只含字符串键值的对象。
|
|
20
|
+
*/
|
|
21
|
+
export declare function envTextOf(layer: EnvLayer): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* 在 PATH 前面插一组目录(去重、空项丢掉)。
|
|
24
|
+
* @param parentPath 原来的 PATH。
|
|
25
|
+
* @param bins 要前插的目录,按顺序。
|
|
26
|
+
* @param separator 平台分隔符,默认 `path.delimiter`。
|
|
27
|
+
* @returns 新的 PATH。
|
|
28
|
+
*/
|
|
29
|
+
export declare function pathWithBinsOf(parentPath: string | undefined, bins: readonly string[], separator?: string): string;
|
|
30
|
+
export interface ProductEnvInput {
|
|
31
|
+
readonly runtimeRoot: string;
|
|
32
|
+
readonly home?: string | undefined;
|
|
33
|
+
readonly realHome?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 产品域(运行时根)的受控 env:隔离 HOME、npm/pnpm 缓存都钉在运行时根,真 npmrc/gitconfig 只读指过去。
|
|
37
|
+
* @param input 运行时根、可选的 HOME 与真实用户目录。
|
|
38
|
+
* @returns 产品域 env。
|
|
39
|
+
*/
|
|
40
|
+
export declare function productEnvOf(input: ProductEnvInput): Record<string, string>;
|
|
41
|
+
export interface UnitEnvInput extends ProductEnvInput {
|
|
42
|
+
readonly unitRoot: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 模块单元的受控 env:在产品域基础上把 HOME 与 npm/pnpm 目录全钉到单元数据目录。
|
|
46
|
+
* @param input 运行时根、单元数据目录、可选的 TRUE 用户目录。
|
|
47
|
+
* @returns 单元 env。
|
|
48
|
+
*/
|
|
49
|
+
export declare function unitEnvOf(input: UnitEnvInput): Record<string, string>;
|
|
50
|
+
export interface BootstrapEnvInput {
|
|
51
|
+
readonly root: string;
|
|
52
|
+
readonly realHome: string;
|
|
53
|
+
readonly shellKind: ShellKind;
|
|
54
|
+
readonly parent?: NodeJS.ProcessEnv | EnvLayer | undefined;
|
|
55
|
+
readonly path?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 自举/装工具链的受控 env:继承父环境,HOME 与 npm 缓存钉到运行时根,PATH 可换成桌面自带 bin。
|
|
59
|
+
* @param input 运行时根、真实用户目录、shell 种类、父环境与可选的 PATH。
|
|
60
|
+
* @returns 自举 env。
|
|
61
|
+
*/
|
|
62
|
+
export declare function bootstrapEnvOf(input: BootstrapEnvInput): Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* git 的非交互 env:关掉终端提示/askpass;Windows 上不给 askpass 指 POSIX 路径(那台机器上没有)。
|
|
65
|
+
* @param platform 宿主平台。
|
|
66
|
+
* @returns git 非交互 env。
|
|
67
|
+
*/
|
|
68
|
+
export declare function promptlessGitEnvOf(platform: NodeJS.Platform): Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* 在父环境上叠加 `promptlessGitEnvOf`,得到可传给执行缝的 git env。
|
|
71
|
+
* @param parent 父环境。
|
|
72
|
+
* @param platform 宿主平台。
|
|
73
|
+
* @returns 只含字符串的 env。
|
|
74
|
+
*/
|
|
75
|
+
export declare function promptlessProcessEnvOf(parent: NodeJS.ProcessEnv, platform: NodeJS.Platform): Record<string, string>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { dshHome } from './data-root.js';
|
|
5
|
+
/**
|
|
6
|
+
* 把父进程环境里的字符串值拷成一层 env(丢掉 undefined)。
|
|
7
|
+
* @param parent 父环境(通常是 `process.env`)。
|
|
8
|
+
* @returns 可继续叠加的 env 层。
|
|
9
|
+
*/
|
|
10
|
+
export function inheritedEnvOf(parent) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, value] of Object.entries(parent)) {
|
|
13
|
+
if (typeof value === 'string')
|
|
14
|
+
out[key] = value;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 在一层 env 上叠另一层;`undefined` 表示删掉这个键。
|
|
20
|
+
* @param base 底层 env。
|
|
21
|
+
* @param layer 上层 env;值为 undefined 的键被删除。
|
|
22
|
+
* @returns 叠加后的新对象。
|
|
23
|
+
*/
|
|
24
|
+
export function overlayEnvOf(base, layer) {
|
|
25
|
+
const merged = { ...base };
|
|
26
|
+
for (const [key, value] of Object.entries(layer ?? {})) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
delete merged[key];
|
|
29
|
+
else
|
|
30
|
+
merged[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 把 env 层收成只含字符串的 env(用于执行缝 `env` 参数)。
|
|
36
|
+
* @param layer env 层。
|
|
37
|
+
* @returns 只含字符串键值的对象。
|
|
38
|
+
*/
|
|
39
|
+
export function envTextOf(layer) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
42
|
+
if (value !== undefined)
|
|
43
|
+
out[key] = value;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 在 PATH 前面插一组目录(去重、空项丢掉)。
|
|
49
|
+
* @param parentPath 原来的 PATH。
|
|
50
|
+
* @param bins 要前插的目录,按顺序。
|
|
51
|
+
* @param separator 平台分隔符,默认 `path.delimiter`。
|
|
52
|
+
* @returns 新的 PATH。
|
|
53
|
+
*/
|
|
54
|
+
export function pathWithBinsOf(parentPath, bins, separator = delimiter) {
|
|
55
|
+
const parts = (parentPath ?? '').split(separator).filter((part) => part !== '' && !bins.includes(part));
|
|
56
|
+
return [...bins, ...parts].join(separator);
|
|
57
|
+
}
|
|
58
|
+
function existingPointer(realHome, name) {
|
|
59
|
+
const file = join(realHome, name);
|
|
60
|
+
return existsSync(file) ? file : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 产品域(运行时根)的受控 env:隔离 HOME、npm/pnpm 缓存都钉在运行时根,真 npmrc/gitconfig 只读指过去。
|
|
64
|
+
* @param input 运行时根、可选的 HOME 与真实用户目录。
|
|
65
|
+
* @returns 产品域 env。
|
|
66
|
+
*/
|
|
67
|
+
export function productEnvOf(input) {
|
|
68
|
+
const runtimeRoot = input.runtimeRoot;
|
|
69
|
+
const realHome = input.realHome ?? homedir();
|
|
70
|
+
const env = {
|
|
71
|
+
HOME: input.home ?? join(runtimeRoot, 'home'),
|
|
72
|
+
DSH_HOME: dshHome(),
|
|
73
|
+
DSH_WORKBENCH_HOME: runtimeRoot,
|
|
74
|
+
npm_config_cache: join(runtimeRoot, 'npm-cache'),
|
|
75
|
+
NPM_CONFIG_CACHE: join(runtimeRoot, 'npm-cache'),
|
|
76
|
+
npm_config_logs_dir: join(runtimeRoot, 'npm-logs'),
|
|
77
|
+
npm_config_store_dir: join(runtimeRoot, 'pnpm-store'),
|
|
78
|
+
npm_config_cache_dir: join(runtimeRoot, 'pnpm-cache'),
|
|
79
|
+
npm_config_state_dir: join(runtimeRoot, 'pnpm-state'),
|
|
80
|
+
npm_config_update_notifier: 'false',
|
|
81
|
+
npm_config_fund: 'false',
|
|
82
|
+
NO_UPDATE_NOTIFIER: '1',
|
|
83
|
+
};
|
|
84
|
+
const npmrc = existingPointer(realHome, '.npmrc');
|
|
85
|
+
if (npmrc !== undefined)
|
|
86
|
+
env['NPM_CONFIG_USERCONFIG'] = npmrc;
|
|
87
|
+
const gitconfig = existingPointer(realHome, '.gitconfig');
|
|
88
|
+
if (gitconfig !== undefined)
|
|
89
|
+
env['GIT_CONFIG_GLOBAL'] = gitconfig;
|
|
90
|
+
return env;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 模块单元的受控 env:在产品域基础上把 HOME 与 npm/pnpm 目录全钉到单元数据目录。
|
|
94
|
+
* @param input 运行时根、单元数据目录、可选的 TRUE 用户目录。
|
|
95
|
+
* @returns 单元 env。
|
|
96
|
+
*/
|
|
97
|
+
export function unitEnvOf(input) {
|
|
98
|
+
const unitRoot = input.unitRoot;
|
|
99
|
+
return {
|
|
100
|
+
...productEnvOf({ ...input, home: join(unitRoot, 'home') }),
|
|
101
|
+
npm_config_cache: join(unitRoot, 'npm-cache'),
|
|
102
|
+
NPM_CONFIG_CACHE: join(unitRoot, 'npm-cache'),
|
|
103
|
+
npm_config_logs_dir: join(unitRoot, 'npm-logs'),
|
|
104
|
+
npm_config_store_dir: join(unitRoot, 'pnpm-store'),
|
|
105
|
+
npm_config_cache_dir: join(unitRoot, 'pnpm-cache'),
|
|
106
|
+
npm_config_state_dir: join(unitRoot, 'pnpm-state'),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 自举/装工具链的受控 env:继承父环境,HOME 与 npm 缓存钉到运行时根,PATH 可换成桌面自带 bin。
|
|
111
|
+
* @param input 运行时根、真实用户目录、shell 种类、父环境与可选的 PATH。
|
|
112
|
+
* @returns 自举 env。
|
|
113
|
+
*/
|
|
114
|
+
export function bootstrapEnvOf(input) {
|
|
115
|
+
const env = inheritedEnvOf(input.parent ?? process.env);
|
|
116
|
+
if (input.path !== undefined)
|
|
117
|
+
env['PATH'] = input.path;
|
|
118
|
+
const home = join(input.root, 'home');
|
|
119
|
+
env['HOME'] = home;
|
|
120
|
+
if (input.shellKind === 'pwsh')
|
|
121
|
+
env['USERPROFILE'] = home;
|
|
122
|
+
const userConfig = existingPointer(input.realHome, '.npmrc');
|
|
123
|
+
if (userConfig !== undefined)
|
|
124
|
+
env['NPM_CONFIG_USERCONFIG'] = userConfig;
|
|
125
|
+
const cache = join(input.root, 'npm-cache');
|
|
126
|
+
env['npm_config_cache'] = cache;
|
|
127
|
+
env['NPM_CONFIG_CACHE'] = cache;
|
|
128
|
+
env['NO_COLOR'] = '1';
|
|
129
|
+
env['TERM'] = 'dumb';
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* git 的非交互 env:关掉终端提示/askpass;Windows 上不给 askpass 指 POSIX 路径(那台机器上没有)。
|
|
134
|
+
* @param platform 宿主平台。
|
|
135
|
+
* @returns git 非交互 env。
|
|
136
|
+
*/
|
|
137
|
+
export function promptlessGitEnvOf(platform) {
|
|
138
|
+
const shared = {
|
|
139
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
140
|
+
SSH_ASKPASS_REQUIRE: 'never',
|
|
141
|
+
GCM_INTERACTIVE: 'never',
|
|
142
|
+
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new',
|
|
143
|
+
};
|
|
144
|
+
if (platform === 'win32')
|
|
145
|
+
return shared;
|
|
146
|
+
return { ...shared, GIT_ASKPASS: '/usr/bin/false', SSH_ASKPASS: '/usr/bin/false' };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 在父环境上叠加 `promptlessGitEnvOf`,得到可传给执行缝的 git env。
|
|
150
|
+
* @param parent 父环境。
|
|
151
|
+
* @param platform 宿主平台。
|
|
152
|
+
* @returns 只含字符串的 env。
|
|
153
|
+
*/
|
|
154
|
+
export function promptlessProcessEnvOf(parent, platform) {
|
|
155
|
+
const env = { ...inheritedEnvOf(parent), ...promptlessGitEnvOf(platform) };
|
|
156
|
+
if (platform === 'win32') {
|
|
157
|
+
delete env['GIT_ASKPASS'];
|
|
158
|
+
delete env['SSH_ASKPASS'];
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|
package/dist/host/extension.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now } from './time.js';
|
|
1
2
|
import { WorkbenchDataError } from './data-error.js';
|
|
2
3
|
function cliCodeOf(code) {
|
|
3
4
|
if (code === 'auth')
|
|
@@ -18,7 +19,7 @@ export function extensionCallerOf(data, need, callerOptions = {}) {
|
|
|
18
19
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
19
20
|
});
|
|
20
21
|
if (result.ok)
|
|
21
|
-
return { value: result.data, at:
|
|
22
|
+
return { value: result.data, at: now() };
|
|
22
23
|
const code = result.error.pageCode ?? cliCodeOf(result.error.code);
|
|
23
24
|
const command = [need.name, ...args].join(' ');
|
|
24
25
|
const advice = code === 'cli-unclassified'
|
package/dist/host/flow/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now as clockNow } from '../time.js';
|
|
1
2
|
import { evalPredicate, resolveInput, resolveValue } from './expr.js';
|
|
2
3
|
class StepFailure extends Error {
|
|
3
4
|
stepId;
|
|
@@ -158,7 +159,7 @@ async function runOne(options) {
|
|
|
158
159
|
export async function runFlow(options) {
|
|
159
160
|
const { flow, input, actions, signal, runId, workspace, onStep } = options;
|
|
160
161
|
const sleep = options.sleep ?? defaultSleep;
|
|
161
|
-
const now = options.now ?? (() =>
|
|
162
|
+
const now = options.now ?? (() => clockNow());
|
|
162
163
|
const steps = {};
|
|
163
164
|
const emit = (event) => {
|
|
164
165
|
try {
|