@dsh-workbench/shared 1.1.14-beta.2 → 1.1.15
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/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 +68 -0
- package/dist/host/fs-write.js +121 -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 +88 -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 +415 -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-render.d.ts +9 -0
- package/dist/host/shell-render.js +18 -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 +12 -0
- package/dist/host.js +12 -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/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 +354 -0
- package/src/host/__tests__/shell-render.test.ts +58 -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 +147 -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 +96 -0
- package/src/host/seam/process.ts +90 -0
- package/src/host/seam/run.ts +478 -0
- package/src/host/seam/service.ts +8 -0
- package/src/host/seam/unit-seam.ts +62 -0
- package/src/host/shell-render.ts +28 -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 +12 -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/src/host/http.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now, nowIso } from './time.js';
|
|
1
2
|
import type { DshHostContext, DshRequest, DshResponse } from '../api/dsh.js';
|
|
2
3
|
import { unitFactScope } from './facts.js';
|
|
3
4
|
import { parseCardsReport, parseCrashReport, type PanelStateStore } from './panel-state.js';
|
|
@@ -234,8 +235,13 @@ export function preflightHandler(preflight: {
|
|
|
234
235
|
snapshot(): unknown;
|
|
235
236
|
}): RouteHandler {
|
|
236
237
|
return (search) => {
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
if (flag(search, 'hard')) {
|
|
239
|
+
void preflight.run({ hard: true });
|
|
240
|
+
return { ok: true, ...(preflight.snapshot() as Record<string, unknown>) };
|
|
241
|
+
}
|
|
242
|
+
const snapshot = preflight.snapshot() as Record<string, unknown>;
|
|
243
|
+
void preflight.run({ hard: false });
|
|
244
|
+
return { ok: true, ...snapshot };
|
|
239
245
|
};
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -265,7 +271,7 @@ export function healthHandler(
|
|
|
265
271
|
profile: dir === undefined ? null : dir.split('/').pop(),
|
|
266
272
|
profileDir: dir ?? null,
|
|
267
273
|
pid: process.pid,
|
|
268
|
-
now:
|
|
274
|
+
now: nowIso(),
|
|
269
275
|
...(runtimes === undefined ? {} : { runtimes }),
|
|
270
276
|
};
|
|
271
277
|
};
|
|
@@ -310,7 +316,7 @@ export function registerPanelState(ctx: DshHostContext, store: PanelStateStore):
|
|
|
310
316
|
const parsed = parseCardsReport(payload);
|
|
311
317
|
if (!parsed.ok) return { ok: false, text: parsed.message };
|
|
312
318
|
store.cards(parsed.value);
|
|
313
|
-
return { ok: true, at:
|
|
319
|
+
return { ok: true, at: now() };
|
|
314
320
|
},
|
|
315
321
|
},
|
|
316
322
|
{
|
|
@@ -319,7 +325,7 @@ export function registerPanelState(ctx: DshHostContext, store: PanelStateStore):
|
|
|
319
325
|
const parsed = parseCrashReport(payload);
|
|
320
326
|
if (!parsed.ok) return { ok: false, text: parsed.message };
|
|
321
327
|
store.crash(parsed.value);
|
|
322
|
-
return { ok: true, at:
|
|
328
|
+
return { ok: true, at: now() };
|
|
323
329
|
},
|
|
324
330
|
},
|
|
325
331
|
],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { writeTextFile } from './fs-write.js';
|
|
3
4
|
|
|
4
5
|
export interface InstalledSource {
|
|
5
6
|
readonly version: string;
|
|
@@ -50,11 +51,9 @@ export function packageSourceOf(profileDir: string | null, pkg: string): Install
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
function writeInstalledSources(profileDir: string, sources: InstalledSourceMap): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
fs.writeFileSync(temp, text, { mode: 0o600 });
|
|
57
|
-
fs.renameSync(temp, file);
|
|
54
|
+
writeTextFile(installedSourcesFile(profileDir), `${JSON.stringify({ version: FILE_VERSION, packages: sources }, null, 2)}\n`, {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
});
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export function rememberPackageSource(profileDir: string, pkg: string, source: InstalledSource): void {
|
package/src/host/long-task.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { epochOf } from './time.js';
|
|
1
|
+
import { epochOf, now as clockNow, nowIso } from './time.js';
|
|
2
2
|
import { runsRouteOf, type LongTaskStatus, type LongTaskStep } from '../contract/long-task.js';
|
|
3
3
|
|
|
4
4
|
const LIVE: ReadonlySet<string> = new Set<LongTaskStatus>(['running', 'needs-login']);
|
|
@@ -66,9 +66,9 @@ export interface LongTaskRegistry<S extends LongTaskBase> {
|
|
|
66
66
|
export function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDeps<S>): LongTaskRegistry<S> {
|
|
67
67
|
const runs = new Map<string, LongTaskBase>();
|
|
68
68
|
const controls = new Map<string, AbortController>();
|
|
69
|
-
const now = deps.now ?? (() =>
|
|
69
|
+
const now = deps.now ?? (() => nowIso());
|
|
70
70
|
let counter = 0;
|
|
71
|
-
const nextId = deps.nextId ?? (() => `run-${
|
|
71
|
+
const nextId = deps.nextId ?? (() => `run-${clockNow()}-${(counter += 1)}`);
|
|
72
72
|
const recentMs = deps.recentMs ?? RECENT_MS;
|
|
73
73
|
const listeners = new Set<(change: LongTaskChange<S>) => void>();
|
|
74
74
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import { productWrite } from './seam/intent.js';
|
|
5
|
+
import type { Seam } from './seam/run.js';
|
|
5
6
|
import { INSTALL_TIMEOUT_MS } from '../config/constants.js';
|
|
6
7
|
import { parseJsonLoose } from '../lib/text.js';
|
|
7
8
|
import { quoteShellArg } from './shell-text.js';
|
|
8
|
-
import { runShell } from './shell.js';
|
|
9
9
|
import { splitNpmSpec } from './package-kind.js';
|
|
10
10
|
import { localDirOf } from './local-declaration.js';
|
|
11
11
|
|
|
@@ -34,7 +34,7 @@ function packedFileFrom(stdout: string, root: string): string | undefined {
|
|
|
34
34
|
|
|
35
35
|
export async function withPackageDir<T>(
|
|
36
36
|
options: {
|
|
37
|
-
readonly
|
|
37
|
+
readonly seam?: Seam | undefined;
|
|
38
38
|
readonly spec: string;
|
|
39
39
|
readonly registry?: string | undefined;
|
|
40
40
|
readonly cacheDir?: string | undefined;
|
|
@@ -44,8 +44,8 @@ export async function withPackageDir<T>(
|
|
|
44
44
|
const local = localDirOf(options.spec);
|
|
45
45
|
if (local !== undefined) return visit(local);
|
|
46
46
|
if (splitNpmSpec(options.spec) === undefined) return undefined;
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
47
|
+
const seam = options.seam;
|
|
48
|
+
if (seam === undefined) return undefined;
|
|
49
49
|
|
|
50
50
|
let root: string;
|
|
51
51
|
try {
|
|
@@ -55,17 +55,27 @@ export async function withPackageDir<T>(
|
|
|
55
55
|
}
|
|
56
56
|
try {
|
|
57
57
|
const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
|
|
58
|
-
const packed = await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const packed = await seam
|
|
59
|
+
.run({
|
|
60
|
+
command: `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
|
|
61
|
+
intent: productWrite(),
|
|
62
|
+
retry: 'safe',
|
|
63
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
64
|
+
bestEffort: true,
|
|
65
|
+
})
|
|
66
|
+
.catch(() => undefined);
|
|
63
67
|
if (packed === undefined || packed.exitCode !== 0) return undefined;
|
|
64
68
|
const file = packedFileFrom(packed.stdout, root);
|
|
65
69
|
if (file === undefined) return undefined;
|
|
66
|
-
const unpacked = await
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const unpacked = await seam
|
|
71
|
+
.run({
|
|
72
|
+
command: `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`,
|
|
73
|
+
intent: productWrite(),
|
|
74
|
+
retry: 'safe',
|
|
75
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
76
|
+
bestEffort: true,
|
|
77
|
+
})
|
|
78
|
+
.catch(() => undefined);
|
|
69
79
|
if (unpacked === undefined || unpacked.exitCode !== 0) return undefined;
|
|
70
80
|
const dir = path.join(root, 'package');
|
|
71
81
|
if (!fs.existsSync(dir)) return undefined;
|
|
@@ -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
|
|