@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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { now } from './time.js';
|
|
2
|
+
import type { Seam } from './seam/run.js';
|
|
2
3
|
import { VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
3
4
|
import { dshMarkersOf, splitNpmSpec, verdictOf, type PackageVerdict } from './package-kind.js';
|
|
4
5
|
import { withPackageDir } from './package-content.js';
|
|
@@ -30,7 +31,7 @@ export interface PackageRouter {
|
|
|
30
31
|
|
|
31
32
|
interface RouteOptions {
|
|
32
33
|
readonly spec: string;
|
|
33
|
-
readonly
|
|
34
|
+
readonly seam?: Seam | undefined;
|
|
34
35
|
readonly registry?: string | undefined;
|
|
35
36
|
readonly force?: boolean;
|
|
36
37
|
}
|
|
@@ -38,7 +39,7 @@ interface RouteOptions {
|
|
|
38
39
|
export interface PackageRouterOptions {
|
|
39
40
|
readonly manifestFile: string;
|
|
40
41
|
readonly cacheDir?: string | undefined;
|
|
41
|
-
readonly dshLookup?: ((options: {
|
|
42
|
+
readonly dshLookup?: ((options: { seam: Seam; name: string; registry: string }) => Promise<{ readable: boolean; raw: unknown }>) | undefined;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function createPackageRouter(options: PackageRouterOptions): PackageRouter {
|
|
@@ -64,30 +65,30 @@ async function routeOf(
|
|
|
64
65
|
|
|
65
66
|
const ref = splitNpmSpec(routeOptions.spec);
|
|
66
67
|
if (ref === undefined) return undefined;
|
|
67
|
-
const
|
|
68
|
+
const seam = routeOptions.seam;
|
|
68
69
|
const registry = routeOptions.registry;
|
|
69
|
-
if (
|
|
70
|
+
if (seam === undefined || registry === undefined) return undefined;
|
|
70
71
|
|
|
71
72
|
const key = `${registry}|${routeOptions.spec}`;
|
|
72
73
|
const hit = cache.get(key);
|
|
73
|
-
if (routeOptions.force !== true && hit !== undefined &&
|
|
74
|
+
if (routeOptions.force !== true && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
74
75
|
return hit.route ?? undefined;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
const found =
|
|
78
79
|
options.dshLookup === undefined
|
|
79
80
|
? { readable: false, raw: undefined }
|
|
80
|
-
: await options.dshLookup({
|
|
81
|
+
: await options.dshLookup({ seam, name: routeOptions.spec, registry });
|
|
81
82
|
let route: PackageRoute | undefined;
|
|
82
83
|
if (found.readable && dshMarkersOf(found.raw)) {
|
|
83
84
|
route = { kind: 'dsh-bundle' };
|
|
84
85
|
} else {
|
|
85
86
|
const verdict = await withPackageDir(
|
|
86
|
-
{
|
|
87
|
+
{ seam, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) },
|
|
87
88
|
(dir) => verdictOf(dir, options.manifestFile),
|
|
88
89
|
);
|
|
89
90
|
route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
|
|
90
91
|
}
|
|
91
|
-
cache.set(key, { at:
|
|
92
|
+
cache.set(key, { at: now(), route: route ?? null });
|
|
92
93
|
return route;
|
|
93
94
|
}
|
package/src/host/panel-state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now } from './time.js';
|
|
1
2
|
/**
|
|
2
3
|
* 一张面板卡片取数走到哪一步:`idle` 还没取、`loading` 取着、`ready` 拿到了、`error` 失败了。
|
|
3
4
|
*/
|
|
@@ -188,7 +189,7 @@ export function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport>
|
|
|
188
189
|
if (workspace === '') return { ok: false, message: 'workspace 不能是空的' };
|
|
189
190
|
if (unit === '') return { ok: false, message: 'unit 不能是空的' };
|
|
190
191
|
if (reason === '') return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
|
|
191
|
-
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ??
|
|
192
|
+
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? now() } };
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
/**
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface PatchBlock {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly insert: boolean;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
readonly names: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SpannedBlock extends PatchBlock {
|
|
10
|
+
readonly from: number;
|
|
11
|
+
readonly to: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
|
|
15
|
+
const INSERT_LINE = /^- insert:[ \t]*$/;
|
|
16
|
+
const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
|
|
17
|
+
const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
|
|
18
|
+
|
|
19
|
+
function trimmedTail(lines: readonly string[]): readonly string[] {
|
|
20
|
+
let end = lines.length;
|
|
21
|
+
while (end > 0 && (lines[end - 1] ?? '').trim() === '') end -= 1;
|
|
22
|
+
return lines.slice(0, end);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function matched(text: string, pattern: RegExp): string | undefined {
|
|
26
|
+
const found = pattern.exec(text);
|
|
27
|
+
return found === null ? undefined : found[1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function blockOf(body: string, index: number): PatchBlock {
|
|
31
|
+
const lines = body.split('\n');
|
|
32
|
+
const first = lines[0] ?? '';
|
|
33
|
+
const id = matched(first, ID_LINE);
|
|
34
|
+
const nested = lines.flatMap((line) => {
|
|
35
|
+
const one = matched(line, NESTED_ID);
|
|
36
|
+
return one === undefined ? [] : [one];
|
|
37
|
+
});
|
|
38
|
+
const insert = id === undefined && INSERT_LINE.test(first);
|
|
39
|
+
const label = id ?? nested.join(', ');
|
|
40
|
+
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
|
|
41
|
+
const names = lines.flatMap((line) => {
|
|
42
|
+
const one = matched(line, NAME_LINE);
|
|
43
|
+
return one === undefined ? [] : [one];
|
|
44
|
+
});
|
|
45
|
+
return { key, id: label === '' ? key : label, insert, body, names };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function spannedBlocks(text: string): SpannedBlock[] {
|
|
49
|
+
const lines = text.split('\n');
|
|
50
|
+
const starts: number[] = [];
|
|
51
|
+
lines.forEach((line, index) => {
|
|
52
|
+
if (/^- /.test(line) || line === '-') starts.push(index);
|
|
53
|
+
});
|
|
54
|
+
const blocks: SpannedBlock[] = [];
|
|
55
|
+
for (let i = 0; i < starts.length; i += 1) {
|
|
56
|
+
const from = starts[i] ?? 0;
|
|
57
|
+
const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
|
|
58
|
+
const body = trimmedTail(lines.slice(from, to)).join('\n');
|
|
59
|
+
if (body.trim() === '') continue;
|
|
60
|
+
blocks.push({ ...blockOf(body, blocks.length), from, to });
|
|
61
|
+
}
|
|
62
|
+
return blocks;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function patchBlocks(text: string): PatchBlock[] {
|
|
66
|
+
return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
|
|
67
|
+
}
|
package/src/host/patch.ts
CHANGED
|
@@ -1,93 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeTextFile } from './fs-write.js';
|
|
2
|
+
import { patchBlocks, type PatchBlock } from './patch-blocks.js';
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
readonly key: string;
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly insert: boolean;
|
|
7
|
-
readonly body: string;
|
|
8
|
-
readonly names: readonly string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface SpannedBlock extends PatchBlock {
|
|
12
|
-
readonly from: number;
|
|
13
|
-
readonly to: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
|
|
17
|
-
const INSERT_LINE = /^- insert:[ \t]*$/;
|
|
18
|
-
const NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
|
|
19
|
-
const NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
|
|
20
|
-
|
|
21
|
-
function trimmedTail(lines: readonly string[]): readonly string[] {
|
|
22
|
-
let end = lines.length;
|
|
23
|
-
while (end > 0 && (lines[end - 1] ?? '').trim() === '') end -= 1;
|
|
24
|
-
return lines.slice(0, end);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function matched(text: string, pattern: RegExp): string | undefined {
|
|
28
|
-
const found = pattern.exec(text);
|
|
29
|
-
return found === null ? undefined : found[1];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function blockOf(body: string, index: number): PatchBlock {
|
|
33
|
-
const lines = body.split('\n');
|
|
34
|
-
const first = lines[0] ?? '';
|
|
35
|
-
const id = matched(first, ID_LINE);
|
|
36
|
-
const nested = lines.flatMap((line) => {
|
|
37
|
-
const one = matched(line, NESTED_ID);
|
|
38
|
-
return one === undefined ? [] : [one];
|
|
39
|
-
});
|
|
40
|
-
const insert = id === undefined && INSERT_LINE.test(first);
|
|
41
|
-
const label = id ?? nested.join(', ');
|
|
42
|
-
const key = id !== undefined ? `id:${id}` : insert ? `insert:${nested.join(', ')}` : `block:${String(index)}`;
|
|
43
|
-
const names = lines.flatMap((line) => {
|
|
44
|
-
const one = matched(line, NAME_LINE);
|
|
45
|
-
return one === undefined ? [] : [one];
|
|
46
|
-
});
|
|
47
|
-
return { key, id: label === '' ? key : label, insert, body, names };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function spannedBlocks(text: string): SpannedBlock[] {
|
|
51
|
-
const lines = text.split('\n');
|
|
52
|
-
const starts: number[] = [];
|
|
53
|
-
lines.forEach((line, index) => {
|
|
54
|
-
if (/^- /.test(line) || line === '-') starts.push(index);
|
|
55
|
-
});
|
|
56
|
-
const blocks: SpannedBlock[] = [];
|
|
57
|
-
for (let i = 0; i < starts.length; i += 1) {
|
|
58
|
-
const from = starts[i] ?? 0;
|
|
59
|
-
const to = i + 1 < starts.length ? (starts[i + 1] ?? lines.length) : lines.length;
|
|
60
|
-
const body = trimmedTail(lines.slice(from, to)).join('\n');
|
|
61
|
-
if (body.trim() === '') continue;
|
|
62
|
-
blocks.push({ ...blockOf(body, blocks.length), from, to });
|
|
63
|
-
}
|
|
64
|
-
return blocks;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function patchBlocks(text: string): PatchBlock[] {
|
|
68
|
-
return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function patchMode(path: string): number {
|
|
72
|
-
try {
|
|
73
|
-
return statSync(path).mode & 0o777;
|
|
74
|
-
} catch {
|
|
75
|
-
return 0o644;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
4
|
+
export { patchBlocks, type PatchBlock };
|
|
78
5
|
|
|
6
|
+
/** 把 patch 文本写回原文件:原子写,保留已有权限位(新文件 0644)。 */
|
|
79
7
|
export function writePatchFile(path: string, text: string): void {
|
|
80
|
-
|
|
81
|
-
const tmp = `${path}.wb-sync-${String(process.pid)}`;
|
|
82
|
-
writeFileSync(tmp, text, { mode });
|
|
83
|
-
try {
|
|
84
|
-
renameSync(tmp, path);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
try {
|
|
87
|
-
unlinkSync(tmp);
|
|
88
|
-
} catch {
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
8
|
+
writeTextFile(path, text, { mode: 'keep' });
|
|
93
9
|
}
|
package/src/host/port-types.ts
CHANGED
|
@@ -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';
|
|
@@ -57,14 +58,14 @@ export interface SupplyPluginInstallOptions {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface SupplyLatestOptions {
|
|
60
|
-
readonly
|
|
61
|
+
readonly seam: Seam;
|
|
61
62
|
readonly name: string;
|
|
62
63
|
readonly registry: string;
|
|
63
64
|
readonly force?: boolean;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface SupplyMissingPartsOptions {
|
|
67
|
-
readonly
|
|
68
|
+
readonly seam: Seam;
|
|
68
69
|
readonly name: string;
|
|
69
70
|
readonly registry: string;
|
|
70
71
|
readonly version: string;
|
|
@@ -72,7 +73,7 @@ export interface SupplyMissingPartsOptions {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export interface SupplyProbeReleaseOptions {
|
|
75
|
-
readonly
|
|
76
|
+
readonly seam: Seam;
|
|
76
77
|
readonly package: string;
|
|
77
78
|
readonly registry?: string | undefined;
|
|
78
79
|
readonly force?: boolean;
|
|
@@ -116,7 +117,7 @@ export interface ModulePort {
|
|
|
116
117
|
declarationGaps(dir: string | null): readonly { readonly pkg: string }[] | undefined;
|
|
117
118
|
moduleDeclarationOf(dir: string | null, pkg: string): unknown;
|
|
118
119
|
registryDeclarationOf(options: {
|
|
119
|
-
readonly
|
|
120
|
+
readonly seam: Seam;
|
|
120
121
|
readonly name: string;
|
|
121
122
|
readonly registry: string;
|
|
122
123
|
readonly force?: boolean;
|
|
@@ -126,7 +127,7 @@ export interface ModulePort {
|
|
|
126
127
|
refusalReason(): string;
|
|
127
128
|
packageRouteOf(options: {
|
|
128
129
|
readonly spec: string;
|
|
129
|
-
readonly
|
|
130
|
+
readonly seam?: Seam | undefined;
|
|
130
131
|
readonly registry?: string | undefined;
|
|
131
132
|
readonly force?: boolean;
|
|
132
133
|
}): Promise<SupplyPackageRoute | undefined>;
|
|
@@ -140,7 +141,7 @@ export interface SupplyPort extends EnvSupplyPort, ModulePort {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
export interface ProbeUnitsOptions {
|
|
143
|
-
readonly
|
|
144
|
+
readonly seam: Seam;
|
|
144
145
|
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
145
146
|
readonly units: readonly NeedProbeInput[];
|
|
146
147
|
readonly force?: boolean;
|
|
@@ -149,7 +150,7 @@ export interface ProbeUnitsOptions {
|
|
|
149
150
|
export interface ProbePreflightInput {
|
|
150
151
|
readonly routes: RouteTable;
|
|
151
152
|
readonly ctx: DshHostContext;
|
|
152
|
-
readonly
|
|
153
|
+
readonly seam: Seam;
|
|
153
154
|
readonly runtimeCommands: Readonly<Record<string, string>>;
|
|
154
155
|
readonly runtimeCapabilities: RuntimeCapabilities;
|
|
155
156
|
readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
|
|
@@ -195,7 +196,6 @@ export interface RuntimePort {
|
|
|
195
196
|
runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
|
|
196
197
|
health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
|
|
197
198
|
seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
|
|
198
|
-
warnUnwritableRoot(ctx: DshHostContext): void;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
export interface PluginDeclarationSource {
|
|
@@ -2,6 +2,7 @@ import type { RegistryEntry, RegistryNotebook } from './adapter-types.js';
|
|
|
2
2
|
import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { writeTextFile } from './fs-write.js';
|
|
5
6
|
export const REGISTRY_MARKER =
|
|
6
7
|
'# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
|
|
7
8
|
|
|
@@ -93,9 +94,7 @@ function readNpmrc(file: string): string {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function writeNpmrc(file: string, text: string): void {
|
|
96
|
-
|
|
97
|
-
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
98
|
-
fs.renameSync(temp, file);
|
|
97
|
+
writeTextFile(file, text, { mode: 0o600 });
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
export function createRegistryNotebook(dir: string | null): RegistryNotebook {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { now } from './time.js';
|
|
2
|
+
import { productWrite } from './seam/intent.js';
|
|
3
|
+
import type { Seam } from './seam/run.js';
|
|
3
4
|
import { quoteShellArg } from './shell-text.js';
|
|
4
5
|
import { parseJsonLoose } from '../lib/text.js';
|
|
5
6
|
import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
@@ -19,7 +20,7 @@ export interface RegistryLookup {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface RegistryDshOptions {
|
|
22
|
-
readonly
|
|
23
|
+
readonly seam: Seam;
|
|
23
24
|
readonly name: string;
|
|
24
25
|
readonly registry?: string | undefined;
|
|
25
26
|
readonly force?: boolean;
|
|
@@ -42,29 +43,33 @@ export function createRegistryLookup(): RegistryLookup {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
async function dshOf(cache: Map<string, { at: number; raw: unknown }>, options: {
|
|
45
|
-
|
|
46
|
+
seam: Seam;
|
|
46
47
|
name: string;
|
|
47
48
|
registry?: string | undefined;
|
|
48
49
|
force?: boolean;
|
|
49
50
|
cacheDir?: string | undefined;
|
|
50
51
|
}): Promise<RegistryDshLookup> {
|
|
51
|
-
const {
|
|
52
|
+
const { seam, name, registry, force = false, cacheDir } = options;
|
|
52
53
|
const key = `${registry ?? ''}|${name}`;
|
|
53
54
|
const hit = cache.get(key);
|
|
54
|
-
if (!force && hit !== undefined &&
|
|
55
|
+
if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
55
56
|
return { readable: true, raw: hit.raw };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const at = registry === undefined ? '' : ` --registry ${quoteShellArg(registry)}`;
|
|
59
60
|
const cached = cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
60
|
-
const result = await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const result = await seam
|
|
62
|
+
.run({
|
|
63
|
+
command: `npm view ${quoteShellArg(name)} dsh --json${at}${cached}`,
|
|
64
|
+
intent: productWrite(),
|
|
65
|
+
retry: 'safe',
|
|
66
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
67
|
+
bestEffort: true,
|
|
68
|
+
})
|
|
69
|
+
.catch(() => undefined);
|
|
65
70
|
const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : undefined;
|
|
66
71
|
if (parsed === undefined) return { readable: false, raw: undefined };
|
|
67
|
-
cache.set(key, { at:
|
|
72
|
+
cache.set(key, { at: now(), raw: parsed });
|
|
68
73
|
return { readable: true, raw: parsed };
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { now } from './time.js';
|
|
2
|
+
import type { Seam } from './seam/run.js';
|
|
3
|
+
import { productWrite } from './seam/intent.js';
|
|
2
4
|
import type { PluginRelease } from '../contract/unit.js';
|
|
3
5
|
|
|
4
6
|
import { RELEASE_PACKAGES } from './release-packages.generated.js';
|
|
@@ -6,7 +8,6 @@ import { VERSION_CHECK_TIMEOUT_MS, VERSION_CHECK_TTL_MS } from '../config/consta
|
|
|
6
8
|
import { isValidVersion } from './version.js';
|
|
7
9
|
import { parseJsonLoose } from '../lib/text.js';
|
|
8
10
|
import { quoteShellArg } from './shell-text.js';
|
|
9
|
-
import { runShell } from './shell.js';
|
|
10
11
|
|
|
11
12
|
function cacheFlag(cacheDir: string | undefined): string {
|
|
12
13
|
return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
@@ -61,7 +62,7 @@ export function createReleaseProbe(): ReleaseProbe {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface LatestVersionOptions {
|
|
64
|
-
readonly
|
|
65
|
+
readonly seam: Seam;
|
|
65
66
|
readonly name: string;
|
|
66
67
|
readonly registry: string;
|
|
67
68
|
readonly force?: boolean;
|
|
@@ -69,7 +70,7 @@ interface LatestVersionOptions {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
interface MissingPartsOptions {
|
|
72
|
-
readonly
|
|
73
|
+
readonly seam: Seam;
|
|
73
74
|
readonly name: string;
|
|
74
75
|
readonly registry: string;
|
|
75
76
|
readonly version: string;
|
|
@@ -78,7 +79,7 @@ interface MissingPartsOptions {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
interface ProbeReleaseOptions {
|
|
81
|
-
readonly
|
|
82
|
+
readonly seam: Seam;
|
|
82
83
|
readonly package: string;
|
|
83
84
|
readonly registry?: string | undefined;
|
|
84
85
|
readonly force?: boolean;
|
|
@@ -88,25 +89,29 @@ interface ProbeReleaseOptions {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async function latestVersionOf(latestCache: Map<string, CacheEntry>, options: {
|
|
91
|
-
|
|
92
|
+
seam: Seam;
|
|
92
93
|
name: string;
|
|
93
94
|
registry: string;
|
|
94
95
|
force?: boolean;
|
|
95
96
|
cacheDir?: string | undefined;
|
|
96
97
|
}): Promise<string | undefined> {
|
|
97
|
-
const {
|
|
98
|
+
const { seam, name, registry, force = false, cacheDir } = options;
|
|
98
99
|
const key = `${registry}|${name}`;
|
|
99
100
|
const hit = latestCache.get(key);
|
|
100
|
-
if (!force && hit !== undefined &&
|
|
101
|
+
if (!force && hit !== undefined && now() - hit.at < VERSION_CHECK_TTL_MS)
|
|
101
102
|
return hit.value;
|
|
102
103
|
|
|
103
|
-
const result = await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const result = await seam
|
|
105
|
+
.run({
|
|
106
|
+
command: `npm view ${quoteShellArg(name)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
|
|
107
|
+
intent: productWrite(),
|
|
108
|
+
retry: 'safe',
|
|
109
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
110
|
+
bestEffort: true,
|
|
111
|
+
})
|
|
112
|
+
.catch(() => undefined);
|
|
108
113
|
const read = result?.exitCode === 0 ? parseVersion(result.stdout) : undefined;
|
|
109
|
-
latestCache.set(key, { at:
|
|
114
|
+
latestCache.set(key, { at: now(), value: read });
|
|
110
115
|
return read;
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -119,26 +124,30 @@ interface ExistsEntry {
|
|
|
119
124
|
async function versionExists(
|
|
120
125
|
existsCache: Map<string, ExistsEntry>,
|
|
121
126
|
options: {
|
|
122
|
-
|
|
127
|
+
seam: Seam;
|
|
123
128
|
name: string;
|
|
124
129
|
version: string;
|
|
125
130
|
registry: string;
|
|
126
131
|
force?: boolean;
|
|
127
132
|
cacheDir?: string | undefined;
|
|
128
133
|
}): Promise<boolean> {
|
|
129
|
-
const {
|
|
134
|
+
const { seam, name, version, registry, force = false, cacheDir } = options;
|
|
130
135
|
const key = `${registry}|${name}@${version}`;
|
|
131
136
|
const hit = existsCache.get(key);
|
|
132
|
-
if (!force && hit !== undefined && (hit.value ||
|
|
137
|
+
if (!force && hit !== undefined && (hit.value || now() - hit.at < VERSION_CHECK_TTL_MS))
|
|
133
138
|
return hit.value;
|
|
134
139
|
|
|
135
|
-
const result = await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
const result = await seam
|
|
141
|
+
.run({
|
|
142
|
+
command: `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)}${cacheFlag(cacheDir)}`,
|
|
143
|
+
intent: productWrite(),
|
|
144
|
+
retry: 'safe',
|
|
145
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
146
|
+
bestEffort: true,
|
|
147
|
+
})
|
|
148
|
+
.catch(() => undefined);
|
|
140
149
|
const value = result?.exitCode === 0 && parseVersion(result.stdout) !== undefined;
|
|
141
|
-
existsCache.set(key, { at:
|
|
150
|
+
existsCache.set(key, { at: now(), value });
|
|
142
151
|
return value;
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -146,18 +155,18 @@ async function missingPartsOf(
|
|
|
146
155
|
latestCache: Map<string, CacheEntry>,
|
|
147
156
|
existsCache: Map<string, ExistsEntry>,
|
|
148
157
|
options: {
|
|
149
|
-
|
|
158
|
+
seam: Seam;
|
|
150
159
|
name: string;
|
|
151
160
|
registry: string;
|
|
152
161
|
version: string;
|
|
153
162
|
force?: boolean;
|
|
154
163
|
cacheDir?: string | undefined;
|
|
155
164
|
}): Promise<readonly string[]> {
|
|
156
|
-
const {
|
|
165
|
+
const { seam, name, registry, version, force = false, cacheDir } = options;
|
|
157
166
|
const checked = await Promise.all(
|
|
158
167
|
RELEASE_PACKAGES.filter((part) => part !== name).map(async (part) => ({
|
|
159
168
|
part,
|
|
160
|
-
present: await versionExists(existsCache, {
|
|
169
|
+
present: await versionExists(existsCache, { seam, name: part, version, registry, force, cacheDir }),
|
|
161
170
|
})),
|
|
162
171
|
);
|
|
163
172
|
return checked.filter((entry) => !entry.present).map((entry) => entry.part);
|
|
@@ -166,7 +175,7 @@ async function missingPartsOf(
|
|
|
166
175
|
async function probeOf(
|
|
167
176
|
latestCache: Map<string, CacheEntry>,
|
|
168
177
|
options: {
|
|
169
|
-
|
|
178
|
+
seam: Seam;
|
|
170
179
|
package: string;
|
|
171
180
|
registry?: string | undefined;
|
|
172
181
|
force?: boolean;
|
|
@@ -174,14 +183,18 @@ async function probeOf(
|
|
|
174
183
|
cacheDir?: string | undefined;
|
|
175
184
|
prefix?: string | undefined;
|
|
176
185
|
}): Promise<PluginRelease | undefined> {
|
|
177
|
-
const {
|
|
186
|
+
const { seam, package: pkg, registry, force = false, check = true, cacheDir, prefix } = options;
|
|
178
187
|
if (pkg === "") return undefined;
|
|
179
188
|
|
|
180
|
-
const ledger = await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
const ledger = await seam
|
|
190
|
+
.run({
|
|
191
|
+
command: `npm ls --prefix ${quoteShellArg(prefix ?? '')} ${quoteShellArg(pkg)} --json --depth=0${cacheFlag(cacheDir)}`,
|
|
192
|
+
intent: productWrite(),
|
|
193
|
+
retry: 'safe',
|
|
194
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
195
|
+
bestEffort: true,
|
|
196
|
+
})
|
|
197
|
+
.catch(() => undefined);
|
|
185
198
|
const installed =
|
|
186
199
|
ledger === undefined ? undefined : parseLedgerVersion(ledger.stdout, pkg);
|
|
187
200
|
if (installed === undefined) return undefined;
|
|
@@ -193,7 +206,7 @@ async function probeOf(
|
|
|
193
206
|
const latest =
|
|
194
207
|
source === undefined || !check
|
|
195
208
|
? undefined
|
|
196
|
-
: await latestVersionOf(latestCache, {
|
|
209
|
+
: await latestVersionOf(latestCache, { seam, name: pkg, registry: source, force, cacheDir });
|
|
197
210
|
|
|
198
211
|
return {
|
|
199
212
|
installed,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const PATTERNS: readonly RegExp[] = [
|
|
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
|
+
|
|
11
|
+
function firstMatch(text: string): string | undefined {
|
|
12
|
+
for (const pattern of PATTERNS) {
|
|
13
|
+
const found = pattern.exec(text);
|
|
14
|
+
const value = found?.[1]?.trim();
|
|
15
|
+
if (value !== undefined && value !== '') return value;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deniedPathOf(stderr: string, stdout = ''): string | undefined {
|
|
21
|
+
return firstMatch(stderr) ?? firstMatch(stdout);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sandboxDeniedOf(facts: { readonly denied?: boolean | undefined } | undefined): boolean {
|
|
25
|
+
return facts?.denied === true;
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { productEnvOf, unitEnvOf, type ProductEnvInput, type UnitEnvInput } from '../env-policy.js';
|
|
2
|
+
|
|
3
|
+
export type SeamEnvInput = ProductEnvInput;
|
|
4
|
+
|
|
5
|
+
export type SeamUnitEnvInput = UnitEnvInput;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 产品域(运行时根)的受控 env:隔离 HOME、npm/pnpm 缓存钉在运行时根、真 npmrc/gitconfig 只读指过去。
|
|
9
|
+
* @param input 运行时根、可选的 HOME 与真实用户目录。
|
|
10
|
+
* @returns 产品域 env。
|
|
11
|
+
*/
|
|
12
|
+
export function seamProductEnv(input: SeamEnvInput): Record<string, string> {
|
|
13
|
+
return productEnvOf(input);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 模块单元的受控 env:HOME 与 npm/pnpm 目录钉到单元数据目录。
|
|
18
|
+
* @param input 运行时根、单元数据目录、可选的 TRUE 用户目录。
|
|
19
|
+
* @returns 单元 env。
|
|
20
|
+
*/
|
|
21
|
+
export function seamUnitEnv(input: SeamUnitEnvInput): Record<string, string> {
|
|
22
|
+
return unitEnvOf(input);
|
|
23
|
+
}
|