@dsh-workbench/shared 1.1.13 → 1.1.14-beta.1
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/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 +2 -0
- package/dist/host/shell-text.d.ts +11 -1
- package/dist/host/shell-text.js +11 -3
- package/dist/host/shell.d.ts +1 -0
- package/dist/host/shell.js +2 -1
- 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 +1 -0
- package/dist/i18n/index.d.ts +2 -1
- package/dist/i18n/zh.d.ts +1 -0
- package/dist/i18n/zh.js +1 -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/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 +24 -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 +2 -0
- package/src/host/shell-text.ts +16 -2
- package/src/host/shell.ts +8 -2
- 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 +1 -0
- package/src/i18n/zh.ts +1 -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/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
|
-
}
|
package/dist/host/runner.d.ts
DELETED
|
@@ -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;
|
package/dist/host/runner.js
DELETED
|
@@ -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,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 让共用依赖也能 import opencli:把隔离运行时里那份 opencli 链接到 libs 的 node_modules。
|
|
3
|
-
* 跑在 libs 里的命令(比如 opencli-workflow 带的 gitops)会按包名找 @jackwener/opencli,
|
|
4
|
-
* 找不到就起不来 —— 它不在 libs 的依赖里,是运行时提供的那一份。
|
|
5
|
-
* @returns 这次真的建了链接就 `true`;已经有了、或运行时那份还没装,就是 `false`。
|
|
6
|
-
*/
|
|
7
|
-
export declare function ensureLibsOpencliPeer(): boolean;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, symlinkSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import { OPENCLI_NPM_PACKAGE, libsPrefix, toolchainOpencliDir } from './paths.js';
|
|
4
|
-
/**
|
|
5
|
-
* 让共用依赖也能 import opencli:把隔离运行时里那份 opencli 链接到 libs 的 node_modules。
|
|
6
|
-
* 跑在 libs 里的命令(比如 opencli-workflow 带的 gitops)会按包名找 @jackwener/opencli,
|
|
7
|
-
* 找不到就起不来 —— 它不在 libs 的依赖里,是运行时提供的那一份。
|
|
8
|
-
* @returns 这次真的建了链接就 `true`;已经有了、或运行时那份还没装,就是 `false`。
|
|
9
|
-
*/
|
|
10
|
-
export function ensureLibsOpencliPeer() {
|
|
11
|
-
const link = `${libsPrefix()}/node_modules/${OPENCLI_NPM_PACKAGE}`;
|
|
12
|
-
if (existsSync(link))
|
|
13
|
-
return false;
|
|
14
|
-
const target = toolchainOpencliDir();
|
|
15
|
-
if (!existsSync(target))
|
|
16
|
-
return false;
|
|
17
|
-
try {
|
|
18
|
-
mkdirSync(dirname(link), { recursive: true });
|
|
19
|
-
symlinkSync(target, link);
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
package/dist/lib/version.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function isValidVersion(value: string): boolean;
|
|
2
|
-
export declare function isPrereleaseVersion(value: string): boolean;
|
|
3
|
-
export declare function compareVersions(left: string, right: string): number | undefined;
|
|
4
|
-
export declare function isNewerVersion(latest: string, installed: string): boolean;
|
package/dist/lib/version.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import semver from 'semver';
|
|
2
|
-
export function isValidVersion(value) {
|
|
3
|
-
return semver.valid(value.trim()) !== null;
|
|
4
|
-
}
|
|
5
|
-
export function isPrereleaseVersion(value) {
|
|
6
|
-
const parsed = semver.valid(value.trim());
|
|
7
|
-
return parsed !== null && semver.prerelease(parsed) !== null;
|
|
8
|
-
}
|
|
9
|
-
export function compareVersions(left, right) {
|
|
10
|
-
const a = semver.valid(left.trim());
|
|
11
|
-
const b = semver.valid(right.trim());
|
|
12
|
-
if (a === null || b === null)
|
|
13
|
-
return undefined;
|
|
14
|
-
return semver.compare(a, b);
|
|
15
|
-
}
|
|
16
|
-
export function isNewerVersion(latest, installed) {
|
|
17
|
-
if (latest === installed)
|
|
18
|
-
return false;
|
|
19
|
-
const compared = compareVersions(latest, installed);
|
|
20
|
-
return compared === undefined ? true : compared > 0;
|
|
21
|
-
}
|
package/dist/testing/git.d.ts
DELETED
package/dist/testing/git.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { quoteShellArg, tailLines } from '../host/shell-text.js';
|
|
2
|
-
import { runShell } from '../host/shell.js';
|
|
3
|
-
export function createFakeGit(shell) {
|
|
4
|
-
return {
|
|
5
|
-
async origin(path) {
|
|
6
|
-
const key = path.trim();
|
|
7
|
-
if (!key)
|
|
8
|
-
return { kind: 'unknown', reason: 'no-path' };
|
|
9
|
-
const result = await runShell(shell, `git -C ${quoteShellArg(key)} remote get-url origin`);
|
|
10
|
-
if (result.exitCode !== 0)
|
|
11
|
-
return { kind: 'no-origin', detail: tailLines(result.stderr, 3) };
|
|
12
|
-
return { kind: 'ok', remote: result.stdout.trim() };
|
|
13
|
-
},
|
|
14
|
-
async branch(path) {
|
|
15
|
-
const key = path.trim();
|
|
16
|
-
if (!key)
|
|
17
|
-
return { kind: 'unknown', reason: 'no-path' };
|
|
18
|
-
const result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse --abbrev-ref HEAD`);
|
|
19
|
-
const name = result.stdout.trim();
|
|
20
|
-
if (result.exitCode !== 0)
|
|
21
|
-
return { kind: 'no-branch', detail: tailLines(result.stderr, 3) };
|
|
22
|
-
if (name === '' || name === 'HEAD')
|
|
23
|
-
return { kind: 'no-branch', detail: name };
|
|
24
|
-
return { kind: 'ok', branch: name };
|
|
25
|
-
},
|
|
26
|
-
async head(path) {
|
|
27
|
-
const key = path.trim();
|
|
28
|
-
if (!key)
|
|
29
|
-
return { kind: 'unknown', reason: 'no-path' };
|
|
30
|
-
const result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse HEAD`);
|
|
31
|
-
if (result.exitCode !== 0)
|
|
32
|
-
return { kind: 'no-head', detail: tailLines(result.stderr, 3) };
|
|
33
|
-
return { kind: 'ok', commit: result.stdout.trim() };
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|