@dsh-workbench/shared 1.1.12 → 1.1.14-beta.0
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 +6 -4
- package/dist/api/dsh.d.ts +2 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -0
- package/dist/contract/command.d.ts +2 -0
- package/dist/contract/unit-declaration.js +3 -2
- package/dist/host/data-root.js +5 -3
- package/dist/host/flow/step.d.ts +11 -0
- package/dist/host/flow/step.js +21 -0
- package/dist/host/host-kit.d.ts +7 -2
- package/dist/host/host-kit.js +4 -1
- package/dist/host/install-failure.js +1 -1
- package/dist/host/lib-install.d.ts +1 -0
- package/dist/host/lib-install.js +4 -3
- package/dist/host/paths.js +8 -7
- package/dist/host/payload.d.ts +12 -0
- package/dist/host/payload.js +16 -0
- package/dist/host/preflight.d.ts +4 -2
- package/dist/host/shell-text.d.ts +11 -1
- package/dist/host/shell-text.js +11 -3
- package/dist/host/shell.d.ts +2 -0
- package/dist/host/shell.js +41 -11
- package/dist/host/tool-approval.d.ts +20 -0
- package/dist/host/tool-approval.js +20 -0
- package/dist/host/unit-face.d.ts +5 -0
- package/dist/host/unit-plugin.d.ts +33 -0
- package/dist/host/unit-plugin.js +25 -0
- package/dist/host/workspace-store.js +12 -6
- package/dist/host.d.ts +3 -0
- package/dist/host.js +3 -0
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/index.d.ts +5 -1
- package/dist/i18n/zh.d.ts +4 -0
- package/dist/i18n/zh.js +4 -0
- package/dist/lib/board-fact.d.ts +2 -1
- package/dist/lib/board-fact.js +10 -4
- package/dist/lib/ui.d.ts +8 -1
- package/dist/lib/ui.js +21 -3
- package/dist/lib/unit-plugin.d.ts +28 -0
- package/dist/lib/unit-plugin.js +23 -0
- package/dist/testing/host-fakes.d.ts +2 -0
- package/dist/testing/host-fakes.js +1 -1
- package/package.json +2 -2
- package/src/api/dsh.ts +2 -0
- package/src/components/__tests__/long-task-row.test.ts +1 -1
- package/src/config/constants.ts +3 -0
- package/src/contract/command.ts +2 -0
- package/src/contract/unit-declaration.ts +3 -2
- package/src/host/__tests__/data-root.test.ts +47 -0
- package/src/host/__tests__/plugin-helpers.test.ts +68 -0
- package/src/host/__tests__/runtime-layout.test.ts +5 -4
- package/src/host/__tests__/shell-text.test.ts +32 -7
- package/src/host/__tests__/shell.test.ts +94 -0
- package/src/host/data-root.ts +6 -3
- package/src/host/flow/__tests__/step.test.ts +43 -0
- package/src/host/flow/step.ts +21 -0
- package/src/host/host-kit.ts +11 -3
- package/src/host/install-failure.ts +1 -1
- package/src/host/lib-install.ts +5 -3
- package/src/host/paths.ts +8 -7
- package/src/host/payload.ts +17 -0
- package/src/host/preflight.ts +4 -2
- package/src/host/shell-text.ts +16 -2
- package/src/host/shell.ts +52 -12
- package/src/host/tool-approval.ts +31 -0
- package/src/host/unit-face.ts +5 -0
- package/src/host/unit-plugin.ts +49 -0
- package/src/host/workspace-store.ts +13 -6
- package/src/host.ts +3 -0
- package/src/i18n/en.ts +4 -0
- package/src/i18n/zh.ts +4 -0
- package/src/lib/__tests__/board-fact.test.ts +8 -0
- package/src/lib/__tests__/ui.test.ts +26 -0
- package/src/lib/board-fact.ts +11 -4
- package/src/lib/ui.ts +21 -3
- package/src/testing/host-fakes.ts +3 -1
- package/dist/components/SelectFieldWheel.d.ts +0 -9
- package/dist/components/SelectFieldWheel.js +0 -35
- package/dist/host/channel-plugin.d.ts +0 -15
- package/dist/host/channel-plugin.js +0 -42
- package/dist/host/command-name.d.ts +0 -1
- package/dist/host/command-name.js +0 -14
- package/dist/host/opencli-runner.d.ts +0 -26
- package/dist/host/opencli-runner.js +0 -50
- package/dist/host/plugin-source.d.ts +0 -33
- package/dist/host/plugin-source.js +0 -91
- package/dist/host/runner.d.ts +0 -26
- package/dist/host/runner.js +0 -50
- package/dist/host/runtime-links.d.ts +0 -7
- package/dist/host/runtime-links.js +0 -25
- package/dist/lib/version.d.ts +0 -4
- package/dist/lib/version.js +0 -21
- package/dist/testing/git.d.ts +0 -3
- package/dist/testing/git.js +0 -36
package/README.md
CHANGED
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
不会打架。
|
|
6
6
|
|
|
7
7
|
对模块作者来说,这里是「不用自己重画」的那一层:卡片壳、行、表单、链接、加载态都在
|
|
8
|
-
`components/`,契约类型都在 `
|
|
8
|
+
`components/`,契约类型都在 `contract/`。**dsh 有的原语走 kit,工作台自己的视觉用这里的组件**,
|
|
9
9
|
两者拼出面板,不要从 DOM + CSS 重写一遍。
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 四个导出面
|
|
12
12
|
|
|
13
13
|
| 入口 | 谁用 | 有什么 |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `@dsh-workbench/shared` | 浏览器侧 | 契约类型、纯函数、哑组件(样式随组件)、i18n、读通道 `apiGet` / `factOfRoute` |
|
|
16
|
-
| `@dsh-workbench/shared/host` | 宿主侧(Node) | 数据根、
|
|
16
|
+
| `@dsh-workbench/shared/host` | 宿主侧(Node) | 数据根、git、http、面板状态、单元声明、请求配方 composer 等,**有 `node:*`** |
|
|
17
17
|
| `@dsh-workbench/shared/testing` | 测试 | 宿主侧假件、从 `package.json` 读模块声明 |
|
|
18
|
+
| `@dsh-workbench/shared/contract/*` | 插件 / 适配器(仅类型) | 契约子路径;`exports` 里的 `./contract/*`,只给类型用 |
|
|
18
19
|
|
|
19
20
|
两条硬边界:
|
|
20
21
|
|
|
@@ -40,7 +41,8 @@ CSS 处理。
|
|
|
40
41
|
|
|
41
42
|
| 落点 | 是什么 |
|
|
42
43
|
|---|---|
|
|
43
|
-
| `src/api/` |
|
|
44
|
+
| `src/api/` | 浏览器侧接缝:读 / 写通道、平台客户端 |
|
|
45
|
+
| `src/contract/` | 契约:单元清单、卡片面、读 / 写通道、长任务、审批、面板状态 |
|
|
44
46
|
| `src/components/` | 哑组件:`Card` / `CardHead` / `Rows` / `Form` / `FormRow(s)` / `FormActions` / `Link` / `Stack` / 各态块 |
|
|
45
47
|
| `src/config/` | 常量与存储键 |
|
|
46
48
|
| `src/i18n/` | zh / en 字典、合并与设置 |
|
package/dist/api/dsh.d.ts
CHANGED
|
@@ -144,6 +144,7 @@ export interface DshShellExecution {
|
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
146
|
* dsh 的 shell 服务:先 resolve 出执行规格,再把它原样喂给 execute。
|
|
147
|
+
* `env` 是**叠加**在宿主环境上的变量覆盖(不是替换),所以设一个变量不必自己拼 shell 语法。
|
|
147
148
|
*/
|
|
148
149
|
export interface DshShell {
|
|
149
150
|
resolve(request: {
|
|
@@ -153,6 +154,7 @@ export interface DshShell {
|
|
|
153
154
|
onExpiry?: 'kill' | 'none';
|
|
154
155
|
stdoutMaxBytes?: number;
|
|
155
156
|
signal?: AbortSignal;
|
|
157
|
+
env?: Readonly<Record<string, string>>;
|
|
156
158
|
}): unknown;
|
|
157
159
|
execute(spec: unknown): Promise<DshShellExecution>;
|
|
158
160
|
}
|
|
@@ -50,6 +50,8 @@ export declare const WARM_TIMEOUT_MS = 20000;
|
|
|
50
50
|
export declare const PREFLIGHT_FRESH_MS = 30000;
|
|
51
51
|
/** 装 / 卸模块包的超时(毫秒):pnpm 与取包内容都按它算。 */
|
|
52
52
|
export declare const INSTALL_TIMEOUT_MS = 300000;
|
|
53
|
+
/** 启动体检里查版本号那一条的超时(毫秒):只印个版本号,留够 opencli 每次调用约 5 秒的开销;`doctor` 要起浏览器会话,另按 `OPENCLI_TIMEOUT_MS`。 */
|
|
54
|
+
export declare const PREFLIGHT_VERSION_TIMEOUT_MS = 30000;
|
|
53
55
|
/** 查 registry 版本那一次请求的超时(毫秒)。 */
|
|
54
56
|
export declare const VERSION_CHECK_TIMEOUT_MS = 10000;
|
|
55
57
|
/** 版本查询结果的缓存有效期(毫秒):期内不发第二次请求。 */
|
package/dist/config/constants.js
CHANGED
|
@@ -50,6 +50,8 @@ export const WARM_TIMEOUT_MS = 20_000;
|
|
|
50
50
|
export const PREFLIGHT_FRESH_MS = 30_000;
|
|
51
51
|
/** 装 / 卸模块包的超时(毫秒):pnpm 与取包内容都按它算。 */
|
|
52
52
|
export const INSTALL_TIMEOUT_MS = 300_000;
|
|
53
|
+
/** 启动体检里查版本号那一条的超时(毫秒):只印个版本号,留够 opencli 每次调用约 5 秒的开销;`doctor` 要起浏览器会话,另按 `OPENCLI_TIMEOUT_MS`。 */
|
|
54
|
+
export const PREFLIGHT_VERSION_TIMEOUT_MS = 30_000;
|
|
53
55
|
/** 查 registry 版本那一次请求的超时(毫秒)。 */
|
|
54
56
|
export const VERSION_CHECK_TIMEOUT_MS = 10_000;
|
|
55
57
|
/** 版本查询结果的缓存有效期(毫秒):期内不发第二次请求。 */
|
|
@@ -6,6 +6,7 @@ export interface CommandRequest {
|
|
|
6
6
|
readonly argv: readonly string[];
|
|
7
7
|
readonly timeoutMs?: number | undefined;
|
|
8
8
|
readonly cwd?: string | undefined;
|
|
9
|
+
readonly env?: Readonly<Record<string, string>> | undefined;
|
|
9
10
|
readonly files?: readonly CommandFile[] | undefined;
|
|
10
11
|
readonly appendFileArgs?: string | undefined;
|
|
11
12
|
}
|
|
@@ -14,6 +15,7 @@ export interface CommandResult {
|
|
|
14
15
|
readonly stderr: string;
|
|
15
16
|
readonly exitCode: number;
|
|
16
17
|
readonly timedOut: boolean;
|
|
18
|
+
readonly truncated: boolean;
|
|
17
19
|
readonly filePaths: readonly string[];
|
|
18
20
|
readonly error?: string | undefined;
|
|
19
21
|
}
|
|
@@ -77,14 +77,15 @@ export function declarationProblemsOf(subject, raw) {
|
|
|
77
77
|
if (version === undefined) {
|
|
78
78
|
return [
|
|
79
79
|
`${subject}: dsh.workbench 没有 apiVersion —— 协议从 v${WORKBENCH_API_VERSION} 起,` +
|
|
80
|
-
`写 \`"apiVersion": ${WORKBENCH_API_VERSION}\`。缺它 =
|
|
80
|
+
`写 \`"apiVersion": ${WORKBENCH_API_VERSION}\`。缺它 = 外壳不认这个模块` +
|
|
81
|
+
'(`dsh-wb-module migrate` 能自动补这一项)',
|
|
81
82
|
];
|
|
82
83
|
}
|
|
83
84
|
if (!Number.isInteger(version) || !SUPPORTED_API_VERSIONS.includes(version)) {
|
|
84
85
|
const hint = version === 2
|
|
85
86
|
? ' —— v3 把卡片标题挪成了顶层 `title`(`{ "zh": "…", "en": "…" }` 字面量):' +
|
|
86
87
|
'把 `card.titleKey` 删掉、在顶层写 title,再把 apiVersion 改成 3'
|
|
87
|
-
: '';
|
|
88
|
+
: ' —— 先跑 `dsh-wb-module migrate` 把版本号改到 3,再按 check 的提示改源码';
|
|
88
89
|
return [
|
|
89
90
|
`${subject}: dsh.workbench.apiVersion 是 ${JSON.stringify(version)},这里只认 ` +
|
|
90
91
|
`${SUPPORTED_API_VERSIONS.join(' / ')}(要整数)${hint}`,
|
package/dist/host/data-root.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
export function dshHome() {
|
|
2
4
|
const configured = process.env.DSH_HOME?.trim();
|
|
3
5
|
if (configured !== undefined && configured !== '')
|
|
4
6
|
return configured;
|
|
5
|
-
return
|
|
7
|
+
return join(homedir(), '.dsh');
|
|
6
8
|
}
|
|
7
9
|
export function workbenchRoot() {
|
|
8
10
|
const configured = process.env.DSH_WORKBENCH_HOME?.trim();
|
|
9
11
|
if (configured !== undefined && configured !== '')
|
|
10
12
|
return configured;
|
|
11
|
-
return
|
|
13
|
+
return join(dshHome(), 'workbench');
|
|
12
14
|
}
|
|
13
15
|
export function workspacesRoot() {
|
|
14
|
-
return
|
|
16
|
+
return join(workbenchRoot(), 'workspaces');
|
|
15
17
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LongTaskStep } from '../../contract/long-task.js';
|
|
2
|
+
import type { FlowStepEvent } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 把流程的一步事件翻成长任务步骤流的默认一行。
|
|
5
|
+
* `running` → `start`、`ok` → `succeed`、`skipped` → `info`、`failed` → `fail`、
|
|
6
|
+
* `needs-login` → `info`(带 `loginUrl`);文字取 `message ?? title`。
|
|
7
|
+
* 要改文案或过滤步骤,就在模块里自己映射 —— 这只是默认值。
|
|
8
|
+
* @param event `flow.run({ onStep })` 收到的那条事件。
|
|
9
|
+
* @returns 能直接交给长任务 `run({ onStep })` 的一行。
|
|
10
|
+
*/
|
|
11
|
+
export declare function longTaskStepOf(event: FlowStepEvent): LongTaskStep;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把流程的一步事件翻成长任务步骤流的默认一行。
|
|
3
|
+
* `running` → `start`、`ok` → `succeed`、`skipped` → `info`、`failed` → `fail`、
|
|
4
|
+
* `needs-login` → `info`(带 `loginUrl`);文字取 `message ?? title`。
|
|
5
|
+
* 要改文案或过滤步骤,就在模块里自己映射 —— 这只是默认值。
|
|
6
|
+
* @param event `flow.run({ onStep })` 收到的那条事件。
|
|
7
|
+
* @returns 能直接交给长任务 `run({ onStep })` 的一行。
|
|
8
|
+
*/
|
|
9
|
+
export function longTaskStepOf(event) {
|
|
10
|
+
const text = event.message ?? event.title;
|
|
11
|
+
if (event.status === 'failed')
|
|
12
|
+
return { kind: 'fail', text };
|
|
13
|
+
if (event.status === 'needs-login') {
|
|
14
|
+
return { kind: 'info', text, ...(event.loginUrl === undefined ? {} : { loginUrl: event.loginUrl }) };
|
|
15
|
+
}
|
|
16
|
+
if (event.status === 'ok')
|
|
17
|
+
return { kind: 'succeed', text };
|
|
18
|
+
if (event.status === 'skipped')
|
|
19
|
+
return { kind: 'info', text };
|
|
20
|
+
return { kind: 'start', text };
|
|
21
|
+
}
|
package/dist/host/host-kit.d.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
import type { HostSurface } from '../contract/protocol/platform.js';
|
|
1
2
|
import type { EnvSupplyPort } from './port-types.js';
|
|
2
3
|
import { type FlowHost } from './flow/kit.js';
|
|
3
4
|
import { sessionIdOf, sessionWorkspace } from './tool-kit.js';
|
|
4
5
|
/**
|
|
5
|
-
* 宿主 kit 的公共部分:flow、工具侧 session
|
|
6
|
+
* 宿主 kit 的公共部分:flow、工具侧 session 读法、外部供给装法、宿主平台事实。
|
|
6
7
|
*/
|
|
7
8
|
export interface HostKit {
|
|
8
9
|
readonly flow: FlowHost;
|
|
9
10
|
readonly sessionIdOf: typeof sessionIdOf;
|
|
10
11
|
readonly sessionWorkspace: typeof sessionWorkspace;
|
|
11
12
|
readonly libInstallCommand: EnvSupplyPort['libInstallCommand'];
|
|
13
|
+
readonly surface: HostSurface;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* 建一份宿主 kit;每个挂载点一份,没有模块级状态。
|
|
15
|
-
* @
|
|
17
|
+
* @param parts.libInstallCommand 拼「装这个共用依赖」的那条命令。
|
|
18
|
+
* @param parts.surface 宿主平台事实;由组合根按 profile 与宿主进程算好再交进来。
|
|
19
|
+
* @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface。
|
|
16
20
|
*/
|
|
17
21
|
export declare function createHostKit(parts: {
|
|
18
22
|
readonly libInstallCommand: EnvSupplyPort['libInstallCommand'];
|
|
23
|
+
readonly surface: HostSurface;
|
|
19
24
|
}): HostKit;
|
package/dist/host/host-kit.js
CHANGED
|
@@ -2,7 +2,9 @@ import { createFlowHost } from './flow/kit.js';
|
|
|
2
2
|
import { sessionIdOf, sessionWorkspace } from './tool-kit.js';
|
|
3
3
|
/**
|
|
4
4
|
* 建一份宿主 kit;每个挂载点一份,没有模块级状态。
|
|
5
|
-
* @
|
|
5
|
+
* @param parts.libInstallCommand 拼「装这个共用依赖」的那条命令。
|
|
6
|
+
* @param parts.surface 宿主平台事实;由组合根按 profile 与宿主进程算好再交进来。
|
|
7
|
+
* @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand / surface。
|
|
6
8
|
*/
|
|
7
9
|
export function createHostKit(parts) {
|
|
8
10
|
return {
|
|
@@ -10,5 +12,6 @@ export function createHostKit(parts) {
|
|
|
10
12
|
sessionIdOf,
|
|
11
13
|
sessionWorkspace,
|
|
12
14
|
libInstallCommand: parts.libInstallCommand,
|
|
15
|
+
surface: parts.surface,
|
|
13
16
|
};
|
|
14
17
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, statSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
const TARGET_FLAGS = ['--prefix', '--cache', '--dir', '--store-dir'];
|
|
4
|
-
const INSTALL_VERBS = /^\s*npm\s+(i|install|add)\b/;
|
|
4
|
+
const INSTALL_VERBS = /^\s*(?:npm\s+(?:i|install|add)|pnpm\s+(?:add|install))\b/;
|
|
5
5
|
const NPM_PATH = /^npm (?:error|ERR!) path (.+)$/m;
|
|
6
6
|
export function commandTokens(command) {
|
|
7
7
|
const tokens = [];
|
|
@@ -10,6 +10,7 @@ export declare function libInstallCommand(options: {
|
|
|
10
10
|
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
11
11
|
readonly prefix: string;
|
|
12
12
|
readonly cacheDir?: string | undefined;
|
|
13
|
+
readonly pnpm?: string | undefined;
|
|
13
14
|
}): string;
|
|
14
15
|
export declare function binInstallCommand(need: BinNeed, os: HostOs, env: NodeJS.ProcessEnv, prefix: string, cacheDir?: string): string | undefined;
|
|
15
16
|
export declare function libUninstallCommand(options: {
|
package/dist/host/lib-install.js
CHANGED
|
@@ -2,11 +2,12 @@ import { npmRegistryOf } from './npm-registry.js';
|
|
|
2
2
|
import { quoteShellArg } from './shell-text.js';
|
|
3
3
|
export function libInstallCommand(options) {
|
|
4
4
|
const { need, source, version } = options;
|
|
5
|
+
const pnpm = options.pnpm ?? process.env.DSH_WB_PNPM?.trim() ?? 'pnpm';
|
|
5
6
|
const registry = npmRegistryOf(source, options.env ?? process.env);
|
|
6
7
|
const spec = version ?? need.version ?? 'latest';
|
|
7
8
|
const registryFlag = registry.ok ? ` --registry ${quoteShellArg(registry.value)}` : '';
|
|
8
|
-
const
|
|
9
|
-
return
|
|
9
|
+
const storeFlag = options.cacheDir === undefined ? '' : ` --store-dir ${quoteShellArg(options.cacheDir)}`;
|
|
10
|
+
return `${pnpm} add --dir ${quoteShellArg(options.prefix)} ${quoteShellArg(`${need.name}@${spec}`)}${registryFlag}${storeFlag} --allow-build=${quoteShellArg(need.name)}`;
|
|
10
11
|
}
|
|
11
12
|
export function binInstallCommand(need, os, env, prefix, cacheDir) {
|
|
12
13
|
const install = need.install;
|
|
@@ -35,5 +36,5 @@ export function binInstallCommand(need, os, env, prefix, cacheDir) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
export function libUninstallCommand(options) {
|
|
38
|
-
return `
|
|
39
|
+
return `pnpm remove --dir ${quoteShellArg(options.prefix)} ${quoteShellArg(options.need.name)}`;
|
|
39
40
|
}
|
package/dist/host/paths.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import { workbenchRoot } from './data-root.js';
|
|
2
3
|
export function runtimeRootOf(id) {
|
|
3
|
-
return
|
|
4
|
+
return join(workbenchRoot(), id);
|
|
4
5
|
}
|
|
5
6
|
export function runtimeLayoutOf(id) {
|
|
6
7
|
const root = runtimeRootOf(id);
|
|
7
|
-
const vendorPrefix =
|
|
8
|
-
const libsPrefix =
|
|
8
|
+
const vendorPrefix = join(root, 'plugins-npm');
|
|
9
|
+
const libsPrefix = join(root, 'libs');
|
|
9
10
|
return {
|
|
10
11
|
root,
|
|
11
|
-
home:
|
|
12
|
+
home: join(root, 'home'),
|
|
12
13
|
vendorPrefix,
|
|
13
|
-
vendorPluginsDir:
|
|
14
|
+
vendorPluginsDir: join(vendorPrefix, 'node_modules'),
|
|
14
15
|
libsPrefix,
|
|
15
|
-
libsBinDir:
|
|
16
|
-
npmCacheDir:
|
|
16
|
+
libsBinDir: join(libsPrefix, 'node_modules', '.bin'),
|
|
17
|
+
npmCacheDir: join(root, 'npm-cache'),
|
|
17
18
|
};
|
|
18
19
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把任意 payload 当对象看;不是对象就回空对象。
|
|
3
|
+
* @param value 任意入参。
|
|
4
|
+
* @returns 可按下标取值的安全对象。
|
|
5
|
+
*/
|
|
6
|
+
export declare function recordOf(value: unknown): Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* 把任意 payload 当一段文字看:字符串就 trim,别的都算空串。
|
|
9
|
+
* @param value 任意入参。
|
|
10
|
+
* @returns 去掉首尾空白的字符串;不是字符串时是空串。
|
|
11
|
+
*/
|
|
12
|
+
export declare function textOf(value: unknown): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把任意 payload 当对象看;不是对象就回空对象。
|
|
3
|
+
* @param value 任意入参。
|
|
4
|
+
* @returns 可按下标取值的安全对象。
|
|
5
|
+
*/
|
|
6
|
+
export function recordOf(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 把任意 payload 当一段文字看:字符串就 trim,别的都算空串。
|
|
11
|
+
* @param value 任意入参。
|
|
12
|
+
* @returns 去掉首尾空白的字符串;不是字符串时是空串。
|
|
13
|
+
*/
|
|
14
|
+
export function textOf(value) {
|
|
15
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
16
|
+
}
|
package/dist/host/preflight.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type PreflightStepId = 'toolchain' | 'cli' | 'bridge' | 'runtime' | 'diy' | 'warm';
|
|
2
2
|
export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
|
|
3
|
-
export type PreflightFailure = 'permission' | 'network' | 'missing' | 'unknown';
|
|
4
|
-
export type PreflightGuidance = 'install' | 'path' | 'shell';
|
|
3
|
+
export type PreflightFailure = 'permission' | 'network' | 'missing' | 'host' | 'unknown';
|
|
4
|
+
export type PreflightGuidance = 'install' | 'path' | 'shell' | 'host';
|
|
5
5
|
export type PreflightBridgeIssue = string;
|
|
6
6
|
export interface PreflightStep {
|
|
7
7
|
readonly id: PreflightStepId;
|
|
@@ -10,6 +10,7 @@ export interface PreflightStep {
|
|
|
10
10
|
readonly detail?: string;
|
|
11
11
|
readonly action?: string;
|
|
12
12
|
readonly command?: string;
|
|
13
|
+
readonly log?: readonly string[];
|
|
13
14
|
}
|
|
14
15
|
export interface PreflightDiyGap {
|
|
15
16
|
readonly rows: readonly string[];
|
|
@@ -26,4 +27,5 @@ export interface PreflightSnapshot {
|
|
|
26
27
|
readonly bridgeIssues?: readonly PreflightBridgeIssue[];
|
|
27
28
|
readonly diyGap?: PreflightDiyGap;
|
|
28
29
|
readonly canWakeBrowser?: boolean;
|
|
30
|
+
readonly log?: readonly string[];
|
|
29
31
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 一台机器上跑命令的那种 shell:POSIX 系(sh / bash / zsh),或 dsh 在 Windows 上用的 pwsh。
|
|
3
|
+
*/
|
|
4
|
+
export type ShellKind = 'posix' | 'pwsh';
|
|
5
|
+
/**
|
|
6
|
+
* 按目标平台判 shell 种类:win32 是 pwsh(dsh 的 Windows 执行器就是 PowerShell),其余是 POSIX。
|
|
7
|
+
* @param platform - 目标平台,默认当前 `process.platform`。
|
|
8
|
+
* @returns `'pwsh'` 或 `'posix'`。
|
|
9
|
+
*/
|
|
10
|
+
export declare function shellKindOf(platform?: NodeJS.Platform): ShellKind;
|
|
11
|
+
/**
|
|
12
|
+
* 按目标平台把一个值裹成 shell 字面量:POSIX 用单引号并按 `'\''` 逃逸,pwsh 也用单引号但按 `''` 逃逸。
|
|
3
13
|
* @param value - 任意值;不是字符串的先 `String()`。
|
|
4
14
|
* @param platform - 目标平台,默认当前 `process.platform`。
|
|
5
15
|
* @returns 可安全拼进该平台 shell 命令的一段文本;含空格 / 元字符的值不会被当成语法。
|
package/dist/host/shell-text.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 按目标平台判 shell 种类:win32 是 pwsh(dsh 的 Windows 执行器就是 PowerShell),其余是 POSIX。
|
|
3
|
+
* @param platform - 目标平台,默认当前 `process.platform`。
|
|
4
|
+
* @returns `'pwsh'` 或 `'posix'`。
|
|
5
|
+
*/
|
|
6
|
+
export function shellKindOf(platform = process.platform) {
|
|
7
|
+
return platform === 'win32' ? 'pwsh' : 'posix';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 按目标平台把一个值裹成 shell 字面量:POSIX 用单引号并按 `'\''` 逃逸,pwsh 也用单引号但按 `''` 逃逸。
|
|
3
11
|
* @param value - 任意值;不是字符串的先 `String()`。
|
|
4
12
|
* @param platform - 目标平台,默认当前 `process.platform`。
|
|
5
13
|
* @returns 可安全拼进该平台 shell 命令的一段文本;含空格 / 元字符的值不会被当成语法。
|
|
6
14
|
*/
|
|
7
15
|
export function quoteShellArgFor(value, platform = process.platform) {
|
|
8
16
|
const text = String(value);
|
|
9
|
-
if (platform === '
|
|
10
|
-
return `
|
|
17
|
+
if (shellKindOf(platform) === 'pwsh')
|
|
18
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
11
19
|
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
12
20
|
}
|
|
13
21
|
/**
|
package/dist/host/shell.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ export interface ShellResult {
|
|
|
6
6
|
readonly stdout: string;
|
|
7
7
|
readonly stderr: string;
|
|
8
8
|
readonly truncated: boolean;
|
|
9
|
+
readonly stalled: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function runShell(shell: DshShell, command: string, options?: {
|
|
11
12
|
workdir?: string;
|
|
12
13
|
timeoutMs?: number;
|
|
13
14
|
signal?: AbortSignal;
|
|
15
|
+
env?: Readonly<Record<string, string>>;
|
|
14
16
|
}): Promise<ShellResult>;
|
package/dist/host/shell.js
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
import { GIT_TIMEOUT_MS } from '../config/constants.js';
|
|
2
2
|
const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
const GUARD_SLACK_MS = 1_000;
|
|
4
|
+
function stalledResult() {
|
|
5
|
+
return {
|
|
6
|
+
exitCode: null,
|
|
7
|
+
timedOut: true,
|
|
8
|
+
aborted: false,
|
|
9
|
+
stdout: '',
|
|
10
|
+
stderr: '',
|
|
11
|
+
truncated: false,
|
|
12
|
+
stalled: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function settleWithin(work, limitMs) {
|
|
16
|
+
let timer;
|
|
17
|
+
const guard = new Promise((resolve) => {
|
|
18
|
+
timer = setTimeout(() => resolve(null), limitMs);
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
return await Promise.race([work, guard]);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (timer !== undefined)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
3
28
|
export async function runShell(shell, command, options = {}) {
|
|
4
|
-
const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
|
|
29
|
+
const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal, env } = options;
|
|
5
30
|
const spec = shell.resolve({
|
|
6
31
|
command,
|
|
7
32
|
...(workdir === undefined ? {} : { workdir }),
|
|
@@ -9,15 +34,20 @@ export async function runShell(shell, command, options = {}) {
|
|
|
9
34
|
onExpiry: 'kill',
|
|
10
35
|
stdoutMaxBytes: DEFAULT_MAX_BYTES,
|
|
11
36
|
...(signal === undefined ? {} : { signal }),
|
|
37
|
+
...(env === undefined ? {} : { env }),
|
|
12
38
|
});
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
const settled = await settleWithin((async () => {
|
|
40
|
+
const execution = await shell.execute(spec);
|
|
41
|
+
const result = await execution.result();
|
|
42
|
+
return {
|
|
43
|
+
exitCode: result.exitCode,
|
|
44
|
+
timedOut: result.timedOut === true,
|
|
45
|
+
aborted: result.aborted === true,
|
|
46
|
+
stdout: result.stdout.text,
|
|
47
|
+
stderr: result.stderr.text,
|
|
48
|
+
truncated: result.stdout.truncated || result.stderr.truncated,
|
|
49
|
+
stalled: false,
|
|
50
|
+
};
|
|
51
|
+
})(), timeoutMs + GUARD_SLACK_MS);
|
|
52
|
+
return settled ?? stalledResult();
|
|
23
53
|
}
|
|
@@ -63,3 +63,23 @@ export interface ApprovalContext {
|
|
|
63
63
|
readonly detailOf?: ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>) | undefined;
|
|
64
64
|
}
|
|
65
65
|
export declare function approvalFor(call: ApprovalCall, context?: ApprovalContext): Promise<AskDecision | undefined>;
|
|
66
|
+
/**
|
|
67
|
+
* 「写工具要点头」那一对的默认口径:点名的工具一律 `ask` + 同一份明细,其余一律 `read`。
|
|
68
|
+
* 把它直接摊在 `mount()` 的返回值里:`...writeApprovalOf({ … })`。
|
|
69
|
+
* @param options.tools 需要点头的工具名。
|
|
70
|
+
* @param options.reason 为什么问(给 agent 看的理由)。
|
|
71
|
+
* @param options.question 问句(zh / en,≤40 字)。
|
|
72
|
+
* @param options.fallback 没有明细时的整句(zh / en,≤40 字)。
|
|
73
|
+
* @param options.detail 审批屏头行那句明细(zh / en,≤20 字)。
|
|
74
|
+
* @returns `{ needsApproval, approvalDetail }`。
|
|
75
|
+
*/
|
|
76
|
+
export declare function writeApprovalOf(options: {
|
|
77
|
+
readonly tools: readonly string[];
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
readonly question: ApprovalDetail;
|
|
80
|
+
readonly fallback: ApprovalDetail;
|
|
81
|
+
readonly detail: ApprovalDetail;
|
|
82
|
+
}): {
|
|
83
|
+
needsApproval(call: ApprovalCall): ApprovalVerdict;
|
|
84
|
+
approvalDetail(call: ApprovalCall): ApprovalFacts | undefined;
|
|
85
|
+
};
|
|
@@ -93,3 +93,23 @@ export async function approvalFor(call, context = {}) {
|
|
|
93
93
|
displayReason: compose(facts, verdict, note),
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* 「写工具要点头」那一对的默认口径:点名的工具一律 `ask` + 同一份明细,其余一律 `read`。
|
|
98
|
+
* 把它直接摊在 `mount()` 的返回值里:`...writeApprovalOf({ … })`。
|
|
99
|
+
* @param options.tools 需要点头的工具名。
|
|
100
|
+
* @param options.reason 为什么问(给 agent 看的理由)。
|
|
101
|
+
* @param options.question 问句(zh / en,≤40 字)。
|
|
102
|
+
* @param options.fallback 没有明细时的整句(zh / en,≤40 字)。
|
|
103
|
+
* @param options.detail 审批屏头行那句明细(zh / en,≤20 字)。
|
|
104
|
+
* @returns `{ needsApproval, approvalDetail }`。
|
|
105
|
+
*/
|
|
106
|
+
export function writeApprovalOf(options) {
|
|
107
|
+
const tools = new Set(options.tools);
|
|
108
|
+
const named = (call) => call.name !== undefined && tools.has(call.name);
|
|
109
|
+
return {
|
|
110
|
+
needsApproval: (call) => named(call)
|
|
111
|
+
? { kind: 'ask', reason: options.reason, question: options.question, fallback: options.fallback }
|
|
112
|
+
: { kind: 'read' },
|
|
113
|
+
approvalDetail: (call) => (named(call) ? options.detail : undefined),
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/host/unit-face.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export interface UnitCall {
|
|
|
49
49
|
readonly args: Readonly<Record<string, unknown>>;
|
|
50
50
|
readonly result: unknown;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 挂载结果:路由表 + 工具注册。
|
|
54
|
+
* `registerTools()` 是**调用时才注册**,返回一组清理函数(卸载 / 热换时外壳逐个调);
|
|
55
|
+
* 工具名必须与清单 `dsh.workbench.tools` 一字不差,没有工具就 `() => []`。
|
|
56
|
+
*/
|
|
52
57
|
export interface UnitMountResult {
|
|
53
58
|
readonly routes: RouteTable;
|
|
54
59
|
registerTools(): Array<() => void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { UnitModuleFace } from './unit-face.js';
|
|
2
|
+
/**
|
|
3
|
+
* 宿主面注册只需要的那点上下文:`get` / `effect` / 可选的 `logger`。
|
|
4
|
+
*/
|
|
5
|
+
export interface UnitHostContextLike {
|
|
6
|
+
get<T = unknown>(name: string): T | undefined;
|
|
7
|
+
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
8
|
+
readonly logger?: {
|
|
9
|
+
warn?(message: string): void;
|
|
10
|
+
} | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 一个宿主插件导出的三件:`name` / `inject` / `apply`。
|
|
14
|
+
*/
|
|
15
|
+
export interface UnitHostPlugin {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly inject: readonly string[];
|
|
18
|
+
apply(ctx: UnitHostContextLike): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 把「宿主面注册进外壳」这段在每个模块里都一样的接线收成一份。
|
|
22
|
+
* 用法:先 `const plugin = defineUnitHostPlugin({ … })`,再逐个导出 `name` / `inject` / `apply`
|
|
23
|
+
* (逐名 `export const` 才过 arch 的导出对账;解构导出它认不出)。
|
|
24
|
+
* @param options.name 插件名(包名)。
|
|
25
|
+
* @param options.unit 单元 id。
|
|
26
|
+
* @param options.face 这个单元的宿主面。
|
|
27
|
+
* @returns `{ name, inject, apply }`;`apply` 里取 `workbenchUnits`、缺了就 warn 一声。
|
|
28
|
+
*/
|
|
29
|
+
export declare function defineUnitHostPlugin(options: {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly unit: string;
|
|
32
|
+
readonly face: UnitModuleFace;
|
|
33
|
+
}): UnitHostPlugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { UNITS_SERVICE } from '../config/constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* 把「宿主面注册进外壳」这段在每个模块里都一样的接线收成一份。
|
|
4
|
+
* 用法:先 `const plugin = defineUnitHostPlugin({ … })`,再逐个导出 `name` / `inject` / `apply`
|
|
5
|
+
* (逐名 `export const` 才过 arch 的导出对账;解构导出它认不出)。
|
|
6
|
+
* @param options.name 插件名(包名)。
|
|
7
|
+
* @param options.unit 单元 id。
|
|
8
|
+
* @param options.face 这个单元的宿主面。
|
|
9
|
+
* @returns `{ name, inject, apply }`;`apply` 里取 `workbenchUnits`、缺了就 warn 一声。
|
|
10
|
+
*/
|
|
11
|
+
export function defineUnitHostPlugin(options) {
|
|
12
|
+
const { name, unit, face } = options;
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
inject: [UNITS_SERVICE],
|
|
16
|
+
apply(ctx) {
|
|
17
|
+
const units = ctx.get(UNITS_SERVICE);
|
|
18
|
+
if (units === undefined) {
|
|
19
|
+
ctx.logger?.warn?.(`[${unit}] 外壳没有提供 workbenchUnits 服务:这个模块装不进来`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ctx.effect(() => units.register(face), `${name}: 注册 ${unit}`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -13,15 +13,21 @@ export function workspaceKey(workspace) {
|
|
|
13
13
|
const abs = path.resolve(workspace);
|
|
14
14
|
return `${slug(path.basename(abs))}-${createHash('sha1').update(abs).digest('hex').slice(0, 8)}`;
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function safeName(name) {
|
|
17
17
|
const trimmed = name.trim();
|
|
18
18
|
if (trimmed === '' || trimmed === '.' || trimmed === '..') {
|
|
19
19
|
throw new Error(`模块文件夹里只接受相对文件名,收到的是「${name}」`);
|
|
20
20
|
}
|
|
21
|
-
if (trimmed.includes('\0') || path.isAbsolute(trimmed)) {
|
|
21
|
+
if (trimmed.includes('\0') || path.isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
22
22
|
throw new Error(`模块只能写自己的文件夹:不接受绝对路径「${name}」`);
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
if (trimmed.replace(/\\/g, '/').split('/').includes('..')) {
|
|
25
|
+
throw new Error(`模块只能写自己的文件夹:路径越界「${name}」`);
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
function inside(dir, name) {
|
|
30
|
+
const full = path.resolve(dir, safeName(name));
|
|
25
31
|
if (full !== dir && !full.startsWith(dir + path.sep)) {
|
|
26
32
|
throw new Error(`模块只能写自己的文件夹:路径越界「${name}」`);
|
|
27
33
|
}
|
|
@@ -32,15 +38,15 @@ function memoryStore(dir) {
|
|
|
32
38
|
return {
|
|
33
39
|
dir,
|
|
34
40
|
read: (name) => {
|
|
35
|
-
|
|
41
|
+
safeName(name);
|
|
36
42
|
return files.get(name) ?? null;
|
|
37
43
|
},
|
|
38
44
|
write: (name, value) => {
|
|
39
|
-
|
|
45
|
+
safeName(name);
|
|
40
46
|
files.set(name, value);
|
|
41
47
|
},
|
|
42
48
|
remove: (name) => {
|
|
43
|
-
|
|
49
|
+
safeName(name);
|
|
44
50
|
files.delete(name);
|
|
45
51
|
},
|
|
46
52
|
list: () => [...files.keys()].sort(),
|
package/dist/host.d.ts
CHANGED
|
@@ -47,13 +47,16 @@ export * from './host/flow/expr.js';
|
|
|
47
47
|
export * from './host/flow/check.js';
|
|
48
48
|
export * from './host/flow/run.js';
|
|
49
49
|
export * from './host/flow/kit.js';
|
|
50
|
+
export * from './host/flow/step.js';
|
|
50
51
|
export * from './host/host-kit.js';
|
|
51
52
|
export * from './host/panel-state.js';
|
|
53
|
+
export * from './host/payload.js';
|
|
52
54
|
export * from './host/plugin-ledger.js';
|
|
53
55
|
export * from './host/time.js';
|
|
54
56
|
export * from './host/tokens.js';
|
|
55
57
|
export * from './host/tool-approval.js';
|
|
56
58
|
export * from './host/tool-kit.js';
|
|
59
|
+
export * from './host/unit-plugin.js';
|
|
57
60
|
export * from './host/unit-face.js';
|
|
58
61
|
export * from './host/lib-runners.js';
|
|
59
62
|
export * from './host/value-handoff.js';
|