@mearl/setup 3.0.0-beta.1 → 3.0.0-beta.10
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 +174 -156
- package/README.zh-CN.md +260 -0
- package/dist/browserExtension.js +24 -8
- package/dist/browserPreset.d.ts +20 -0
- package/dist/browserPreset.js +82 -0
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +43 -12
- package/dist/lifecycle.d.ts +10 -2
- package/dist/lifecycle.js +70 -13
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +51 -13
- package/dist/preset.d.ts +16 -5
- package/dist/preset.js +50 -13
- package/dist/presetStore.d.ts +2 -48
- package/dist/presetStore.js +1 -252
- package/dist/setupPreset.d.ts +9 -0
- package/dist/setupPreset.js +14 -0
- package/dist/setupVersion.js +1 -1
- package/dist/skillSourceReceipts.d.ts +25 -0
- package/dist/skillSourceReceipts.js +94 -0
- package/dist/skillSources.d.ts +7 -22
- package/dist/skillSources.js +43 -96
- package/dist/skills.js +8 -6
- package/package.json +16 -11
- package/dist/presetUpgrade.d.ts +0 -13
- package/dist/presetUpgrade.js +0 -42
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,14 +10,17 @@ 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';
|
|
16
19
|
import { uninstallProviderPackage } from './provider.js';
|
|
17
20
|
import { packageRegistryEnvironment } from './npmRegistry.js';
|
|
18
21
|
import { cleanupUninstalledRuntime } from './runtimeCleanup.js';
|
|
19
|
-
import { prepareSetupPreset } from './
|
|
22
|
+
import { prepareSetupPreset } from './setupPreset.js';
|
|
23
|
+
import { prepareBrowserSetupPreset } from './browserPreset.js';
|
|
20
24
|
const LEGACY_CLOUD_CLIENT = '@mearl/cloud-client';
|
|
21
25
|
const UNIFIED_CLIENT = '@mearl/client';
|
|
22
26
|
const PROFILE_ORDER = ['local', 'cloud', 'mcp'];
|
|
@@ -135,12 +139,12 @@ async function runHook(profile, operation, detected, runner, existsSync, env, wr
|
|
|
135
139
|
return false;
|
|
136
140
|
}
|
|
137
141
|
const running = /\[CloudServer\]\s+Status:\s+running\b/.test(status.stdout);
|
|
138
|
-
const
|
|
139
|
-
if (operation === 'update' && !running && !
|
|
142
|
+
const pluginMode = Boolean(cloudMode && cloudMode !== 'websocket') || /\[CloudServer\]\s+Mode:\s+(?!websocket\b)\S+/.test(status.stdout);
|
|
143
|
+
if (operation === 'update' && !running && !pluginMode) {
|
|
140
144
|
writeLine('Hook: cloud update skipped because cloud-server is not running.');
|
|
141
145
|
return true;
|
|
142
146
|
}
|
|
143
|
-
args = [running && !
|
|
147
|
+
args = [running && !pluginMode ? 'restart' : 'start'];
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
else {
|
|
@@ -175,6 +179,38 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
175
179
|
if (options.targetLine && !isMearlReleaseLine(options.targetLine)) {
|
|
176
180
|
throw new Error('targetLine must be a valid Mearl release line such as 2.3.x');
|
|
177
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) {
|
|
178
214
|
const env = options.env ?? process.env;
|
|
179
215
|
const writeLine = dependencies.writeLine ?? (line => console.error(line));
|
|
180
216
|
const existsSync = dependencies.existsSync ?? fs.existsSync;
|
|
@@ -281,17 +317,16 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
281
317
|
? detectPackage(UNIFIED_CLIENT, ['mearl'], packageDetectionOptions)
|
|
282
318
|
: null;
|
|
283
319
|
const detectedClient = detectedClientCandidate?.packageName === UNIFIED_CLIENT ? detectedClientCandidate : null;
|
|
320
|
+
const preparesPreset = options.operation !== 'uninstall' && !dependencies.prepareSkills && (!options.targetLine || Number(options.targetLine.split('.')[0]) >= 3);
|
|
321
|
+
const selectsInitialPreset = preparesPreset && !options.skipSkill && !env.MEARL_AUTO_UPDATE_CLIENT_ROOT && !env.MEARL_AUTO_UPDATE_PROFILES;
|
|
284
322
|
let stagedPreset;
|
|
285
|
-
if (options.operation !== 'uninstall' &&
|
|
323
|
+
if (preparesPreset || (options.operation !== 'uninstall' && options.presetSource && !dependencies.prepareSkills)) {
|
|
286
324
|
try {
|
|
287
325
|
stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({
|
|
288
326
|
source: options.presetSource, env,
|
|
289
|
-
installed: [
|
|
290
|
-
...lifecycleSelected.flatMap(profile => detected.get(profile) ? [detected.get(profile)] : []),
|
|
291
|
-
...(detectedClient ? [detectedClient] : []),
|
|
292
|
-
...(autoUpdateClient && env.MEARL_AUTO_UPDATE_CLIENT_ROOT ? [{ packageName: UNIFIED_CLIENT, packageRoot: env.MEARL_AUTO_UPDATE_CLIENT_ROOT }] : []),
|
|
293
|
-
],
|
|
294
327
|
});
|
|
328
|
+
if (stagedPreset)
|
|
329
|
+
await acceptPreset(stagedPreset.revision);
|
|
295
330
|
}
|
|
296
331
|
catch (error) {
|
|
297
332
|
writeLine(`Preset preparation: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -324,7 +359,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
324
359
|
detected.get('local') ??
|
|
325
360
|
detected.get('cloud') ??
|
|
326
361
|
resolveInvokingManager(env);
|
|
327
|
-
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);
|
|
328
363
|
if (!sharedClient.ok) {
|
|
329
364
|
writeLine('Mearl setup incomplete. Review the messages above.');
|
|
330
365
|
return false;
|
|
@@ -364,7 +399,7 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
364
399
|
if (packages.length === 0) {
|
|
365
400
|
continue;
|
|
366
401
|
}
|
|
367
|
-
const packageResult = await runPackageOperation(installation, packageOperation, packages,
|
|
402
|
+
const packageResult = await runPackageOperation(installation, packageOperation, packages, packageSelector(), runner, env, `Packages (${profile})`, writeLine);
|
|
368
403
|
if (!packageResult.ok) {
|
|
369
404
|
packagesOk = false;
|
|
370
405
|
continue;
|
|
@@ -422,6 +457,28 @@ export async function runMearlLifecycle(options, dependencies = {}) {
|
|
|
422
457
|
}
|
|
423
458
|
}
|
|
424
459
|
}
|
|
460
|
+
if (packagesOk && selectsInitialPreset && !stagedPreset && !options.presetSource && lifecycleSelected.some(profile => profile === 'local' || profile === 'mcp')) {
|
|
461
|
+
try {
|
|
462
|
+
stagedPreset = await (dependencies.prepareBrowserPreset ?? prepareBrowserSetupPreset)({ env, waitMs: 65_000, writeLine });
|
|
463
|
+
if (stagedPreset)
|
|
464
|
+
await acceptPreset(stagedPreset.revision);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
writeLine(`Browser preset: ${error instanceof Error ? error.message : String(error)}`);
|
|
468
|
+
packagesOk = false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (packagesOk && selectsInitialPreset && !stagedPreset && options.defaultPresetSource) {
|
|
472
|
+
try {
|
|
473
|
+
stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({ fallbackSource: options.defaultPresetSource, env });
|
|
474
|
+
if (stagedPreset)
|
|
475
|
+
await acceptPreset(stagedPreset.revision);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
writeLine(`Default preset: ${error instanceof Error ? error.message : String(error)}`);
|
|
479
|
+
packagesOk = false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
425
482
|
if (packagesOk && options.operation !== 'uninstall' && (stagedPreset || dependencies.prepareSkills)) {
|
|
426
483
|
try {
|
|
427
484
|
if (stagedPreset) {
|
package/dist/plugin.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface PluginSetupResult {
|
|
|
5
5
|
entry: string;
|
|
6
6
|
name: string;
|
|
7
7
|
}
|
|
8
|
-
export declare function setupPlugin(target: string, requestedName?: string): Promise<PluginSetupResult>;
|
|
8
|
+
export declare function setupPlugin(target: string, requestedName?: string, runner?: CommandRunner): Promise<PluginSetupResult>;
|
|
9
9
|
export declare function ensureOfficialPlugin(name: string, runner?: CommandRunner): Promise<'existing' | 'installed'>;
|
|
10
10
|
export interface PluginUninstallResult {
|
|
11
11
|
entryMissing?: boolean;
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readlinkSync, realpathSync, rmdirSync, statSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { assertPluginName, findPluginDirectoryEntry, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
4
|
+
import { assertPluginName, findPluginDirectoryEntry, skillManifestPath, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
5
|
+
import { preserveSkillDisabled } from './skillSources.js';
|
|
5
6
|
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
6
7
|
import { ensurePluginRuntime } from './pluginRuntime.js';
|
|
7
8
|
import { captureSkillsCli, parseInstalledSkills, stripAnsi } from './skills.js';
|
|
@@ -29,14 +30,15 @@ function defaultPluginName(target, entry) {
|
|
|
29
30
|
? basename(dirname(entryDirectory))
|
|
30
31
|
: basename(entryDirectory);
|
|
31
32
|
}
|
|
32
|
-
export async function setupPlugin(target, requestedName) {
|
|
33
|
+
export async function setupPlugin(target, requestedName, runner = runCommand) {
|
|
33
34
|
if (requestedName)
|
|
34
35
|
assertPluginName(requestedName);
|
|
35
36
|
const entry = resolvePluginEntry(target);
|
|
36
37
|
const plugin = await loadPlugin(entry, registrationContext);
|
|
37
38
|
const name = requestedName || plugin.name || defaultPluginName(target, entry);
|
|
38
39
|
assertPluginName(name);
|
|
39
|
-
await ensurePluginRuntime();
|
|
40
|
+
await ensurePluginRuntime(runner);
|
|
41
|
+
await plugin.setup?.({ runCommand: runner });
|
|
40
42
|
return {
|
|
41
43
|
entry: registerPlugin(name, entry, plugin.description, {
|
|
42
44
|
hasPublicCommands: Boolean(plugin.commandHelp(name)),
|
|
@@ -52,8 +54,22 @@ export async function ensureOfficialPlugin(name, runner = runCommand) {
|
|
|
52
54
|
const definition = accepted?.plugins[name];
|
|
53
55
|
if (!definition)
|
|
54
56
|
throw new Error(`未知官方插件: ${name}`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const existing = findPluginRegistration(name);
|
|
58
|
+
if (existing) {
|
|
59
|
+
if (!accepted.selection || existing.managedBy !== OFFICIAL_PLUGIN_MANAGER)
|
|
60
|
+
return 'existing';
|
|
61
|
+
const cli = await captureSkillsCli({ runCommand: runner });
|
|
62
|
+
if (!cli || cli.selection?.path !== accepted.selection.path || cli.selection.sha256 !== accepted.selection.sha256)
|
|
63
|
+
throw new Error('PLUGIN_CATALOG_SELECTION_CHANGED');
|
|
64
|
+
const declaration = cli.managedSkills[definition.skill];
|
|
65
|
+
if (!declaration?.source || declaration.source !== definition.installArgs[0])
|
|
66
|
+
throw new Error('PLUGIN_CATALOG_SKILL_UNDECLARED');
|
|
67
|
+
const status = cli.inspectGlobal(declaration.source, definition.skill, declaration.version, { migrateFrom: declaration.migrateFrom });
|
|
68
|
+
if (status.state === 'ready')
|
|
69
|
+
return 'existing';
|
|
70
|
+
if (status.state === 'conflict')
|
|
71
|
+
throw new Error(`${status.reason}: ${definition.skill}`);
|
|
72
|
+
}
|
|
57
73
|
const lease = claimPresetInstallation();
|
|
58
74
|
try {
|
|
59
75
|
return await withOwnedCommands(lease.lifecycle, async () => {
|
|
@@ -276,13 +292,22 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
276
292
|
if (status.state !== 'ready' && (registered || status.state !== 'registration'))
|
|
277
293
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
278
294
|
};
|
|
279
|
-
|
|
280
|
-
|
|
295
|
+
const assertSource = () => {
|
|
296
|
+
if (!options.expectedSkill || !declaration)
|
|
297
|
+
return;
|
|
298
|
+
const status = cli.inspectGlobal(declaration.source, options.expectedSkill, declaration.version, {
|
|
299
|
+
migrateFrom: options.registrationManagedBy === OFFICIAL_PLUGIN_MANAGER ? declaration.migrateFrom : undefined,
|
|
300
|
+
});
|
|
281
301
|
if (status.state === 'conflict')
|
|
282
302
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
283
|
-
}
|
|
303
|
+
};
|
|
304
|
+
assertSource();
|
|
284
305
|
await options.confirm?.();
|
|
285
|
-
|
|
306
|
+
assertSource();
|
|
307
|
+
const runInstall = () => cli.run(['add', ...args, '--global'], { tee: true });
|
|
308
|
+
const installed = await (options.expectedSkill
|
|
309
|
+
? preserveSkillDisabled(join(homedir(), '.agents', 'skills', options.expectedSkill), runInstall)
|
|
310
|
+
: runInstall());
|
|
286
311
|
if (installed.code !== 0)
|
|
287
312
|
return installed.code;
|
|
288
313
|
await options.confirm?.();
|
|
@@ -307,7 +332,7 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
307
332
|
}
|
|
308
333
|
const plugins = new Map();
|
|
309
334
|
for (const skillPath of paths) {
|
|
310
|
-
if (!statSync(join(skillPath, 'SKILL.md')).isFile())
|
|
335
|
+
if (!statSync(skillManifestPath(skillPath) ?? join(skillPath, 'SKILL.md')).isFile())
|
|
311
336
|
throw new Error(`安装结果缺少 SKILL.md: ${skillPath}`);
|
|
312
337
|
const entry = findPluginDirectoryEntry(skillPath);
|
|
313
338
|
if (!entry)
|
|
@@ -321,6 +346,7 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
321
346
|
throw new Error(`多个 skill 使用插件名称 ${name},请调整入口的 name metadata。`);
|
|
322
347
|
plugins.set(name, {
|
|
323
348
|
entry,
|
|
349
|
+
setup: plugin.setup,
|
|
324
350
|
hasPublicCommands: Boolean(plugin.commandHelp(name)),
|
|
325
351
|
description: plugin.description,
|
|
326
352
|
...(plugin.toolbox ? { toolbox: plugin.toolbox } : {}),
|
|
@@ -333,14 +359,26 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
333
359
|
return 0;
|
|
334
360
|
}
|
|
335
361
|
await ensurePluginRuntime(runner);
|
|
336
|
-
for (const [name, { entry, description, toolbox, appProfiles, profileSources, hasPublicCommands },] of plugins) {
|
|
362
|
+
for (const [name, { entry, description, toolbox, appProfiles, profileSources, hasPublicCommands, setup },] of plugins) {
|
|
337
363
|
await options.confirm?.();
|
|
338
364
|
verifySkill();
|
|
339
|
-
const
|
|
340
|
-
|
|
365
|
+
const hasUserRegistration = () => {
|
|
366
|
+
const existing = findPluginRegistration(name);
|
|
367
|
+
return options.registrationManagedBy && existing && existing.managedBy !== options.registrationManagedBy;
|
|
368
|
+
};
|
|
369
|
+
if (hasUserRegistration()) {
|
|
341
370
|
console.error(`Plugin ${name}: existing user registration kept.`);
|
|
342
371
|
continue;
|
|
343
372
|
}
|
|
373
|
+
if (setup) {
|
|
374
|
+
await setup({ runCommand: runner });
|
|
375
|
+
await options.confirm?.();
|
|
376
|
+
verifySkill();
|
|
377
|
+
if (hasUserRegistration()) {
|
|
378
|
+
console.error(`Plugin ${name}: existing user registration kept.`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
344
382
|
registerPlugin(name, entry, description, {
|
|
345
383
|
hasPublicCommands,
|
|
346
384
|
managedBy: options.registrationManagedBy ?? (cli.id === 'skills' ? undefined : cli.id),
|
package/dist/preset.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { invoke } from '@mearl/client/local';
|
|
2
2
|
import { type JsonObject } from '@mearl/preset';
|
|
3
|
-
import { type BundleLoader } from '@mearl/preset/bundles';
|
|
3
|
+
import { type BundleLoader, type PreparedPreset } from '@mearl/preset/bundles';
|
|
4
4
|
import { PresetInstallationStore } from './presetStore.js';
|
|
5
|
+
export declare const MEARL_INSTALLATION_CONFIG_URL: string;
|
|
6
|
+
/** Runtime overrides apply to this local process, never to browser-supplied install requests. */
|
|
7
|
+
export declare function resolveInstallationConfigUrl(value?: string | undefined, allowLoopback?: boolean): URL;
|
|
5
8
|
export { claimPresetInstallation } from './presetInstallationLease.js';
|
|
6
9
|
export { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
|
|
7
10
|
export type { PresetInstallReceipt } from './presetStore.js';
|
|
@@ -59,8 +62,9 @@ export interface PresetInstallerOptions {
|
|
|
59
62
|
invoke?: typeof invoke;
|
|
60
63
|
}
|
|
61
64
|
export interface PresetApplyOptions {
|
|
65
|
+
validatePrepared?: (prepared: PreparedPreset) => Promise<void>;
|
|
66
|
+
skipUnchanged?: boolean;
|
|
62
67
|
overrides?: JsonObject;
|
|
63
|
-
nodeConfig?: Record<string, JsonObject>;
|
|
64
68
|
signal?: AbortSignal;
|
|
65
69
|
expectedRevision?: string | null;
|
|
66
70
|
expectedId?: string;
|
|
@@ -87,8 +91,15 @@ export declare class PresetInstaller {
|
|
|
87
91
|
/** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
|
|
88
92
|
stage(source: string, options?: PresetApplyOptions): Promise<StagedPresetInstallation>;
|
|
89
93
|
apply(source: string, options?: PresetApplyOptions): Promise<PresetInstallStatus>;
|
|
90
|
-
update(): Promise<PresetInstallStatus>;
|
|
91
|
-
resume(expectedRevision: string
|
|
94
|
+
update(options?: Pick<PresetApplyOptions, 'validatePrepared' | 'skipUnchanged' | 'signal'>): Promise<PresetInstallStatus>;
|
|
95
|
+
resume(expectedRevision: string, options?: {
|
|
96
|
+
replaceSkills?: readonly string[];
|
|
97
|
+
}): Promise<PresetInstallStatus>;
|
|
98
|
+
repair(options?: {
|
|
99
|
+
replaceSkills?: readonly string[];
|
|
100
|
+
}): Promise<PresetInstallStatus>;
|
|
92
101
|
configure(value: JsonObject): Promise<PresetInstallStatus>;
|
|
93
102
|
}
|
|
94
|
-
export declare function runPresetCommand(command: 'apply' | 'update' | 'status', source?: string
|
|
103
|
+
export declare function runPresetCommand(command: 'apply' | 'update' | 'status' | 'repair', source?: string, options?: {
|
|
104
|
+
replaceSkills?: readonly string[];
|
|
105
|
+
}): Promise<PresetInstallStatus>;
|
package/dist/preset.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { lstat, open } from 'node:fs/promises';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { invoke, invokeLocalHost, listLiveBrowsers, SOCKET_PATH } from '@mearl/client/local';
|
|
6
7
|
import { mergePresetDefaults } from '@mearl/preset';
|
|
7
8
|
import { BundleError, createBundleFetchLoader, preparePreset } from '@mearl/preset/bundles';
|
|
8
9
|
import { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
|
|
10
|
+
const installationSite = createRequire(import.meta.url)('@mearl/preset/installation-site.json');
|
|
11
|
+
export const MEARL_INSTALLATION_CONFIG_URL = new URL('installation.json', installationSite.url).href;
|
|
12
|
+
/** Runtime overrides apply to this local process, never to browser-supplied install requests. */
|
|
13
|
+
export function resolveInstallationConfigUrl(value = process.env.MEARL_INSTALLATION_CONFIG_URL, allowLoopback = false) {
|
|
14
|
+
const url = new URL(value || MEARL_INSTALLATION_CONFIG_URL);
|
|
15
|
+
if (url.username || url.password || url.hash || (url.protocol !== 'https:' && !(allowLoopback && url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))))
|
|
16
|
+
throw new Error('Invalid installation configuration URL');
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
9
19
|
export { claimPresetInstallation } from './presetInstallationLease.js';
|
|
10
20
|
export { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
|
|
11
21
|
export { PresetBrowserTransfer } from './presetBrowserTransfer.js';
|
|
@@ -102,12 +112,12 @@ export class PresetInstaller {
|
|
|
102
112
|
}
|
|
103
113
|
}));
|
|
104
114
|
}
|
|
105
|
-
async componentStatus(install, receipt) {
|
|
115
|
+
async componentStatus(install, receipt, replaceSkills) {
|
|
106
116
|
const target = (this.options.hosts?.() ?? localHosts(install))[0];
|
|
107
117
|
if (!target)
|
|
108
118
|
return {};
|
|
109
119
|
try {
|
|
110
|
-
const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', { installationRevision: receipt.revision }, target, install ? 900 : 15, install);
|
|
120
|
+
const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', { installationRevision: receipt.revision, ...(install && replaceSkills?.length ? { replaceSkills } : {}) }, target, install ? 900 : 15, install);
|
|
111
121
|
if (result?.installationRevision !== receipt.revision || result?.nodeRevision !== receipt.nodeRevision || !Array.isArray(result?.components))
|
|
112
122
|
throw new Error('Component installation host requires an update');
|
|
113
123
|
if (result.catalogReady !== undefined && typeof result.catalogReady !== 'boolean')
|
|
@@ -141,7 +151,7 @@ export class PresetInstaller {
|
|
|
141
151
|
result.nodeRevision = receipt.nodeRevision;
|
|
142
152
|
result.browserRevision = receipt.browserRevision;
|
|
143
153
|
result.browserSyncRequired = receipt.browserSyncRequired === true;
|
|
144
|
-
result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.
|
|
154
|
+
result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.targets?.includes('node')).length
|
|
145
155
|
+ [...Object.values(preset.skills), ...Object.values(preset.providers)].filter(entry => entry.enabled !== false).length;
|
|
146
156
|
result.hosts = await this.hostStatus(false, receipt.nodeRevision, receipt.revision);
|
|
147
157
|
const nodeReady = result.hosts.length > 0 && result.hosts.every(host => host.state === 'active');
|
|
@@ -151,12 +161,12 @@ export class PresetInstaller {
|
|
|
151
161
|
for (const [id, entry] of Object.entries(preset.packages)) {
|
|
152
162
|
if (entry.enabled === false)
|
|
153
163
|
continue;
|
|
154
|
-
for (const runtime of entry.
|
|
164
|
+
for (const runtime of entry.targets ?? []) {
|
|
155
165
|
if (runtime === 'browser') {
|
|
156
166
|
if (!browserReady)
|
|
157
167
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'BROWSER_SYNC_REQUIRED' });
|
|
158
168
|
}
|
|
159
|
-
else if (!receipt.managedModules.includes(id))
|
|
169
|
+
else if (!receipt.managedModules.includes(id) && Object.keys(current.prepared.bundles.find(bundle => bundle.manifest.id === id)?.manifest.contributions?.nodeRuntimes ?? {}).length)
|
|
160
170
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'NODE_HOST_NOT_SUPPORTED' });
|
|
161
171
|
else if (!nodeReady)
|
|
162
172
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'HOST_SYNC_REQUIRED' });
|
|
@@ -186,7 +196,7 @@ export class PresetInstaller {
|
|
|
186
196
|
result.ready = result.pending.length === 0;
|
|
187
197
|
return result;
|
|
188
198
|
}
|
|
189
|
-
async synchronize(receipt, previousRevision) {
|
|
199
|
+
async synchronize(receipt, previousRevision, replaceSkills) {
|
|
190
200
|
const hosts = await this.hostStatus(true, receipt.nodeRevision, receipt.revision);
|
|
191
201
|
const rejection = hosts.find(host => host.state === 'rejected');
|
|
192
202
|
if (rejection) {
|
|
@@ -207,7 +217,7 @@ export class PresetInstaller {
|
|
|
207
217
|
if (installed.receipt?.revision !== receipt.revision || !installed.preset)
|
|
208
218
|
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before component installation');
|
|
209
219
|
const hasExternal = [...Object.values(installed.preset.skills), ...Object.values(installed.preset.providers)].some(entry => entry.enabled !== false);
|
|
210
|
-
const components = hasExternal || installed.activation?.bindings['plugins.catalog'] ? await this.componentStatus(true, receipt) : {};
|
|
220
|
+
const components = hasExternal || installed.activation?.bindings['plugins.catalog'] ? await this.componentStatus(true, receipt, replaceSkills) : {};
|
|
211
221
|
const status = await this.status();
|
|
212
222
|
if (components.error)
|
|
213
223
|
status.warnings.push(`Component installation failed; completed assets retained for retry: ${components.error}`);
|
|
@@ -225,6 +235,20 @@ export class PresetInstaller {
|
|
|
225
235
|
const prepared = await preparePreset(sourceUrl(source), { mearlApiVersion: this.store.mearlApiVersion, load: this.options.load ?? createNodePresetLoader(), signal: options.signal });
|
|
226
236
|
if (options.expectedId !== undefined && prepared.preset.id !== options.expectedId)
|
|
227
237
|
throw new BundleError('BUNDLE_INVALID', 'The preset identity differs from the accepted installation');
|
|
238
|
+
await options.validatePrepared?.(prepared);
|
|
239
|
+
if (options.skipUnchanged) {
|
|
240
|
+
const current = await this.store.current();
|
|
241
|
+
if (presetSelectionRevision(current) !== expectedRevision)
|
|
242
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
|
|
243
|
+
if (current.receipt && current.prepared?.snapshot.revision === prepared.snapshot.revision) {
|
|
244
|
+
return { revision: current.receipt.revision, activate: async () => {
|
|
245
|
+
const status = await this.status();
|
|
246
|
+
if (status.revision !== expectedRevision)
|
|
247
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
|
|
248
|
+
return status.ready ? status : this.resume(current.receipt.revision);
|
|
249
|
+
} };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
228
252
|
const receipt = await this.store.apply(prepared, { ...options, expectedRevision });
|
|
229
253
|
let activated = false;
|
|
230
254
|
return { revision: receipt.revision, activate: async () => {
|
|
@@ -239,7 +263,7 @@ export class PresetInstaller {
|
|
|
239
263
|
}
|
|
240
264
|
/** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
|
|
241
265
|
async stage(source, options = {}) {
|
|
242
|
-
const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) })
|
|
266
|
+
const accepted = { ...options, ...(options.overrides === undefined ? {} : { overrides: mergePresetDefaults({}, options.overrides) }) };
|
|
243
267
|
const current = await this.store.current({ verifyResources: false });
|
|
244
268
|
const revision = presetSelectionRevision(current);
|
|
245
269
|
if (accepted.expectedRevision !== undefined && accepted.expectedRevision !== revision)
|
|
@@ -249,17 +273,26 @@ export class PresetInstaller {
|
|
|
249
273
|
async apply(source, options = {}) {
|
|
250
274
|
return (await this.stage(source, options)).activate();
|
|
251
275
|
}
|
|
252
|
-
async update() {
|
|
276
|
+
async update(options = {}) {
|
|
253
277
|
const current = await this.store.current({ verifyResources: false });
|
|
254
278
|
if (!current.receipt)
|
|
255
279
|
throw new Error('No preset is installed');
|
|
256
|
-
return this.apply(current.receipt.snapshot.source, { expectedRevision: presetSelectionRevision(current) });
|
|
280
|
+
return this.apply(current.receipt.snapshot.source, { ...options, expectedRevision: presetSelectionRevision(current) });
|
|
257
281
|
}
|
|
258
|
-
async resume(expectedRevision) {
|
|
282
|
+
async resume(expectedRevision, options = {}) {
|
|
259
283
|
const current = await this.store.current();
|
|
260
284
|
if (!current.receipt || current.receipt.revision !== expectedRevision)
|
|
261
285
|
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed before resuming');
|
|
262
|
-
|
|
286
|
+
const names = options.replaceSkills;
|
|
287
|
+
if (names !== undefined && (!Array.isArray(names) || !names.length || names.length > 256 || new Set(names).size !== names.length || names.some(name => typeof name !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(name) || !current.preset || !Object.hasOwn(current.preset.skills, name) || current.preset.skills[name].enabled === false)))
|
|
288
|
+
throw new Error('Invalid Skill replacement selection');
|
|
289
|
+
return this.synchronize(current.receipt, expectedRevision, options.replaceSkills);
|
|
290
|
+
}
|
|
291
|
+
async repair(options = {}) {
|
|
292
|
+
const current = await this.store.current({ verifyResources: false });
|
|
293
|
+
if (!current.receipt)
|
|
294
|
+
throw new Error('No preset is installed');
|
|
295
|
+
return this.resume(current.receipt.revision, options);
|
|
263
296
|
}
|
|
264
297
|
async configure(value) {
|
|
265
298
|
const current = await this.store.current();
|
|
@@ -269,7 +302,11 @@ export class PresetInstaller {
|
|
|
269
302
|
return this.synchronize(receipt, presetSelectionRevision(current));
|
|
270
303
|
}
|
|
271
304
|
}
|
|
272
|
-
export async function runPresetCommand(command, source) {
|
|
305
|
+
export async function runPresetCommand(command, source, options = {}) {
|
|
306
|
+
if (command !== 'repair' && options.replaceSkills !== undefined)
|
|
307
|
+
throw new Error('Skill replacements require preset repair');
|
|
273
308
|
const installer = new PresetInstaller();
|
|
309
|
+
if (command === 'repair')
|
|
310
|
+
return installer.repair(options);
|
|
274
311
|
return command === 'status' ? installer.status() : command === 'update' ? installer.update() : installer.apply(source);
|
|
275
312
|
}
|
package/dist/presetStore.d.ts
CHANGED
|
@@ -1,48 +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
|
-
nodeConfig: Record<string, JsonObject>;
|
|
13
|
-
overrides: JsonObject;
|
|
14
|
-
revision: string;
|
|
15
|
-
}
|
|
16
|
-
interface SelectionState {
|
|
17
|
-
rawHash: string | null;
|
|
18
|
-
activation?: NodeCapabilityActivation;
|
|
19
|
-
receipt?: PresetInstallReceipt;
|
|
20
|
-
prepared?: PreparedPreset;
|
|
21
|
-
preset?: ResolvedPreset;
|
|
22
|
-
}
|
|
23
|
-
/** One pointer binds the desired preset, user overrides and exact Node selection. */
|
|
24
|
-
export declare class PresetInstallationStore {
|
|
25
|
-
readonly directory: string;
|
|
26
|
-
readonly mearlApiVersion: string;
|
|
27
|
-
private readonly pointer;
|
|
28
|
-
constructor(options?: {
|
|
29
|
-
directory?: string;
|
|
30
|
-
mearlApiVersion?: string;
|
|
31
|
-
});
|
|
32
|
-
private assertCurrent;
|
|
33
|
-
current(options?: {
|
|
34
|
-
verifyResources?: boolean;
|
|
35
|
-
}): Promise<SelectionState>;
|
|
36
|
-
apply(input: Pick<PreparedPreset, 'snapshot' | 'bundles'>, options: {
|
|
37
|
-
expectedRevision: string | null;
|
|
38
|
-
overrides?: JsonObject;
|
|
39
|
-
nodeConfig?: Record<string, JsonObject>;
|
|
40
|
-
signal?: AbortSignal;
|
|
41
|
-
confirm?: () => Promise<void>;
|
|
42
|
-
}): Promise<PresetInstallReceipt>;
|
|
43
|
-
rollback(expectedRevision: string): Promise<void>;
|
|
44
|
-
collectGarbage(): Promise<void>;
|
|
45
|
-
private collectLocked;
|
|
46
|
-
}
|
|
47
|
-
export declare function presetSelectionRevision(state: SelectionState): string | null;
|
|
48
|
-
export {};
|
|
1
|
+
export { PresetInstallationStore, presetSelectionRevision } from '@mearl/daemon-core/installation';
|
|
2
|
+
export type { PresetInstallReceipt } from '@mearl/daemon-core/installation';
|