@mearl/setup 3.0.0-beta.0 → 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/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 './presetUpgrade.js';
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 qoderMode = cloudMode === 'qoder' || /\[CloudServer\]\s+Mode:\s+qoder\b/.test(status.stdout);
139
- if (operation === 'update' && !running && !qoderMode) {
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 && !qoderMode ? 'restart' : 'start'];
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' && (options.presetSource || !options.targetLine || Number(options.targetLine.split('.')[0]) >= 3) && !dependencies.prepareSkills) {
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], 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);
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, mearlPackageSelector(SETUP_VERSION, options.targetLine), runner, env, `Packages (${profile})`, writeLine);
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
- if (findPluginRegistration(name))
56
- return 'existing';
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 () => {
@@ -111,7 +127,7 @@ function globalSkill(entry) {
111
127
  return undefined;
112
128
  }
113
129
  function projectSkill(entry, managedBy) {
114
- if (managedBy !== 'skills' && managedBy !== 'ali-skills')
130
+ if (!managedBy)
115
131
  return undefined;
116
132
  const absolute = resolve(entry);
117
133
  const marker = `${sep}.agents${sep}skills${sep}`;
@@ -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
- if (options.expectedSkill && declaration) {
280
- const status = cli.inspectGlobal(declaration.source, options.expectedSkill, declaration.version);
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
- const installed = await cli.run(['add', ...args, '--global'], { tee: true });
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 existing = findPluginRegistration(name);
340
- if (options.registrationManagedBy && existing && existing.managedBy !== options.registrationManagedBy) {
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): Promise<PresetInstallStatus>;
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): Promise<PresetInstallStatus>;
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';
@@ -95,19 +105,19 @@ export class PresetInstaller {
95
105
  const result = await this.invokeHost(reload ? 'preset_browser_sync' : 'preset_browser_status', reload ? { installationRevision } : {}, target, reload ? 130 : 15, reload);
96
106
  if (result?.accepted === false)
97
107
  return { id: target.id, state: 'rejected', settingsReady: false, error: typeof result.error === 'string' ? result.error : 'Browser preset activation rejected' };
98
- return { id: target.id, state: result?.ready === true && result?.browserRevision === browserRevision && result?.installationRevision === installationRevision ? 'active' : 'pending', settingsReady: result?.settingsReady === true };
108
+ return { id: target.id, state: result?.ready === true && result?.browserRevision === browserRevision && result?.installationRevision === installationRevision ? 'active' : 'pending', settingsReady: result?.settingsReady === true, ...(Array.isArray(result?.permissionPending) && result.permissionPending.length ? { error: 'PRESET_DOMAIN_PERMISSION_REQUIRED: Open extension settings and authorize preset domains' } : {}) };
99
109
  }
100
110
  catch {
101
111
  return { id: target.id, state: 'pending', settingsReady: false, error: 'BROWSER_UNAVAILABLE' };
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.runtimes?.includes('node')).length
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.runtimes ?? []) {
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) }), ...(options.nodeConfig === undefined ? {} : { nodeConfig: mergePresetDefaults({}, options.nodeConfig) }) };
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
- return this.synchronize(current.receipt, expectedRevision);
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
  }
@@ -1,48 +1,2 @@
1
- import { type JsonObject, type NodeCapabilityActivation, 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
- 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';