@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UNITS_SERVICE } from '../config/constants.js';
|
|
2
|
+
import type { UnitModuleFace } from './unit-face.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 宿主面注册只需要的那点上下文:`get` / `effect` / 可选的 `logger`。
|
|
6
|
+
*/
|
|
7
|
+
export interface UnitHostContextLike {
|
|
8
|
+
get<T = unknown>(name: string): T | undefined;
|
|
9
|
+
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
10
|
+
readonly logger?: { warn?(message: string): void } | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 一个宿主插件导出的三件:`name` / `inject` / `apply`。
|
|
15
|
+
*/
|
|
16
|
+
export interface UnitHostPlugin {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly inject: readonly string[];
|
|
19
|
+
apply(ctx: UnitHostContextLike): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 把「宿主面注册进外壳」这段在每个模块里都一样的接线收成一份。
|
|
24
|
+
* 用法:先 `const plugin = defineUnitHostPlugin({ … })`,再逐个导出 `name` / `inject` / `apply`
|
|
25
|
+
* (逐名 `export const` 才过 arch 的导出对账;解构导出它认不出)。
|
|
26
|
+
* @param options.name 插件名(包名)。
|
|
27
|
+
* @param options.unit 单元 id。
|
|
28
|
+
* @param options.face 这个单元的宿主面。
|
|
29
|
+
* @returns `{ name, inject, apply }`;`apply` 里取 `workbenchUnits`、缺了就 warn 一声。
|
|
30
|
+
*/
|
|
31
|
+
export function defineUnitHostPlugin(options: {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly unit: string;
|
|
34
|
+
readonly face: UnitModuleFace;
|
|
35
|
+
}): UnitHostPlugin {
|
|
36
|
+
const { name, unit, face } = options;
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
inject: [UNITS_SERVICE],
|
|
40
|
+
apply(ctx: UnitHostContextLike): void {
|
|
41
|
+
const units = ctx.get<{ register(face: UnitModuleFace): () => void }>(UNITS_SERVICE);
|
|
42
|
+
if (units === undefined) {
|
|
43
|
+
ctx.logger?.warn?.(`[${unit}] 外壳没有提供 workbenchUnits 服务:这个模块装不进来`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
ctx.effect(() => units.register(face), `${name}: 注册 ${unit}`);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -30,15 +30,22 @@ export function workspaceKey(workspace: string): string {
|
|
|
30
30
|
return `${slug(path.basename(abs))}-${createHash('sha1').update(abs).digest('hex').slice(0, 8)}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function safeName(name: string): string {
|
|
34
34
|
const trimmed = name.trim();
|
|
35
35
|
if (trimmed === '' || trimmed === '.' || trimmed === '..') {
|
|
36
36
|
throw new Error(`模块文件夹里只接受相对文件名,收到的是「${name}」`);
|
|
37
37
|
}
|
|
38
|
-
if (trimmed.includes('\0') || path.isAbsolute(trimmed)) {
|
|
38
|
+
if (trimmed.includes('\0') || path.isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
39
39
|
throw new Error(`模块只能写自己的文件夹:不接受绝对路径「${name}」`);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
if (trimmed.replace(/\\/g, '/').split('/').includes('..')) {
|
|
42
|
+
throw new Error(`模块只能写自己的文件夹:路径越界「${name}」`);
|
|
43
|
+
}
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function inside(dir: string, name: string): string {
|
|
48
|
+
const full = path.resolve(dir, safeName(name));
|
|
42
49
|
if (full !== dir && !full.startsWith(dir + path.sep)) {
|
|
43
50
|
throw new Error(`模块只能写自己的文件夹:路径越界「${name}」`);
|
|
44
51
|
}
|
|
@@ -50,15 +57,15 @@ function memoryStore(dir: string): ModuleStore {
|
|
|
50
57
|
return {
|
|
51
58
|
dir,
|
|
52
59
|
read: (name) => {
|
|
53
|
-
|
|
60
|
+
safeName(name);
|
|
54
61
|
return files.get(name) ?? null;
|
|
55
62
|
},
|
|
56
63
|
write: (name, value) => {
|
|
57
|
-
|
|
64
|
+
safeName(name);
|
|
58
65
|
files.set(name, value);
|
|
59
66
|
},
|
|
60
67
|
remove: (name) => {
|
|
61
|
-
|
|
68
|
+
safeName(name);
|
|
62
69
|
files.delete(name);
|
|
63
70
|
},
|
|
64
71
|
list: () => [...files.keys()].sort(),
|
package/src/host.ts
CHANGED
|
@@ -57,13 +57,16 @@ export * from './host/flow/expr.js';
|
|
|
57
57
|
export * from './host/flow/check.js';
|
|
58
58
|
export * from './host/flow/run.js';
|
|
59
59
|
export * from './host/flow/kit.js';
|
|
60
|
+
export * from './host/flow/step.js';
|
|
60
61
|
export * from './host/host-kit.js';
|
|
61
62
|
export * from './host/panel-state.js';
|
|
63
|
+
export * from './host/payload.js';
|
|
62
64
|
export * from './host/plugin-ledger.js';
|
|
63
65
|
export * from './host/time.js';
|
|
64
66
|
export * from './host/tokens.js';
|
|
65
67
|
export * from './host/tool-approval.js';
|
|
66
68
|
export * from './host/tool-kit.js';
|
|
69
|
+
export * from './host/unit-plugin.js';
|
|
67
70
|
export * from './host/unit-face.js';
|
|
68
71
|
export * from './host/lib-runners.js';
|
|
69
72
|
export * from './host/value-handoff.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -600,6 +600,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
600
600
|
'env.stepFailed.install': 'install failed',
|
|
601
601
|
'env.stepFailed.path': 'installed but not found',
|
|
602
602
|
'env.stepFailed.shell': 'cannot reach the command line',
|
|
603
|
+
'env.stepFailed.host': 'the command never returned',
|
|
603
604
|
'env.toolchain.ok': 'Looks good',
|
|
604
605
|
'env.toolchain.warn': 'Almost ready',
|
|
605
606
|
'env.runtime.ok': 'Ready',
|
|
@@ -612,12 +613,15 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
612
613
|
'env.fix.why': 'The workbench uses its own background tool (installed separately from the one in your terminal), and it is not set up on this machine yet.',
|
|
613
614
|
'env.fix.whyPath': 'The background tool is installed, but this environment cannot find it \u2014 usually a PATH or shell-session issue.',
|
|
614
615
|
'env.fix.whyShell': 'The workbench cannot reach the command line on this machine, so it cannot fetch data \u2014 you cannot install this from here; someone needs to fix the background environment.',
|
|
616
|
+
'env.fix.whyHost': 'The workbench runs a few checks on the command line and collects what they return; this time the command went out and nothing came back. The tools themselves are most likely fine \u2014 reinstalling Node / npm / pnpm will not help.',
|
|
615
617
|
'env.fix.step': 'Run this in a terminal, then hit Retry:',
|
|
616
618
|
'env.fix.stepPath': 'Open a new terminal window (or make sure npm\u2019s global bin directory is on PATH), then hit Retry.',
|
|
617
619
|
'env.fix.stepShell': 'Restart the workbench (or dsh); if it keeps happening, the workbench still cannot reach the command line.',
|
|
620
|
+
'env.fix.stepHost': 'Quit the workbench completely (including the tray icon), clear the dsh-* leftovers in the system temp folder, then open it again. If it keeps happening, update dsh.',
|
|
618
621
|
'env.fix.permission': 'Permission denied: rerun with sudo, or point npm\u2019s global directory at your home.',
|
|
619
622
|
'env.fix.network': 'Network unreachable: check your network or proxy, then retry.',
|
|
620
623
|
'env.fix.details': 'Details (for troubleshooting)',
|
|
624
|
+
'env.log': 'Detailed output',
|
|
621
625
|
'env.retry': 'Retry',
|
|
622
626
|
'env.wake': 'Open browser',
|
|
623
627
|
'env.wake.opening': 'Opening the browser…',
|
package/src/i18n/zh.ts
CHANGED
|
@@ -596,6 +596,7 @@ export const zh = {
|
|
|
596
596
|
'env.stepFailed.install': '装不上',
|
|
597
597
|
'env.stepFailed.path': '装了但还找不到',
|
|
598
598
|
'env.stepFailed.shell': '连不上命令行',
|
|
599
|
+
'env.stepFailed.host': '命令行没有返回',
|
|
599
600
|
'env.toolchain.ok': '正常',
|
|
600
601
|
'env.toolchain.warn': '还没完全准备好',
|
|
601
602
|
'env.runtime.ok': '已就绪',
|
|
@@ -608,12 +609,15 @@ export const zh = {
|
|
|
608
609
|
'env.fix.why': '工作台用它自己那份后台工具(与你在终端里那套分开装,互不影响),这台机器上还没装好它。',
|
|
609
610
|
'env.fix.whyPath': '后台工具装上了,但当前环境还找不到它 —— 通常是 PATH 或终端会话的问题。',
|
|
610
611
|
'env.fix.whyShell': '工作台拿不到这台机器的命令行,所以取不了数据 —— 这一步你装不了,需要有人处理后台环境。',
|
|
612
|
+
'env.fix.whyHost': '工作台要靠命令行跑几条检查,再把结果收回来;这次命令发出去了,结果一直没回来。工具本身多半是好的 —— 重装 Node / npm / pnpm 不会有帮助。',
|
|
611
613
|
'env.fix.step': '在终端里运行下面这条命令,装好之后点「重试」:',
|
|
612
614
|
'env.fix.stepPath': '新开一个终端窗口(或确认 npm 的全局 bin 目录在 PATH 里),然后点「重试」。',
|
|
613
615
|
'env.fix.stepShell': '重启一次工作台(或 dsh);如果反复出现,说明工作台还是拿不到命令行。',
|
|
616
|
+
'env.fix.stepHost': '完全退出工作台(托盘图标也要退),清掉系统临时目录里 dsh- 开头的残留,再重新打开;反复出现就升级 dsh。',
|
|
614
617
|
'env.fix.permission': '权限不够:在命令前加 sudo 重跑,或先把 npm 的全局目录配到你的用户目录。',
|
|
615
618
|
'env.fix.network': '网络不通:检查网络或代理设置后重试。',
|
|
616
619
|
'env.fix.details': '详细输出(排障用)',
|
|
620
|
+
'env.log': '详细输出',
|
|
617
621
|
'env.retry': '重试',
|
|
618
622
|
'env.wake': '打开浏览器',
|
|
619
623
|
'env.wake.opening': '正在打开浏览器…',
|
|
@@ -51,4 +51,12 @@ describe('干不了活是为什么:先看人在不在工坊,再看板画没
|
|
|
51
51
|
expect(insideWorkshop('/a/b', '/a/bc')).toBe(false);
|
|
52
52
|
expect(insideWorkshop('/a/b/', '/a/b/c')).toBe(true);
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
it('★ Windows 路径:反斜杠、混合分隔符与盘符大小写都认', () => {
|
|
56
|
+
expect(insideWorkshop('C:\\w\\ws', 'C:\\w\\ws\\packages\\module-demo')).toBe(true);
|
|
57
|
+
expect(insideWorkshop('C:\\w\\ws', 'C:/w/ws/packages/module-demo')).toBe(true);
|
|
58
|
+
expect(insideWorkshop('c:\\w\\ws', 'C:\\W\\WS\\packages')).toBe(true);
|
|
59
|
+
expect(insideWorkshop('C:\\w\\ws', 'C:\\w\\ws2')).toBe(false);
|
|
60
|
+
expect(insideWorkshop('C:\\w\\ws', 'D:\\w\\ws\\packages')).toBe(false);
|
|
61
|
+
});
|
|
54
62
|
});
|
|
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import {
|
|
4
4
|
cx,
|
|
5
5
|
parseLocalTimestamp,
|
|
6
|
+
pathBase,
|
|
6
7
|
relativeTime,
|
|
7
8
|
relativeTimeAgo,
|
|
8
9
|
relativeTimeBucket,
|
|
@@ -112,8 +113,14 @@ describe('shortenPath', () => {
|
|
|
112
113
|
expect(shortenPath('/Users/you/dev/app')).toBe('~/dev/app');
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
it('Linux 与 Windows 的用户主目录也缩', () => {
|
|
117
|
+
expect(shortenPath('/home/you/dev/app')).toBe('~/dev/app');
|
|
118
|
+
expect(shortenPath('C:\\Users\\you\\dev\\app')).toBe('~\\dev\\app');
|
|
119
|
+
});
|
|
120
|
+
|
|
115
121
|
it('非用户目录原样返回', () => {
|
|
116
122
|
expect(shortenPath('/Volumes/dev/app')).toBe('/Volumes/dev/app');
|
|
123
|
+
expect(shortenPath('D:\\work\\app')).toBe('D:\\work\\app');
|
|
117
124
|
});
|
|
118
125
|
|
|
119
126
|
it('非字符串给空串', () => {
|
|
@@ -122,6 +129,25 @@ describe('shortenPath', () => {
|
|
|
122
129
|
});
|
|
123
130
|
});
|
|
124
131
|
|
|
132
|
+
describe('pathBase', () => {
|
|
133
|
+
it('取最后一段,POSIX 与 Windows 都认', () => {
|
|
134
|
+
expect(pathBase('/Users/you/dev/app')).toBe('app');
|
|
135
|
+
expect(pathBase('C:\\Users\\you\\dev\\app')).toBe('app');
|
|
136
|
+
expect(pathBase('C:/Users/you/dev/app')).toBe('app');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('尾部有分隔符先去掉', () => {
|
|
140
|
+
expect(pathBase('/Users/you/dev/app/')).toBe('app');
|
|
141
|
+
expect(pathBase('C:\\Users\\you\\dev\\app\\')).toBe('app');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('没有分隔符就整串,非字符串给空串', () => {
|
|
145
|
+
expect(pathBase('app')).toBe('app');
|
|
146
|
+
expect(pathBase(undefined)).toBe('');
|
|
147
|
+
expect(pathBase(42)).toBe('');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
125
151
|
describe('parseLocalTimestamp', () => {
|
|
126
152
|
it('按本地时区解析 Coding 的时间戳形态', () => {
|
|
127
153
|
const parsed = parseLocalTimestamp('2026-09-27 08:18:36');
|
package/src/lib/board-fact.ts
CHANGED
|
@@ -29,16 +29,23 @@ export interface BoardProbe {
|
|
|
29
29
|
readonly drawn: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function normalizePath(value: string): string {
|
|
33
|
+
const forward = value.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
34
|
+
return /^[A-Za-z]:\//.test(forward) || forward.startsWith('//') ? forward.toLowerCase() : forward;
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
/**
|
|
33
38
|
* 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
|
|
34
39
|
* @param root - 工作区根路径。
|
|
35
40
|
* @param target - 要判的路径。
|
|
36
|
-
* @returns 同一个路径或在其下给 `true
|
|
41
|
+
* @returns 同一个路径或在其下给 `true`;分隔符先归一(Windows 的 `\` 与 `/` 都认),
|
|
42
|
+
* 盘符路径按大小写不敏感比;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
37
43
|
*/
|
|
38
44
|
export function insideWorkshop(root: string, target: string): boolean {
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
45
|
+
const base = normalizePath(root);
|
|
46
|
+
const real = normalizePath(target);
|
|
47
|
+
if (real === base) return true;
|
|
48
|
+
return real.startsWith(base === '' ? '/' : `${base}/`);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
package/src/lib/ui.ts
CHANGED
|
@@ -95,15 +95,33 @@ export function relativeTime(epochMs: number, t: TimeTranslate): string {
|
|
|
95
95
|
return dayjs(epochMs).format('YYYY/M/D HH:mm:ss');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const HOME_PREFIXES = [/^\/Users\/[^/]+/, /^\/home\/[^/]+/, /^[A-Za-z]:\\Users\\[^\\]+/];
|
|
99
|
+
|
|
98
100
|
/**
|
|
99
|
-
*
|
|
101
|
+
* 把用户主目录(macOS `/Users/<用户名>`、Linux `/home/<用户名>`、Windows `C:\Users\<用户名>`)缩成 `~`,
|
|
102
|
+
* 免得会话行里全是一样的长前缀。
|
|
100
103
|
* @param path - 任意值。
|
|
101
104
|
* @returns 缩好的路径;不在用户主目录下原样返回,不是字符串给空串。
|
|
102
105
|
*/
|
|
103
106
|
export function shortenPath(path: unknown): string {
|
|
104
107
|
if (typeof path !== 'string') return '';
|
|
105
|
-
const
|
|
106
|
-
|
|
108
|
+
for (const pattern of HOME_PREFIXES) {
|
|
109
|
+
const hit = path.match(pattern);
|
|
110
|
+
if (hit !== null) return path.replace(hit[0], '~');
|
|
111
|
+
}
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 取路径最后一段,`/` 与 `\` 都认(Windows 路径、混合分隔符都行)。
|
|
117
|
+
* @param path - 任意值。
|
|
118
|
+
* @returns 末段;尾部有分隔符先去掉,没有分隔符就整串,不是字符串给空串。
|
|
119
|
+
*/
|
|
120
|
+
export function pathBase(path: unknown): string {
|
|
121
|
+
if (typeof path !== 'string') return '';
|
|
122
|
+
const trimmed = path.replace(/[\\/]+$/, '');
|
|
123
|
+
const at = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
124
|
+
return at === -1 ? trimmed : trimmed.slice(at + 1);
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
const LOCAL_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/;
|
|
@@ -7,6 +7,7 @@ export interface ShellCall {
|
|
|
7
7
|
timeoutMs?: number;
|
|
8
8
|
onExpiry?: string;
|
|
9
9
|
stdoutMaxBytes?: number;
|
|
10
|
+
env?: Readonly<Record<string, string>>;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ShellReply {
|
|
@@ -15,6 +16,7 @@ export interface ShellReply {
|
|
|
15
16
|
exitCode?: number | null;
|
|
16
17
|
timedOut?: boolean;
|
|
17
18
|
aborted?: boolean;
|
|
19
|
+
truncated?: boolean;
|
|
18
20
|
failWith?: Error;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -49,7 +51,7 @@ export function createFakeShell(replies: ShellReply[] | ((call: ShellCall, index
|
|
|
49
51
|
exitCode: reply.exitCode ?? 0,
|
|
50
52
|
timedOut: reply.timedOut ?? false,
|
|
51
53
|
aborted: reply.aborted ?? false,
|
|
52
|
-
stdout: { text: reply.stdout ?? '', truncated:
|
|
54
|
+
stdout: { text: reply.stdout ?? '', truncated: reply.truncated === true },
|
|
53
55
|
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
54
56
|
};
|
|
55
57
|
},
|
|
@@ -1,9 +0,0 @@
|
|
|
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;
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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;
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function commandNamesMatch(declared: string, resolved: string): boolean;
|
|
@@ -1,14 +0,0 @@
|
|
|
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,26 +0,0 @@
|
|
|
1
|
-
import type { DataRequest, DataResult } from '../contract/data.js';
|
|
2
|
-
/**
|
|
3
|
-
* 一条 opencli 命令的执行器:同一形状的 `execFile('opencli', args)`,结果只留 stdout。
|
|
4
|
-
*/
|
|
5
|
-
export type LibRunner = (args: readonly string[]) => Promise<{
|
|
6
|
-
readonly stdout: string;
|
|
7
|
-
}>;
|
|
8
|
-
export type OpencliRunner = LibRunner;
|
|
9
|
-
/**
|
|
10
|
-
* 读缝(`DataReader.read`)的形状;模块直接把自己的 `data.read` 传进来即可。
|
|
11
|
-
*/
|
|
12
|
-
export type OpencliRead = (request: DataRequest) => Promise<DataResult<unknown>>;
|
|
13
|
-
export interface OpencliRunnerOptions {
|
|
14
|
-
readonly timeoutMs?: number;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
|
|
18
|
-
* 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
|
|
19
|
-
*
|
|
20
|
-
* 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
|
|
21
|
-
* 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
|
|
22
|
-
* @param read 模块手里的读缝(`data.read`)。
|
|
23
|
-
* @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
|
|
24
|
-
* @returns 执行器;直接交给编排库的 `setOpencliRunner`。
|
|
25
|
-
*/
|
|
26
|
-
export declare function opencliRunnerOf(read: OpencliRead, options?: OpencliRunnerOptions): OpencliRunner;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const AUTH_REQUIRED_EXIT_CODE = 77;
|
|
2
|
-
function textOf(value) {
|
|
3
|
-
return typeof value === 'string' ? value : '';
|
|
4
|
-
}
|
|
5
|
-
function failedOf(args, error) {
|
|
6
|
-
const head = `Command failed: opencli ${args.join(' ')}`;
|
|
7
|
-
const auth = error?.pageCode === 'auth-required';
|
|
8
|
-
const helpUrl = textOf(error?.helpUrl);
|
|
9
|
-
const detail = textOf(error?.detail);
|
|
10
|
-
const hint = textOf(error?.hint);
|
|
11
|
-
const failed = new Error([
|
|
12
|
-
head,
|
|
13
|
-
...(auth
|
|
14
|
-
? ['code: AUTH_REQUIRED', `message: ${textOf(error?.message)}`]
|
|
15
|
-
: [textOf(error?.message) || 'opencli 这条命令没跑通']),
|
|
16
|
-
...(detail === '' ? [] : [detail]),
|
|
17
|
-
...(hint === '' ? [] : [hint]),
|
|
18
|
-
...(auth && helpUrl !== ''
|
|
19
|
-
? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`]
|
|
20
|
-
: []),
|
|
21
|
-
].join('\n'));
|
|
22
|
-
if (auth)
|
|
23
|
-
failed.code = AUTH_REQUIRED_EXIT_CODE;
|
|
24
|
-
return failed;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
|
|
28
|
-
* 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
|
|
29
|
-
*
|
|
30
|
-
* 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
|
|
31
|
-
* 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
|
|
32
|
-
* @param read 模块手里的读缝(`data.read`)。
|
|
33
|
-
* @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
|
|
34
|
-
* @returns 执行器;直接交给编排库的 `setOpencliRunner`。
|
|
35
|
-
*/
|
|
36
|
-
export function opencliRunnerOf(read, options = {}) {
|
|
37
|
-
return async (args) => {
|
|
38
|
-
const result = await read({
|
|
39
|
-
kind: 'command',
|
|
40
|
-
adapter: 'opencli',
|
|
41
|
-
argv: [...args],
|
|
42
|
-
...(options.timeoutMs === undefined
|
|
43
|
-
? {}
|
|
44
|
-
: { timeoutMs: options.timeoutMs }),
|
|
45
|
-
});
|
|
46
|
-
if (result.ok)
|
|
47
|
-
return { stdout: JSON.stringify(result.data) };
|
|
48
|
-
throw failedOf(args, result.error);
|
|
49
|
-
};
|
|
50
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { SupplySource } from '../contract/needs.js';
|
|
2
|
-
import type { ProvisionProblem } from '../contract/plugin-source.js';
|
|
3
|
-
export type { ProvisionProblem } from '../contract/plugin-source.js';
|
|
4
|
-
export interface PluginSourceCommand {
|
|
5
|
-
readonly line: string;
|
|
6
|
-
readonly optional?: true;
|
|
7
|
-
}
|
|
8
|
-
export type ResolvedPluginSource = {
|
|
9
|
-
readonly ok: true;
|
|
10
|
-
readonly commands: readonly PluginSourceCommand[];
|
|
11
|
-
readonly line: string;
|
|
12
|
-
} | {
|
|
13
|
-
readonly ok: false;
|
|
14
|
-
readonly problem: ProvisionProblem;
|
|
15
|
-
readonly env?: string;
|
|
16
|
-
};
|
|
17
|
-
export interface PluginSourceOptions {
|
|
18
|
-
readonly command?: string;
|
|
19
|
-
readonly plugin?: string;
|
|
20
|
-
readonly version?: string;
|
|
21
|
-
readonly mode?: 'install' | 'update';
|
|
22
|
-
}
|
|
23
|
-
export declare function npmRegistryOf(source: {
|
|
24
|
-
readonly registryEnv?: string;
|
|
25
|
-
readonly registryDefault?: string;
|
|
26
|
-
}, env: Readonly<Record<string, string | undefined>>): {
|
|
27
|
-
readonly ok: true;
|
|
28
|
-
readonly value: string;
|
|
29
|
-
} | {
|
|
30
|
-
readonly ok: false;
|
|
31
|
-
readonly env: string;
|
|
32
|
-
};
|
|
33
|
-
export declare function resolvePluginSource(source: SupplySource | undefined, env: Readonly<Record<string, string | undefined>>, options?: PluginSourceOptions): ResolvedPluginSource;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { quoteShellArg } from './shell-text.js';
|
|
2
|
-
import { npmCacheDir, toolchainOpencliDir, vendorPeerLink, vendorPrefix } from './paths.js';
|
|
3
|
-
function mergeLines(commands) {
|
|
4
|
-
return commands.map((item) => (item.optional === true ? `(${item.line} || true)` : item.line)).join(' && ');
|
|
5
|
-
}
|
|
6
|
-
function envValue(env, name) {
|
|
7
|
-
const value = env[name]?.trim();
|
|
8
|
-
return value === undefined || value === '' ? { ok: false } : { ok: true, value };
|
|
9
|
-
}
|
|
10
|
-
export function npmRegistryOf(source, env) {
|
|
11
|
-
const name = source.registryEnv ?? '';
|
|
12
|
-
if (name !== '') {
|
|
13
|
-
const fromEnv = envValue(env, name);
|
|
14
|
-
if (fromEnv.ok)
|
|
15
|
-
return fromEnv;
|
|
16
|
-
}
|
|
17
|
-
const fallback = source.registryDefault?.trim();
|
|
18
|
-
if (fallback === undefined || fallback === '')
|
|
19
|
-
return { ok: false, env: name };
|
|
20
|
-
return { ok: true, value: fallback };
|
|
21
|
-
}
|
|
22
|
-
function installOne(command, spec) {
|
|
23
|
-
const line = `${quoteShellArg(command)} plugin install ${quoteShellArg(spec)}`;
|
|
24
|
-
return { ok: true, commands: [{ line }], line };
|
|
25
|
-
}
|
|
26
|
-
function pluginPackageOf(source, plugin) {
|
|
27
|
-
if (source.kind === 'npm') {
|
|
28
|
-
const pkg = source.package?.trim() ?? '';
|
|
29
|
-
if (pkg !== '')
|
|
30
|
-
return pkg;
|
|
31
|
-
return plugin.trim() === '' ? '' : `opencli-plugin-${plugin.trim()}`;
|
|
32
|
-
}
|
|
33
|
-
if (source.kind === 'git')
|
|
34
|
-
return source.url.trim();
|
|
35
|
-
return '';
|
|
36
|
-
}
|
|
37
|
-
function npmSource(source, env, options) {
|
|
38
|
-
const registry = npmRegistryOf(source, env);
|
|
39
|
-
if (!registry.ok)
|
|
40
|
-
return { ok: false, problem: 'env-missing', env: registry.env };
|
|
41
|
-
const { command = 'opencli', plugin = '', version = 'latest', mode = 'install' } = options;
|
|
42
|
-
const name = pluginPackageOf(source, plugin);
|
|
43
|
-
if (name === '')
|
|
44
|
-
return { ok: false, problem: 'malformed' };
|
|
45
|
-
const prefix = vendorPrefix();
|
|
46
|
-
const dirInPrefix = `${prefix}/node_modules/${name}`;
|
|
47
|
-
const peer = vendorPeerLink();
|
|
48
|
-
const commands = [
|
|
49
|
-
{
|
|
50
|
-
line: [
|
|
51
|
-
'npm i',
|
|
52
|
-
`--prefix ${quoteShellArg(prefix)}`,
|
|
53
|
-
quoteShellArg(`${name}@${version}`),
|
|
54
|
-
`--registry ${quoteShellArg(registry.value)}`,
|
|
55
|
-
`--cache ${quoteShellArg(npmCacheDir())}`,
|
|
56
|
-
].join(' '),
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
line: `rm -rf ${quoteShellArg(peer.dir)} && mkdir -p ${quoteShellArg(peer.parent)} && ln -s ${quoteShellArg(toolchainOpencliDir())} ${quoteShellArg(peer.dir)}`,
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
if (mode === 'install') {
|
|
63
|
-
if (plugin !== '') {
|
|
64
|
-
commands.push({
|
|
65
|
-
line: `${quoteShellArg(command)} plugin uninstall ${quoteShellArg(plugin)}`,
|
|
66
|
-
optional: true,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
commands.push({
|
|
70
|
-
line: `NPM_CONFIG_REGISTRY=${quoteShellArg(registry.value)} ${quoteShellArg(command)} plugin install ${quoteShellArg(dirInPrefix)}`,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return { ok: true, commands, line: mergeLines(commands) };
|
|
74
|
-
}
|
|
75
|
-
export function resolvePluginSource(source, env, options = {}) {
|
|
76
|
-
if (source === undefined)
|
|
77
|
-
return { ok: false, problem: 'not-declared' };
|
|
78
|
-
const command = options.command ?? 'opencli';
|
|
79
|
-
if (source.kind === 'git') {
|
|
80
|
-
const url = source.url.trim();
|
|
81
|
-
return url === '' ? { ok: false, problem: 'malformed' } : installOne(command, url);
|
|
82
|
-
}
|
|
83
|
-
if (source.kind === 'npm')
|
|
84
|
-
return npmSource(source, env, options);
|
|
85
|
-
const value = source.env === undefined ? { ok: false } : envValue(env, source.env);
|
|
86
|
-
if (value.ok)
|
|
87
|
-
return installOne(command, value.value);
|
|
88
|
-
if (source.path !== undefined && source.path.trim() !== '')
|
|
89
|
-
return installOne(command, source.path.trim());
|
|
90
|
-
return { ok: false, problem: 'env-missing', env: source.env ?? '' };
|
|
91
|
-
}
|