@mearl/setup 3.0.0-beta.8 → 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.
@@ -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], mearlPackageSelector(SETUP_VERSION, options.targetLine), runner, env, 'Packages (shared client)', writeLine);
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, mearlPackageSelector(SETUP_VERSION, options.targetLine), runner, env, `Packages (${profile})`, writeLine);
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)}`);
@@ -1,46 +1,2 @@
1
- import { type JsonObject, type CapabilityActivation, type ResolvedPreset } from '@mearl/preset';
2
- import { type PreparedBundle, type PreparedPreset, type PresetSnapshot } from '@mearl/preset/bundles';
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';
@@ -1,248 +1 @@
1
- import { createHash, randomUUID } from 'node:crypto';
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.8",
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.8",
82
- "@mearl/provider": "3.0.0-beta.8",
83
- "@mearl/daemon-core": "3.0.0-beta.8",
84
- "@mearl/browser-core": "3.0.0-beta.8",
85
- "@mearl/preset": "3.0.0-beta.8"
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",