@mearl/setup 3.0.1 → 3.0.2
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.en.md +6 -3
- package/README.md +6 -3
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +50 -19
- package/dist/preset.d.ts +7 -2
- package/dist/preset.js +296 -49
- package/dist/skillSourceReceipts.d.ts +1 -1
- package/dist/skillSourceReceipts.js +54 -10
- package/dist/skillSources.d.ts +1 -1
- package/dist/skillSources.js +85 -33
- package/package.json +6 -6
package/README.en.md
CHANGED
|
@@ -19,7 +19,8 @@ npx @mearl/setup plugin install <skills add arguments...>
|
|
|
19
19
|
npx @mearl/setup plugin uninstall <name>
|
|
20
20
|
npx @mearl/setup provider <install|update|uninstall> <package|id>
|
|
21
21
|
npx @mearl/setup profile install <source> [--browser <id-or-name>]
|
|
22
|
-
npx @mearl/setup preset
|
|
22
|
+
npx @mearl/setup preset apply [source]
|
|
23
|
+
npx @mearl/setup preset <update|status|uninstall>
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Browser Providers install and evolve independently. After installation, run `mearl <providerId>` to inspect that version's capabilities,
|
|
@@ -52,14 +53,16 @@ The installation host's local plugin catalog determines preset-managed capabilit
|
|
|
52
53
|
|
|
53
54
|
New registrations created by Native preset batches are setup-managed and retain that ownership through Agent updates. Existing user registrations in the same directory and custom user aliases retain their ownership. Managed removal encountering user aliases sharing a Skill removes only managed registrations and keeps Skill files.
|
|
54
55
|
|
|
55
|
-
GUI and CLI share the preset installer and override rules.
|
|
56
|
+
GUI and CLI share the preset installer and override rules. `preset apply` accepts a URL or local file path. Omitting the source retries from the saved snapshot. `preset update` rereads the current source. Repeat `--replace-skill` during a retry to authorize replacing those Skill sources or same-name plugin registrations for this invocation.
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
|
-
npx @mearl/setup preset
|
|
59
|
+
npx @mearl/setup preset apply --replace-skill example-skill --replace-skill another-skill
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
Already installed Skills receive registration repair only; source conflicts reinstall. Unlisted conflicts still block installation, and undeclared tool names or changed installation identities are rejected. Replacement authorization does not persist into later repairs or automatic updates. Missing browser permissions still require extension authorization. Output uses preset JSON: ready exits `0`, pending components `3`, installation failure `1`, and invalid arguments `2`.
|
|
62
63
|
|
|
64
|
+
Run `preset uninstall` to remove the local preset and matching selections in connected extensions. Removal clears preset-managed capability packages and setup-managed registrations in the selected plugin catalog. It preserves independent capabilities, Skill files, Providers, and browser permissions. [See what preset removal covers](../../docs/preset-installation.en.md)
|
|
65
|
+
|
|
63
66
|
Automatic registration defaults to the installation directory name; an entry can declare `name` for a shorter command.
|
|
64
67
|
Reinstallation refreshes same-name bindings. JavaScript entries must default-export `register(app)`; Python entries must export a `register(app)` function. See the
|
|
65
68
|
[plugin runtime contract](../../docs/plugin-runtime.en.md).
|
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ npx @mearl/setup plugin install <skills add 参数...>
|
|
|
19
19
|
npx @mearl/setup plugin uninstall <name>
|
|
20
20
|
npx @mearl/setup provider <install|update|uninstall> <package|id>
|
|
21
21
|
npx @mearl/setup profile install <来源> [--browser <标识或名称>]
|
|
22
|
-
npx @mearl/setup preset
|
|
22
|
+
npx @mearl/setup preset apply [来源]
|
|
23
|
+
npx @mearl/setup preset <update|status|uninstall>
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
浏览器 Provider 独立安装和演进。安装后运行 `mearl <providerId>` 查看该版本声明的能力、
|
|
@@ -52,14 +53,16 @@ mearl tabs summary
|
|
|
52
53
|
|
|
53
54
|
Native preset 批次新建的插件注册使用 setup 托管归属,后续通过 Agent 更新仍保留该归属;同目录已有的用户注册保持原归属,自定义用户别名也会保留。托管卸载遇到用户别名共用同一 Skill 时,只清除托管注册并保留 Skill 文件。
|
|
54
55
|
|
|
55
|
-
GUI 和 CLI 使用同一套 preset
|
|
56
|
+
GUI 和 CLI 使用同一套 preset 安装器及覆盖规则。`preset apply` 接受 URL 或本地文件路径。省略来源时使用已保存的快照重试。`preset update` 重新读取当前来源。重试时可显式重复指定 `--replace-skill`,授权本次替换这些 Skill 的来源或同名旧插件注册。
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
|
-
npx @mearl/setup preset
|
|
59
|
+
npx @mearl/setup preset apply --replace-skill example-skill --replace-skill another-skill
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
已安装的 Skill 仅修复注册;来源冲突项重新安装。未列出的冲突仍会阻止安装,声明外的工具名与变更后的安装身份也会被拒绝。覆盖授权不会保留到后续普通修复或自动更新。缺少浏览器权限时仍需在扩展中授权。输出沿用 preset JSON,已就绪退出 `0`、仍有待完成项退出 `3`、安装失败退出 `1`,无效参数退出 `2`。
|
|
62
63
|
|
|
64
|
+
运行 `preset uninstall` 清理本机 preset 和匹配的已连接扩展。卸载会移除 preset 托管的能力包和当前目录中由 setup 托管的插件注册,保留独立安装的能力、Skill 文件、Provider 和浏览器权限。[查看卸载范围](../../docs/preset-installation.md)
|
|
65
|
+
|
|
63
66
|
自动注册默认使用安装目录名;入口可声明 `name` 指定简短命令名。
|
|
64
67
|
重装会刷新同名绑定。JavaScript 入口必须默认导出 `register(app)`,Python 入口必须导出 `register(app)` 函数;完整协议见
|
|
65
68
|
[插件运行时合同](../../docs/plugin-runtime.md)。
|
package/dist/cli.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface ParsedArgs {
|
|
|
16
16
|
operation?: MearlLifecycleOperation | 'extension' | 'plugin' | 'provider' | 'preset' | 'profile';
|
|
17
17
|
profileSource?: string;
|
|
18
18
|
profileBrowser?: string;
|
|
19
|
-
presetCommand?:
|
|
19
|
+
presetCommand?: Exclude<Parameters<typeof runPresetCommand>[0], 'repair'>;
|
|
20
20
|
presetSource?: string;
|
|
21
21
|
presetReplaceSkills?: string[];
|
|
22
22
|
pluginName?: string;
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { Command, CommanderError } from 'commander';
|
|
3
3
|
import { invoke, listLiveBrowsers } from '@mearl/client/local';
|
|
4
4
|
import { parseProfileSource } from '@mearl/preset/bundles';
|
|
5
|
-
import { downloadMearlBrowserExtension, installMearlBrowserExtension, isUnsupportedCdpMethodError } from './browserExtension.js';
|
|
6
|
-
import { runMearlLifecycle } from './lifecycle.js';
|
|
5
|
+
import { downloadMearlBrowserExtension, installMearlBrowserExtension, isUnsupportedCdpMethodError, } from './browserExtension.js';
|
|
6
|
+
import { runMearlLifecycle, } from './lifecycle.js';
|
|
7
7
|
import { isMearlReleaseLine } from './version.js';
|
|
8
8
|
import { ensureOfficialPlugin, installPlugins, setupPlugin, uninstallPlugin } from './plugin.js';
|
|
9
9
|
import { installProviderPackage, uninstallProviderPackage, updateProviderPackage, } from './provider.js';
|
|
@@ -30,18 +30,28 @@ export function parseSetupArgs(argv) {
|
|
|
30
30
|
if (argv[0] === 'profile') {
|
|
31
31
|
if (argv.length === 2 && (argv[1] === '--help' || argv[1] === '-h'))
|
|
32
32
|
return { ...baseParsedArgs(), help: true };
|
|
33
|
-
if (argv[1] !== 'install' ||
|
|
33
|
+
if (argv[1] !== 'install' ||
|
|
34
|
+
!argv[2] ||
|
|
35
|
+
(argv.length !== 3 && !(argv.length === 5 && argv[3] === '--browser' && argv[4]))) {
|
|
34
36
|
throw new Error('profile requires install <GitHub owner/repo[@ref] or HTTPS bundle URL> [--browser <id-or-name>]');
|
|
35
37
|
}
|
|
36
38
|
parseProfileSource(argv[2]);
|
|
37
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
...baseParsedArgs(),
|
|
41
|
+
operation: 'profile',
|
|
42
|
+
profileSource: argv[2],
|
|
43
|
+
profileBrowser: argv[4],
|
|
44
|
+
};
|
|
38
45
|
}
|
|
39
46
|
if (argv[0] === 'preset') {
|
|
40
|
-
if (argv[1] === '
|
|
41
|
-
|
|
47
|
+
if (argv.length === 2 && (argv[1] === '--help' || argv[1] === '-h'))
|
|
48
|
+
return { ...baseParsedArgs(), help: true };
|
|
49
|
+
if (argv[1] === 'apply') {
|
|
50
|
+
const command = new Command('preset apply')
|
|
42
51
|
.exitOverride()
|
|
43
52
|
.configureOutput({ writeErr: () => { }, writeOut: () => { } })
|
|
44
53
|
.allowExcessArguments(false)
|
|
54
|
+
.argument('[source]')
|
|
45
55
|
.option('--replace-skill <name>', 'Replace the named Skill source or conflicting plugin registration', (name, previous) => [...previous, name], []);
|
|
46
56
|
try {
|
|
47
57
|
command.parse(argv.slice(2), { from: 'user' });
|
|
@@ -55,15 +65,27 @@ export function parseSetupArgs(argv) {
|
|
|
55
65
|
throw error;
|
|
56
66
|
}
|
|
57
67
|
const names = command.opts().replaceSkill;
|
|
58
|
-
if (names.length > 256 ||
|
|
68
|
+
if (names.length > 256 ||
|
|
69
|
+
new Set(names).size !== names.length ||
|
|
70
|
+
names.some(name => !/^[a-z][a-z0-9-]{0,79}$/.test(name)))
|
|
59
71
|
throw new Error('--replace-skill requires unique valid Skill names (up to 256)');
|
|
60
|
-
|
|
72
|
+
const source = command.args[0];
|
|
73
|
+
if (source !== undefined && !source.trim())
|
|
74
|
+
throw new Error('Expected a preset source URL or path');
|
|
75
|
+
if (source !== undefined && names.length)
|
|
76
|
+
throw new Error('--replace-skill requires preset apply without a source');
|
|
77
|
+
return {
|
|
78
|
+
...baseParsedArgs(),
|
|
79
|
+
operation: 'preset',
|
|
80
|
+
presetCommand: 'apply',
|
|
81
|
+
presetSource: source,
|
|
82
|
+
...(names.length ? { presetReplaceSkills: names } : {}),
|
|
83
|
+
};
|
|
61
84
|
}
|
|
62
|
-
if (argv[1] === '
|
|
63
|
-
|
|
64
|
-
if ((argv[1] === 'update' || argv[1] === 'status') && argv.length === 2)
|
|
85
|
+
if ((argv[1] === 'update' || argv[1] === 'status' || argv[1] === 'uninstall') &&
|
|
86
|
+
argv.length === 2)
|
|
65
87
|
return { ...baseParsedArgs(), operation: 'preset', presetCommand: argv[1] };
|
|
66
|
-
throw new Error('preset requires apply
|
|
88
|
+
throw new Error('preset requires apply [source-url-or-path], update, status, or uninstall');
|
|
67
89
|
}
|
|
68
90
|
if (argv[0] === 'provider') {
|
|
69
91
|
const command = argv[1];
|
|
@@ -223,10 +245,10 @@ Environment commands:
|
|
|
223
245
|
|
|
224
246
|
Integration commands:
|
|
225
247
|
profile install <source> Install one App profile bundle in a connected extension
|
|
226
|
-
preset apply
|
|
248
|
+
preset apply [source] Apply a URL or local file, or retry the saved snapshot
|
|
227
249
|
preset update Update the selected preset, preserving user overrides
|
|
228
|
-
preset repair Retry the installed preset using its accepted resources
|
|
229
250
|
preset status Report actual readiness (exit 3 when synchronization is pending)
|
|
251
|
+
preset uninstall Remove the preset from the local host and connected extensions
|
|
230
252
|
plugin install <arguments...> Install Skills and register their CLI plugins
|
|
231
253
|
plugin uninstall <name> Uninstall one CLI plugin
|
|
232
254
|
plugin [name] <path> Register an existing plugin; name overrides metadata
|
|
@@ -254,7 +276,7 @@ Extension options:
|
|
|
254
276
|
--full Install the extension and complete local environment
|
|
255
277
|
--force Replace an extension managed by another source
|
|
256
278
|
|
|
257
|
-
Preset
|
|
279
|
+
Preset apply options (without a source):
|
|
258
280
|
--replace-skill <name> Replace one Skill source or conflicting registration; repeat for more
|
|
259
281
|
|
|
260
282
|
General options:
|
|
@@ -271,13 +293,19 @@ Examples:
|
|
|
271
293
|
npx @mearl/setup uninstall --full
|
|
272
294
|
npx @mearl/setup plugin uninstall tabs
|
|
273
295
|
npx @mearl/setup provider install @example/mearl-provider-device
|
|
274
|
-
npx @mearl/setup preset
|
|
296
|
+
npx @mearl/setup preset apply --replace-skill example-skill
|
|
275
297
|
npx @mearl/setup extension --full`;
|
|
276
298
|
console.log(output.replaceAll('npx @mearl/setup', SETUP_COMMAND));
|
|
277
299
|
}
|
|
278
300
|
async function runSetupLifecycle(options, dependencies) {
|
|
279
301
|
let preset;
|
|
280
|
-
const callback = options.operation === 'uninstall'
|
|
302
|
+
const callback = options.operation === 'uninstall'
|
|
303
|
+
? undefined
|
|
304
|
+
: {
|
|
305
|
+
onPresetStatus: (status) => {
|
|
306
|
+
preset = status;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
281
309
|
const lifecycle = dependencies.runLifecycle ?? runMearlLifecycle;
|
|
282
310
|
const defaultPresetSource = process.env.MEARL_SETUP_DEFAULT_PRESET;
|
|
283
311
|
const configured = defaultPresetSource ? { ...options, defaultPresetSource } : options;
|
|
@@ -298,7 +326,8 @@ async function runSetupLifecycle(options, dependencies) {
|
|
|
298
326
|
}
|
|
299
327
|
async function installProfile(source, browser) {
|
|
300
328
|
if (!browser) {
|
|
301
|
-
const extensions = listLiveBrowsers().filter(target => target.transport === 'extension' &&
|
|
329
|
+
const extensions = listLiveBrowsers().filter(target => target.transport === 'extension' &&
|
|
330
|
+
(target.status === undefined || target.status === 'connected'));
|
|
302
331
|
if (extensions.length !== 1)
|
|
303
332
|
throw new Error(extensions.length
|
|
304
333
|
? 'Multiple extensions are connected. Select one with --browser <id-or-name>.'
|
|
@@ -341,7 +370,9 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
|
|
|
341
370
|
try {
|
|
342
371
|
const run = dependencies.runPreset ?? runPresetCommand;
|
|
343
372
|
const result = parsed.presetReplaceSkills
|
|
344
|
-
? await run(parsed.presetCommand, parsed.presetSource, {
|
|
373
|
+
? await run(parsed.presetCommand, parsed.presetSource, {
|
|
374
|
+
replaceSkills: parsed.presetReplaceSkills,
|
|
375
|
+
})
|
|
345
376
|
: await run(parsed.presetCommand, parsed.presetSource);
|
|
346
377
|
console.log(JSON.stringify(result, null, 2));
|
|
347
378
|
return result.ready ? 0 : 3;
|
package/dist/preset.d.ts
CHANGED
|
@@ -93,16 +93,21 @@ export declare class PresetInstaller {
|
|
|
93
93
|
private stageSource;
|
|
94
94
|
/** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
|
|
95
95
|
stage(source: string, options?: PresetApplyOptions): Promise<StagedPresetInstallation>;
|
|
96
|
-
apply(source
|
|
96
|
+
apply(source?: string, options?: PresetApplyOptions & {
|
|
97
|
+
replaceSkills?: readonly string[];
|
|
98
|
+
}): Promise<PresetInstallStatus>;
|
|
97
99
|
update(options?: Pick<PresetApplyOptions, 'validatePrepared' | 'skipUnchanged' | 'signal'>): Promise<PresetInstallStatus>;
|
|
98
100
|
resume(expectedRevision: string, options?: {
|
|
99
101
|
replaceSkills?: readonly string[];
|
|
100
102
|
}): Promise<PresetInstallStatus>;
|
|
103
|
+
private resumeSelected;
|
|
104
|
+
/** @deprecated Use apply() without a source. */
|
|
101
105
|
repair(options?: {
|
|
102
106
|
replaceSkills?: readonly string[];
|
|
103
107
|
}): Promise<PresetInstallStatus>;
|
|
108
|
+
uninstall(): Promise<PresetInstallStatus>;
|
|
104
109
|
configure(value: JsonObject): Promise<PresetInstallStatus>;
|
|
105
110
|
}
|
|
106
|
-
export declare function runPresetCommand(command: 'apply' | 'update' | 'status' | 'repair', source?: string, options?: {
|
|
111
|
+
export declare function runPresetCommand(command: 'apply' | 'update' | 'status' | 'uninstall' | 'repair', source?: string, options?: {
|
|
107
112
|
replaceSkills?: readonly string[];
|
|
108
113
|
}): Promise<PresetInstallStatus>;
|
package/dist/preset.js
CHANGED
|
@@ -4,15 +4,24 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { invoke, invokeLocalHost, listLiveBrowsers, SOCKET_PATH } from '@mearl/client/local';
|
|
7
|
+
import { uninstallOfficialPluginCatalog } from '@mearl/client/plugin';
|
|
7
8
|
import { mergePresetDefaults } from '@mearl/preset';
|
|
8
|
-
import { BundleError, createBundleFetchLoader, preparePreset } from '@mearl/preset/bundles';
|
|
9
|
+
import { BundleError, createBundleFetchLoader, preparePreset, } from '@mearl/preset/bundles';
|
|
9
10
|
import { PresetInstallationStore, presetSelectionRevision } from './presetStore.js';
|
|
11
|
+
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
10
12
|
const installationSite = createRequire(import.meta.url)('@mearl/preset/installation-site.json');
|
|
11
|
-
export const MEARL_INSTALLATION_CONFIG_URL = new URL('installation.json', installationSite.url)
|
|
13
|
+
export const MEARL_INSTALLATION_CONFIG_URL = new URL('installation.json', installationSite.url)
|
|
14
|
+
.href;
|
|
12
15
|
/** Runtime overrides apply to this local process, never to browser-supplied install requests. */
|
|
13
16
|
export function resolveInstallationConfigUrl(value = process.env.MEARL_INSTALLATION_CONFIG_URL, allowLoopback = false) {
|
|
14
17
|
const url = new URL(value || MEARL_INSTALLATION_CONFIG_URL);
|
|
15
|
-
if (url.username ||
|
|
18
|
+
if (url.username ||
|
|
19
|
+
url.password ||
|
|
20
|
+
url.hash ||
|
|
21
|
+
(url.protocol !== 'https:' &&
|
|
22
|
+
!(allowLoopback &&
|
|
23
|
+
url.protocol === 'http:' &&
|
|
24
|
+
['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))))
|
|
16
25
|
throw new Error('Invalid installation configuration URL');
|
|
17
26
|
return url;
|
|
18
27
|
}
|
|
@@ -52,7 +61,8 @@ export function createNodePresetLoader() {
|
|
|
52
61
|
await handle.close();
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
|
-
if (url.protocol !== 'https:' &&
|
|
64
|
+
if (url.protocol !== 'https:' &&
|
|
65
|
+
!(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {
|
|
56
66
|
throw new BundleError('BUNDLE_INVALID', 'Preset sources require HTTPS, local files or loopback HTTP');
|
|
57
67
|
}
|
|
58
68
|
return network(source, options);
|
|
@@ -68,12 +78,20 @@ function sourceUrl(source) {
|
|
|
68
78
|
}
|
|
69
79
|
function localHosts(allowStart = false) {
|
|
70
80
|
const configured = process.env.MEARL_SOCKET_PATH?.trim();
|
|
71
|
-
const hosts = configured
|
|
72
|
-
|
|
81
|
+
const hosts = configured
|
|
82
|
+
? [{ id: 'local', socketPath: configured }]
|
|
83
|
+
: listLiveBrowsers()
|
|
84
|
+
.filter(browser => !browser.provider)
|
|
85
|
+
.map(browser => ({ id: browser.browserId, socketPath: browser.socketPath }));
|
|
86
|
+
return hosts.length
|
|
87
|
+
? Array.from(new Map(hosts.map(host => [host.socketPath, host])).values())
|
|
88
|
+
: [{ id: 'local', ...(allowStart ? {} : { socketPath: SOCKET_PATH }) }];
|
|
73
89
|
}
|
|
74
90
|
export function connectedPresetExtensions(browsers, socketPath) {
|
|
75
|
-
return browsers.filter(browser => !browser.provider &&
|
|
76
|
-
|
|
91
|
+
return browsers.filter(browser => !browser.provider &&
|
|
92
|
+
browser.transport === 'extension' &&
|
|
93
|
+
(browser.status === undefined || browser.status === 'connected') &&
|
|
94
|
+
(!socketPath || browser.socketPath === socketPath));
|
|
77
95
|
}
|
|
78
96
|
/** Installation and live acknowledgements are distinct; browser/external installs remain pending. */
|
|
79
97
|
export class PresetInstaller {
|
|
@@ -86,16 +104,30 @@ export class PresetInstaller {
|
|
|
86
104
|
invokeHost(action, data, target, timeoutSec, allowStart = false) {
|
|
87
105
|
if (!this.options.invoke && !target.socketPath && !allowStart)
|
|
88
106
|
return Promise.reject(new Error('HOST_UNAVAILABLE'));
|
|
89
|
-
return (this.options.invoke ?? (target.socketPath ? invoke : invokeLocalHost))(action, data, {
|
|
107
|
+
return (this.options.invoke ?? (target.socketPath ? invoke : invokeLocalHost))(action, data, {
|
|
108
|
+
socketPath: target.socketPath,
|
|
109
|
+
timeoutSec,
|
|
110
|
+
});
|
|
90
111
|
}
|
|
91
|
-
async hostStatus(reload, nodeRevision, installationRevision) {
|
|
92
|
-
const targets = this.options.hosts?.() ?? localHosts(reload);
|
|
112
|
+
async hostStatus(reload, nodeRevision, installationRevision, targets = this.options.hosts?.() ?? localHosts(reload)) {
|
|
93
113
|
return Promise.all(targets.map(async (target) => {
|
|
94
114
|
try {
|
|
95
115
|
const result = await this.invokeHost(reload ? 'node_capabilities_reload' : 'node_capabilities_status', reload ? { structured: true } : {}, target, 15, reload);
|
|
96
116
|
if (reload && result?.accepted === false)
|
|
97
|
-
return {
|
|
98
|
-
|
|
117
|
+
return {
|
|
118
|
+
id: target.id,
|
|
119
|
+
state: 'rejected',
|
|
120
|
+
error: typeof result.error === 'string'
|
|
121
|
+
? result.error
|
|
122
|
+
: 'Node capability activation was rejected',
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
id: target.id,
|
|
126
|
+
state: result?.revision === nodeRevision &&
|
|
127
|
+
result?.installationRevision === installationRevision
|
|
128
|
+
? 'active'
|
|
129
|
+
: 'pending',
|
|
130
|
+
};
|
|
99
131
|
}
|
|
100
132
|
catch {
|
|
101
133
|
return { id: target.id, state: 'pending', error: 'HOST_UNAVAILABLE' };
|
|
@@ -105,19 +137,42 @@ export class PresetInstaller {
|
|
|
105
137
|
async browserStatus(reload, browserRevision, installationRevision) {
|
|
106
138
|
const targets = this.options.connectedExtensionsOnly
|
|
107
139
|
? connectedPresetExtensions(listLiveBrowsers(), process.env.MEARL_SOCKET_PATH?.trim()).map(browser => ({ id: browser.browserId, socketPath: browser.socketPath }))
|
|
108
|
-
: this.options.hosts?.() ?? localHosts(reload);
|
|
140
|
+
: (this.options.hosts?.() ?? localHosts(reload));
|
|
109
141
|
return Promise.all(targets.map(async (target) => {
|
|
110
142
|
try {
|
|
111
143
|
const result = await this.invokeHost(reload ? 'preset_browser_sync' : 'preset_browser_status', reload ? { installationRevision } : {}, target, reload ? 130 : 15, reload);
|
|
112
144
|
if (result?.accepted === false)
|
|
113
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
id: target.id,
|
|
147
|
+
state: 'rejected',
|
|
148
|
+
settingsReady: false,
|
|
149
|
+
error: typeof result.error === 'string'
|
|
150
|
+
? result.error
|
|
151
|
+
: 'Browser preset activation rejected',
|
|
152
|
+
};
|
|
114
153
|
const error = Array.isArray(result?.permissionPending) && result.permissionPending.length
|
|
115
154
|
? 'PRESET_DOMAIN_PERMISSION_REQUIRED: Open extension settings and authorize preset domains'
|
|
116
|
-
: typeof result?.error === 'string'
|
|
117
|
-
|
|
155
|
+
: typeof result?.error === 'string'
|
|
156
|
+
? result.error
|
|
157
|
+
: undefined;
|
|
158
|
+
return {
|
|
159
|
+
id: target.id,
|
|
160
|
+
state: result?.ready === true &&
|
|
161
|
+
result?.browserRevision === browserRevision &&
|
|
162
|
+
result?.installationRevision === installationRevision
|
|
163
|
+
? 'active'
|
|
164
|
+
: 'pending',
|
|
165
|
+
settingsReady: result?.settingsReady === true,
|
|
166
|
+
...(error ? { error } : {}),
|
|
167
|
+
};
|
|
118
168
|
}
|
|
119
169
|
catch {
|
|
120
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
id: target.id,
|
|
172
|
+
state: 'pending',
|
|
173
|
+
settingsReady: false,
|
|
174
|
+
error: 'BROWSER_UNAVAILABLE',
|
|
175
|
+
};
|
|
121
176
|
}
|
|
122
177
|
}));
|
|
123
178
|
}
|
|
@@ -126,14 +181,23 @@ export class PresetInstaller {
|
|
|
126
181
|
if (!target)
|
|
127
182
|
return {};
|
|
128
183
|
try {
|
|
129
|
-
const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', {
|
|
130
|
-
|
|
184
|
+
const result = await this.invokeHost(install ? 'preset_components_install' : 'preset_components_status', {
|
|
185
|
+
installationRevision: receipt.revision,
|
|
186
|
+
...(install && replaceSkills?.length ? { replaceSkills } : {}),
|
|
187
|
+
}, target, install ? 900 : 15, install);
|
|
188
|
+
if (result?.installationRevision !== receipt.revision ||
|
|
189
|
+
result?.nodeRevision !== receipt.nodeRevision ||
|
|
190
|
+
!Array.isArray(result?.components))
|
|
131
191
|
throw new Error('Component installation host requires an update');
|
|
132
192
|
if (result.catalogReady !== undefined && typeof result.catalogReady !== 'boolean')
|
|
133
193
|
throw new Error('Invalid plugin catalog readiness');
|
|
134
194
|
const seen = new Set();
|
|
135
195
|
for (const entry of result.components) {
|
|
136
|
-
if (!entry ||
|
|
196
|
+
if (!entry ||
|
|
197
|
+
!['skills', 'providers'].includes(entry.kind) ||
|
|
198
|
+
typeof entry.id !== 'string' ||
|
|
199
|
+
!['ready', 'pending', 'conflict'].includes(entry.state) ||
|
|
200
|
+
(entry.reason !== undefined && typeof entry.reason !== 'string'))
|
|
137
201
|
throw new Error('Invalid component installation status');
|
|
138
202
|
const key = `${entry.kind}:${entry.id}`;
|
|
139
203
|
if (seen.has(key))
|
|
@@ -148,7 +212,14 @@ export class PresetInstaller {
|
|
|
148
212
|
}
|
|
149
213
|
async status() {
|
|
150
214
|
const current = await this.store.current();
|
|
151
|
-
const result = {
|
|
215
|
+
const result = {
|
|
216
|
+
revision: presetSelectionRevision(current),
|
|
217
|
+
ready: false,
|
|
218
|
+
hosts: [],
|
|
219
|
+
browsers: [],
|
|
220
|
+
pending: [],
|
|
221
|
+
warnings: [],
|
|
222
|
+
};
|
|
152
223
|
if (!current.receipt || !current.prepared)
|
|
153
224
|
return result;
|
|
154
225
|
const { preset } = current.prepared;
|
|
@@ -161,8 +232,9 @@ export class PresetInstaller {
|
|
|
161
232
|
result.browserRevision = receipt.browserRevision;
|
|
162
233
|
const browserSyncRequired = receipt.browserSyncRequired === true && !this.options.connectedExtensionsOnly;
|
|
163
234
|
result.browserSyncRequired = browserSyncRequired;
|
|
164
|
-
result.localComponentCount =
|
|
165
|
-
|
|
235
|
+
result.localComponentCount =
|
|
236
|
+
Object.values(preset.packages).filter(entry => entry.enabled !== false && entry.targets?.includes('node')).length +
|
|
237
|
+
[...Object.values(preset.skills), ...Object.values(preset.providers)].filter(entry => entry.enabled !== false).length;
|
|
166
238
|
result.hosts = await this.hostStatus(false, receipt.nodeRevision, receipt.revision);
|
|
167
239
|
const nodeReady = result.hosts.length > 0 && result.hosts.every(host => host.state === 'active');
|
|
168
240
|
if (receipt.browserSyncRequired)
|
|
@@ -176,7 +248,9 @@ export class PresetInstaller {
|
|
|
176
248
|
if (browserSyncRequired && !browserReady)
|
|
177
249
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'BROWSER_SYNC_REQUIRED' });
|
|
178
250
|
}
|
|
179
|
-
else if (!receipt.managedModules.includes(id) &&
|
|
251
|
+
else if (!receipt.managedModules.includes(id) &&
|
|
252
|
+
Object.keys(current.prepared.bundles.find(bundle => bundle.manifest.id === id)?.manifest
|
|
253
|
+
.contributions?.nodeRuntimes ?? {}).length)
|
|
180
254
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'NODE_HOST_NOT_SUPPORTED' });
|
|
181
255
|
else if (!nodeReady)
|
|
182
256
|
result.pending.push({ kind: 'packages', id, runtime, reason: 'HOST_SYNC_REQUIRED' });
|
|
@@ -186,7 +260,12 @@ export class PresetInstaller {
|
|
|
186
260
|
const hasCatalog = Boolean(current.activation?.bindings['plugins.catalog']);
|
|
187
261
|
const components = hasExternal || hasCatalog ? await this.componentStatus(false, receipt) : {};
|
|
188
262
|
if (hasCatalog && components.status?.catalogReady !== true)
|
|
189
|
-
result.pending.push({
|
|
263
|
+
result.pending.push({
|
|
264
|
+
kind: 'preset',
|
|
265
|
+
id: preset.id,
|
|
266
|
+
runtime: 'node',
|
|
267
|
+
reason: 'PLUGIN_CATALOG_SYNC_REQUIRED',
|
|
268
|
+
});
|
|
190
269
|
if (components.error)
|
|
191
270
|
result.warnings.push(`Component readiness check failed: ${components.error}`);
|
|
192
271
|
for (const kind of ['skills', 'providers'])
|
|
@@ -195,14 +274,41 @@ export class PresetInstaller {
|
|
|
195
274
|
continue;
|
|
196
275
|
const actual = components.status?.components.find(component => component.kind === kind && component.id === id);
|
|
197
276
|
if (actual?.state !== 'ready')
|
|
198
|
-
result.pending.push({
|
|
277
|
+
result.pending.push({
|
|
278
|
+
kind,
|
|
279
|
+
id,
|
|
280
|
+
runtime: 'node',
|
|
281
|
+
reason: actual?.reason ?? 'INSTALL_REQUIRED',
|
|
282
|
+
});
|
|
199
283
|
}
|
|
200
|
-
if (receipt.nodeSyncRequired &&
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
result.pending.push({
|
|
204
|
-
|
|
205
|
-
|
|
284
|
+
if (receipt.nodeSyncRequired &&
|
|
285
|
+
!nodeReady &&
|
|
286
|
+
!result.pending.some(entry => entry.reason === 'HOST_SYNC_REQUIRED'))
|
|
287
|
+
result.pending.push({
|
|
288
|
+
kind: 'preset',
|
|
289
|
+
id: preset.id,
|
|
290
|
+
runtime: 'node',
|
|
291
|
+
reason: 'HOST_SYNC_REQUIRED',
|
|
292
|
+
});
|
|
293
|
+
if (browserSyncRequired &&
|
|
294
|
+
!browserReady &&
|
|
295
|
+
!result.pending.some(entry => entry.reason === 'BROWSER_SYNC_REQUIRED'))
|
|
296
|
+
result.pending.push({
|
|
297
|
+
kind: 'preset',
|
|
298
|
+
id: preset.id,
|
|
299
|
+
runtime: 'browser',
|
|
300
|
+
reason: 'BROWSER_SYNC_REQUIRED',
|
|
301
|
+
});
|
|
302
|
+
if (browserSyncRequired &&
|
|
303
|
+
browserReady &&
|
|
304
|
+
Object.keys(result.settings ?? {}).length &&
|
|
305
|
+
!result.browsers.every(host => host.settingsReady))
|
|
306
|
+
result.pending.push({
|
|
307
|
+
kind: 'preset',
|
|
308
|
+
id: preset.id,
|
|
309
|
+
runtime: 'browser',
|
|
310
|
+
reason: 'SETTINGS_APPLY_REQUIRED',
|
|
311
|
+
});
|
|
206
312
|
result.ready = result.pending.length === 0;
|
|
207
313
|
return result;
|
|
208
314
|
}
|
|
@@ -217,15 +323,22 @@ export class PresetInstaller {
|
|
|
217
323
|
await this.hostStatus(true, previous.activation?.revision ?? '', previous.activation?.installationRevision ?? '');
|
|
218
324
|
throw new Error(`Node activation rejected; previous selection restored: ${rejection.error}`);
|
|
219
325
|
}
|
|
220
|
-
const browsers = receipt.browserSyncRequired
|
|
326
|
+
const browsers = receipt.browserSyncRequired
|
|
327
|
+
? await this.browserStatus(true, receipt.browserRevision, receipt.revision)
|
|
328
|
+
: [];
|
|
221
329
|
const rejected = browsers.find(host => host.state === 'rejected');
|
|
222
330
|
if (rejected && !this.options.connectedExtensionsOnly)
|
|
223
331
|
throw new Error(`Browser activation rejected; installed selection retained for retry: ${rejected.error}`);
|
|
224
332
|
const installed = await this.store.current({ verifyResources: false });
|
|
225
333
|
if (installed.receipt?.revision !== receipt.revision || !installed.preset)
|
|
226
334
|
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before component installation');
|
|
227
|
-
const hasExternal = [
|
|
228
|
-
|
|
335
|
+
const hasExternal = [
|
|
336
|
+
...Object.values(installed.preset.skills),
|
|
337
|
+
...Object.values(installed.preset.providers),
|
|
338
|
+
].some(entry => entry.enabled !== false);
|
|
339
|
+
const components = hasExternal || installed.activation?.bindings['plugins.catalog']
|
|
340
|
+
? await this.componentStatus(true, receipt, replaceSkills)
|
|
341
|
+
: {};
|
|
229
342
|
const status = await this.status();
|
|
230
343
|
if (rejected)
|
|
231
344
|
status.warnings.push(`Browser preset activation rejected by ${rejected.id}: ${rejected.error}`);
|
|
@@ -242,7 +355,11 @@ export class PresetInstaller {
|
|
|
242
355
|
return status;
|
|
243
356
|
}
|
|
244
357
|
async stageSource(source, expectedRevision, options) {
|
|
245
|
-
const prepared = await preparePreset(sourceUrl(source), {
|
|
358
|
+
const prepared = await preparePreset(sourceUrl(source), {
|
|
359
|
+
mearlApiVersion: this.store.mearlApiVersion,
|
|
360
|
+
load: this.options.load ?? createNodePresetLoader(),
|
|
361
|
+
signal: options.signal,
|
|
362
|
+
});
|
|
246
363
|
if (options.expectedId !== undefined && prepared.preset.id !== options.expectedId)
|
|
247
364
|
throw new BundleError('BUNDLE_INVALID', 'The preset identity differs from the accepted installation');
|
|
248
365
|
await options.validatePrepared?.(prepared);
|
|
@@ -251,17 +368,22 @@ export class PresetInstaller {
|
|
|
251
368
|
if (presetSelectionRevision(current) !== expectedRevision)
|
|
252
369
|
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
|
|
253
370
|
if (current.receipt && current.prepared?.snapshot.revision === prepared.snapshot.revision) {
|
|
254
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
revision: current.receipt.revision,
|
|
373
|
+
activate: async () => {
|
|
255
374
|
const status = await this.status();
|
|
256
375
|
if (status.revision !== expectedRevision)
|
|
257
376
|
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during update');
|
|
258
377
|
return status.ready ? status : this.resume(current.receipt.revision);
|
|
259
|
-
}
|
|
378
|
+
},
|
|
379
|
+
};
|
|
260
380
|
}
|
|
261
381
|
}
|
|
262
382
|
const receipt = await this.store.apply(prepared, { ...options, expectedRevision });
|
|
263
383
|
let activated = false;
|
|
264
|
-
return {
|
|
384
|
+
return {
|
|
385
|
+
revision: receipt.revision,
|
|
386
|
+
activate: async () => {
|
|
265
387
|
if (activated)
|
|
266
388
|
throw new Error('Staged installation was already activated; use resume with its revision to retry');
|
|
267
389
|
activated = true;
|
|
@@ -269,11 +391,17 @@ export class PresetInstaller {
|
|
|
269
391
|
if (current.receipt?.revision !== receipt.revision)
|
|
270
392
|
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before runtime setup completed');
|
|
271
393
|
return this.synchronize(current.receipt, expectedRevision);
|
|
272
|
-
}
|
|
394
|
+
},
|
|
395
|
+
};
|
|
273
396
|
}
|
|
274
397
|
/** Save exact resources and desired selection; activate once after runtime setup, then retry via resume. */
|
|
275
398
|
async stage(source, options = {}) {
|
|
276
|
-
const accepted = {
|
|
399
|
+
const accepted = {
|
|
400
|
+
...options,
|
|
401
|
+
...(options.overrides === undefined
|
|
402
|
+
? {}
|
|
403
|
+
: { overrides: mergePresetDefaults({}, options.overrides) }),
|
|
404
|
+
};
|
|
277
405
|
const current = await this.store.current({ verifyResources: false });
|
|
278
406
|
const revision = presetSelectionRevision(current);
|
|
279
407
|
if (accepted.expectedRevision !== undefined && accepted.expectedRevision !== revision)
|
|
@@ -281,42 +409,161 @@ export class PresetInstaller {
|
|
|
281
409
|
return this.stageSource(source, revision, accepted);
|
|
282
410
|
}
|
|
283
411
|
async apply(source, options = {}) {
|
|
412
|
+
if (source === undefined)
|
|
413
|
+
return this.resumeSelected(options);
|
|
414
|
+
if (options.replaceSkills !== undefined)
|
|
415
|
+
throw new Error('Skill replacements require preset apply without a source');
|
|
284
416
|
return (await this.stage(source, options)).activate();
|
|
285
417
|
}
|
|
286
418
|
async update(options = {}) {
|
|
287
419
|
const current = await this.store.current({ verifyResources: false });
|
|
288
420
|
if (!current.receipt)
|
|
289
421
|
throw new Error('No preset is installed');
|
|
290
|
-
return this.apply(current.receipt.snapshot.source, {
|
|
422
|
+
return this.apply(current.receipt.snapshot.source, {
|
|
423
|
+
...options,
|
|
424
|
+
expectedRevision: presetSelectionRevision(current),
|
|
425
|
+
});
|
|
291
426
|
}
|
|
292
427
|
async resume(expectedRevision, options = {}) {
|
|
293
428
|
const current = await this.store.current();
|
|
294
429
|
if (!current.receipt || current.receipt.revision !== expectedRevision)
|
|
295
430
|
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed before resuming');
|
|
296
431
|
const names = options.replaceSkills;
|
|
297
|
-
if (names !== undefined &&
|
|
432
|
+
if (names !== undefined &&
|
|
433
|
+
(!Array.isArray(names) ||
|
|
434
|
+
!names.length ||
|
|
435
|
+
names.length > 256 ||
|
|
436
|
+
new Set(names).size !== names.length ||
|
|
437
|
+
names.some(name => typeof name !== 'string' ||
|
|
438
|
+
!/^[a-z][a-z0-9-]{0,79}$/.test(name) ||
|
|
439
|
+
!current.preset ||
|
|
440
|
+
!Object.hasOwn(current.preset.skills, name) ||
|
|
441
|
+
current.preset.skills[name].enabled === false)))
|
|
298
442
|
throw new Error('Invalid Skill replacement selection');
|
|
299
443
|
return this.synchronize(current.receipt, expectedRevision, options.replaceSkills);
|
|
300
444
|
}
|
|
301
|
-
async
|
|
445
|
+
async resumeSelected(options) {
|
|
302
446
|
const current = await this.store.current({ verifyResources: false });
|
|
303
447
|
if (!current.receipt)
|
|
304
448
|
throw new Error('No preset is installed');
|
|
305
449
|
return this.resume(current.receipt.revision, options);
|
|
306
450
|
}
|
|
451
|
+
/** @deprecated Use apply() without a source. */
|
|
452
|
+
async repair(options = {}) {
|
|
453
|
+
return this.resumeSelected(options);
|
|
454
|
+
}
|
|
455
|
+
async uninstall() {
|
|
456
|
+
const current = await this.store.current({ verifyResources: false });
|
|
457
|
+
const { receipt, preset } = current;
|
|
458
|
+
const configured = process.env.MEARL_SOCKET_PATH?.trim();
|
|
459
|
+
const live = this.options.hosts
|
|
460
|
+
? []
|
|
461
|
+
: listLiveBrowsers().filter(browser => !browser.provider && (!configured || browser.socketPath === configured));
|
|
462
|
+
const explicitHosts = this.options
|
|
463
|
+
.hosts?.()
|
|
464
|
+
.filter(target => !configured || target.socketPath === configured);
|
|
465
|
+
const browserTargets = explicitHosts ??
|
|
466
|
+
connectedPresetExtensions(live).map(browser => ({
|
|
467
|
+
id: browser.browserId,
|
|
468
|
+
socketPath: browser.socketPath,
|
|
469
|
+
}));
|
|
470
|
+
if (configured && !browserTargets.length)
|
|
471
|
+
throw new Error('MEARL_SOCKET_PATH does not identify a connected extension.');
|
|
472
|
+
const selections = (await Promise.all(browserTargets.map(async (target) => {
|
|
473
|
+
const status = await this.invokeHost('preset_browser_status', {}, target, 15);
|
|
474
|
+
if (status?.browserRevision === null && !status.error)
|
|
475
|
+
return;
|
|
476
|
+
if (typeof status?.browserRevision !== 'string')
|
|
477
|
+
throw new Error(`Cannot read the preset in ${target.id}. Reconnect the extension and retry.`);
|
|
478
|
+
if (receipt &&
|
|
479
|
+
status.installationRevision !== receipt.revision &&
|
|
480
|
+
(status.presetId !== preset?.id || status.presetSource !== receipt.snapshot.source))
|
|
481
|
+
return;
|
|
482
|
+
if (!receipt && !configured)
|
|
483
|
+
throw new Error('Set MEARL_SOCKET_PATH to remove a browser preset without a local selection.');
|
|
484
|
+
if (status.uninstallSupported !== true || status.installationToken === undefined) {
|
|
485
|
+
throw new Error(`Update the Native Host and extension for ${target.id} before removing its preset.`);
|
|
486
|
+
}
|
|
487
|
+
return { target, status };
|
|
488
|
+
}))).filter(selection => selection !== undefined);
|
|
489
|
+
const result = {
|
|
490
|
+
revision: null,
|
|
491
|
+
ready: true,
|
|
492
|
+
hosts: [],
|
|
493
|
+
browsers: [],
|
|
494
|
+
pending: [],
|
|
495
|
+
warnings: [],
|
|
496
|
+
};
|
|
497
|
+
let remaining = current.activation;
|
|
498
|
+
if (receipt) {
|
|
499
|
+
const lease = claimPresetInstallation();
|
|
500
|
+
try {
|
|
501
|
+
remaining = await this.store.uninstall(receipt.revision, uninstallOfficialPluginCatalog);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
lease.release();
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
await this.store.collectGarbage();
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
result.warnings.push(`Preset resource cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const hosts = explicitHosts ?? [
|
|
514
|
+
...new Map(live.map(browser => [
|
|
515
|
+
browser.socketPath,
|
|
516
|
+
{ id: browser.browserId, socketPath: browser.socketPath },
|
|
517
|
+
])).values(),
|
|
518
|
+
];
|
|
519
|
+
result.hosts = await this.hostStatus(true, remaining?.revision ?? null, null, hosts);
|
|
520
|
+
result.browsers = await Promise.all(selections.map(async ({ target, status }) => {
|
|
521
|
+
try {
|
|
522
|
+
const removed = await this.invokeHost('preset_browser_uninstall', {
|
|
523
|
+
browserRevision: status.browserRevision,
|
|
524
|
+
installationToken: status.installationToken,
|
|
525
|
+
}, target, 130);
|
|
526
|
+
if (removed?.accepted !== true || removed.browserRevision !== null)
|
|
527
|
+
throw new Error(removed?.error ?? 'Browser preset removal was not acknowledged');
|
|
528
|
+
return { id: target.id, state: 'active', settingsReady: false };
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return {
|
|
532
|
+
id: target.id,
|
|
533
|
+
state: 'pending',
|
|
534
|
+
settingsReady: false,
|
|
535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}));
|
|
539
|
+
result.ready = [...result.hosts, ...result.browsers].every(host => host.state === 'active');
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
307
542
|
async configure(value) {
|
|
308
543
|
const current = await this.store.current();
|
|
309
544
|
if (!current.prepared)
|
|
310
545
|
throw new Error('No preset is installed');
|
|
311
|
-
const receipt = await this.store.apply(current.prepared, {
|
|
546
|
+
const receipt = await this.store.apply(current.prepared, {
|
|
547
|
+
expectedRevision: presetSelectionRevision(current),
|
|
548
|
+
overrides: value,
|
|
549
|
+
});
|
|
312
550
|
return this.synchronize(receipt, presetSelectionRevision(current));
|
|
313
551
|
}
|
|
314
552
|
}
|
|
315
553
|
export async function runPresetCommand(command, source, options = {}) {
|
|
316
|
-
if (command !== 'repair' && options.replaceSkills !== undefined)
|
|
317
|
-
throw new Error('Skill replacements require preset repair');
|
|
318
554
|
const installer = new PresetInstaller({ connectedExtensionsOnly: true });
|
|
319
555
|
if (command === 'repair')
|
|
320
556
|
return installer.repair(options);
|
|
321
|
-
|
|
557
|
+
if ((command !== 'apply' || source !== undefined) && options.replaceSkills !== undefined)
|
|
558
|
+
throw new Error('Skill replacements require preset apply without a source');
|
|
559
|
+
switch (command) {
|
|
560
|
+
case 'apply':
|
|
561
|
+
return installer.apply(source, options);
|
|
562
|
+
case 'update':
|
|
563
|
+
return installer.update();
|
|
564
|
+
case 'status':
|
|
565
|
+
return installer.status();
|
|
566
|
+
case 'uninstall':
|
|
567
|
+
return installer.uninstall();
|
|
568
|
+
}
|
|
322
569
|
}
|
|
@@ -22,4 +22,4 @@ export declare function gitSource(source: string, ref?: string): {
|
|
|
22
22
|
subdirectory?: string;
|
|
23
23
|
};
|
|
24
24
|
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
25
|
-
export declare function matchesTrackedSkillSource(requested: string, tracked: TrackedSkillSource, normalize?: (source: string, sourceType?: string) => string): boolean;
|
|
25
|
+
export declare function matchesTrackedSkillSource(requested: string, tracked: TrackedSkillSource, normalize?: (source: string, sourceType?: string) => string, refMatch?: 'exact' | 'missing'): boolean;
|
|
@@ -4,7 +4,11 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
function lockFile(scope, cwd, options) {
|
|
6
6
|
const env = options.env ?? process.env;
|
|
7
|
-
return scope === 'project'
|
|
7
|
+
return scope === 'project'
|
|
8
|
+
? join(cwd, 'skills-lock.json')
|
|
9
|
+
: env.XDG_STATE_HOME
|
|
10
|
+
? join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json')
|
|
11
|
+
: join(options.homeDirectory ?? homedir(), '.agents', '.skill-lock.json');
|
|
8
12
|
}
|
|
9
13
|
export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
10
14
|
const file = lockFile(scope, cwd, options);
|
|
@@ -33,12 +37,28 @@ export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
|
33
37
|
if (source.length > 4096 || /[\0\r\n]/.test(source))
|
|
34
38
|
throw new Error('Invalid tracked Skill source');
|
|
35
39
|
const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
|
|
36
|
-
return [
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
name,
|
|
43
|
+
source,
|
|
44
|
+
scope,
|
|
45
|
+
...(typeof entry.sourceType === 'string' ? { sourceType: entry.sourceType } : {}),
|
|
46
|
+
...(typeof entry.ref === 'string' ? { ref: entry.ref } : {}),
|
|
47
|
+
...(typeof entry.skillPath === 'string' ? { skillPath: entry.skillPath } : {}),
|
|
48
|
+
...(typeof expectedHash === 'string' && expectedHash ? { expectedHash } : {}),
|
|
49
|
+
hashKind: scope === 'global' &&
|
|
50
|
+
typeof expectedHash === 'string' &&
|
|
51
|
+
/^[a-f0-9]{40}$/.test(expectedHash)
|
|
52
|
+
? 'git-tree'
|
|
53
|
+
: 'folder',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
37
56
|
});
|
|
38
57
|
}
|
|
39
58
|
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
40
59
|
export function isPublicSkillSource(source) {
|
|
41
|
-
return source.sourceType === undefined ||
|
|
60
|
+
return (source.sourceType === undefined ||
|
|
61
|
+
['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType));
|
|
42
62
|
}
|
|
43
63
|
export function gitSource(source, ref) {
|
|
44
64
|
if (/^[\w.-]+\/[\w.-]+$/.test(source))
|
|
@@ -46,17 +66,31 @@ export function gitSource(source, ref) {
|
|
|
46
66
|
if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
|
|
47
67
|
return { repository: source, ...(ref ? { ref } : {}) };
|
|
48
68
|
const url = new URL(source);
|
|
49
|
-
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) ||
|
|
69
|
+
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) ||
|
|
70
|
+
url.password ||
|
|
71
|
+
(url.username && url.protocol !== 'ssh:') ||
|
|
72
|
+
url.search ||
|
|
73
|
+
url.hash)
|
|
50
74
|
throw new Error('Skill source is not a supported Git repository');
|
|
51
75
|
if (url.hostname === 'github.com' && !url.port) {
|
|
52
76
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
53
77
|
if (parts.length >= 2)
|
|
54
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
repository: `https://github.com/${parts[0]}/${parts[1].replace(/\.git$/, '')}.git`,
|
|
80
|
+
...(ref || (['tree', 'blob'].includes(parts[2]) && parts[3])
|
|
81
|
+
? { ref: ref || decodeURIComponent(parts[3]) }
|
|
82
|
+
: {}),
|
|
83
|
+
...(['tree', 'blob'].includes(parts[2]) && parts.length > 4
|
|
84
|
+
? {
|
|
85
|
+
subdirectory: decodeURIComponent(parts.slice(4).join('/')).replace(/\/SKILL\.md$/, ''),
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
};
|
|
55
89
|
}
|
|
56
90
|
return { repository: url.href, ...(ref ? { ref } : {}) };
|
|
57
91
|
}
|
|
58
92
|
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
59
|
-
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
|
|
93
|
+
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source, refMatch = 'exact') {
|
|
60
94
|
const location = (source, ref, sourceType) => {
|
|
61
95
|
const value = normalize(source, sourceType);
|
|
62
96
|
if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
|
|
@@ -64,7 +98,9 @@ export function matchesTrackedSkillSource(requested, tracked, normalize = source
|
|
|
64
98
|
const hash = value.indexOf('#');
|
|
65
99
|
const fragment = hash < 0 ? undefined : value.slice(hash + 1);
|
|
66
100
|
const plain = hash < 0 ? value : value.slice(0, hash);
|
|
67
|
-
const parsed = isAbsolute(plain)
|
|
101
|
+
const parsed = isAbsolute(plain)
|
|
102
|
+
? { repository: plain, ref: ref ?? fragment }
|
|
103
|
+
: gitSource(plain, ref ?? fragment);
|
|
68
104
|
let repository = parsed.repository;
|
|
69
105
|
if (repository.startsWith('file:'))
|
|
70
106
|
repository = fileURLToPath(repository);
|
|
@@ -81,14 +117,22 @@ export function matchesTrackedSkillSource(requested, tracked, normalize = source
|
|
|
81
117
|
try {
|
|
82
118
|
const expected = location(requested);
|
|
83
119
|
const actual = location(tracked.source, tracked.ref, tracked.sourceType);
|
|
84
|
-
if (expected.repository !== actual.repository
|
|
120
|
+
if (expected.repository !== actual.repository)
|
|
121
|
+
return false;
|
|
122
|
+
if (refMatch === 'missing'
|
|
123
|
+
? !expected.ref || actual.ref !== undefined
|
|
124
|
+
: expected.ref !== actual.ref)
|
|
85
125
|
return false;
|
|
86
126
|
if (!expected.subdirectory)
|
|
87
127
|
return true;
|
|
88
|
-
const path = (tracked.skillPath ?? actual.subdirectory ?? '')
|
|
128
|
+
const path = (tracked.skillPath ?? actual.subdirectory ?? '')
|
|
129
|
+
.replace(/(?:^|\/)SKILL\.md$/, '')
|
|
130
|
+
.replace(/\/+$/, '');
|
|
89
131
|
return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
|
|
90
132
|
}
|
|
91
133
|
catch {
|
|
92
|
-
return
|
|
134
|
+
return (refMatch === 'exact' &&
|
|
135
|
+
normalize(requested) === normalize(tracked.source, tracked.sourceType) &&
|
|
136
|
+
!tracked.ref);
|
|
93
137
|
}
|
|
94
138
|
}
|
package/dist/skillSources.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CommandRunner } from '@mearl/daemon-core';
|
|
2
2
|
import { readTrackedSkillSources, type SourceOptions, type SkillSourceScope, type TrackedSkillSource } from './skillSourceReceipts.js';
|
|
3
|
-
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
3
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources, } from './skillSourceReceipts.js';
|
|
4
4
|
export type { SkillSourceScope, TrackedSkillSource } from './skillSourceReceipts.js';
|
|
5
5
|
/** Matches the public Skills folder hash without writing to the installed Skill. */
|
|
6
6
|
export declare function hashSkillDirectory(directory: string, ignoreRuntimeFiles?: boolean): Promise<string>;
|
package/dist/skillSources.js
CHANGED
|
@@ -6,12 +6,15 @@ import { homedir, tmpdir } from 'node:os';
|
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { runCommand } from '@mearl/daemon-core';
|
|
8
8
|
import { satisfies, valid } from 'semver';
|
|
9
|
-
import { DISABLED_SKILL_MANIFEST, getSkillEnabled, findPluginDirectoryEntry, findPluginRegistration, listPluginNames, OFFICIAL_PLUGIN_MANAGER, pluginRegistrationNeedsRefresh, readInstalledSkillMetadata } from '@mearl/client/plugin';
|
|
9
|
+
import { DISABLED_SKILL_MANIFEST, getSkillEnabled, findPluginDirectoryEntry, findPluginRegistration, listPluginNames, OFFICIAL_PLUGIN_MANAGER, pluginRegistrationNeedsRefresh, readInstalledSkillMetadata, } from '@mearl/client/plugin';
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
|
-
import { gitSource, isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
12
|
-
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
11
|
+
import { gitSource, isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources, } from './skillSourceReceipts.js';
|
|
12
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources, } from './skillSourceReceipts.js';
|
|
13
13
|
function skillName(text) {
|
|
14
|
-
return text
|
|
14
|
+
return text
|
|
15
|
+
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
|
|
16
|
+
.match(/^name:\s*["']?([^"'\r\n]+?)["']?\s*$/m)?.[1]
|
|
17
|
+
?.trim();
|
|
15
18
|
}
|
|
16
19
|
async function findSkillDirectory(root, name, skillPath) {
|
|
17
20
|
if (skillPath) {
|
|
@@ -47,7 +50,8 @@ export async function hashSkillDirectory(directory, ignoreRuntimeFiles = false)
|
|
|
47
50
|
let total = 0;
|
|
48
51
|
const visit = async (current) => {
|
|
49
52
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
50
|
-
if (['.git', 'node_modules'].includes(entry.name) ||
|
|
53
|
+
if (['.git', 'node_modules'].includes(entry.name) ||
|
|
54
|
+
(ignoreRuntimeFiles && ['__pycache__', '.DS_Store'].includes(entry.name)))
|
|
51
55
|
continue;
|
|
52
56
|
const file = join(current, entry.name);
|
|
53
57
|
if (entry.isDirectory())
|
|
@@ -57,7 +61,13 @@ export async function hashSkillDirectory(directory, ignoreRuntimeFiles = false)
|
|
|
57
61
|
total += info.size;
|
|
58
62
|
if (total > 64 * 1024 * 1024 || files.length >= 10_000)
|
|
59
63
|
throw new Error('Skill source exceeds the comparison limit');
|
|
60
|
-
files.push({
|
|
64
|
+
files.push({
|
|
65
|
+
path: relative(directory, file)
|
|
66
|
+
.split(sep)
|
|
67
|
+
.join('/')
|
|
68
|
+
.replace(ignoreRuntimeFiles ? /^SKILL\.md\.mearl-disabled$/ : /$^/, 'SKILL.md'),
|
|
69
|
+
bytes: await readFile(file),
|
|
70
|
+
});
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
};
|
|
@@ -71,7 +81,10 @@ export async function hashSkillDirectory(directory, ignoreRuntimeFiles = false)
|
|
|
71
81
|
}
|
|
72
82
|
/** Checks source content in a temporary checkout; never invokes the mutating Skills `check` alias. */
|
|
73
83
|
export async function checkTrackedSkillUpdates(options = {}) {
|
|
74
|
-
const sources = [
|
|
84
|
+
const sources = [
|
|
85
|
+
...readTrackedSkillSources('global', undefined, options),
|
|
86
|
+
...(options.cwd ? readTrackedSkillSources('project', options.cwd, options) : []),
|
|
87
|
+
];
|
|
75
88
|
if (sources.some(source => !isPublicSkillSource(source)))
|
|
76
89
|
throw new Error('Installed Skills require an external manager. Enable its preset.');
|
|
77
90
|
if (sources.length > 500)
|
|
@@ -88,12 +101,27 @@ export async function checkTrackedSkillUpdates(options = {}) {
|
|
|
88
101
|
let checkout = checkouts.get(key);
|
|
89
102
|
if (!checkout) {
|
|
90
103
|
const target = join(temporary, String(checkouts.size));
|
|
91
|
-
checkout = local
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
checkout = local
|
|
105
|
+
? Promise.resolve(local)
|
|
106
|
+
: (async () => {
|
|
107
|
+
const result = await runner('git', [
|
|
108
|
+
'clone',
|
|
109
|
+
'--depth',
|
|
110
|
+
'1',
|
|
111
|
+
'--filter=blob:none',
|
|
112
|
+
...(location.ref ? ['--branch', location.ref] : []),
|
|
113
|
+
'--',
|
|
114
|
+
location.repository,
|
|
115
|
+
target,
|
|
116
|
+
], {
|
|
117
|
+
capture: true,
|
|
118
|
+
env: { ...(options.env ?? process.env), GIT_TERMINAL_PROMPT: '0' },
|
|
119
|
+
timeoutMs: 120_000,
|
|
120
|
+
});
|
|
121
|
+
if (result.code !== 0)
|
|
122
|
+
throw new Error(`Unable to inspect Skill source: ${source.name}`);
|
|
123
|
+
return target;
|
|
124
|
+
})();
|
|
97
125
|
checkouts.set(key, checkout);
|
|
98
126
|
}
|
|
99
127
|
const root = await realpath(await checkout);
|
|
@@ -107,10 +135,12 @@ export async function checkTrackedSkillUpdates(options = {}) {
|
|
|
107
135
|
changed = result.stdout.trim() !== source.expectedHash;
|
|
108
136
|
}
|
|
109
137
|
else if (source.expectedHash && source.hashKind === 'folder')
|
|
110
|
-
changed = await hashSkillDirectory(directory) !== source.expectedHash;
|
|
138
|
+
changed = (await hashSkillDirectory(directory)) !== source.expectedHash;
|
|
111
139
|
else {
|
|
112
|
-
const installed = join(source.scope === 'global' ? options.homeDirectory ?? homedir() : options.cwd, '.agents', 'skills', source.name);
|
|
113
|
-
changed =
|
|
140
|
+
const installed = join(source.scope === 'global' ? (options.homeDirectory ?? homedir()) : options.cwd, '.agents', 'skills', source.name);
|
|
141
|
+
changed =
|
|
142
|
+
(await hashSkillDirectory(directory, true)) !==
|
|
143
|
+
(await hashSkillDirectory(installed, true));
|
|
114
144
|
}
|
|
115
145
|
if (changed)
|
|
116
146
|
updates.push({ name: source.name, source: source.source, scope: source.scope });
|
|
@@ -128,7 +158,10 @@ export function readLinkedSkillSource(directory, name) {
|
|
|
128
158
|
return undefined;
|
|
129
159
|
const actual = realpathSync(directory);
|
|
130
160
|
const git = (...args) => execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', actual, ...args], {
|
|
131
|
-
encoding: 'utf8',
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
|
+
timeout: 2_000,
|
|
164
|
+
maxBuffer: 16_384,
|
|
132
165
|
}).trim();
|
|
133
166
|
const root = git('rev-parse', '--show-toplevel');
|
|
134
167
|
const folder = relative(root, actual);
|
|
@@ -169,29 +202,44 @@ function canonicalPath(path) {
|
|
|
169
202
|
return parent === absolute ? absolute : join(canonicalPath(parent), basename(absolute));
|
|
170
203
|
}
|
|
171
204
|
}
|
|
172
|
-
function hasOnlySetupRegistrations(directory) {
|
|
205
|
+
function hasOnlySetupRegistrations(directory, allowUnregistered = false) {
|
|
173
206
|
const contains = (root, entry) => {
|
|
174
207
|
const path = relative(root, entry);
|
|
175
208
|
return path !== '' && path !== '..' && !path.startsWith('..' + sep) && !isAbsolute(path);
|
|
176
209
|
};
|
|
177
|
-
const registrations = listPluginNames()
|
|
178
|
-
|
|
210
|
+
const registrations = listPluginNames()
|
|
211
|
+
.flatMap(name => findPluginRegistration(name) ?? [])
|
|
212
|
+
.filter(registration => contains(resolve(directory), resolve(registration.entry)) ||
|
|
213
|
+
contains(canonicalPath(directory), canonicalPath(registration.entry)));
|
|
214
|
+
return ((allowUnregistered || registrations.length > 0) &&
|
|
215
|
+
registrations.every(registration => registration.managedBy === OFFICIAL_PLUGIN_MANAGER));
|
|
179
216
|
}
|
|
180
217
|
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
181
218
|
export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
182
219
|
if (!source.trim() || !name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
183
220
|
throw new Error('Invalid Skill identity');
|
|
184
221
|
const directory = join(options.homeDirectory ?? homedir(), '.agents', 'skills', name);
|
|
185
|
-
if (existsSync(join(directory, 'SKILL.md')) &&
|
|
222
|
+
if (existsSync(join(directory, 'SKILL.md')) &&
|
|
223
|
+
existsSync(join(directory, DISABLED_SKILL_MANIFEST)))
|
|
186
224
|
return { state: 'conflict', reason: 'SKILL_MANIFEST_CONFLICT' };
|
|
187
|
-
const receipt = (options.readSources ?? readTrackedSkillSources)('global', undefined, {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
225
|
+
const receipt = (options.readSources ?? readTrackedSkillSources)('global', undefined, {
|
|
226
|
+
homeDirectory: options.homeDirectory,
|
|
227
|
+
}).find(entry => entry.name === name);
|
|
228
|
+
const tracked = receipt ??
|
|
229
|
+
(!/(?:#|\/(?:tree|blob)\/)/.test(source) ? readLinkedSkillSource(directory, name) : undefined);
|
|
230
|
+
if (tracked &&
|
|
231
|
+
((!options.externalManager && !isPublicSkillSource(tracked)) ||
|
|
232
|
+
!matchesTrackedSkillSource(source, tracked, options.normalizeSource))) {
|
|
191
233
|
const metadata = readInstalledSkillMetadata(directory);
|
|
192
|
-
if (receipt &&
|
|
193
|
-
options.
|
|
194
|
-
|
|
234
|
+
if (receipt &&
|
|
235
|
+
(options.externalManager || isPublicSkillSource(receipt)) &&
|
|
236
|
+
metadata?.name === name) {
|
|
237
|
+
// A missing ref requires installing the requested ref before the Skill can be ready.
|
|
238
|
+
const missingRef = matchesTrackedSkillSource(source, receipt, options.normalizeSource, 'missing');
|
|
239
|
+
const declaredMigration = options.migrateFrom?.some(previous => matchesTrackedSkillSource(previous, receipt, options.normalizeSource));
|
|
240
|
+
if ((missingRef || declaredMigration) && hasOnlySetupRegistrations(directory, missingRef)) {
|
|
241
|
+
return { state: 'migration', reason: 'SKILL_SOURCE_MIGRATION_REQUIRED' };
|
|
242
|
+
}
|
|
195
243
|
}
|
|
196
244
|
return { state: 'conflict', reason: 'SKILL_SOURCE_CONFLICT' };
|
|
197
245
|
}
|
|
@@ -202,7 +250,8 @@ export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
|
202
250
|
return { state: 'conflict', reason: 'SKILL_SOURCE_UNKNOWN' };
|
|
203
251
|
if (metadata.name !== name)
|
|
204
252
|
return { state: 'conflict', reason: 'SKILL_MANIFEST_IDENTITY' };
|
|
205
|
-
if (version &&
|
|
253
|
+
if (version &&
|
|
254
|
+
(!metadata.version || !valid(metadata.version) || !satisfies(metadata.version, version)))
|
|
206
255
|
return { state: 'version', reason: 'SKILL_VERSION_REQUIRED' };
|
|
207
256
|
const entry = findPluginDirectoryEntry(directory);
|
|
208
257
|
const names = listPluginNames().filter(name => {
|
|
@@ -212,10 +261,13 @@ export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
|
212
261
|
const managedNames = names.filter(name => findPluginRegistration(name)?.managedBy === OFFICIAL_PLUGIN_MANAGER);
|
|
213
262
|
// User aliases may share a managed Skill without participating in its readiness or cleanup.
|
|
214
263
|
const requiredNames = managedNames.length ? managedNames : names;
|
|
215
|
-
if (entry &&
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
264
|
+
if (entry &&
|
|
265
|
+
(requiredNames.length !== 1 ||
|
|
266
|
+
!requiredNames.some(name => {
|
|
267
|
+
const registration = findPluginRegistration(name);
|
|
268
|
+
return (resolve(registration.entry) === resolve(entry) &&
|
|
269
|
+
!pluginRegistrationNeedsRefresh(registration, true));
|
|
270
|
+
})))
|
|
219
271
|
return { state: 'registration', reason: 'SKILL_PLUGIN_REGISTRATION_REQUIRED' };
|
|
220
272
|
if (!entry && names.length)
|
|
221
273
|
return { state: 'registration', reason: 'SKILL_PLUGIN_REGISTRATION_REQUIRED' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -79,11 +79,11 @@
|
|
|
79
79
|
"ws": "^8.18.3",
|
|
80
80
|
"semver": "^7.8.5",
|
|
81
81
|
"adm-zip": "^0.6.1",
|
|
82
|
-
"@mearl/
|
|
83
|
-
"@mearl/
|
|
84
|
-
"@mearl/
|
|
85
|
-
"@mearl/
|
|
86
|
-
"@mearl/
|
|
82
|
+
"@mearl/provider": "3.0.2",
|
|
83
|
+
"@mearl/preset": "3.0.2",
|
|
84
|
+
"@mearl/browser-core": "3.0.2",
|
|
85
|
+
"@mearl/daemon-core": "3.0.2",
|
|
86
|
+
"@mearl/client": "3.0.2"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@types/node": "^24.9.1",
|