@mearl/setup 3.0.0-beta.8 → 3.0.0

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.
@@ -1,6 +1,6 @@
1
1
  import { invokeLocalHost, listLiveBrowsers } from '@mearl/client/local';
2
2
  import { installationOriginCovered, loadInstallationConfig, matchesInstallationOrigin, trustedInstallationPreset } from '@mearl/preset/installation';
3
- import { createNodePresetLoader, PresetInstaller, resolveInstallationConfigUrl } from './preset.js';
3
+ import { connectedPresetExtensions, createNodePresetLoader, PresetInstaller, resolveInstallationConfigUrl } from './preset.js';
4
4
  /** Explicit setup may complete a trusted browser selection before installing any Skills. */
5
5
  export async function prepareBrowserSetupPreset(options = {}, dependencies = {}) {
6
6
  const env = options.env ?? process.env;
@@ -8,27 +8,29 @@ export async function prepareBrowserSetupPreset(options = {}, dependencies = {})
8
8
  const current = await new PresetInstaller({ directory }).store.current();
9
9
  if (current.activation || current.receipt)
10
10
  return;
11
- const now = dependencies.now ?? Date.now;
12
- const deadline = now() + (options.waitMs ?? 0);
13
11
  const browsers = dependencies.browsers ?? listLiveBrowsers;
14
12
  const invoke = dependencies.invoke ?? invokeLocalHost;
15
- const sleep = dependencies.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
16
13
  const configuration = dependencies.configuration ?? (() => {
17
14
  const url = resolveInstallationConfigUrl(env.MEARL_INSTALLATION_CONFIG_URL);
18
15
  return loadInstallationConfig(url, url.origin, AbortSignal.timeout(5_000));
19
16
  });
20
- const read = async (timeoutSec = 5) => {
21
- const targets = browsers().filter(browser => !browser.provider && browser.transport === 'extension' && (browser.status === undefined || browser.status === 'connected') && (!env.MEARL_SOCKET_PATH || browser.socketPath === env.MEARL_SOCKET_PATH));
17
+ const connected = () => connectedPresetExtensions(browsers(), env.MEARL_SOCKET_PATH?.trim());
18
+ const read = async () => {
19
+ const targets = connected();
22
20
  if (!targets.length)
23
21
  return;
24
22
  return (await Promise.all(targets.map(async (browser) => {
25
23
  let value;
26
24
  try {
27
- value = await invoke('preset_browser_status', {}, { socketPath: browser.socketPath, timeoutSec });
25
+ value = await invoke('preset_browser_status', {}, { socketPath: browser.socketPath, timeoutSec: 5 });
28
26
  }
29
27
  catch {
28
+ if (!connected().some(target => target.browserId === browser.browserId && target.socketPath === browser.socketPath))
29
+ return [];
30
30
  throw new Error(`Cannot read preset from ${browser.browserId}. Reconnect the extension and retry setup, or use --preset <source>.`);
31
31
  }
32
+ if (value?.error === 'EXTENSION_NOT_CONNECTED')
33
+ return [];
32
34
  if (value?.error || typeof value?.browserReady !== 'boolean' || !Object.hasOwn(value, 'browserRevision') || (value.presetId !== undefined && (typeof value.presetId !== 'string' || !value.presetId || typeof value.presetSource !== 'string' || typeof value.browserRevision !== 'string' || value.browserReady !== true)))
33
35
  throw new Error(`Browser preset is unavailable in ${browser.browserId}. Reconnect the extension and retry setup, or use --preset <source>.`);
34
36
  if (!value.presetId)
@@ -36,21 +38,9 @@ export async function prepareBrowserSetupPreset(options = {}, dependencies = {})
36
38
  return [{ browserId: browser.browserId, presetId: value.presetId, presetSource: value.presetSource, browserRevision: value.browserRevision }];
37
39
  }))).flat();
38
40
  };
39
- let selected = await read();
40
- if (!selected && now() < deadline)
41
- options.writeLine?.('Waiting for the Mearl extension to connect and checking its installed preset…');
42
- while (!selected && now() < deadline) {
43
- await sleep(Math.min(1000, deadline - now()));
44
- if (now() < deadline)
45
- selected = await read(Math.max(0.1, Math.min(5, (deadline - now()) / 1000)));
46
- }
47
- if (!selected) {
48
- options.writeLine?.('No connected Mearl extension was found. Continuing without a browser preset; connect and rerun setup to adopt it, or use --preset <source> to select a preset.');
49
- return;
50
- }
51
- if (!selected.length)
41
+ const accepted = await read();
42
+ if (!accepted?.length)
52
43
  return;
53
- const accepted = selected;
54
44
  const first = accepted[0];
55
45
  if (accepted.some(value => value.presetId !== first.presetId || value.presetSource !== first.presetSource))
56
46
  throw new Error('Connected browsers use different presets. Select one explicitly with --preset <source>.');
@@ -58,7 +48,7 @@ export async function prepareBrowserSetupPreset(options = {}, dependencies = {})
58
48
  if (!trusted)
59
49
  throw new Error('The browser preset is not in the trusted installation catalog. Install it explicitly with --preset <source>.');
60
50
  const load = dependencies.load ?? createNodePresetLoader();
61
- const installer = new PresetInstaller({ directory, load: (source, resourceOptions) => {
51
+ const installer = new PresetInstaller({ directory, connectedExtensionsOnly: true, load: (source, resourceOptions) => {
62
52
  const url = new URL(source);
63
53
  if (url.protocol !== 'https:' || url.username || url.password || !trusted.origins.some(origin => matchesInstallationOrigin(origin, url)))
64
54
  throw new Error('Browser preset resource is outside the trusted installation catalog');
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { installMearlBrowserExtension } from './browserExtension.js';
2
+ import { downloadMearlBrowserExtension, installMearlBrowserExtension } from './browserExtension.js';
3
3
  import { runMearlLifecycle, type MearlLifecycleOperation } from './lifecycle.js';
4
4
  import type { MearlProfile } from './profiles.js';
5
5
  import { ensureOfficialPlugin, installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
@@ -9,10 +9,13 @@ import { prepareSetupPreset } from './setupPreset.js';
9
9
  export { SETUP_VERSION } from './setupVersion.js';
10
10
  interface ParsedArgs {
11
11
  cloudMode?: string;
12
+ extract?: boolean;
12
13
  force?: boolean;
13
14
  full?: boolean;
14
15
  help: boolean;
15
- operation?: MearlLifecycleOperation | 'extension' | 'plugin' | 'provider' | 'preset';
16
+ operation?: MearlLifecycleOperation | 'extension' | 'plugin' | 'provider' | 'preset' | 'profile';
17
+ profileSource?: string;
18
+ profileBrowser?: string;
16
19
  presetCommand?: 'apply' | 'update' | 'status' | 'repair';
17
20
  presetSource?: string;
18
21
  presetReplaceSkills?: string[];
@@ -30,12 +33,14 @@ interface ParsedArgs {
30
33
  yes: boolean;
31
34
  }
32
35
  export interface SetupCliDependencies {
36
+ installProfile?: typeof installProfile;
33
37
  preparePreset?: typeof prepareSetupPreset;
34
38
  runPreset?: typeof runPresetCommand;
35
39
  registerPlugin?: typeof setupPlugin;
36
40
  ensureOfficialPlugin?: typeof ensureOfficialPlugin;
37
41
  installPlugins?: typeof installPlugins;
38
42
  uninstallPlugin?: typeof uninstallPlugin;
43
+ downloadBrowserExtension?: typeof downloadMearlBrowserExtension;
39
44
  installBrowserExtension?: typeof installMearlBrowserExtension;
40
45
  installProvider?: typeof installProviderPackage;
41
46
  runLifecycle?: typeof runMearlLifecycle;
@@ -43,4 +48,5 @@ export interface SetupCliDependencies {
43
48
  updateProvider?: typeof updateProviderPackage;
44
49
  }
45
50
  export declare function parseSetupArgs(argv: string[]): ParsedArgs;
51
+ declare function installProfile(source: string, browser?: string): Promise<unknown>;
46
52
  export declare function main(argv?: string[], interactiveTerminal?: boolean, dependencies?: SetupCliDependencies): Promise<number>;
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError } from 'commander';
3
- import { installMearlBrowserExtension, isUnsupportedCdpMethodError } from './browserExtension.js';
3
+ import { invoke, listLiveBrowsers } from '@mearl/client/local';
4
+ import { parseProfileSource } from '@mearl/preset/bundles';
5
+ import { downloadMearlBrowserExtension, installMearlBrowserExtension, isUnsupportedCdpMethodError } from './browserExtension.js';
4
6
  import { runMearlLifecycle } from './lifecycle.js';
5
7
  import { isMearlReleaseLine } from './version.js';
6
8
  import { ensureOfficialPlugin, installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
@@ -25,6 +27,15 @@ function commanderError(error) {
25
27
  return new Error(message);
26
28
  }
27
29
  export function parseSetupArgs(argv) {
30
+ if (argv[0] === 'profile') {
31
+ if (argv.length === 2 && (argv[1] === '--help' || argv[1] === '-h'))
32
+ return { ...baseParsedArgs(), help: true };
33
+ if (argv[1] !== 'install' || !argv[2] || (argv.length !== 3 && !(argv.length === 5 && argv[3] === '--browser' && argv[4]))) {
34
+ throw new Error('profile requires install <GitHub owner/repo[@ref] or HTTPS bundle URL> [--browser <id-or-name>]');
35
+ }
36
+ parseProfileSource(argv[2]);
37
+ return { ...baseParsedArgs(), operation: 'profile', profileSource: argv[2], profileBrowser: argv[4] };
38
+ }
28
39
  if (argv[0] === 'preset') {
29
40
  if (argv[1] === 'repair') {
30
41
  const command = new Command('preset repair')
@@ -163,12 +174,17 @@ export function parseSetupArgs(argv) {
163
174
  .configureOutput({ writeErr: () => { }, writeOut: () => { } })
164
175
  .option('--full')
165
176
  .option('--force')
177
+ .option('--extract')
166
178
  .option('-y, --yes')
167
179
  .option('--non-interactive')
168
180
  .action(() => {
169
181
  const options = extension.opts();
182
+ if (options.extract && (options.full || options.force)) {
183
+ throw new Error('--extract cannot be combined with --full or --force');
184
+ }
170
185
  parsed = {
171
186
  ...baseParsedArgs(),
187
+ ...(options.extract ? { extract: true } : {}),
172
188
  ...(options.force ? { force: true } : {}),
173
189
  ...(options.full ? { full: true } : {}),
174
190
  operation: 'extension',
@@ -206,6 +222,7 @@ Environment commands:
206
222
  uninstall [options] Uninstall detected or selected environments
207
223
 
208
224
  Integration commands:
225
+ profile install <source> Install one App profile bundle in a connected extension
209
226
  preset apply <source> Apply a preset, synchronize hosts and install declared components
210
227
  preset update Update the selected preset, preserving user overrides
211
228
  preset repair Retry the installed preset using its accepted resources
@@ -219,6 +236,7 @@ Integration commands:
219
236
 
220
237
  Browser extension:
221
238
  extension [--full] [--force] Install or update the browser extension
239
+ extension --extract Download and unpack the extension for manual loading
222
240
 
223
241
  Environment options:
224
242
  --local Manage @mearl/native-host and @mearl/client
@@ -251,8 +269,8 @@ Examples:
251
269
  npx @mearl/setup --local --cloud
252
270
  npx @mearl/setup update
253
271
  npx @mearl/setup uninstall --full
254
- npx @mearl/setup plugin uninstall yuque
255
- npx @mearl/setup provider install @example/mearl-provider-brow
272
+ npx @mearl/setup plugin uninstall tabs
273
+ npx @mearl/setup provider install @example/mearl-provider-device
256
274
  npx @mearl/setup preset repair --replace-skill example-skill
257
275
  npx @mearl/setup extension --full`;
258
276
  console.log(output.replaceAll('npx @mearl/setup', SETUP_COMMAND));
@@ -274,9 +292,22 @@ async function runSetupLifecycle(options, dependencies) {
274
292
  const actual = await (dependencies.runPreset ?? runPresetCommand)('status');
275
293
  if (actual.revision !== preset.revision)
276
294
  throw new Error('The selected preset changed while setup was running');
277
- console.log(JSON.stringify(actual, null, 2));
295
+ const warnings = [...new Set([...(preset.warnings ?? []), ...(actual.warnings ?? [])])];
296
+ console.log(JSON.stringify({ ...actual, warnings }, null, 2));
278
297
  return actual.ready ? 0 : 3;
279
298
  }
299
+ async function installProfile(source, browser) {
300
+ if (!browser) {
301
+ const extensions = listLiveBrowsers().filter(target => target.transport === 'extension' && (target.status === undefined || target.status === 'connected'));
302
+ if (extensions.length !== 1)
303
+ throw new Error(extensions.length
304
+ ? 'Multiple extensions are connected. Select one with --browser <id-or-name>.'
305
+ : 'Connect a Mearl extension before installing App profiles.');
306
+ browser = extensions[0].browserId;
307
+ }
308
+ console.error('If the bundle source needs permission, confirm it in the browser.');
309
+ return invoke('profile_install', { source }, { browser, timeoutSec: 150 });
310
+ }
280
311
  export async function main(argv = process.argv.slice(2), interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true, dependencies = {}) {
281
312
  let parsed;
282
313
  try {
@@ -295,6 +326,17 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
295
326
  console.log(SETUP_VERSION);
296
327
  return 0;
297
328
  }
329
+ if (parsed.operation === 'profile') {
330
+ try {
331
+ const result = await (dependencies.installProfile ?? installProfile)(parsed.profileSource, parsed.profileBrowser);
332
+ console.log(JSON.stringify(result, null, 2));
333
+ return 0;
334
+ }
335
+ catch (error) {
336
+ console.error(error instanceof Error ? error.message : String(error));
337
+ return 1;
338
+ }
339
+ }
298
340
  if (parsed.operation === 'preset') {
299
341
  try {
300
342
  const run = dependencies.runPreset ?? runPresetCommand;
@@ -376,6 +418,18 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
376
418
  }
377
419
  }
378
420
  if (parsed.operation === 'extension') {
421
+ if (parsed.extract) {
422
+ try {
423
+ const result = await (dependencies.downloadBrowserExtension ?? downloadMearlBrowserExtension)();
424
+ console.log(`Mearl v${result.version} is ready to load from ${result.path}`);
425
+ console.log('Open chrome://extensions/, enable Developer mode, click Load unpacked, and select that directory.');
426
+ return 0;
427
+ }
428
+ catch (error) {
429
+ console.error(error instanceof Error ? error.message : String(error));
430
+ return 1;
431
+ }
432
+ }
379
433
  if (!interactiveTerminal && !parsed.yes) {
380
434
  console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
381
435
  return 2;
@@ -411,6 +465,7 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
411
465
  : `Browser extension v${result.version} was not installed.`);
412
466
  if (result.cdpError) {
413
467
  console.error(`CDP installation failed: ${result.cdpError}`);
468
+ console.error(`To load the extension manually, run: ${SETUP_COMMAND} extension --extract`);
414
469
  if (isUnsupportedCdpMethodError(result.cdpError)) {
415
470
  console.error(`This Chrome version does not support installing extensions through CDP. Update Chrome to version 145 or later, then rerun: ${SETUP_COMMAND} extension`);
416
471
  extensionFailureIsTerminal = true;
@@ -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;
@@ -422,7 +459,9 @@ export async function runMearlLifecycle(options, dependencies = {}) {
422
459
  }
423
460
  if (packagesOk && selectsInitialPreset && !stagedPreset && !options.presetSource && lifecycleSelected.some(profile => profile === 'local' || profile === 'mcp')) {
424
461
  try {
425
- stagedPreset = await (dependencies.prepareBrowserPreset ?? prepareBrowserSetupPreset)({ env, waitMs: 65_000, writeLine });
462
+ stagedPreset = await (dependencies.prepareBrowserPreset ?? prepareBrowserSetupPreset)({ env, writeLine });
463
+ if (stagedPreset)
464
+ await acceptPreset(stagedPreset.revision);
426
465
  }
427
466
  catch (error) {
428
467
  writeLine(`Browser preset: ${error instanceof Error ? error.message : String(error)}`);
@@ -432,6 +471,8 @@ export async function runMearlLifecycle(options, dependencies = {}) {
432
471
  if (packagesOk && selectsInitialPreset && !stagedPreset && options.defaultPresetSource) {
433
472
  try {
434
473
  stagedPreset = await (dependencies.preparePreset ?? prepareSetupPreset)({ fallbackSource: options.defaultPresetSource, env });
474
+ if (stagedPreset)
475
+ await acceptPreset(stagedPreset.revision);
435
476
  }
436
477
  catch (error) {
437
478
  writeLine(`Default preset: ${error instanceof Error ? error.message : String(error)}`);
package/dist/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
@@ -30,14 +30,15 @@ function defaultPluginName(target, entry) {
30
30
  ? basename(dirname(entryDirectory))
31
31
  : basename(entryDirectory);
32
32
  }
33
- export async function setupPlugin(target, requestedName) {
33
+ export async function setupPlugin(target, requestedName, runner = runCommand) {
34
34
  if (requestedName)
35
35
  assertPluginName(requestedName);
36
36
  const entry = resolvePluginEntry(target);
37
37
  const plugin = await loadPlugin(entry, registrationContext);
38
38
  const name = requestedName || plugin.name || defaultPluginName(target, entry);
39
39
  assertPluginName(name);
40
- await ensurePluginRuntime();
40
+ await ensurePluginRuntime(runner);
41
+ await plugin.setup?.({ runCommand: runner });
41
42
  return {
42
43
  entry: registerPlugin(name, entry, plugin.description, {
43
44
  hasPublicCommands: Boolean(plugin.commandHelp(name)),
@@ -345,6 +346,7 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
345
346
  throw new Error(`多个 skill 使用插件名称 ${name},请调整入口的 name metadata。`);
346
347
  plugins.set(name, {
347
348
  entry,
349
+ setup: plugin.setup,
348
350
  hasPublicCommands: Boolean(plugin.commandHelp(name)),
349
351
  description: plugin.description,
350
352
  ...(plugin.toolbox ? { toolbox: plugin.toolbox } : {}),
@@ -357,14 +359,26 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
357
359
  return 0;
358
360
  }
359
361
  await ensurePluginRuntime(runner);
360
- for (const [name, { entry, description, toolbox, appProfiles, profileSources, hasPublicCommands },] of plugins) {
362
+ for (const [name, { entry, description, toolbox, appProfiles, profileSources, hasPublicCommands, setup },] of plugins) {
361
363
  await options.confirm?.();
362
364
  verifySkill();
363
- const existing = findPluginRegistration(name);
364
- 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()) {
365
370
  console.error(`Plugin ${name}: existing user registration kept.`);
366
371
  continue;
367
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
+ }
368
382
  registerPlugin(name, entry, description, {
369
383
  hasPublicCommands,
370
384
  managedBy: options.registrationManagedBy ?? (cli.id === 'skills' ? undefined : cli.id),
package/dist/preset.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { invoke } from '@mearl/client/local';
1
+ import { invoke, listLiveBrowsers } from '@mearl/client/local';
2
2
  import { type JsonObject } from '@mearl/preset';
3
3
  import { type BundleLoader, type PreparedPreset } from '@mearl/preset/bundles';
4
4
  import { PresetInstallationStore } from './presetStore.js';
@@ -60,6 +60,8 @@ export interface PresetInstallerOptions {
60
60
  load?: BundleLoader;
61
61
  hosts?: () => PresetHostTarget[];
62
62
  invoke?: typeof invoke;
63
+ /** CLI setup synchronizes connected extensions without requiring their acknowledgement. */
64
+ connectedExtensionsOnly?: boolean;
63
65
  }
64
66
  export interface PresetApplyOptions {
65
67
  validatePrepared?: (prepared: PreparedPreset) => Promise<void>;
@@ -76,6 +78,7 @@ export interface StagedPresetInstallation {
76
78
  }
77
79
  /** The caller chooses the source; this loader adds no identity or credential discovery. */
78
80
  export declare function createNodePresetLoader(): BundleLoader;
81
+ export declare function connectedPresetExtensions(browsers: ReturnType<typeof listLiveBrowsers>, socketPath?: string): import("@mearl/client/local").LiveBrowser[];
79
82
  /** Installation and live acknowledgements are distinct; browser/external installs remain pending. */
80
83
  export declare class PresetInstaller {
81
84
  readonly store: PresetInstallationStore;
package/dist/preset.js CHANGED
@@ -71,6 +71,10 @@ function localHosts(allowStart = false) {
71
71
  const hosts = configured ? [{ id: 'local', socketPath: configured }] : listLiveBrowsers().filter(browser => !browser.provider).map(browser => ({ id: browser.browserId, socketPath: browser.socketPath }));
72
72
  return hosts.length ? Array.from(new Map(hosts.map(host => [host.socketPath, host])).values()) : [{ id: 'local', ...(allowStart ? {} : { socketPath: SOCKET_PATH }) }];
73
73
  }
74
+ export function connectedPresetExtensions(browsers, socketPath) {
75
+ return browsers.filter(browser => !browser.provider && browser.transport === 'extension' &&
76
+ (browser.status === undefined || browser.status === 'connected') && (!socketPath || browser.socketPath === socketPath));
77
+ }
74
78
  /** Installation and live acknowledgements are distinct; browser/external installs remain pending. */
75
79
  export class PresetInstaller {
76
80
  store;
@@ -99,13 +103,18 @@ export class PresetInstaller {
99
103
  }));
100
104
  }
101
105
  async browserStatus(reload, browserRevision, installationRevision) {
102
- const targets = this.options.hosts?.() ?? localHosts(reload);
106
+ const targets = this.options.connectedExtensionsOnly
107
+ ? connectedPresetExtensions(listLiveBrowsers(), process.env.MEARL_SOCKET_PATH?.trim()).map(browser => ({ id: browser.browserId, socketPath: browser.socketPath }))
108
+ : this.options.hosts?.() ?? localHosts(reload);
103
109
  return Promise.all(targets.map(async (target) => {
104
110
  try {
105
111
  const result = await this.invokeHost(reload ? 'preset_browser_sync' : 'preset_browser_status', reload ? { installationRevision } : {}, target, reload ? 130 : 15, reload);
106
112
  if (result?.accepted === false)
107
113
  return { id: target.id, state: 'rejected', settingsReady: false, error: typeof result.error === 'string' ? result.error : 'Browser preset activation rejected' };
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' } : {}) };
114
+ const error = Array.isArray(result?.permissionPending) && result.permissionPending.length
115
+ ? 'PRESET_DOMAIN_PERMISSION_REQUIRED: Open extension settings and authorize preset domains'
116
+ : typeof result?.error === 'string' ? result.error : undefined;
117
+ return { id: target.id, state: result?.ready === true && result?.browserRevision === browserRevision && result?.installationRevision === installationRevision ? 'active' : 'pending', settingsReady: result?.settingsReady === true, ...(error ? { error } : {}) };
109
118
  }
110
119
  catch {
111
120
  return { id: target.id, state: 'pending', settingsReady: false, error: 'BROWSER_UNAVAILABLE' };
@@ -150,7 +159,8 @@ export class PresetInstaller {
150
159
  result.settings = mergePresetDefaults(preset.defaults, receipt.overrides);
151
160
  result.nodeRevision = receipt.nodeRevision;
152
161
  result.browserRevision = receipt.browserRevision;
153
- result.browserSyncRequired = receipt.browserSyncRequired === true;
162
+ const browserSyncRequired = receipt.browserSyncRequired === true && !this.options.connectedExtensionsOnly;
163
+ result.browserSyncRequired = browserSyncRequired;
154
164
  result.localComponentCount = Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.targets?.includes('node')).length
155
165
  + [...Object.values(preset.skills), ...Object.values(preset.providers)].filter(entry => entry.enabled !== false).length;
156
166
  result.hosts = await this.hostStatus(false, receipt.nodeRevision, receipt.revision);
@@ -163,7 +173,7 @@ export class PresetInstaller {
163
173
  continue;
164
174
  for (const runtime of entry.targets ?? []) {
165
175
  if (runtime === 'browser') {
166
- if (!browserReady)
176
+ if (browserSyncRequired && !browserReady)
167
177
  result.pending.push({ kind: 'packages', id, runtime, reason: 'BROWSER_SYNC_REQUIRED' });
168
178
  }
169
179
  else if (!receipt.managedModules.includes(id) && Object.keys(current.prepared.bundles.find(bundle => bundle.manifest.id === id)?.manifest.contributions?.nodeRuntimes ?? {}).length)
@@ -189,9 +199,9 @@ export class PresetInstaller {
189
199
  }
190
200
  if (receipt.nodeSyncRequired && !nodeReady && !result.pending.some(entry => entry.reason === 'HOST_SYNC_REQUIRED'))
191
201
  result.pending.push({ kind: 'preset', id: preset.id, runtime: 'node', reason: 'HOST_SYNC_REQUIRED' });
192
- if (receipt.browserSyncRequired && !browserReady && !result.pending.some(entry => entry.reason === 'BROWSER_SYNC_REQUIRED'))
202
+ if (browserSyncRequired && !browserReady && !result.pending.some(entry => entry.reason === 'BROWSER_SYNC_REQUIRED'))
193
203
  result.pending.push({ kind: 'preset', id: preset.id, runtime: 'browser', reason: 'BROWSER_SYNC_REQUIRED' });
194
- if (receipt.browserSyncRequired && browserReady && Object.keys(result.settings ?? {}).length && !result.browsers.every(host => host.settingsReady))
204
+ if (browserSyncRequired && browserReady && Object.keys(result.settings ?? {}).length && !result.browsers.every(host => host.settingsReady))
195
205
  result.pending.push({ kind: 'preset', id: preset.id, runtime: 'browser', reason: 'SETTINGS_APPLY_REQUIRED' });
196
206
  result.ready = result.pending.length === 0;
197
207
  return result;
@@ -207,18 +217,18 @@ export class PresetInstaller {
207
217
  await this.hostStatus(true, previous.activation?.revision ?? '', previous.activation?.installationRevision ?? '');
208
218
  throw new Error(`Node activation rejected; previous selection restored: ${rejection.error}`);
209
219
  }
210
- if (receipt.browserSyncRequired) {
211
- const browsers = await this.browserStatus(true, receipt.browserRevision, receipt.revision);
212
- const rejected = browsers.find(host => host.state === 'rejected');
213
- if (rejected)
214
- throw new Error(`Browser activation rejected; installed selection retained for retry: ${rejected.error}`);
215
- }
220
+ const browsers = receipt.browserSyncRequired ? await this.browserStatus(true, receipt.browserRevision, receipt.revision) : [];
221
+ const rejected = browsers.find(host => host.state === 'rejected');
222
+ if (rejected && !this.options.connectedExtensionsOnly)
223
+ throw new Error(`Browser activation rejected; installed selection retained for retry: ${rejected.error}`);
216
224
  const installed = await this.store.current({ verifyResources: false });
217
225
  if (installed.receipt?.revision !== receipt.revision || !installed.preset)
218
226
  throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before component installation');
219
227
  const hasExternal = [...Object.values(installed.preset.skills), ...Object.values(installed.preset.providers)].some(entry => entry.enabled !== false);
220
228
  const components = hasExternal || installed.activation?.bindings['plugins.catalog'] ? await this.componentStatus(true, receipt, replaceSkills) : {};
221
229
  const status = await this.status();
230
+ if (rejected)
231
+ status.warnings.push(`Browser preset activation rejected by ${rejected.id}: ${rejected.error}`);
222
232
  if (components.error)
223
233
  status.warnings.push(`Component installation failed; completed assets retained for retry: ${components.error}`);
224
234
  if (status.revision !== receipt.revision)
@@ -305,7 +315,7 @@ export class PresetInstaller {
305
315
  export async function runPresetCommand(command, source, options = {}) {
306
316
  if (command !== 'repair' && options.replaceSkills !== undefined)
307
317
  throw new Error('Skill replacements require preset repair');
308
- const installer = new PresetInstaller();
318
+ const installer = new PresetInstaller({ connectedExtensionsOnly: true });
309
319
  if (command === 'repair')
310
320
  return installer.repair(options);
311
321
  return command === 'status' ? installer.status() : command === 'update' ? installer.update() : installer.apply(source);
@@ -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';