@mearl/preset 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.
package/README.en.md ADDED
@@ -0,0 +1,108 @@
1
+ # @mearl/preset
2
+
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ Use a Preset to distribute a configured set of Mearl capabilities, Skills, and Providers. This package resolves the manifest and prepares the resources that browser and Node hosts use.
6
+
7
+ See the [capability model](../../docs/capability-model.en.md) for 3.x declarations, configuration, catalogs, and object storage. Contributions infer supported targets; optional Preset `targets` selects a subset. Operations and logins belong to packages; configuration overrides belong to Presets and users.
8
+
9
+ See [browser bundles](../../docs/browser-bundles.en.md) for preparation and local storage, and [browser Presets](../../docs/browser-presets.en.md) for atomic activation and App implementation mappings. Both are exported through `@mearl/preset/bundles`.
10
+
11
+ Browser and Node share one inheritance resolution and resource download; see [complete resource preparation](../../docs/preset-preparation.en.md). `preparePreset` returns the full exact graph and a compatible browser projection. Node module selection reuses the same bytes. Preparation executes no code and installs no external environment.
12
+
13
+ The main entry shares preset validation and inheritance resolution across browsers and Node, without filesystem, network, credential, or browser permission side effects. Callers supply manifest loading. Optional `@mearl/preset/bundles` provides resource validation, bounded downloads, and explicitly invoked browser-local storage; see [browser bundles](../../docs/browser-bundles.en.md).
14
+
15
+ ```ts
16
+ import { resolvePreset, mergePresetDefaults } from '@mearl/preset';
17
+
18
+ const preset = await resolvePreset('https://example.com/presets/team.json', {
19
+ mearlApiVersion: '1.0.0',
20
+ async load(source) {
21
+ const response = await fetch(source);
22
+ if (!response.ok) throw new Error(`Preset load failed: ${response.status}`);
23
+ return response.json();
24
+ },
25
+ });
26
+
27
+ const effectiveSettings = mergePresetDefaults(preset.defaults, userSettings);
28
+ ```
29
+
30
+ Loaders check source policy, response size, redirects, authentication, and cancellation; the example only demonstrates the contract. Browser loaders should restrict URLs according to product source policies. The resolver does not automatically trust HTTP(S)/file URLs.
31
+
32
+ See [Node capabilities](../../docs/node-capabilities.en.md) for module snapshots and host loading contracts.
33
+
34
+ ## Manifest
35
+
36
+ ```json
37
+ {
38
+ "schemaVersion": 1,
39
+ "id": "team",
40
+ "version": "1.0.0",
41
+ "extends": { "source": "../base/preset.json", "version": "^1" },
42
+ "requires": { "mearlApi": "^1" },
43
+ "packages": {
44
+ "capture": { "source": "./capture/package.json", "targets": ["browser"] },
45
+ "unused": { "enabled": false }
46
+ },
47
+ "skills": {},
48
+ "providers": {},
49
+ "defaults": { "simulation": { "app": "sample" } },
50
+ "permissions": { "origins": ["https://*.example.com/*"] }
51
+ }
52
+ ```
53
+
54
+ - Single inheritance, at most 32 levels; cycles and unmet parent version/API requirements are rejected. `version` is exact semver; component `version` is a version requirement, with concrete installation versions resolved by installers.
55
+ - packages, skills, and providers merge by ID; child fields override the same ID, while arrays replace entirely. Enabled entries must ultimately have a source; `enabled:false` retains a disabled declaration.
56
+ - Component `dependsOn` references IDs of the same component type. The final inherited graph checks missing, disabled, and cyclic dependencies. This does not establish runtime compatibility or completed installation.
57
+ - source resolves relative to the declaring manifest. A child changing only the version does not alter an inherited source's base URL.
58
+ - Skills may declare up to 16 previous sources in `migrateFrom`, resolved relative to the declaring manifest. Children replace the whole list; an empty array clears it. Installers migrate only with a matching source receipt, valid Skill identity, and setup ownership of all associated plugins. Manual registrations, shared manual aliases, and other sources remain and report conflicts. Source matching still checks refs strictly.
59
+ - defaults recursively merges objects; arrays, scalars, and null replace values. User overrides are saved separately and applied to the resolved result.
60
+ - Permissions are aggregated and deduplicated as requirements only; browser installers obtain actual authorization separately.
61
+ - appProfiles maps App IDs to implementation identifiers. Later capability catalogs verify profile existence and package executability; the resolver does not claim simulation validation.
62
+ - Results retain the exact parent-to-child version chain and each component's latest declaration source. Loading failures do not produce partial success.
63
+ - Unknown fields, non-JSON configuration, and prototype-pollution keys are rejected. Total manifests are limited to 256 Ki UTF-16 code units; defaults nesting to 32 levels.
64
+
65
+ This package owns generic manifest parsing, validation, and resource graphs, without installation or UI. Browser hosts and setup own full installation, preset lock persistence, permissions, registration, and activation; see the [installation host](../../docs/preset-installation.en.md). Bundle-local storage transactionally replaces validated snapshots.
66
+
67
+ The Skill manager's optional `readSources(scope, cwd)` returns manager-verified source receipts matching setup's `TrackedSkillSource`. CLI and Native installation status share this reader. Managers persist, validate, and invalidate their supplementary records; public hosts still verify sources, refs, manifest identity, and managed ownership. Without it, the standard Skills lockfile is read.
68
+
69
+ ## Tool catalogs
70
+
71
+ Presets can register additional installable tool catalogs through `defaults.extension.toolCatalogs`:
72
+
73
+ ```json
74
+ {
75
+ "defaults": {
76
+ "extension": {
77
+ "toolCatalogs": {
78
+ "team": {
79
+ "url": "https://tools.example.com/toolbox.json",
80
+ "label": "Team"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ Use absolute HTTPS URLs (loopback HTTP is allowed for local development), with at most 16 catalogs. Use stable IDs; child presets can override only `url` or `label`, or disable a parent catalog with `enabled: false`. The official catalog always remains first; `official` is reserved. Declare cross-origin domains in `permissions.origins` and obtain access through the existing authorization flow.
89
+
90
+ Catalog JSON shares the official structure, limited to 256 entries and 256 KiB:
91
+
92
+ ```json
93
+ {
94
+ "tools": [{
95
+ "id": "plugin:team-tool",
96
+ "plugin": "team-tool",
97
+ "repository": "example/skills",
98
+ "skill": "team-tool",
99
+ "title": "Team tool",
100
+ "description": "A tool for the team",
101
+ "icon": "wrench"
102
+ }]
103
+ }
104
+ ```
105
+
106
+ `repository` also supports HTTPS/SSH Git sources and pinned refs such as `git@code.example.com:team/skills.git#release/v3`, passed unchanged to the preset-selected Skills manager. Manifests reject shell arguments, credentials, and local paths, and execute no embedded code. Listing tools does not install them.
107
+
108
+ Sources load concurrently and cache independently. Retry refreshes the current catalog; failure retains its cache without blocking other sources. Identical plugin IDs and sources deduplicate. Conflicting repositories or Skills for one ID retain the higher-priority catalog and report a conflict. Lower-priority install buttons wait for earlier catalogs to finish checking, so load order cannot change installation targets. Replaced or disabled catalogs and late responses do not remain in the current list.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @mearl/preset
2
2
 
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ 用 Preset 将一组配置好的 Mearl 能力、Skills 和 Providers 一起分发。本包负责解析清单,并为浏览器与 Node 宿主准备资源。
6
+
3
7
  3.x 的声明、配置、能力目录及对象存储设计见 [能力包模型](../../docs/capability-model.md)。包支持的位置从贡献推导,Preset 的可选 `targets` 仅选择启用子集;操作和登录定义归包,配置覆盖归 Preset 与用户。
4
8
 
5
9
  浏览器包的准备与本地存储见 [能力包](../../docs/browser-bundles.md),整套原子激活与 App 实现映射见 [浏览器 Preset](../../docs/browser-presets.md)。这两类能力通过 `@mearl/preset/bundles` 导出。
@@ -54,7 +58,7 @@ Node 模块快照与宿主加载合同见 [Node 能力包](../../docs/node-capab
54
58
  - Skill 可声明 `migrateFrom` 旧来源列表(最多 16 项),相对地址按声明清单解析;子级整体覆盖,空数组清除。安装器仅在来源收据匹配旧来源、Skill 身份有效且所有关联插件均由 setup 托管时迁移。手动注册、共享的手动别名或其他来源保留并报告冲突;来源匹配仍严格检查 ref。
55
59
  - defaults 递归合并对象,数组、标量和 null 替换;用户覆盖单独保存,在解析结果上应用。
56
60
  - 权限汇总去重,结果仅表示需求;必须由浏览器安装器另行取得实际授权。
57
- - appProfiles 是 App ID 到实现标识的覆盖映射。具体档案是否存在、所需包是否可执行,由后续能力目录校验,解析器不宣称已验证模拟效果。
61
+ - appProfiles 是 App ID 到实现标识的覆盖映射。具体端模拟配置是否存在、所需包是否可执行,由后续能力目录校验,解析器不宣称已验证模拟效果。
58
62
  - 结果保留父级到子级的确切版本链和各组件最近声明来源,加载失败不会生成部分成功的结果。
59
63
  - 拒绝未知字段、非 JSON 配置和原型污染键。清单总量不超过 256 Ki UTF-16 code units,defaults 最大嵌套 32 层。
60
64
 
package/dist/bundle.d.ts CHANGED
@@ -48,7 +48,7 @@ export type BundleLoader = (source: string, options: BundleLoadOptions) => Promi
48
48
  export interface PrepareBundleOptions {
49
49
  load: BundleLoader;
50
50
  mearlApiVersion: string;
51
- expectedId: string;
51
+ expectedId?: string;
52
52
  version?: string;
53
53
  signal?: AbortSignal;
54
54
  }
package/dist/bundle.js CHANGED
@@ -186,7 +186,7 @@ export async function prepareBundle(source, options) {
186
186
  invalid('Expected a UTF-8 JSON manifest');
187
187
  }
188
188
  const manifest = validateBundleManifest(value, options.mearlApiVersion);
189
- if (manifest.id !== options.expectedId)
189
+ if (options.expectedId !== undefined && manifest.id !== options.expectedId)
190
190
  invalid('Bundle identity does not match the requested package');
191
191
  if (options.version && !satisfies(manifest.version, options.version))
192
192
  throw new BundleError('BUNDLE_INCOMPATIBLE', 'Bundle version does not satisfy the requested range');
@@ -292,9 +292,9 @@ export class BrowserBundleStore {
292
292
  /** Pins the verified selection while reading its package from the same IndexedDB snapshot. */
293
293
  async profileBinding(name) {
294
294
  const activation = await this.activePreset();
295
- const target = activation?.preset.appProfiles[name];
296
- if (!activation || !target)
295
+ if (!activation || !Object.hasOwn(activation.preset.appProfiles, name))
297
296
  return undefined;
297
+ const target = activation.preset.appProfiles[name];
298
298
  const id = target.split('/')[0];
299
299
  const database = await this.open();
300
300
  let objects = new Map();
package/dist/bundles.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { BundleManifest, PreparedBundle, BundleLoader, BundleLoadOptions, P
3
3
  export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
4
4
  export type { ExtensionContributions } from './extensionContributions.js';
5
5
  export { createBundleFetchLoader } from './bundleFetch.js';
6
+ export { parseProfileSource, resolveProfileSource } from './profileSource.js';
6
7
  export { BrowserBundleStore } from './bundleStore.js';
7
8
  export type { BrowserPresetInstallation } from './bundleStore.js';
8
9
  export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
package/dist/bundles.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { BundleError, MAX_BUNDLE_BYTES, MAX_BUNDLE_MANIFEST_BYTES, MAX_BROWSER_STORAGE_BYTES, MAX_PAGE_RUNTIME_BYTES, MAX_REQUEST_UI_BYTES, bundleDigest, bundleSourceUrl, prepareBundle, readBundleJson, readBundleText, validateBundleManifest, verifyBundle, } from './bundle.js';
2
2
  export { selectExtensionContributions, validateExtensionContributions } from './extensionContributions.js';
3
3
  export { createBundleFetchLoader } from './bundleFetch.js';
4
+ export { parseProfileSource, resolveProfileSource } from './profileSource.js';
4
5
  export { BrowserBundleStore } from './bundleStore.js';
5
6
  export { prepareBrowserPreset, presetRequiresBrowser, verifyBrowserPreset, verifyPreparedBrowserPreset } from './browserPreset.js';
6
7
  export { preparePreset, verifyPreset, verifyPreparedPreset } from './preset.js';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
2
  export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
3
  export type * from './types.js';
4
+ export { parseSetupManagement, setupManagementCommand, setupRuntimeCompatible } from './management.js';
5
+ export type { SetupManagement } from './management.js';
4
6
  export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
5
7
  export type { CapabilitySelection, CapabilitySnapshot, CapabilityActivation } from './capabilitySnapshot.js';
6
8
  export { parseToolboxCatalogs } from './toolboxCatalogs.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { PresetError, validatePreset, clonePresetDefaults } from './manifest.js';
2
2
  export { mergePresetDefaults, resolvePreset } from './resolve.js';
3
+ export { parseSetupManagement, setupManagementCommand, setupRuntimeCompatible } from './management.js';
3
4
  export { prepareNodeBundleSnapshot, prepareCapabilitySnapshot, verifyCapabilitySnapshot, verifyCapabilityActivation, validateCapabilityConfig, resolveCapabilityConfig, MAX_CAPABILITY_SELECTION_BYTES } from './capabilitySnapshot.js';
4
5
  export { parseToolboxCatalogs } from './toolboxCatalogs.js';
@@ -0,0 +1,10 @@
1
+ export interface SetupManagement {
2
+ package: string;
3
+ version: string;
4
+ registry?: string;
5
+ runtime?: string;
6
+ }
7
+ /** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
8
+ export declare function parseSetupManagement(value: unknown): SetupManagement | undefined;
9
+ export declare function setupManagementCommand(setup: SetupManagement): string;
10
+ export declare function setupRuntimeCompatible(setup: SetupManagement, version: string, targetLine?: string): boolean;
@@ -0,0 +1,36 @@
1
+ import { valid, validRange, satisfies, subset } from 'semver';
2
+ import { isJsonObject, PresetError } from './manifest.js';
3
+ /** Declarative installation guidance; never accepts commands, arguments or executable hooks. */
4
+ export function parseSetupManagement(value) {
5
+ if (value === undefined)
6
+ return;
7
+ const invalid = () => { throw new PresetError('PRESET_INVALID', 'Invalid defaults.management.setup declaration'); };
8
+ if (!isJsonObject(value) || Object.keys(value).some(key => key !== 'setup') || !isJsonObject(value.setup))
9
+ return invalid();
10
+ const setup = value.setup;
11
+ if (Object.keys(setup).some(key => !['package', 'version', 'registry', 'runtime'].includes(key)) ||
12
+ typeof setup.package !== 'string' || setup.package.length > 214 || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(setup.package) ||
13
+ typeof setup.version !== 'string' || setup.version.length > 64 || !(valid(setup.version) === setup.version || /^[a-z][a-z0-9.-]*$/.test(setup.version)))
14
+ return invalid();
15
+ if (setup.registry !== undefined) {
16
+ if (typeof setup.registry !== 'string' || setup.registry.length > 2048 || !/^https:\/\/[a-zA-Z0-9./:_-]+$/.test(setup.registry))
17
+ return invalid();
18
+ try {
19
+ const url = new URL(setup.registry);
20
+ if (!url.hostname || url.username || url.password || url.search || url.hash)
21
+ return invalid();
22
+ }
23
+ catch {
24
+ return invalid();
25
+ }
26
+ }
27
+ if (setup.runtime !== undefined && (typeof setup.runtime !== 'string' || setup.runtime.length > 128 || !validRange(setup.runtime)))
28
+ return invalid();
29
+ return { ...setup };
30
+ }
31
+ export function setupManagementCommand(setup) {
32
+ return `npx${setup.registry ? ` --registry=${setup.registry}` : ''} ${setup.package}@${setup.version}`;
33
+ }
34
+ export function setupRuntimeCompatible(setup, version, targetLine) {
35
+ return !setup.runtime || (satisfies(version, setup.runtime, { includePrerelease: true }) && (!targetLine || subset(targetLine, setup.runtime, { includePrerelease: true })));
36
+ }
@@ -0,0 +1,9 @@
1
+ export declare function parseProfileSource(input: string): {
2
+ origin: string;
3
+ url: string;
4
+ github?: {
5
+ repository: string;
6
+ ref: string;
7
+ };
8
+ };
9
+ export declare function resolveProfileSource(input: string, fetcher?: typeof fetch): Promise<string>;
@@ -0,0 +1,44 @@
1
+ import { BundleError } from './bundle.js';
2
+ import { createBundleFetchLoader } from './bundleFetch.js';
3
+ const githubReference = /^([A-Za-z0-9-]+)\/([A-Za-z0-9_.-]+)(?:@([A-Za-z0-9._/-]+))?$/;
4
+ export function parseProfileSource(input) {
5
+ if (typeof input !== 'string' || input.length > 2048)
6
+ throw new BundleError('BUNDLE_INVALID', 'Invalid App profile source');
7
+ const match = githubReference.exec(input.trim());
8
+ if (match && match[2] !== '.' && match[2] !== '..') {
9
+ return {
10
+ origin: 'https://raw.githubusercontent.com',
11
+ url: input.trim(),
12
+ github: { repository: `${match[1]}/${match[2]}`, ref: match[3] ?? 'HEAD' },
13
+ };
14
+ }
15
+ let url;
16
+ try {
17
+ url = new URL(input);
18
+ }
19
+ catch {
20
+ throw new BundleError('BUNDLE_INVALID', 'Use a GitHub owner/repo or an HTTPS bundle manifest URL');
21
+ }
22
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
23
+ throw new BundleError('BUNDLE_INVALID', 'App profile bundle URLs require HTTPS without credentials or a fragment');
24
+ }
25
+ return { origin: url.origin, url: url.href };
26
+ }
27
+ export async function resolveProfileSource(input, fetcher = fetch) {
28
+ const { url, github } = parseProfileSource(input);
29
+ if (!github)
30
+ return url;
31
+ const { repository, ref } = github;
32
+ let sha = ref;
33
+ if (!/^[a-f0-9]{40}$/.test(sha)) {
34
+ const load = createBundleFetchLoader({ fetch: fetcher });
35
+ const bytes = await load(`https://api.github.com/repos/${repository}/commits/${encodeURIComponent(ref)}`, {
36
+ maxBytes: 2 * 1024 * 1024,
37
+ signal: AbortSignal.timeout(10000),
38
+ });
39
+ sha = JSON.parse(new TextDecoder().decode(bytes)).sha;
40
+ if (typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha))
41
+ throw new BundleError('BUNDLE_INVALID', 'GitHub did not return a commit ID');
42
+ }
43
+ return `https://raw.githubusercontent.com/${repository}/${sha}/bundle.json`;
44
+ }
package/dist/resolve.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseToolboxCatalogs } from './toolboxCatalogs.js';
2
+ import { parseSetupManagement } from './management.js';
2
3
  import { satisfies, valid } from 'semver';
3
4
  import { clonePresetDefaults, isJsonObject, PresetError, validatePreset } from './manifest.js';
4
5
  const kinds = ['packages', 'skills', 'providers'];
@@ -128,6 +129,7 @@ export async function resolvePreset(source, options) {
128
129
  return result;
129
130
  };
130
131
  const result = await load(sourceUrl(source));
132
+ parseSetupManagement(result.defaults.management);
131
133
  const extension = result.defaults.extension;
132
134
  if (isJsonObject(extension))
133
135
  parseToolboxCatalogs(extension.toolCatalogs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/preset",
3
- "version": "3.0.0-beta.8",
3
+ "version": "3.0.0",
4
4
  "description": "Browser-compatible preset validation, inheritance and capability resolution for Mearl",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,6 +26,7 @@
26
26
  }
27
27
  },
28
28
  "files": [
29
+ "README.en.md",
29
30
  "dist",
30
31
  "!dist/**/*.map",
31
32
  "installation-site.json"