@mearl/setup 3.0.0-beta.7 → 3.0.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.js +44 -3
- package/dist/presetStore.d.ts +2 -46
- package/dist/presetStore.js +1 -248
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mearl/setup
|
|
2
2
|
|
|
3
|
-
3.0 当前以 beta 发行。使用 `@beta` 入口安装同版本的 setup
|
|
3
|
+
3.0 当前以 beta 发行。使用 `@beta` 入口安装同版本的 setup、运行包和扩展;正式版使用 `latest` 标签。
|
|
4
4
|
|
|
5
5
|
Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全局安装 setup:
|
|
6
6
|
|
|
@@ -29,13 +29,13 @@ npx @mearl/setup@beta preset repair [--replace-skill <name> ...]
|
|
|
29
29
|
一个命令安装 skill 并注册它的 CLI、SDK 和 MCP 能力:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx @mearl/setup@beta plugin install https://github.com/mearlhq/mearl
|
|
32
|
+
npx @mearl/setup@beta plugin install https://github.com/mearlhq/mearl --skill browser-tab-manager --yes
|
|
33
33
|
mearl tabs summary
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
安装参数传给固定版本的公共 Skills CLI(当前 skills@1.5.0,兼容项目的 Node 最低版本),使用全局共享目录。公共 CLI 的默认 symlink 和复制模式都可注册;复制模式通过 list --global --json 确认本次成功结果对应的 canonical path,不猜测目录,也不扫描其他 Skills。自定义 CLI 可通过 MEARL_SKILLS_PATH 指定。参数不接受截断后置选项的 -- 分隔符。
|
|
37
37
|
|
|
38
|
-
安装宿主提供的本地插件目录决定哪些能力由 preset 管理。install/update
|
|
38
|
+
安装宿主提供的本地插件目录决定哪些能力由 preset 管理。install/update 维护目录中的托管注册,保留用户自行注册的插件。卸载最后一个依赖环境时移除托管插件与 Mearl Skill;其他环境需要时保留。配置准备、宿主同步和实际安装状态共用 [安装合同](../../docs/preset-installation.md)。
|
|
39
39
|
|
|
40
40
|
Native preset 批次新建的插件注册使用 setup 托管归属,后续通过 Agent 更新仍保留该归属;同目录已有的用户注册保持原归属,自定义用户别名也会保留。托管卸载遇到用户别名共用同一 Skill 时,只清除托管注册并保留 Skill 文件。
|
|
41
41
|
|
|
@@ -89,7 +89,7 @@ npx @mearl/setup@beta provider update example-device
|
|
|
89
89
|
npx @mearl/setup@beta provider uninstall example-device
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope
|
|
92
|
+
安装参数接受包名、版本或 dist-tag;更新和卸载可使用 providerId 或完整包名。核心 @mearl 包固定使用公共 npm registry;其余 scope 沿用调用方的包管理器配置。安装沿用启动 setup 的包管理器,
|
|
93
93
|
更新和卸载沿用当前 Provider 的原包管理器。Provider 登记只保存
|
|
94
94
|
经过校验的包名、安装目录和包内入口;不接受包自行注入启动命令、参数、工作目录或环境变量。
|
|
95
95
|
卸载仅在包管理器成功移除包后清除登记,以便失败时可以重试。
|
|
@@ -245,9 +245,9 @@ npx @mearl/setup@beta provider install @example/mearl-provider-cloud
|
|
|
245
245
|
|
|
246
246
|
`mearl-setup preset apply <source-url-or-path>` 准备确切资源、激活本机 Node 选择,并向兼容扩展同步同一批浏览器资源;`preset update` 保留用户覆盖,`preset status` 只读检查实际就绪状态。退出码 3 表示存在待同步/安装组件。完整扩展、默认设置、Skills/Provider 和网页一键安装使用同一实际状态核验,见 [本机安装合同](../../docs/preset-installation.md)。
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
install/update 在替换包前保存选中 Preset 的确切资源和期望选择,运行时初始化后同步宿主并安装 Skills;`extension --full` 在替换扩展前完成同一准备流程。已有本机选择优先保留,显式 `--preset` 用于更新或修复资源。配置或资源不可用时停止包变更;部分升级后的重试使用已保存记录。卸载不准备或安装 Preset。
|
|
249
249
|
|
|
250
|
-
默认目录为公共站点的 `installation.json`,可通过 `MEARL_INSTALLATION_CONFIG_URL` 指定其他 HTTPS 目录(本地验证允许回环 HTTP
|
|
250
|
+
默认目录为公共站点的 `installation.json`,可通过 `MEARL_INSTALLATION_CONFIG_URL` 指定其他 HTTPS 目录(本地验证允许回环 HTTP)。目录采用网页安装的校验合同;本机与浏览器未全部确认时报告 pending。
|
|
251
251
|
|
|
252
252
|
## 浏览器 preset 与发行入口
|
|
253
253
|
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { detectGlobalPackage, type PackageManagerInstallation, type PackageDetec
|
|
|
5
5
|
import { type DetectedMearlProfiles, type MearlProfile } from './profiles.js';
|
|
6
6
|
import { uninstallNativeHostArtifacts } from './nativeHost.js';
|
|
7
7
|
import { type CommandRunner } from '@mearl/daemon-core';
|
|
8
|
+
import { readInstallationManagement } from '@mearl/daemon-core/installation';
|
|
9
|
+
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
8
10
|
import { installManagedPlugins, uninstallManagedPlugins } from './managedPlugins.js';
|
|
9
11
|
import { uninstallPlugin } from './plugin.js';
|
|
10
12
|
import { uninstallProviderPackage } from './provider.js';
|
|
@@ -30,6 +32,8 @@ export interface MearlLifecycleOptions {
|
|
|
30
32
|
env?: NodeJS.ProcessEnv;
|
|
31
33
|
}
|
|
32
34
|
export interface MearlLifecycleDependencies {
|
|
35
|
+
readManagement?: typeof readInstallationManagement;
|
|
36
|
+
claimLifecycle?: () => ReturnType<typeof claimPresetInstallation>;
|
|
33
37
|
prepareBrowserPreset?: typeof prepareBrowserSetupPreset;
|
|
34
38
|
preparePreset?: typeof prepareSetupPreset;
|
|
35
39
|
onPresetStatus?: (status: PresetInstallStatus) => void;
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SETUP_VERSION } from './setupVersion.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
7
|
import { listPluginNames } from '@mearl/client/plugin';
|
|
@@ -9,7 +10,9 @@ import { runGlobalMearlSkillOperation } from './skills.js';
|
|
|
9
10
|
import { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
|
|
10
11
|
import { MEARL_PROFILES, detectMearlProfiles, } from './profiles.js';
|
|
11
12
|
import { uninstallNativeHostArtifacts } from './nativeHost.js';
|
|
12
|
-
import { executableNames, prependCommandPath, runCommand, } from '@mearl/daemon-core';
|
|
13
|
+
import { executableNames, prependCommandPath, runCommand, withOwnedCommands, } from '@mearl/daemon-core';
|
|
14
|
+
import { readInstallationManagement, setupManagementCommand, setupRuntimeCompatible } from '@mearl/daemon-core/installation';
|
|
15
|
+
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
13
16
|
import { isMearlReleaseLine, mearlPackageSelector } from './version.js';
|
|
14
17
|
import { installManagedPlugins, uninstallManagedPlugins, } from './managedPlugins.js';
|
|
15
18
|
import { uninstallPlugin } from './plugin.js';
|
|
@@ -176,6 +179,38 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
176
179
|
if (options.targetLine && !isMearlReleaseLine(options.targetLine)) {
|
|
177
180
|
throw new Error('targetLine must be a valid Mearl release line such as 2.3.x');
|
|
178
181
|
}
|
|
182
|
+
const writeLine = dependencies.writeLine ?? (line => console.error(line));
|
|
183
|
+
let lease;
|
|
184
|
+
try {
|
|
185
|
+
lease = (dependencies.claimLifecycle ?? (() => claimPresetInstallation(path.join(homedir(), '.agents', '.mearl-setup-lifecycle.json'))))();
|
|
186
|
+
const readManagement = () => (dependencies.readManagement ?? readInstallationManagement)({ directory: (options.env ?? process.env).MEARL_CAPABILITY_DIR });
|
|
187
|
+
let acceptedRevision = (await readManagement()).revision;
|
|
188
|
+
let selector = mearlPackageSelector(SETUP_VERSION, options.targetLine);
|
|
189
|
+
const assertManagement = async (expected = acceptedRevision) => {
|
|
190
|
+
const { setup, revision } = await readManagement();
|
|
191
|
+
if (revision !== expected)
|
|
192
|
+
throw new Error('PRESET_SELECTION_CHANGED: The selected preset changed during setup.');
|
|
193
|
+
if (setup && options.operation !== 'uninstall' && !setupRuntimeCompatible(setup, SETUP_VERSION, options.targetLine)) {
|
|
194
|
+
throw new Error(`SETUP_RUNTIME_CONFLICT: The selected preset requires ${setup.runtime}. Use ${setupManagementCommand(setup)} ${options.operation}.`);
|
|
195
|
+
}
|
|
196
|
+
selector = setup?.runtime ? options.targetLine ?? SETUP_VERSION : mearlPackageSelector(SETUP_VERSION, options.targetLine);
|
|
197
|
+
};
|
|
198
|
+
await assertManagement();
|
|
199
|
+
const runner = dependencies.runCommand ?? runCommand;
|
|
200
|
+
return await withOwnedCommands(lease.lifecycle, () => executeLifecycle(options, {
|
|
201
|
+
...dependencies,
|
|
202
|
+
runCommand: async (...args) => { await assertManagement(); return runner(...args); },
|
|
203
|
+
}, async (revision) => { await assertManagement(revision); acceptedRevision = revision; }, () => selector));
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
writeLine(`Setup: ${error instanceof Error ? error.message : String(error)}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
lease?.release();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function executeLifecycle(options, dependencies, acceptPreset, packageSelector) {
|
|
179
214
|
const env = options.env ?? process.env;
|
|
180
215
|
const writeLine = dependencies.writeLine ?? (line => console.error(line));
|
|
181
216
|
const existsSync = dependencies.existsSync ?? fs.existsSync;
|
|
@@ -290,6 +325,8 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
290
325
|
stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({
|
|
291
326
|
source: options.presetSource, env,
|
|
292
327
|
});
|
|
328
|
+
if (stagedPreset)
|
|
329
|
+
await acceptPreset(stagedPreset.revision);
|
|
293
330
|
}
|
|
294
331
|
catch (error) {
|
|
295
332
|
writeLine(`Preset preparation: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -322,7 +359,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
322
359
|
detected.get('local') ??
|
|
323
360
|
detected.get('cloud') ??
|
|
324
361
|
resolveInvokingManager(env);
|
|
325
|
-
const sharedClient = await runPackageOperation(clientInstallation, options.operation, [UNIFIED_CLIENT],
|
|
362
|
+
const sharedClient = await runPackageOperation(clientInstallation, options.operation, [UNIFIED_CLIENT], packageSelector(), runner, env, 'Packages (shared client)', writeLine);
|
|
326
363
|
if (!sharedClient.ok) {
|
|
327
364
|
writeLine('Mearl setup incomplete. Review the messages above.');
|
|
328
365
|
return false;
|
|
@@ -362,7 +399,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
362
399
|
if (packages.length === 0) {
|
|
363
400
|
continue;
|
|
364
401
|
}
|
|
365
|
-
const packageResult = await runPackageOperation(installation, packageOperation, packages,
|
|
402
|
+
const packageResult = await runPackageOperation(installation, packageOperation, packages, packageSelector(), runner, env, `Packages (${profile})`, writeLine);
|
|
366
403
|
if (!packageResult.ok) {
|
|
367
404
|
packagesOk = false;
|
|
368
405
|
continue;
|
|
@@ -423,6 +460,8 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
423
460
|
if (packagesOk && selectsInitialPreset && !stagedPreset && !options.presetSource && lifecycleSelected.some(profile => profile === 'local' || profile === 'mcp')) {
|
|
424
461
|
try {
|
|
425
462
|
stagedPreset = await (dependencies.prepareBrowserPreset ?? prepareBrowserSetupPreset)({ env, waitMs: 65_000, writeLine });
|
|
463
|
+
if (stagedPreset)
|
|
464
|
+
await acceptPreset(stagedPreset.revision);
|
|
426
465
|
}
|
|
427
466
|
catch (error) {
|
|
428
467
|
writeLine(`Browser preset: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -432,6 +471,8 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
432
471
|
if (packagesOk && selectsInitialPreset && !stagedPreset && options.defaultPresetSource) {
|
|
433
472
|
try {
|
|
434
473
|
stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({ fallbackSource: options.defaultPresetSource, env });
|
|
474
|
+
if (stagedPreset)
|
|
475
|
+
await acceptPreset(stagedPreset.revision);
|
|
435
476
|
}
|
|
436
477
|
catch (error) {
|
|
437
478
|
writeLine(`Default preset: ${error instanceof Error ? error.message : String(error)}`);
|
package/dist/presetStore.d.ts
CHANGED
|
@@ -1,46 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface PresetInstallReceipt {
|
|
4
|
-
schemaVersion: 1;
|
|
5
|
-
snapshot: PresetSnapshot;
|
|
6
|
-
bundles: Array<Omit<PreparedBundle, 'resources'>>;
|
|
7
|
-
browserRevision: string;
|
|
8
|
-
nodeRevision: string;
|
|
9
|
-
managedModules: string[];
|
|
10
|
-
nodeSyncRequired: boolean;
|
|
11
|
-
browserSyncRequired?: boolean;
|
|
12
|
-
overrides: JsonObject;
|
|
13
|
-
revision: string;
|
|
14
|
-
}
|
|
15
|
-
interface SelectionState {
|
|
16
|
-
rawHash: string | null;
|
|
17
|
-
activation?: CapabilityActivation;
|
|
18
|
-
receipt?: PresetInstallReceipt;
|
|
19
|
-
prepared?: PreparedPreset;
|
|
20
|
-
preset?: ResolvedPreset;
|
|
21
|
-
}
|
|
22
|
-
/** One pointer binds the desired preset, user overrides and exact Node selection. */
|
|
23
|
-
export declare class PresetInstallationStore {
|
|
24
|
-
readonly directory: string;
|
|
25
|
-
readonly mearlApiVersion: string;
|
|
26
|
-
private readonly pointer;
|
|
27
|
-
constructor(options?: {
|
|
28
|
-
directory?: string;
|
|
29
|
-
mearlApiVersion?: string;
|
|
30
|
-
});
|
|
31
|
-
private assertCurrent;
|
|
32
|
-
current(options?: {
|
|
33
|
-
verifyResources?: boolean;
|
|
34
|
-
}): Promise<SelectionState>;
|
|
35
|
-
apply(input: Pick<PreparedPreset, 'snapshot' | 'bundles'>, options: {
|
|
36
|
-
expectedRevision: string | null;
|
|
37
|
-
overrides?: JsonObject;
|
|
38
|
-
signal?: AbortSignal;
|
|
39
|
-
confirm?: () => Promise<void>;
|
|
40
|
-
}): Promise<PresetInstallReceipt>;
|
|
41
|
-
rollback(expectedRevision: string): Promise<void>;
|
|
42
|
-
collectGarbage(): Promise<void>;
|
|
43
|
-
private collectLocked;
|
|
44
|
-
}
|
|
45
|
-
export declare function presetSelectionRevision(state: SelectionState): string | null;
|
|
46
|
-
export {};
|
|
1
|
+
export { PresetInstallationStore, presetSelectionRevision } from '@mearl/daemon-core/installation';
|
|
2
|
+
export type { PresetInstallReceipt } from '@mearl/daemon-core/installation';
|
package/dist/presetStore.js
CHANGED
|
@@ -1,248 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { constants, closeSync, fstatSync, lstatSync, linkSync, mkdirSync, openSync, readSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { withRecordLock } from '@mearl/daemon-core';
|
|
6
|
-
import { mergePresetDefaults, prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilityActivation, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from '@mearl/preset';
|
|
7
|
-
import { BundleError, MAX_BROWSER_STORAGE_BYTES, presetRequiresBrowser, validateBundleManifest, verifyPreset, verifyBrowserPreset, verifyPreparedPreset } from '@mearl/preset/bundles';
|
|
8
|
-
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
-
const hashPattern = /^[a-f0-9]{64}$/;
|
|
10
|
-
const MAX_RECEIPT_BYTES = 16 * 1024 * 1024;
|
|
11
|
-
const MAX_OVERRIDES_BYTES = 262_144;
|
|
12
|
-
const POINTER_BYTES = MAX_CAPABILITY_SELECTION_BYTES + 128;
|
|
13
|
-
function readBounded(file, limit) {
|
|
14
|
-
const before = lstatSync(file);
|
|
15
|
-
if (!before.isFile() || before.isSymbolicLink() || before.size > limit)
|
|
16
|
-
throw new Error('Invalid preset resource file or size');
|
|
17
|
-
const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
18
|
-
try {
|
|
19
|
-
const stat = fstatSync(descriptor);
|
|
20
|
-
if (!stat.isFile() || stat.size > limit)
|
|
21
|
-
throw new Error('Invalid preset resource file or size');
|
|
22
|
-
const bytes = Buffer.alloc(Math.min(stat.size, limit) + 1);
|
|
23
|
-
let length = 0;
|
|
24
|
-
while (length < bytes.byteLength) {
|
|
25
|
-
const count = readSync(descriptor, bytes, length, bytes.byteLength - length, null);
|
|
26
|
-
if (count === 0)
|
|
27
|
-
break;
|
|
28
|
-
length += count;
|
|
29
|
-
}
|
|
30
|
-
if (length > stat.size)
|
|
31
|
-
throw new Error('Preset resource changed while reading');
|
|
32
|
-
return bytes.subarray(0, length);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
closeSync(descriptor);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function optionalFile(file, limit) {
|
|
39
|
-
try {
|
|
40
|
-
return readBounded(file, limit);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (error.code === 'ENOENT')
|
|
44
|
-
return undefined;
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function atomicWrite(file, bytes) {
|
|
49
|
-
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
|
-
try {
|
|
51
|
-
writeFileSync(temporary, bytes, { flag: 'wx', mode: 0o600 });
|
|
52
|
-
renameSync(temporary, file);
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
rmSync(temporary, { force: true });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function revision(state) {
|
|
59
|
-
return state.activation?.installationRevision ?? (state.activation ? `node:${state.activation.revision}` : null);
|
|
60
|
-
}
|
|
61
|
-
function overrides(value) {
|
|
62
|
-
const accepted = mergePresetDefaults({}, value);
|
|
63
|
-
if (Buffer.byteLength(JSON.stringify(accepted)) > MAX_OVERRIDES_BYTES)
|
|
64
|
-
throw new BundleError('BUNDLE_QUOTA', 'User overrides exceed 256 KiB');
|
|
65
|
-
return accepted;
|
|
66
|
-
}
|
|
67
|
-
/** One pointer binds the desired preset, user overrides and exact Node selection. */
|
|
68
|
-
export class PresetInstallationStore {
|
|
69
|
-
directory;
|
|
70
|
-
mearlApiVersion;
|
|
71
|
-
pointer;
|
|
72
|
-
constructor(options = {}) {
|
|
73
|
-
this.directory = resolve(options.directory ?? (process.env.MEARL_CAPABILITY_DIR || join(homedir(), '.mearl/capabilities')));
|
|
74
|
-
this.mearlApiVersion = options.mearlApiVersion ?? '1.0.0';
|
|
75
|
-
this.pointer = join(this.directory, 'active-node.json');
|
|
76
|
-
}
|
|
77
|
-
assertCurrent(expected) {
|
|
78
|
-
const bytes = optionalFile(this.pointer, POINTER_BYTES);
|
|
79
|
-
if ((bytes ? digest(bytes) : null) !== expected)
|
|
80
|
-
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during preparation');
|
|
81
|
-
}
|
|
82
|
-
async current(options = {}) {
|
|
83
|
-
const raw = optionalFile(this.pointer, POINTER_BYTES);
|
|
84
|
-
if (!raw)
|
|
85
|
-
return { rawHash: null };
|
|
86
|
-
const rawHash = digest(raw);
|
|
87
|
-
const activation = await verifyCapabilityActivation(JSON.parse(raw.toString('utf8')));
|
|
88
|
-
if (!activation.installationRevision)
|
|
89
|
-
return { rawHash, activation };
|
|
90
|
-
const captured = withRecordLock(this.pointer, () => {
|
|
91
|
-
this.assertCurrent(rawHash);
|
|
92
|
-
const receiptBytes = readBounded(join(this.directory, 'installations', `${activation.installationRevision}.json`), MAX_RECEIPT_BYTES);
|
|
93
|
-
const receipt = JSON.parse(receiptBytes.toString('utf8'));
|
|
94
|
-
const { revision: receiptRevision, ...data } = receipt;
|
|
95
|
-
if (receiptRevision !== activation.installationRevision || digest(JSON.stringify(data)) !== receiptRevision || data.schemaVersion !== 1 ||
|
|
96
|
-
Object.keys(data).some(key => !['schemaVersion', 'snapshot', 'bundles', 'browserRevision', 'nodeRevision', 'managedModules', 'nodeSyncRequired', 'browserSyncRequired', 'overrides'].includes(key)) || typeof data.nodeSyncRequired !== 'boolean' || (data.browserSyncRequired !== undefined && typeof data.browserSyncRequired !== 'boolean') ||
|
|
97
|
-
data.nodeRevision !== activation.revision || !Array.isArray(data.bundles) || data.bundles.length > 64 ||
|
|
98
|
-
!Array.isArray(data.managedModules) || new Set(data.managedModules).size !== data.managedModules.length || data.managedModules.some(id => typeof id !== 'string' || !Object.hasOwn(activation.modules, id))) {
|
|
99
|
-
throw new BundleError('BUNDLE_INTEGRITY', 'Installed preset receipt does not match its selection');
|
|
100
|
-
}
|
|
101
|
-
const acceptedOverrides = overrides(data.overrides);
|
|
102
|
-
let total = 0;
|
|
103
|
-
const metadata = data.bundles.map(bundle => {
|
|
104
|
-
if (Object.keys(bundle).some(key => !['source', 'manifest', 'revision'].includes(key)))
|
|
105
|
-
throw new BundleError('BUNDLE_INVALID', 'Invalid installed package metadata');
|
|
106
|
-
const manifest = validateBundleManifest(bundle.manifest, this.mearlApiVersion);
|
|
107
|
-
total += Object.values(manifest.resources).reduce((sum, entry) => sum + entry.byteLength, 0);
|
|
108
|
-
return { ...bundle, manifest };
|
|
109
|
-
});
|
|
110
|
-
if (total > MAX_BROWSER_STORAGE_BYTES)
|
|
111
|
-
throw new BundleError('BUNDLE_QUOTA', 'Installed preset resources exceed 64 MiB');
|
|
112
|
-
const bundles = options.verifyResources === false ? [] : metadata.map(bundle => ({ ...bundle, resources: Object.fromEntries(Object.entries(bundle.manifest.resources).map(([name, entry]) => [name, readBounded(join(this.directory, 'blobs', `${entry.sha256}.bin`), entry.byteLength)])) }));
|
|
113
|
-
for (const entry of options.verifyResources === false ? [] : Object.values(activation.modules)) {
|
|
114
|
-
const bytes = readBounded(join(this.directory, 'objects', `${entry.sha256}.cjs`), entry.byteLength);
|
|
115
|
-
if (bytes.byteLength !== entry.byteLength || digest(bytes) !== entry.sha256)
|
|
116
|
-
throw new BundleError('BUNDLE_INTEGRITY', 'Installed Node object is missing or corrupt');
|
|
117
|
-
}
|
|
118
|
-
return { receipt: { ...receipt, overrides: acceptedOverrides }, bundles };
|
|
119
|
-
});
|
|
120
|
-
if (options.verifyResources === false) {
|
|
121
|
-
const graph = await verifyPreset(captured.receipt.snapshot, this.mearlApiVersion);
|
|
122
|
-
await verifyBrowserPreset({ source: graph.snapshot.source, manifests: graph.snapshot.manifests, packageTargets: graph.snapshot.packageTargets,
|
|
123
|
-
bundleRevisions: Object.fromEntries(Object.entries(graph.snapshot.bundleRevisions).filter(([id]) => graph.preset.packages[id].targets?.includes('browser'))), revision: captured.receipt.browserRevision }, this.mearlApiVersion);
|
|
124
|
-
return { rawHash, activation, receipt: captured.receipt, preset: graph.preset };
|
|
125
|
-
}
|
|
126
|
-
const prepared = await verifyPreparedPreset({ snapshot: captured.receipt.snapshot, bundles: captured.bundles }, this.mearlApiVersion);
|
|
127
|
-
if (prepared.browser.snapshot.revision !== captured.receipt.browserRevision)
|
|
128
|
-
throw new BundleError('BUNDLE_INTEGRITY', 'Installed browser projection does not match');
|
|
129
|
-
const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].targets?.includes('node')), 'native-host', this.mearlApiVersion, resolveCapabilityConfig(mergePresetDefaults(prepared.preset.defaults, captured.receipt.overrides)));
|
|
130
|
-
if (Object.keys(selected.snapshot.modules).length !== captured.receipt.managedModules.length || captured.receipt.managedModules.some(id => !Object.hasOwn(selected.snapshot.modules, id)) ||
|
|
131
|
-
Object.entries(selected.snapshot.modules).some(([id, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.modules[id])) ||
|
|
132
|
-
Object.entries(selected.snapshot.bindings).some(([hook, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.bindings[hook])) ||
|
|
133
|
-
Object.values(activation.bindings).filter(entry => captured.receipt.managedModules.includes(entry.module)).length !== Object.keys(selected.snapshot.bindings).length) {
|
|
134
|
-
throw new BundleError('BUNDLE_INTEGRITY', 'Installed managed Node selection does not match its packages');
|
|
135
|
-
}
|
|
136
|
-
return { rawHash, activation, prepared, receipt: captured.receipt, preset: prepared.preset };
|
|
137
|
-
}
|
|
138
|
-
async apply(input, options) {
|
|
139
|
-
options.signal?.throwIfAborted();
|
|
140
|
-
const acceptedOverrides = options.overrides === undefined ? undefined : overrides(options.overrides);
|
|
141
|
-
const [current, prepared] = await Promise.all([this.current({ verifyResources: false }), verifyPreparedPreset(input, this.mearlApiVersion)]);
|
|
142
|
-
if (revision(current) !== options.expectedRevision)
|
|
143
|
-
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed');
|
|
144
|
-
const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].targets?.includes('node')), 'native-host', this.mearlApiVersion, resolveCapabilityConfig(mergePresetDefaults(prepared.preset.defaults, acceptedOverrides ?? current.receipt?.overrides ?? {})));
|
|
145
|
-
const previousManaged = new Set(current.receipt?.managedModules ?? []);
|
|
146
|
-
const unmanaged = Object.fromEntries(Object.entries(current.activation?.modules ?? {}).filter(([id]) => !previousManaged.has(id)));
|
|
147
|
-
for (const entry of Object.values(unmanaged)) {
|
|
148
|
-
const bytes = readBounded(join(this.directory, 'objects', `${entry.sha256}.cjs`), entry.byteLength);
|
|
149
|
-
if (bytes.byteLength !== entry.byteLength || digest(bytes) !== entry.sha256)
|
|
150
|
-
throw new BundleError('BUNDLE_INTEGRITY', 'An independent Node module is corrupt');
|
|
151
|
-
}
|
|
152
|
-
if (Object.keys(selected.snapshot.modules).some(id => Object.hasOwn(unmanaged, id)))
|
|
153
|
-
throw new BundleError('BUNDLE_CONFLICT', 'A preset module conflicts with an independently installed Node module');
|
|
154
|
-
const bindings = Object.fromEntries(Object.entries(current.activation?.bindings ?? {}).filter(([, entry]) => Object.hasOwn(unmanaged, entry.module)));
|
|
155
|
-
if (Object.keys(selected.snapshot.bindings).some(hook => Object.hasOwn(bindings, hook)))
|
|
156
|
-
throw new BundleError('BUNDLE_CONFLICT', 'A preset hook conflicts with an independently installed Node module');
|
|
157
|
-
const snapshot = await prepareCapabilitySnapshot({ schemaVersion: 1, modules: { ...unmanaged, ...selected.snapshot.modules }, bindings: { ...bindings, ...selected.snapshot.bindings } });
|
|
158
|
-
const data = {
|
|
159
|
-
schemaVersion: 1, snapshot: prepared.snapshot,
|
|
160
|
-
bundles: prepared.bundles.map(({ source, revision, manifest }) => ({ source, revision, manifest })),
|
|
161
|
-
browserRevision: prepared.browser.snapshot.revision, nodeRevision: snapshot.revision,
|
|
162
|
-
managedModules: Object.keys(selected.snapshot.modules), overrides: acceptedOverrides ?? current.receipt?.overrides ?? {},
|
|
163
|
-
nodeSyncRequired: Boolean(current.receipt?.nodeSyncRequired || current.receipt?.managedModules.length || Object.values(prepared.preset.packages).some(entry => entry.enabled !== false && entry.targets?.includes('node'))),
|
|
164
|
-
browserSyncRequired: Boolean(current.receipt?.browserSyncRequired || presetRequiresBrowser(prepared.preset)),
|
|
165
|
-
};
|
|
166
|
-
const receipt = { ...data, revision: digest(JSON.stringify(data)) };
|
|
167
|
-
const receiptBytes = JSON.stringify(receipt);
|
|
168
|
-
if (Buffer.byteLength(receiptBytes) > MAX_RECEIPT_BYTES)
|
|
169
|
-
throw new BundleError('BUNDLE_QUOTA', 'Preset installation receipt exceeds 16 MiB');
|
|
170
|
-
const activation = { ...snapshot, installationRevision: receipt.revision };
|
|
171
|
-
await options.confirm?.();
|
|
172
|
-
withRecordLock(this.pointer, () => {
|
|
173
|
-
this.assertCurrent(current.rawHash);
|
|
174
|
-
options.signal?.throwIfAborted();
|
|
175
|
-
for (const child of ['blobs', 'objects', 'installations'])
|
|
176
|
-
mkdirSync(join(this.directory, child), { recursive: true, mode: 0o700 });
|
|
177
|
-
for (const bundle of prepared.bundles)
|
|
178
|
-
for (const [name, entry] of Object.entries(bundle.manifest.resources)) {
|
|
179
|
-
atomicWrite(join(this.directory, 'blobs', `${entry.sha256}.bin`), bundle.resources[name]);
|
|
180
|
-
}
|
|
181
|
-
for (const entry of Object.values(selected.snapshot.modules)) {
|
|
182
|
-
const destination = join(this.directory, 'objects', `${entry.sha256}.cjs`);
|
|
183
|
-
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
184
|
-
try {
|
|
185
|
-
linkSync(join(this.directory, 'blobs', `${entry.sha256}.bin`), temporary);
|
|
186
|
-
renameSync(temporary, destination);
|
|
187
|
-
}
|
|
188
|
-
finally {
|
|
189
|
-
rmSync(temporary, { force: true });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
atomicWrite(join(this.directory, 'installations', `${receipt.revision}.json`), receiptBytes);
|
|
193
|
-
if (current.receipt?.revision !== receipt.revision)
|
|
194
|
-
atomicWrite(join(this.directory, 'rollback.json'), JSON.stringify({ candidate: receipt.revision, previous: current.activation ?? null }));
|
|
195
|
-
atomicWrite(this.pointer, JSON.stringify(activation));
|
|
196
|
-
});
|
|
197
|
-
return receipt;
|
|
198
|
-
}
|
|
199
|
-
async rollback(expectedRevision) {
|
|
200
|
-
const current = await this.current({ verifyResources: false });
|
|
201
|
-
if (current.activation?.installationRevision !== expectedRevision)
|
|
202
|
-
throw new BundleError('BUNDLE_CONFLICT', 'A newer preset installation cannot be rolled back');
|
|
203
|
-
const record = JSON.parse(readBounded(join(this.directory, 'rollback.json'), POINTER_BYTES + 256).toString('utf8'));
|
|
204
|
-
if (record.candidate !== expectedRevision)
|
|
205
|
-
throw new BundleError('BUNDLE_CONFLICT', 'The preset rollback record has changed');
|
|
206
|
-
const previous = record.previous === null ? undefined : await verifyCapabilityActivation(record.previous);
|
|
207
|
-
withRecordLock(this.pointer, () => {
|
|
208
|
-
this.assertCurrent(current.rawHash);
|
|
209
|
-
if (previous)
|
|
210
|
-
atomicWrite(this.pointer, JSON.stringify(previous));
|
|
211
|
-
else
|
|
212
|
-
rmSync(this.pointer, { force: true });
|
|
213
|
-
rmSync(join(this.directory, 'rollback.json'), { force: true });
|
|
214
|
-
this.collectLocked(previous);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
async collectGarbage() {
|
|
218
|
-
const current = await this.current({ verifyResources: false });
|
|
219
|
-
const raw = optionalFile(join(this.directory, 'rollback.json'), POINTER_BYTES + 256);
|
|
220
|
-
const record = raw && JSON.parse(raw.toString('utf8'));
|
|
221
|
-
const previous = record?.candidate === current.activation?.installationRevision && record.previous !== null ? await verifyCapabilityActivation(record.previous) : undefined;
|
|
222
|
-
withRecordLock(this.pointer, () => {
|
|
223
|
-
this.assertCurrent(current.rawHash);
|
|
224
|
-
for (const child of ['blobs', 'objects', 'installations'])
|
|
225
|
-
mkdirSync(join(this.directory, child), { recursive: true, mode: 0o700 });
|
|
226
|
-
this.collectLocked(current.activation, previous);
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
collectLocked(active, previous) {
|
|
230
|
-
const receipts = new Set([active?.installationRevision, previous?.installationRevision].filter((value) => Boolean(value)));
|
|
231
|
-
const blobs = new Set();
|
|
232
|
-
const modules = new Set([active, previous].flatMap(selection => Object.values(selection?.modules ?? {}).map(entry => entry.sha256)));
|
|
233
|
-
for (const id of receipts) {
|
|
234
|
-
const record = JSON.parse(readBounded(join(this.directory, 'installations', `${id}.json`), MAX_RECEIPT_BYTES).toString('utf8'));
|
|
235
|
-
for (const bundle of record.bundles)
|
|
236
|
-
for (const entry of Object.values(bundle.manifest.resources))
|
|
237
|
-
blobs.add(entry.sha256);
|
|
238
|
-
}
|
|
239
|
-
for (const [directory, extension, retained] of [['blobs', '.bin', blobs], ['objects', '.cjs', modules], ['installations', '.json', receipts]]) {
|
|
240
|
-
for (const name of readdirSync(join(this.directory, directory))) {
|
|
241
|
-
const id = name.slice(0, -extension.length);
|
|
242
|
-
if (name.endsWith(extension) && hashPattern.test(id) && !retained.has(id))
|
|
243
|
-
rmSync(join(this.directory, directory, name), { force: true });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
export function presetSelectionRevision(state) { return revision(state); }
|
|
1
|
+
export { PresetInstallationStore, presetSelectionRevision } from '@mearl/daemon-core/installation';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.9",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
"ws": "^8.18.3",
|
|
79
79
|
"semver": "^7.8.5",
|
|
80
80
|
"adm-zip": "^0.6.1",
|
|
81
|
-
"@mearl/client": "3.0.0-beta.
|
|
82
|
-
"@mearl/provider": "3.0.0-beta.
|
|
83
|
-
"@mearl/daemon-core": "3.0.0-beta.
|
|
84
|
-
"@mearl/preset": "3.0.0-beta.
|
|
85
|
-
"@mearl/browser-core": "3.0.0-beta.
|
|
81
|
+
"@mearl/client": "3.0.0-beta.9",
|
|
82
|
+
"@mearl/provider": "3.0.0-beta.9",
|
|
83
|
+
"@mearl/daemon-core": "3.0.0-beta.9",
|
|
84
|
+
"@mearl/preset": "3.0.0-beta.9",
|
|
85
|
+
"@mearl/browser-core": "3.0.0-beta.9"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@types/node": "^24.9.1",
|