@dsh-workbench/shared 1.1.14-beta.1 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api/dsh.d.ts +12 -0
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/config/constants.d.ts +5 -1
- package/dist/config/constants.js +5 -1
- package/dist/contract/unit-declaration.js +15 -1
- package/dist/contract/unit.d.ts +3 -0
- package/dist/host/__scratch-guard.d.ts +1 -0
- package/dist/host/__scratch-guard.js +1 -0
- package/dist/host/adapter-set.js +0 -1
- package/dist/host/changelog.js +2 -1
- package/dist/host/channel-plugin.d.ts +15 -0
- package/dist/host/channel-plugin.js +42 -0
- package/dist/host/command-name.d.ts +1 -0
- package/dist/host/command-name.js +14 -0
- package/dist/host/config-sync.d.ts +1 -9
- package/dist/host/config-sync.js +3 -78
- package/dist/host/env-policy.d.ts +75 -0
- package/dist/host/env-policy.js +161 -0
- package/dist/host/extension.js +2 -1
- package/dist/host/flow/run.js +2 -1
- package/dist/host/fs-write.d.ts +62 -0
- package/dist/host/fs-write.js +112 -0
- package/dist/host/host-kit.d.ts +6 -2
- package/dist/host/host-kit.js +3 -1
- package/dist/host/http.js +11 -5
- package/dist/host/installed-sources.js +4 -5
- package/dist/host/long-task.js +3 -3
- package/dist/host/opencli-runner.d.ts +26 -0
- package/dist/host/opencli-runner.js +50 -0
- package/dist/host/package-content.d.ts +2 -2
- package/dist/host/package-content.js +20 -6
- package/dist/host/package-route.d.ts +3 -3
- package/dist/host/package-route.js +7 -6
- package/dist/host/panel-state.js +2 -1
- package/dist/host/patch-blocks.d.ts +13 -0
- package/dist/host/patch-blocks.js +52 -0
- package/dist/host/patch.d.ts +3 -8
- package/dist/host/patch.js +5 -76
- package/dist/host/plugin-source.d.ts +33 -0
- package/dist/host/plugin-source.js +91 -0
- package/dist/host/port-types.d.ts +9 -9
- package/dist/host/profile-registries.js +2 -3
- package/dist/host/registry-declaration.d.ts +2 -2
- package/dist/host/registry-declaration.js +14 -5
- package/dist/host/release-probe.d.ts +4 -4
- package/dist/host/release-probe.js +39 -14
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/runtime-links.d.ts +7 -0
- package/dist/host/runtime-links.js +25 -0
- package/dist/host/seam/denial.d.ts +4 -0
- package/dist/host/seam/denial.js +24 -0
- package/dist/host/seam/env.d.ts +15 -0
- package/dist/host/seam/env.js +17 -0
- package/dist/host/seam/exec.d.ts +84 -0
- package/dist/host/seam/exec.js +1 -0
- package/dist/host/seam/host-env.d.ts +2 -0
- package/dist/host/seam/host-env.js +18 -0
- package/dist/host/seam/intent.d.ts +66 -0
- package/dist/host/seam/intent.js +24 -0
- package/dist/host/seam/policy.d.ts +10 -0
- package/dist/host/seam/policy.js +85 -0
- package/dist/host/seam/process.d.ts +2 -0
- package/dist/host/seam/process.js +80 -0
- package/dist/host/seam/run.d.ts +44 -0
- package/dist/host/seam/run.js +421 -0
- package/dist/host/seam/service.d.ts +4 -0
- package/dist/host/seam/service.js +4 -0
- package/dist/host/seam/unit-seam.d.ts +14 -0
- package/dist/host/seam/unit-seam.js +43 -0
- package/dist/host/shell.d.ts +12 -0
- package/dist/host/shell.js +3 -1
- package/dist/host/time.d.ts +10 -0
- package/dist/host/time.js +14 -0
- package/dist/host/value-handoff.js +3 -2
- package/dist/host/workspace-store.js +2 -8
- package/dist/host.d.ts +11 -0
- package/dist/host.js +11 -0
- package/dist/i18n/en.js +3 -3
- package/dist/i18n/index.d.ts +4 -4
- package/dist/i18n/zh.d.ts +3 -3
- package/dist/i18n/zh.js +3 -3
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.js +21 -0
- package/dist/testing/fake-adapter-set.js +0 -1
- package/dist/testing/git.d.ts +3 -0
- package/dist/testing/git.js +36 -0
- package/dist/testing/host-fakes.d.ts +30 -0
- package/dist/testing/host-fakes.js +104 -0
- package/package.json +5 -2
- package/src/api/dsh.ts +8 -1
- package/src/config/__tests__/constants.test.ts +10 -0
- package/src/config/constants.ts +7 -1
- package/src/contract/unit-declaration.ts +20 -1
- package/src/contract/unit.ts +3 -0
- package/src/host/__tests__/env-policy.test.ts +114 -0
- package/src/host/__tests__/fs-write.test.ts +96 -0
- package/src/host/__tests__/preflight-route.test.ts +29 -0
- package/src/host/__tests__/seam.test.ts +296 -0
- package/src/host/__tests__/shell.test.ts +24 -0
- package/src/host/__tests__/time.test.ts +20 -0
- package/src/host/adapter-set.ts +0 -1
- package/src/host/changelog.ts +2 -1
- package/src/host/config-sync.ts +3 -90
- package/src/host/env-policy.ts +192 -0
- package/src/host/extension.ts +2 -1
- package/src/host/flow/run.ts +2 -1
- package/src/host/fs-write.ts +137 -0
- package/src/host/host-kit.ts +7 -2
- package/src/host/http.ts +11 -5
- package/src/host/installed-sources.ts +4 -5
- package/src/host/long-task.ts +3 -3
- package/src/host/package-content.ts +23 -13
- package/src/host/package-route.ts +10 -9
- package/src/host/panel-state.ts +2 -1
- package/src/host/patch-blocks.ts +67 -0
- package/src/host/patch.ts +5 -89
- package/src/host/port-types.ts +9 -9
- package/src/host/profile-registries.ts +2 -3
- package/src/host/registry-declaration.ts +17 -12
- package/src/host/release-probe.ts +47 -34
- package/src/host/seam/denial.ts +26 -0
- package/src/host/seam/env.ts +23 -0
- package/src/host/seam/exec.ts +94 -0
- package/src/host/seam/host-env.ts +20 -0
- package/src/host/seam/intent.ts +101 -0
- package/src/host/seam/policy.ts +89 -0
- package/src/host/seam/process.ts +90 -0
- package/src/host/seam/run.ts +484 -0
- package/src/host/seam/service.ts +8 -0
- package/src/host/seam/unit-seam.ts +62 -0
- package/src/host/shell.ts +17 -1
- package/src/host/time.ts +16 -0
- package/src/host/value-handoff.ts +3 -2
- package/src/host/workspace-store.ts +2 -8
- package/src/host.ts +11 -0
- package/src/i18n/en.ts +3 -3
- package/src/i18n/zh.ts +3 -3
- package/src/testing/fake-adapter-set.ts +0 -1
- package/src/testing/host-fakes.ts +126 -0
- package/dist/lib/unit-plugin.d.ts +0 -28
- package/dist/lib/unit-plugin.js +0 -23
package/dist/host/patch.js
CHANGED
|
@@ -1,78 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
|
|
6
|
-
function trimmedTail(lines) {
|
|
7
|
-
let end = lines.length;
|
|
8
|
-
while (end > 0 && (lines[end - 1] ?? '').trim() === '')
|
|
9
|
-
end -= 1;
|
|
10
|
-
return lines.slice(0, end);
|
|
11
|
-
}
|
|
12
|
-
function matched(text, pattern) {
|
|
13
|
-
const found = pattern.exec(text);
|
|
14
|
-
return found === null ? undefined : found[1];
|
|
15
|
-
}
|
|
16
|
-
function blockOf(body, index) {
|
|
17
|
-
const lines = body.split('\n');
|
|
18
|
-
const first = lines[0] ?? '';
|
|
19
|
-
const id = matched(first, ID_LINE);
|
|
20
|
-
const nested = lines.flatMap((line) => {
|
|
21
|
-
const one = matched(line, NESTED_ID);
|
|
22
|
-
return one === undefined ? [] : [one];
|
|
23
|
-
});
|
|
24
|
-
const insert = id === undefined && INSERT_LINE.test(first);
|
|
25
|
-
const label = id ?? nested.join(', ');
|
|
26
|
-
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
|
|
27
|
-
const names = lines.flatMap((line) => {
|
|
28
|
-
const one = matched(line, NAME_LINE);
|
|
29
|
-
return one === undefined ? [] : [one];
|
|
30
|
-
});
|
|
31
|
-
return { key, id: label === '' ? key : label, insert, body, names };
|
|
32
|
-
}
|
|
33
|
-
function spannedBlocks(text) {
|
|
34
|
-
const lines = text.split('\n');
|
|
35
|
-
const starts = [];
|
|
36
|
-
lines.forEach((line, index) => {
|
|
37
|
-
if (/^- /.test(line) || line === '-')
|
|
38
|
-
starts.push(index);
|
|
39
|
-
});
|
|
40
|
-
const blocks = [];
|
|
41
|
-
for (let i = 0; i < starts.length; i += 1) {
|
|
42
|
-
const from = starts[i] ?? 0;
|
|
43
|
-
const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
|
|
44
|
-
const body = trimmedTail(lines.slice(from, to)).join('\n');
|
|
45
|
-
if (body.trim() === '')
|
|
46
|
-
continue;
|
|
47
|
-
blocks.push({ ...blockOf(body, blocks.length), from, to });
|
|
48
|
-
}
|
|
49
|
-
return blocks;
|
|
50
|
-
}
|
|
51
|
-
export function patchBlocks(text) {
|
|
52
|
-
return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
|
|
53
|
-
}
|
|
54
|
-
function patchMode(path) {
|
|
55
|
-
try {
|
|
56
|
-
return statSync(path).mode & 0o777;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return 0o644;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
import { writeTextFile } from './fs-write.js';
|
|
2
|
+
import { patchBlocks } from './patch-blocks.js';
|
|
3
|
+
export { patchBlocks };
|
|
4
|
+
/** 把 patch 文本写回原文件:原子写,保留已有权限位(新文件 0644)。 */
|
|
62
5
|
export function writePatchFile(path, text) {
|
|
63
|
-
|
|
64
|
-
const tmp = `${path}.wb-sync-${String(process.pid)}`;
|
|
65
|
-
writeFileSync(tmp, text, { mode });
|
|
66
|
-
try {
|
|
67
|
-
renameSync(tmp, path);
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
try {
|
|
71
|
-
unlinkSync(tmp);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
6
|
+
writeTextFile(path, text, { mode: 'keep' });
|
|
78
7
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { DshHostContext
|
|
1
|
+
import type { DshHostContext } from '../api/dsh.js';
|
|
2
|
+
import type { Seam } from './seam/run.js';
|
|
2
3
|
import type { RuntimeCapabilities, RuntimeProfile } from '../contract/capabilities.js';
|
|
3
4
|
import type { PluginPackageKind } from '../contract/protocol/bundle-install.js';
|
|
4
5
|
import type { LibNeed, SupplySource } from '../contract/needs.js';
|
|
@@ -40,20 +41,20 @@ export interface SupplyPluginInstallOptions {
|
|
|
40
41
|
readonly pluginName?: string | undefined;
|
|
41
42
|
}
|
|
42
43
|
export interface SupplyLatestOptions {
|
|
43
|
-
readonly
|
|
44
|
+
readonly seam: Seam;
|
|
44
45
|
readonly name: string;
|
|
45
46
|
readonly registry: string;
|
|
46
47
|
readonly force?: boolean;
|
|
47
48
|
}
|
|
48
49
|
export interface SupplyMissingPartsOptions {
|
|
49
|
-
readonly
|
|
50
|
+
readonly seam: Seam;
|
|
50
51
|
readonly name: string;
|
|
51
52
|
readonly registry: string;
|
|
52
53
|
readonly version: string;
|
|
53
54
|
readonly force?: boolean;
|
|
54
55
|
}
|
|
55
56
|
export interface SupplyProbeReleaseOptions {
|
|
56
|
-
readonly
|
|
57
|
+
readonly seam: Seam;
|
|
57
58
|
readonly package: string;
|
|
58
59
|
readonly registry?: string | undefined;
|
|
59
60
|
readonly force?: boolean;
|
|
@@ -97,7 +98,7 @@ export interface ModulePort {
|
|
|
97
98
|
}[] | undefined;
|
|
98
99
|
moduleDeclarationOf(dir: string | null, pkg: string): unknown;
|
|
99
100
|
registryDeclarationOf(options: {
|
|
100
|
-
readonly
|
|
101
|
+
readonly seam: Seam;
|
|
101
102
|
readonly name: string;
|
|
102
103
|
readonly registry: string;
|
|
103
104
|
readonly force?: boolean;
|
|
@@ -107,7 +108,7 @@ export interface ModulePort {
|
|
|
107
108
|
refusalReason(): string;
|
|
108
109
|
packageRouteOf(options: {
|
|
109
110
|
readonly spec: string;
|
|
110
|
-
readonly
|
|
111
|
+
readonly seam?: Seam | undefined;
|
|
111
112
|
readonly registry?: string | undefined;
|
|
112
113
|
readonly force?: boolean;
|
|
113
114
|
}): Promise<SupplyPackageRoute | undefined>;
|
|
@@ -119,7 +120,7 @@ export interface SupplyPort extends EnvSupplyPort, ModulePort {
|
|
|
119
120
|
readonly modules: ModulePort;
|
|
120
121
|
}
|
|
121
122
|
export interface ProbeUnitsOptions {
|
|
122
|
-
readonly
|
|
123
|
+
readonly seam: Seam;
|
|
123
124
|
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
124
125
|
readonly units: readonly NeedProbeInput[];
|
|
125
126
|
readonly force?: boolean;
|
|
@@ -127,7 +128,7 @@ export interface ProbeUnitsOptions {
|
|
|
127
128
|
export interface ProbePreflightInput {
|
|
128
129
|
readonly routes: RouteTable;
|
|
129
130
|
readonly ctx: DshHostContext;
|
|
130
|
-
readonly
|
|
131
|
+
readonly seam: Seam;
|
|
131
132
|
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
132
133
|
readonly runtimeCapabilities: RuntimeCapabilities;
|
|
133
134
|
readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
|
|
@@ -167,7 +168,6 @@ export interface RuntimePort {
|
|
|
167
168
|
runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
|
|
168
169
|
health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
|
|
169
170
|
seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
|
|
170
|
-
warnUnwritableRoot(ctx: DshHostContext): void;
|
|
171
171
|
}
|
|
172
172
|
export interface PluginDeclarationSource {
|
|
173
173
|
readonly pkg: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { writeTextFile } from './fs-write.js';
|
|
4
5
|
export const REGISTRY_MARKER = '# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
|
|
5
6
|
function registryRootOf(name, tarball) {
|
|
6
7
|
const at = tarball.indexOf(`/${name}/-/`);
|
|
@@ -91,9 +92,7 @@ function readNpmrc(file) {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
function writeNpmrc(file, text) {
|
|
94
|
-
|
|
95
|
-
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
96
|
-
fs.renameSync(temp, file);
|
|
95
|
+
writeTextFile(file, text, { mode: 0o600 });
|
|
97
96
|
}
|
|
98
97
|
export function createRegistryNotebook(dir) {
|
|
99
98
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Seam } from './seam/run.js';
|
|
2
2
|
export interface RegistryDshLookup {
|
|
3
3
|
readonly readable: boolean;
|
|
4
4
|
readonly raw: unknown;
|
|
@@ -8,7 +8,7 @@ export interface RegistryLookup {
|
|
|
8
8
|
registryDeclarationOf(options: RegistryDeclarationOptions): Promise<unknown | undefined>;
|
|
9
9
|
}
|
|
10
10
|
interface RegistryDshOptions {
|
|
11
|
-
readonly
|
|
11
|
+
readonly seam: Seam;
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly registry?: string | undefined;
|
|
14
14
|
readonly force?: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { now } from './time.js';
|
|
2
|
+
import { productWrite } from './seam/intent.js';
|
|
2
3
|
import { quoteShellArg } from './shell-text.js';
|
|
3
4
|
import { parseJsonLoose } from '../lib/text.js';
|
|
4
5
|
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
@@ -16,18 +17,26 @@ export function createRegistryLookup() {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
async function dshOf(cache, options) {
|
|
19
|
-
const {
|
|
20
|
+
const { seam, name, registry, force = false, cacheDir } = options;
|
|
20
21
|
const key = `${registry ?? ''}|${name}`;
|
|
21
22
|
const hit = cache.get(key);
|
|
22
|
-
if (!force && hit !== undefined &&
|
|
23
|
+
if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
23
24
|
return { readable: true, raw: hit.raw };
|
|
24
25
|
}
|
|
25
26
|
const at = registry === undefined ? '' : ` --registry ${quoteShellArg(registry)}`;
|
|
26
27
|
const cached = cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
27
|
-
const result = await
|
|
28
|
+
const result = await seam
|
|
29
|
+
.run({
|
|
30
|
+
command: `npm view ${quoteShellArg(name)} dsh --json${at}${cached}`,
|
|
31
|
+
intent: productWrite(),
|
|
32
|
+
retry: 'safe',
|
|
33
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
34
|
+
bestEffort: true,
|
|
35
|
+
})
|
|
36
|
+
.catch(() => undefined);
|
|
28
37
|
const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : undefined;
|
|
29
38
|
if (parsed === undefined)
|
|
30
39
|
return { readable: false, raw: undefined };
|
|
31
|
-
cache.set(key, { at:
|
|
40
|
+
cache.set(key, { at: now(), raw: parsed });
|
|
32
41
|
return { readable: true, raw: parsed };
|
|
33
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Seam } from './seam/run.js';
|
|
2
2
|
import type { PluginRelease } from '../contract/unit.js';
|
|
3
3
|
export declare function parseLedgerVersion(stdout: string, name: string): string | undefined;
|
|
4
4
|
export interface ReleaseProbe {
|
|
@@ -9,14 +9,14 @@ export interface ReleaseProbe {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function createReleaseProbe(): ReleaseProbe;
|
|
11
11
|
interface LatestVersionOptions {
|
|
12
|
-
readonly
|
|
12
|
+
readonly seam: Seam;
|
|
13
13
|
readonly name: string;
|
|
14
14
|
readonly registry: string;
|
|
15
15
|
readonly force?: boolean;
|
|
16
16
|
readonly cacheDir?: string | undefined;
|
|
17
17
|
}
|
|
18
18
|
interface MissingPartsOptions {
|
|
19
|
-
readonly
|
|
19
|
+
readonly seam: Seam;
|
|
20
20
|
readonly name: string;
|
|
21
21
|
readonly registry: string;
|
|
22
22
|
readonly version: string;
|
|
@@ -24,7 +24,7 @@ interface MissingPartsOptions {
|
|
|
24
24
|
readonly cacheDir?: string | undefined;
|
|
25
25
|
}
|
|
26
26
|
interface ProbeReleaseOptions {
|
|
27
|
-
readonly
|
|
27
|
+
readonly seam: Seam;
|
|
28
28
|
readonly package: string;
|
|
29
29
|
readonly registry?: string | undefined;
|
|
30
30
|
readonly force?: boolean;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { now } from './time.js';
|
|
2
|
+
import { productWrite } from './seam/intent.js';
|
|
1
3
|
import { RELEASE_PACKAGES } from './release-packages.generated.js';
|
|
2
4
|
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
3
5
|
import { isValidVersion } from './version.js';
|
|
4
6
|
import { parseJsonLoose } from '../lib/text.js';
|
|
5
7
|
import { quoteShellArg } from './shell-text.js';
|
|
6
|
-
import { runShell } from './shell.js';
|
|
7
8
|
function cacheFlag(cacheDir) {
|
|
8
9
|
return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
9
10
|
}
|
|
@@ -38,40 +39,64 @@ export function createReleaseProbe() {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
async function latestVersionOf(latestCache, options) {
|
|
41
|
-
const {
|
|
42
|
+
const { seam, name, registry, force = false, cacheDir } = options;
|
|
42
43
|
const key = `${registry}|${name}`;
|
|
43
44
|
const hit = latestCache.get(key);
|
|
44
|
-
if (!force && hit !== undefined &&
|
|
45
|
+
if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS)
|
|
45
46
|
return hit.value;
|
|
46
|
-
const result = await
|
|
47
|
+
const result = await seam
|
|
48
|
+
.run({
|
|
49
|
+
command: `npm view ${quoteShellArg(name)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
|
|
50
|
+
intent: productWrite(),
|
|
51
|
+
retry: 'safe',
|
|
52
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
53
|
+
bestEffort: true,
|
|
54
|
+
})
|
|
55
|
+
.catch(() => undefined);
|
|
47
56
|
const read = result?.exitCode === 0 ? parseVersion(result.stdout) : undefined;
|
|
48
|
-
latestCache.set(key, { at:
|
|
57
|
+
latestCache.set(key, { at: now(), value: read });
|
|
49
58
|
return read;
|
|
50
59
|
}
|
|
51
60
|
async function versionExists(existsCache, options) {
|
|
52
|
-
const {
|
|
61
|
+
const { seam, name, version, registry, force = false, cacheDir } = options;
|
|
53
62
|
const key = `${registry}|${name}@${version}`;
|
|
54
63
|
const hit = existsCache.get(key);
|
|
55
|
-
if (!force && hit !== undefined && (hit.value ||
|
|
64
|
+
if (!force && hit !== undefined && (hit.value || now() - hit.at < VERSION_CHECK_TTL_MS))
|
|
56
65
|
return hit.value;
|
|
57
|
-
const result = await
|
|
66
|
+
const result = await seam
|
|
67
|
+
.run({
|
|
68
|
+
command: `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
|
|
69
|
+
intent: productWrite(),
|
|
70
|
+
retry: 'safe',
|
|
71
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
72
|
+
bestEffort: true,
|
|
73
|
+
})
|
|
74
|
+
.catch(() => undefined);
|
|
58
75
|
const value = result?.exitCode === 0 && parseVersion(result.stdout) !== undefined;
|
|
59
|
-
existsCache.set(key, { at:
|
|
76
|
+
existsCache.set(key, { at: now(), value });
|
|
60
77
|
return value;
|
|
61
78
|
}
|
|
62
79
|
async function missingPartsOf(latestCache, existsCache, options) {
|
|
63
|
-
const {
|
|
80
|
+
const { seam, name, registry, version, force = false, cacheDir } = options;
|
|
64
81
|
const checked = await Promise.all(RELEASE_PACKAGES.filter((part) => part !== name).map(async (part) => ({
|
|
65
82
|
part,
|
|
66
|
-
present: await versionExists(existsCache, {
|
|
83
|
+
present: await versionExists(existsCache, { seam, name: part, version, registry, force, cacheDir }),
|
|
67
84
|
})));
|
|
68
85
|
return checked.filter((entry) => !entry.present).map((entry) => entry.part);
|
|
69
86
|
}
|
|
70
87
|
async function probeOf(latestCache, options) {
|
|
71
|
-
const {
|
|
88
|
+
const { seam, package: pkg, registry, force = false, check = true, cacheDir, prefix } = options;
|
|
72
89
|
if (pkg === "")
|
|
73
90
|
return undefined;
|
|
74
|
-
const ledger = await
|
|
91
|
+
const ledger = await seam
|
|
92
|
+
.run({
|
|
93
|
+
command: `npm ls --prefix ${quoteShellArg(prefix ?? '')} ${quoteShellArg(pkg)} --json --depth=0${cacheFlag(cacheDir)}`,
|
|
94
|
+
intent: productWrite(),
|
|
95
|
+
retry: 'safe',
|
|
96
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
97
|
+
bestEffort: true,
|
|
98
|
+
})
|
|
99
|
+
.catch(() => undefined);
|
|
75
100
|
const installed = ledger === undefined ? undefined : parseLedgerVersion(ledger.stdout, pkg);
|
|
76
101
|
if (installed === undefined)
|
|
77
102
|
return undefined;
|
|
@@ -80,7 +105,7 @@ async function probeOf(latestCache, options) {
|
|
|
80
105
|
: registry.trim();
|
|
81
106
|
const latest = source === undefined || !check
|
|
82
107
|
? undefined
|
|
83
|
-
: await latestVersionOf(latestCache, {
|
|
108
|
+
: await latestVersionOf(latestCache, { seam, name: pkg, registry: source, force, cacheDir });
|
|
84
109
|
return {
|
|
85
110
|
installed,
|
|
86
111
|
...(latest === undefined ? {} : { latest }),
|
|
@@ -0,0 +1,26 @@
|
|
|
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;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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;
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
/npm (?:error|ERR!) path (.+)$/m,
|
|
3
|
+
/(?:EPERM|EACCES):[^,\n]*, (?:open|unlink|mkdir|scandir|rename|access|stat|lstat|read|write|copyfile|rmdir|opendir|utimes|chmod|chown) '([^']+)'/i,
|
|
4
|
+
/(?:Operation not permitted|Permission denied)[^\n]*?'([^']+)'/i,
|
|
5
|
+
/fatal: could not create work tree dir '([^']+)'/i,
|
|
6
|
+
/mkdir: cannot create directory '([^']+)'/i,
|
|
7
|
+
/cannot (?:create|mkdir) (?:directory )?'([^']+)'/i,
|
|
8
|
+
/(?:Operation not permitted|Permission denied):\s*([^\s'\n]+)/i,
|
|
9
|
+
];
|
|
10
|
+
function firstMatch(text) {
|
|
11
|
+
for (const pattern of PATTERNS) {
|
|
12
|
+
const found = pattern.exec(text);
|
|
13
|
+
const value = found?.[1]?.trim();
|
|
14
|
+
if (value !== undefined && value !== '')
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
export function deniedPathOf(stderr, stdout = '') {
|
|
20
|
+
return firstMatch(stderr) ?? firstMatch(stdout);
|
|
21
|
+
}
|
|
22
|
+
export function sandboxDeniedOf(facts) {
|
|
23
|
+
return facts?.denied === true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ProductEnvInput, type UnitEnvInput } from '../env-policy.js';
|
|
2
|
+
export type SeamEnvInput = ProductEnvInput;
|
|
3
|
+
export type SeamUnitEnvInput = UnitEnvInput;
|
|
4
|
+
/**
|
|
5
|
+
* 产品域(运行时根)的受控 env:隔离 HOME、npm/pnpm 缓存钉在运行时根、真 npmrc/gitconfig 只读指过去。
|
|
6
|
+
* @param input 运行时根、可选的 HOME 与真实用户目录。
|
|
7
|
+
* @returns 产品域 env。
|
|
8
|
+
*/
|
|
9
|
+
export declare function seamProductEnv(input: SeamEnvInput): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* 模块单元的受控 env:HOME 与 npm/pnpm 目录钉到单元数据目录。
|
|
12
|
+
* @param input 运行时根、单元数据目录、可选的 TRUE 用户目录。
|
|
13
|
+
* @returns 单元 env。
|
|
14
|
+
*/
|
|
15
|
+
export declare function seamUnitEnv(input: SeamUnitEnvInput): Record<string, string>;
|