@dsh-workbench/shared 1.1.8 → 1.1.10
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/dist/api/blocker-table.d.ts +1 -0
- package/dist/api/blocker-table.js +1 -1
- package/dist/components/SelectField.css +0 -6
- package/dist/components/SelectField.d.ts +2 -3
- package/dist/components/SelectField.js +9 -30
- package/dist/components/SelectFieldNodes.d.ts +3 -1
- package/dist/components/SelectFieldNodes.js +2 -1
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/config/constants.d.ts +4 -4
- package/dist/config/constants.js +4 -4
- package/dist/contract/capabilities.d.ts +29 -0
- package/dist/contract/capabilities.js +1 -0
- package/dist/contract/needs.d.ts +3 -3
- package/dist/contract/needs.js +1 -1
- package/dist/contract/plugin-source.d.ts +19 -0
- package/dist/contract/protocol/bundle-install.d.ts +3 -1
- package/dist/contract/protocol/bundle-install.js +3 -0
- package/dist/contract/protocol/market-index.d.ts +1 -1
- package/dist/contract/protocol/platform.d.ts +1 -1
- package/dist/contract/runner.d.ts +6 -0
- package/dist/contract/runner.js +1 -0
- package/dist/contract/unit.d.ts +4 -4
- package/dist/host/adapter-contract.d.ts +3 -0
- package/dist/host/adapter-contract.js +93 -0
- package/dist/host/adapter-set.js +6 -4
- package/dist/host/adapter-types.d.ts +1 -1
- package/dist/host/bundle-manager.d.ts +3 -0
- package/dist/host/bundle-manager.js +181 -0
- package/dist/host/cli.js +3 -1
- package/dist/host/config-sync.d.ts +48 -0
- package/dist/host/config-sync.js +342 -0
- package/dist/host/dsh-plugins.d.ts +5 -0
- package/dist/host/dsh-plugins.js +51 -0
- package/dist/host/extension.js +1 -1
- package/dist/host/flow/check.d.ts +9 -0
- package/dist/host/flow/check.js +179 -0
- package/dist/host/flow/expr.d.ts +30 -0
- package/dist/host/flow/expr.js +99 -0
- package/dist/host/flow/kit.d.ts +14 -0
- package/dist/host/flow/kit.js +13 -0
- package/dist/host/flow/run.d.ts +13 -0
- package/dist/host/flow/run.js +287 -0
- package/dist/host/flow/types.d.ts +188 -0
- package/dist/host/flow/types.js +1 -0
- package/dist/host/host-actions.d.ts +2 -0
- package/dist/host/host-actions.js +5 -0
- package/dist/host/host-kit.d.ts +19 -0
- package/dist/host/host-kit.js +14 -0
- package/dist/host/http.d.ts +2 -6
- package/dist/host/http.js +2 -2
- package/dist/host/installed-module.d.ts +23 -0
- package/dist/host/installed-module.js +264 -0
- package/dist/host/lib-install.d.ts +7 -9
- package/dist/host/lib-install.js +9 -12
- package/dist/host/lib-runners.d.ts +3 -5
- package/dist/host/lib-runners.js +2 -2
- package/dist/host/local-declaration.d.ts +7 -0
- package/dist/host/local-declaration.js +33 -0
- package/dist/host/module-root.d.ts +1 -0
- package/dist/host/module-root.js +5 -0
- package/dist/host/npm-registry.d.ts +10 -0
- package/dist/host/npm-registry.js +16 -0
- package/dist/host/opencli-runner.d.ts +2 -1
- package/dist/host/package-content.d.ts +7 -0
- package/dist/host/package-content.js +69 -0
- package/dist/host/package-kind.d.ts +13 -0
- package/dist/host/package-kind.js +51 -0
- package/dist/host/package-route.d.ts +31 -0
- package/dist/host/package-route.js +57 -0
- package/dist/host/paths.d.ts +11 -19
- package/dist/host/paths.js +16 -43
- package/dist/host/platform.js +1 -1
- package/dist/host/plugin-ledger.d.ts +8 -8
- package/dist/host/plugin-ledger.js +23 -16
- package/dist/host/plugin.d.ts +1 -2
- package/dist/host/plugin.js +4 -5
- package/dist/host/port-types.d.ts +42 -9
- package/dist/host/preflight.d.ts +3 -2
- package/dist/host/profile-create.d.ts +18 -0
- package/dist/host/profile-create.js +118 -0
- package/dist/host/profile-registries.d.ts +10 -0
- package/dist/host/profile-registries.js +150 -0
- package/dist/host/registry-declaration.d.ts +21 -0
- package/dist/host/registry-declaration.js +33 -0
- package/dist/host/release-probe.d.ts +34 -0
- package/dist/host/release-probe.js +88 -0
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/shell.d.ts +0 -2
- package/dist/host/shell.js +0 -7
- package/dist/host/spec.js +1 -1
- package/dist/host/supply-index.d.ts +1 -0
- package/dist/host/supply-index.js +8 -7
- package/dist/host/supply-port.js +13 -3
- package/dist/host/unit-face.d.ts +2 -1
- package/dist/host/version.js +11 -6
- package/dist/host.d.ts +24 -6
- package/dist/host.js +23 -6
- package/dist/i18n/en.js +23 -24
- package/dist/i18n/index.d.ts +23 -24
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +22 -23
- package/dist/i18n/zh.js +22 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/testing/fake-adapter-set.d.ts +5 -0
- package/dist/testing/fake-adapter-set.js +150 -0
- package/dist/testing/needs.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/blocker-table.test.ts +25 -0
- package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
- package/src/api/blocker-table.ts +6 -2
- package/src/components/SelectField.css +0 -6
- package/src/components/SelectField.tsx +7 -36
- package/src/components/SelectFieldNodes.tsx +4 -1
- package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
- package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
- package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
- package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
- package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
- package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
- package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
- package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
- package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
- package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
- package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
- package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
- package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
- package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
- package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
- package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
- package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
- package/src/config/constants.ts +4 -4
- package/src/contract/capabilities.ts +30 -0
- package/src/contract/needs.ts +4 -4
- package/src/contract/plugin-source.ts +24 -0
- package/src/contract/protocol/bundle-install.ts +7 -1
- package/src/contract/protocol/market-index.ts +1 -0
- package/src/contract/protocol/platform.ts +1 -1
- package/src/contract/runner.ts +6 -0
- package/src/contract/unit.ts +4 -4
- package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
- package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
- package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
- package/src/host/__tests__/lib-runners.test.ts +11 -2
- package/src/host/__tests__/plugin-ledger.test.ts +19 -6
- package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
- package/src/host/__tests__/runtime-layout.test.ts +14 -0
- package/src/host/__tests__/supply-index.test.ts +2 -2
- package/src/host/__tests__/supply-port.test.ts +32 -0
- package/src/host/adapter-contract.ts +93 -0
- package/src/host/adapter-set.ts +6 -4
- package/src/host/adapter-types.ts +1 -1
- package/src/host/bundle-manager.ts +195 -0
- package/src/host/cli.ts +3 -1
- package/src/host/config-sync.ts +386 -0
- package/src/host/dsh-plugins.ts +60 -0
- package/src/host/extension.ts +1 -1
- package/src/host/flow/__tests__/advanced.test.ts +302 -0
- package/src/host/flow/__tests__/check.test.ts +169 -0
- package/src/host/flow/__tests__/run.test.ts +301 -0
- package/src/host/flow/check.ts +216 -0
- package/src/host/flow/expr.ts +94 -0
- package/src/host/flow/kit.ts +30 -0
- package/src/host/flow/run.ts +325 -0
- package/src/host/flow/types.ts +199 -0
- package/src/host/host-actions.ts +7 -0
- package/src/host/host-kit.ts +26 -0
- package/src/host/http.ts +6 -8
- package/src/host/installed-module.ts +272 -0
- package/src/host/lib-install.ts +14 -12
- package/src/host/lib-runners.ts +6 -8
- package/src/host/local-declaration.ts +37 -0
- package/src/host/module-root.ts +6 -0
- package/src/host/npm-registry.ts +21 -0
- package/src/host/package-content.ts +76 -0
- package/src/host/package-kind.ts +63 -0
- package/src/host/package-route.ts +93 -0
- package/src/host/paths.ts +27 -52
- package/src/host/platform.ts +1 -1
- package/src/host/plugin-ledger.ts +25 -18
- package/src/host/plugin.ts +4 -5
- package/src/host/port-types.ts +43 -9
- package/src/host/preflight.ts +3 -2
- package/src/host/profile-create.ts +133 -0
- package/src/host/profile-registries.ts +145 -0
- package/src/host/registry-declaration.ts +70 -0
- package/src/host/release-probe.ts +202 -0
- package/src/host/shell.ts +0 -9
- package/src/host/spec.ts +1 -1
- package/src/host/supply-index.ts +9 -7
- package/src/host/supply-port.ts +13 -3
- package/src/host/unit-face.ts +2 -1
- package/src/host/version.ts +12 -7
- package/src/host.ts +34 -6
- package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
- package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
- package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
- package/src/i18n/en.ts +23 -25
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +22 -24
- package/src/index.ts +4 -0
- package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
- package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
- package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
- package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
- package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
- package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
- package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
- package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
- package/src/testing/fake-adapter-set.ts +173 -0
- package/src/testing/needs.ts +1 -1
- package/src/testing.ts +1 -0
- package/src/host/__tests__/command-name.test.ts +0 -16
- package/src/host/__tests__/opencli-runner.test.ts +0 -120
- package/src/host/channel-plugin.ts +0 -55
- package/src/host/command-name.ts +0 -15
- package/src/host/opencli-runner.ts +0 -76
- package/src/host/paths.test.ts +0 -26
- package/src/host/plugin-source.ts +0 -137
- package/src/host/runtime-links.test.ts +0 -31
- package/src/host/runtime-links.ts +0 -23
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type { DataRequest, DataResult } from '../../contract/data.js';
|
|
3
|
-
import { opencliRunnerOf } from '../opencli-runner.js';
|
|
4
|
-
|
|
5
|
-
type Read = (request: DataRequest) => Promise<DataResult<unknown>>;
|
|
6
|
-
|
|
7
|
-
function readerOf(result: DataResult<unknown>): {
|
|
8
|
-
readonly read: Read;
|
|
9
|
-
readonly spy: ReturnType<typeof vi.fn>;
|
|
10
|
-
} {
|
|
11
|
-
const spy = vi.fn(async (_request: DataRequest) => result);
|
|
12
|
-
return { read: spy as unknown as Read, spy };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('opencliRunnerOf', () => {
|
|
16
|
-
it('把 argv 原样交给读缝,解析后的值序列化回 stdout', async () => {
|
|
17
|
-
const { read, spy } = readerOf({ ok: true, data: [{ name: 'lsznh' }] });
|
|
18
|
-
const runner = opencliRunnerOf(read);
|
|
19
|
-
|
|
20
|
-
await expect(
|
|
21
|
-
runner(['argocd', 'application-resource-tree', '--name', 'lsznh']),
|
|
22
|
-
).resolves.toEqual({ stdout: JSON.stringify([{ name: 'lsznh' }]) });
|
|
23
|
-
expect(spy).toHaveBeenCalledWith({
|
|
24
|
-
kind: 'command',
|
|
25
|
-
adapter: 'opencli',
|
|
26
|
-
argv: ['argocd', 'application-resource-tree', '--name', 'lsznh'],
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('timeoutMs 透传给读缝;不传就不出现这一格', async () => {
|
|
31
|
-
const withTimeout = readerOf({ ok: true, data: [] });
|
|
32
|
-
await opencliRunnerOf(withTimeout.read, { timeoutMs: 1_800_000 })([
|
|
33
|
-
'coding',
|
|
34
|
-
'ci-status',
|
|
35
|
-
]);
|
|
36
|
-
expect(withTimeout.spy).toHaveBeenCalledWith(
|
|
37
|
-
expect.objectContaining({ timeoutMs: 1_800_000 }),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const withoutTimeout = readerOf({ ok: true, data: [] });
|
|
41
|
-
await opencliRunnerOf(withoutTimeout.read)(['coding', 'ci-status']);
|
|
42
|
-
expect(withoutTimeout.spy).toHaveBeenCalledWith(
|
|
43
|
-
expect.not.objectContaining({ timeoutMs: expect.anything() }),
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('未登录还原成 opencli 的约定:退出码 77 + AUTH_REQUIRED + 登录地址', async () => {
|
|
48
|
-
const { read } = readerOf({
|
|
49
|
-
ok: false,
|
|
50
|
-
error: {
|
|
51
|
-
code: 'auth',
|
|
52
|
-
pageCode: 'auth-required',
|
|
53
|
-
message: 'Coding 未登录',
|
|
54
|
-
helpUrl: 'https://baiguokeji.coding.net/login',
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const failure = await opencliRunnerOf(read)(['coding', 'ci-status']).then(
|
|
59
|
-
() => null,
|
|
60
|
-
(error: unknown) => error as Error & { code?: number },
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
expect(failure).toBeInstanceOf(Error);
|
|
64
|
-
expect(failure?.code).toBe(77);
|
|
65
|
-
expect(failure?.message).toBe(
|
|
66
|
-
[
|
|
67
|
-
'Command failed: opencli coding ci-status',
|
|
68
|
-
'code: AUTH_REQUIRED',
|
|
69
|
-
'message: Coding 未登录',
|
|
70
|
-
'help: Please open Chrome or Chromium and log in to https://baiguokeji.coding.net/login',
|
|
71
|
-
].join('\n'),
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('其它失败保留 Command failed 外壳与原因 / 细节 / 下一步', async () => {
|
|
76
|
-
const { read } = readerOf({
|
|
77
|
-
ok: false,
|
|
78
|
-
error: {
|
|
79
|
-
code: 'command',
|
|
80
|
-
pageCode: 'cli-command-failed',
|
|
81
|
-
message: 'UNKNOWN:插件里没有这条命令',
|
|
82
|
-
detail: 'error: unknown command',
|
|
83
|
-
hint: '升级插件后重试',
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const failure = await opencliRunnerOf(read)([
|
|
88
|
-
'argocd',
|
|
89
|
-
'application-resource-tree',
|
|
90
|
-
]).then(
|
|
91
|
-
() => null,
|
|
92
|
-
(error: unknown) => error as Error,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
expect(failure?.message).toBe(
|
|
96
|
-
[
|
|
97
|
-
'Command failed: opencli argocd application-resource-tree',
|
|
98
|
-
'UNKNOWN:插件里没有这条命令',
|
|
99
|
-
'error: unknown command',
|
|
100
|
-
'升级插件后重试',
|
|
101
|
-
].join('\n'),
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('没有原因时也有一句能看懂的兜底', async () => {
|
|
106
|
-
const { read } = readerOf({
|
|
107
|
-
ok: false,
|
|
108
|
-
error: { code: 'command', message: '' },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const failure = await opencliRunnerOf(read)(['cmdb', 'publish']).then(
|
|
112
|
-
() => null,
|
|
113
|
-
(error: unknown) => error as Error,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
expect(failure?.message).toBe(
|
|
117
|
-
'Command failed: opencli cmdb publish\nopencli 这条命令没跑通',
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,55 +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
|
-
|
|
6
|
-
export const CHANNEL_PLUGIN_NAME = 'opencli-plugin-workbench';
|
|
7
|
-
export const CHANNEL_PLUGIN_SITE = 'workbench';
|
|
8
|
-
|
|
9
|
-
export interface ChannelPluginPlan {
|
|
10
|
-
readonly dest: string;
|
|
11
|
-
readonly version: string;
|
|
12
|
-
readonly commands: readonly string[];
|
|
13
|
-
readonly line: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function textOf(value: unknown): string {
|
|
17
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function channelVersionOf(dir: string): string | undefined {
|
|
21
|
-
try {
|
|
22
|
-
const parsed: unknown = JSON.parse(readFileSync(join(dir, 'opencli-plugin.json'), 'utf8'));
|
|
23
|
-
if (typeof parsed !== 'object' || parsed === null) return undefined;
|
|
24
|
-
const version = textOf((parsed as Record<string, unknown>)['version']);
|
|
25
|
-
return version === '' ? undefined : version;
|
|
26
|
-
} catch {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function channelPluginPlan(options: {
|
|
32
|
-
readonly channelDir: string;
|
|
33
|
-
readonly command?: string;
|
|
34
|
-
}): ChannelPluginPlan | undefined {
|
|
35
|
-
const version = channelVersionOf(options.channelDir);
|
|
36
|
-
if (version === undefined) return undefined;
|
|
37
|
-
const command = options.command ?? 'opencli';
|
|
38
|
-
const dest = join(vendorPluginsDir(), CHANNEL_PLUGIN_NAME);
|
|
39
|
-
const peer = vendorPeerLink();
|
|
40
|
-
const commands = [
|
|
41
|
-
`rm -rf ${quoteShellArg(dest)} && mkdir -p ${quoteShellArg(dest)} && cp -R ${quoteShellArg(options.channelDir)}/. ${quoteShellArg(dest)}/`,
|
|
42
|
-
`rm -rf ${quoteShellArg(peer.dir)} && mkdir -p ${quoteShellArg(peer.parent)} && ln -s ${quoteShellArg(toolchainOpencliDir())} ${quoteShellArg(peer.dir)}`,
|
|
43
|
-
`(${quoteShellArg(command)} plugin uninstall ${quoteShellArg(CHANNEL_PLUGIN_SITE)} || true)`,
|
|
44
|
-
`${quoteShellArg(command)} plugin install ${quoteShellArg(dest)}`,
|
|
45
|
-
];
|
|
46
|
-
return { dest, version, commands, line: commands.join(' && ') };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function channelInstalledVersion(): string | undefined {
|
|
50
|
-
return channelVersionOf(join(vendorPluginsDir(), CHANNEL_PLUGIN_NAME));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function channelSiteInstalled(): boolean {
|
|
54
|
-
return existsSync(join(isolatedHome(), '.opencli', 'plugins', CHANNEL_PLUGIN_SITE));
|
|
55
|
-
}
|
package/src/host/command-name.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
function baseName(value: string): string {
|
|
2
|
-
const parts = value.trim().split(/[\\/]/);
|
|
3
|
-
return parts[parts.length - 1] ?? '';
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function isOpencli(name: string): boolean {
|
|
7
|
-
return name === 'opencli' || name === 'opencli.cmd' || name === 'opencli.exe';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function commandNamesMatch(declared: string, resolved: string): boolean {
|
|
11
|
-
const left = baseName(declared);
|
|
12
|
-
const right = baseName(resolved);
|
|
13
|
-
if (left === '' || right === '') return false;
|
|
14
|
-
return left === right || (isOpencli(left) && isOpencli(right));
|
|
15
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { DataError, DataRequest, DataResult } from '../contract/data.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 一条 opencli 命令的执行器:同一形状的 `execFile('opencli', args)`,结果只留 stdout。
|
|
5
|
-
*/
|
|
6
|
-
export type OpencliRunner = (
|
|
7
|
-
args: readonly string[],
|
|
8
|
-
) => Promise<{ readonly stdout: string }>;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 读缝(`DataReader.read`)的形状;模块直接把自己的 `data.read` 传进来即可。
|
|
12
|
-
*/
|
|
13
|
-
export type OpencliRead = (
|
|
14
|
-
request: DataRequest,
|
|
15
|
-
) => Promise<DataResult<unknown>>;
|
|
16
|
-
|
|
17
|
-
export interface OpencliRunnerOptions {
|
|
18
|
-
readonly timeoutMs?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const AUTH_REQUIRED_EXIT_CODE = 77;
|
|
22
|
-
|
|
23
|
-
function textOf(value: unknown): string {
|
|
24
|
-
return typeof value === 'string' ? value : '';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function failedOf(args: readonly string[], error: DataError | undefined): Error {
|
|
28
|
-
const head = `Command failed: opencli ${args.join(' ')}`;
|
|
29
|
-
const auth = error?.pageCode === 'auth-required';
|
|
30
|
-
const helpUrl = textOf(error?.helpUrl);
|
|
31
|
-
const detail = textOf(error?.detail);
|
|
32
|
-
const hint = textOf(error?.hint);
|
|
33
|
-
const failed = new Error(
|
|
34
|
-
[
|
|
35
|
-
head,
|
|
36
|
-
...(auth
|
|
37
|
-
? ['code: AUTH_REQUIRED', `message: ${textOf(error?.message)}`]
|
|
38
|
-
: [textOf(error?.message) || 'opencli 这条命令没跑通']),
|
|
39
|
-
...(detail === '' ? [] : [detail]),
|
|
40
|
-
...(hint === '' ? [] : [hint]),
|
|
41
|
-
...(auth && helpUrl !== ''
|
|
42
|
-
? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`]
|
|
43
|
-
: []),
|
|
44
|
-
].join('\n'),
|
|
45
|
-
);
|
|
46
|
-
if (auth) (failed as Error & { code?: number }).code = AUTH_REQUIRED_EXIT_CODE;
|
|
47
|
-
return failed;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
|
|
52
|
-
* 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
|
|
53
|
-
*
|
|
54
|
-
* 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
|
|
55
|
-
* 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
|
|
56
|
-
* @param read 模块手里的读缝(`data.read`)。
|
|
57
|
-
* @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
|
|
58
|
-
* @returns 执行器;直接交给编排库的 `setOpencliRunner`。
|
|
59
|
-
*/
|
|
60
|
-
export function opencliRunnerOf(
|
|
61
|
-
read: OpencliRead,
|
|
62
|
-
options: OpencliRunnerOptions = {},
|
|
63
|
-
): OpencliRunner {
|
|
64
|
-
return async (args) => {
|
|
65
|
-
const result = await read({
|
|
66
|
-
kind: 'command',
|
|
67
|
-
adapter: 'opencli',
|
|
68
|
-
argv: [...args],
|
|
69
|
-
...(options.timeoutMs === undefined
|
|
70
|
-
? {}
|
|
71
|
-
: { timeoutMs: options.timeoutMs }),
|
|
72
|
-
});
|
|
73
|
-
if (result.ok) return { stdout: JSON.stringify(result.data) };
|
|
74
|
-
throw failedOf(args, result.error);
|
|
75
|
-
};
|
|
76
|
-
}
|
package/src/host/paths.test.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { delimiter } from 'node:path';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { ensureLibsBinOnPath, libsBinDir, npmCacheDir } from './paths.js';
|
|
4
|
-
|
|
5
|
-
describe('隔离运行时的路径', () => {
|
|
6
|
-
it('共用依赖的 bin 目录就在 libs 下面', () => {
|
|
7
|
-
expect(libsBinDir().endsWith('/opencli/libs/node_modules/.bin')).toBe(true);
|
|
8
|
-
expect(npmCacheDir().endsWith('/opencli/npm-cache')).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('★ 把 bin 目录前插一次;再插不会重复(PATH 不许越跑越长)', () => {
|
|
12
|
-
const env = { PATH: `/usr/bin${delimiter}/bin` };
|
|
13
|
-
const dir = libsBinDir();
|
|
14
|
-
|
|
15
|
-
expect(ensureLibsBinOnPath(env)).toBe(true);
|
|
16
|
-
expect(env.PATH).toBe(`${dir}${delimiter}/usr/bin${delimiter}/bin`);
|
|
17
|
-
expect(ensureLibsBinOnPath(env)).toBe(false);
|
|
18
|
-
expect(env.PATH.split(delimiter).filter((one) => one === dir)).toHaveLength(1);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('PATH 是空的也能插', () => {
|
|
22
|
-
const env: NodeJS.ProcessEnv = {};
|
|
23
|
-
expect(ensureLibsBinOnPath(env)).toBe(true);
|
|
24
|
-
expect(env.PATH).toBe(libsBinDir());
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import type { SupplySource } from '../contract/needs.js';
|
|
2
|
-
import type { ProvisionProblem } from '../contract/plugin-source.js';
|
|
3
|
-
import { quoteShellArg } from './shell-text.js';
|
|
4
|
-
import { npmCacheDir, toolchainOpencliDir, vendorPeerLink, vendorPrefix } from './paths.js';
|
|
5
|
-
|
|
6
|
-
export type { ProvisionProblem } from '../contract/plugin-source.js';
|
|
7
|
-
|
|
8
|
-
export interface PluginSourceCommand {
|
|
9
|
-
readonly line: string;
|
|
10
|
-
readonly optional?: true;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type ResolvedPluginSource =
|
|
14
|
-
| {
|
|
15
|
-
readonly ok: true;
|
|
16
|
-
readonly commands: readonly PluginSourceCommand[];
|
|
17
|
-
readonly line: string;
|
|
18
|
-
}
|
|
19
|
-
| {
|
|
20
|
-
readonly ok: false;
|
|
21
|
-
readonly problem: ProvisionProblem;
|
|
22
|
-
readonly env?: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export interface PluginSourceOptions {
|
|
26
|
-
readonly command?: string;
|
|
27
|
-
readonly plugin?: string;
|
|
28
|
-
readonly version?: string;
|
|
29
|
-
readonly mode?: 'install' | 'update';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mergeLines(commands: readonly PluginSourceCommand[]): string {
|
|
33
|
-
return commands.map((item) => (item.optional === true ? `(${item.line} || true)` : item.line)).join(' && ');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function envValue(
|
|
37
|
-
env: Readonly<Record<string, string | undefined>>,
|
|
38
|
-
name: string,
|
|
39
|
-
): { readonly ok: true; readonly value: string } | { readonly ok: false } {
|
|
40
|
-
const value = env[name]?.trim();
|
|
41
|
-
return value === undefined || value === '' ? { ok: false } : { ok: true, value };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function npmRegistryOf(
|
|
45
|
-
source: { readonly registryEnv?: string; readonly registryDefault?: string },
|
|
46
|
-
env: Readonly<Record<string, string | undefined>>,
|
|
47
|
-
): { readonly ok: true; readonly value: string } | { readonly ok: false; readonly env: string } {
|
|
48
|
-
const name = source.registryEnv ?? '';
|
|
49
|
-
if (name !== '') {
|
|
50
|
-
const fromEnv = envValue(env, name);
|
|
51
|
-
if (fromEnv.ok) return fromEnv;
|
|
52
|
-
}
|
|
53
|
-
const fallback = source.registryDefault?.trim();
|
|
54
|
-
if (fallback === undefined || fallback === '') return { ok: false, env: name };
|
|
55
|
-
return { ok: true, value: fallback };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function installOne(command: string, spec: string): ResolvedPluginSource {
|
|
59
|
-
const line = `${quoteShellArg(command)} plugin install ${quoteShellArg(spec)}`;
|
|
60
|
-
return { ok: true, commands: [{ line }], line };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function pluginPackageOf(source: SupplySource, plugin: string): string {
|
|
64
|
-
if (source.kind === 'npm') {
|
|
65
|
-
const pkg = source.package?.trim() ?? '';
|
|
66
|
-
if (pkg !== '') return pkg;
|
|
67
|
-
return plugin.trim() === '' ? '' : `opencli-plugin-${plugin.trim()}`;
|
|
68
|
-
}
|
|
69
|
-
if (source.kind === 'git') return source.url.trim();
|
|
70
|
-
return '';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function npmSource(
|
|
74
|
-
source: SupplySource & { kind: 'npm' },
|
|
75
|
-
env: Readonly<Record<string, string | undefined>>,
|
|
76
|
-
options: PluginSourceOptions,
|
|
77
|
-
): ResolvedPluginSource {
|
|
78
|
-
const registry = npmRegistryOf(source, env);
|
|
79
|
-
if (!registry.ok) return { ok: false, problem: 'env-missing', env: registry.env };
|
|
80
|
-
|
|
81
|
-
const { command = 'opencli', plugin = '', version = 'latest', mode = 'install' } = options;
|
|
82
|
-
const name = pluginPackageOf(source, plugin);
|
|
83
|
-
if (name === '') return { ok: false, problem: 'malformed' };
|
|
84
|
-
|
|
85
|
-
const prefix = vendorPrefix();
|
|
86
|
-
const dirInPrefix = `${prefix}/node_modules/${name}`;
|
|
87
|
-
const peer = vendorPeerLink();
|
|
88
|
-
|
|
89
|
-
const commands: PluginSourceCommand[] = [
|
|
90
|
-
{
|
|
91
|
-
line: [
|
|
92
|
-
'npm i',
|
|
93
|
-
`--prefix ${quoteShellArg(prefix)}`,
|
|
94
|
-
quoteShellArg(`${name}@${version}`),
|
|
95
|
-
`--registry ${quoteShellArg(registry.value)}`,
|
|
96
|
-
`--cache ${quoteShellArg(npmCacheDir())}`,
|
|
97
|
-
].join(' '),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
line: `rm -rf ${quoteShellArg(peer.dir)} && mkdir -p ${quoteShellArg(peer.parent)} && ln -s ${quoteShellArg(toolchainOpencliDir())} ${quoteShellArg(peer.dir)}`,
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
if (mode === 'install') {
|
|
105
|
-
if (plugin !== '') {
|
|
106
|
-
commands.push({
|
|
107
|
-
line: `${quoteShellArg(command)} plugin uninstall ${quoteShellArg(plugin)}`,
|
|
108
|
-
optional: true,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
commands.push({
|
|
112
|
-
line: `NPM_CONFIG_REGISTRY=${quoteShellArg(registry.value)} ${quoteShellArg(command)} plugin install ${quoteShellArg(dirInPrefix)}`,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { ok: true, commands, line: mergeLines(commands) };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function resolvePluginSource(
|
|
120
|
-
source: SupplySource | undefined,
|
|
121
|
-
env: Readonly<Record<string, string | undefined>>,
|
|
122
|
-
options: PluginSourceOptions = {},
|
|
123
|
-
): ResolvedPluginSource {
|
|
124
|
-
if (source === undefined) return { ok: false, problem: 'not-declared' };
|
|
125
|
-
|
|
126
|
-
const command = options.command ?? 'opencli';
|
|
127
|
-
if (source.kind === 'git') {
|
|
128
|
-
const url = source.url.trim();
|
|
129
|
-
return url === '' ? { ok: false, problem: 'malformed' } : installOne(command, url);
|
|
130
|
-
}
|
|
131
|
-
if (source.kind === 'npm') return npmSource(source, env, options);
|
|
132
|
-
|
|
133
|
-
const value = source.env === undefined ? { ok: false as const } : envValue(env, source.env);
|
|
134
|
-
if (value.ok) return installOne(command, value.value);
|
|
135
|
-
if (source.path !== undefined && source.path.trim() !== '') return installOne(command, source.path.trim());
|
|
136
|
-
return { ok: false, problem: 'env-missing', env: source.env ?? '' };
|
|
137
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, realpathSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { ensureLibsOpencliPeer } from './runtime-links.js';
|
|
6
|
-
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
vi.unstubAllEnvs();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('ensureLibsOpencliPeer', () => {
|
|
12
|
-
it('★ 运行时那份 opencli 在 → 链到 libs,libs 里的命令才 import 得到它', () => {
|
|
13
|
-
const root = mkdtempSync(join(tmpdir(), 'wb-peer-'));
|
|
14
|
-
vi.stubEnv('DSH_WORKBENCH_HOME', root);
|
|
15
|
-
mkdirSync(join(root, 'opencli/toolchain/node_modules/@jackwener/opencli'), { recursive: true });
|
|
16
|
-
|
|
17
|
-
expect(ensureLibsOpencliPeer()).toBe(true);
|
|
18
|
-
const link = join(root, 'opencli/libs/node_modules/@jackwener/opencli');
|
|
19
|
-
expect(existsSync(link)).toBe(true);
|
|
20
|
-
expect(realpathSync(link)).toContain('toolchain/node_modules/@jackwener/opencli');
|
|
21
|
-
expect(ensureLibsOpencliPeer()).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('运行时还没装 opencli → 不建空链接(等它装好再说)', () => {
|
|
25
|
-
const root = mkdtempSync(join(tmpdir(), 'wb-peer-'));
|
|
26
|
-
vi.stubEnv('DSH_WORKBENCH_HOME', root);
|
|
27
|
-
|
|
28
|
-
expect(ensureLibsOpencliPeer()).toBe(false);
|
|
29
|
-
expect(existsSync(join(root, 'opencli/libs/node_modules/@jackwener/opencli'))).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,23 +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
|
-
/**
|
|
6
|
-
* 让共用依赖也能 import opencli:把隔离运行时里那份 opencli 链接到 libs 的 node_modules。
|
|
7
|
-
* 跑在 libs 里的命令(比如 opencli-workflow 带的 gitops)会按包名找 @jackwener/opencli,
|
|
8
|
-
* 找不到就起不来 —— 它不在 libs 的依赖里,是运行时提供的那一份。
|
|
9
|
-
* @returns 这次真的建了链接就 `true`;已经有了、或运行时那份还没装,就是 `false`。
|
|
10
|
-
*/
|
|
11
|
-
export function ensureLibsOpencliPeer(): boolean {
|
|
12
|
-
const link = `${libsPrefix()}/node_modules/${OPENCLI_NPM_PACKAGE}`;
|
|
13
|
-
if (existsSync(link)) return false;
|
|
14
|
-
const target = toolchainOpencliDir();
|
|
15
|
-
if (!existsSync(target)) return false;
|
|
16
|
-
try {
|
|
17
|
-
mkdirSync(dirname(link), { recursive: true });
|
|
18
|
-
symlinkSync(target, link);
|
|
19
|
-
return true;
|
|
20
|
-
} catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|