@mearl/setup 2.21.0 → 3.0.0-beta.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.
Files changed (42) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +59 -59
  3. package/dist/browserExtension.d.ts +2 -2
  4. package/dist/browserExtension.js +25 -19
  5. package/dist/cli.d.ts +8 -3
  6. package/dist/cli.js +74 -17
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.js +2 -1
  9. package/dist/lifecycle.d.ts +11 -4
  10. package/dist/lifecycle.js +48 -15
  11. package/dist/managedPlugins.d.ts +21 -0
  12. package/dist/managedPlugins.js +76 -0
  13. package/dist/npmRegistry.d.ts +1 -1
  14. package/dist/npmRegistry.js +4 -16
  15. package/dist/plugin.d.ts +13 -3
  16. package/dist/plugin.js +106 -44
  17. package/dist/preset.d.ts +94 -0
  18. package/dist/preset.js +275 -0
  19. package/dist/presetBrowserTransfer.d.ts +11 -0
  20. package/dist/presetBrowserTransfer.js +72 -0
  21. package/dist/presetInstallationLease.d.ts +6 -0
  22. package/dist/presetInstallationLease.js +66 -0
  23. package/dist/presetStore.d.ts +48 -0
  24. package/dist/presetStore.js +252 -0
  25. package/dist/presetUpgrade.d.ts +13 -0
  26. package/dist/presetUpgrade.js +42 -0
  27. package/dist/provider.d.ts +2 -0
  28. package/dist/provider.js +24 -2
  29. package/dist/runtimeCleanup.js +1 -8
  30. package/dist/setupVersion.d.ts +2 -0
  31. package/dist/setupVersion.js +4 -0
  32. package/dist/skillSources.d.ts +43 -0
  33. package/dist/skillSources.js +247 -0
  34. package/dist/skills.d.ts +45 -0
  35. package/dist/skills.js +213 -0
  36. package/dist/version.d.ts +2 -0
  37. package/dist/version.js +6 -0
  38. package/package.json +22 -10
  39. package/dist/aliSkills.d.ts +0 -32
  40. package/dist/aliSkills.js +0 -212
  41. package/dist/internalPlugins.d.ts +0 -26
  42. package/dist/internalPlugins.js +0 -197
@@ -0,0 +1,247 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdtemp, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
3
+ import { readFileSync, statSync } from 'node:fs';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { runCommand } from '@mearl/daemon-core';
8
+ import { satisfies, valid } from 'semver';
9
+ import { DISABLED_SKILL_MANIFEST, findPluginDirectoryEntry, findPluginRegistration, listPluginNames, OFFICIAL_PLUGIN_MANAGER, pluginRegistrationNeedsRefresh, readInstalledSkillMetadata } from '@mearl/client/plugin';
10
+ import { existsSync } from 'node:fs';
11
+ function lockFile(scope, cwd, options) {
12
+ const env = options.env ?? process.env;
13
+ return scope === 'project' ? join(cwd, 'skills-lock.json') : env.XDG_STATE_HOME ? join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json') : join(options.homeDirectory ?? homedir(), '.agents', '.skill-lock.json');
14
+ }
15
+ export function readTrackedSkillSources(scope, cwd, options = {}) {
16
+ const file = lockFile(scope, cwd, options);
17
+ let text;
18
+ try {
19
+ if (statSync(file).size > 4 * 1024 * 1024)
20
+ throw new Error('Skill source lock exceeds 4 MiB');
21
+ text = readFileSync(file, 'utf8');
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT')
25
+ return [];
26
+ throw error;
27
+ }
28
+ const lock = JSON.parse(text);
29
+ if (!lock || !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills))
30
+ throw new Error('Invalid Skills source lock');
31
+ return Object.entries(lock.skills).flatMap(([name, entry]) => {
32
+ if (!entry || typeof entry !== 'object' || entry.sourceType === 'node_modules')
33
+ return [];
34
+ if (!name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
35
+ throw new Error('Invalid tracked Skill name');
36
+ const source = typeof entry.sourceUrl === 'string' ? entry.sourceUrl : entry.source;
37
+ if (typeof source !== 'string' || !source.trim())
38
+ return [];
39
+ if (source.length > 4096 || /[\0\r\n]/.test(source))
40
+ throw new Error('Invalid tracked Skill source');
41
+ const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
42
+ return [{ name, source, scope, ...(typeof entry.sourceType === 'string' ? { sourceType: entry.sourceType } : {}), ...(typeof entry.ref === 'string' ? { ref: entry.ref } : {}), ...(typeof entry.skillPath === 'string' ? { skillPath: entry.skillPath } : {}), ...(typeof expectedHash === 'string' && expectedHash ? { expectedHash } : {}), hashKind: scope === 'global' && typeof expectedHash === 'string' && /^[a-f0-9]{40}$/.test(expectedHash) ? 'git-tree' : 'folder' }];
43
+ });
44
+ }
45
+ /** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
46
+ export function isPublicSkillSource(source) {
47
+ return source.sourceType === undefined || ['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType);
48
+ }
49
+ function gitSource(source, ref) {
50
+ if (/^[\w.-]+\/[\w.-]+$/.test(source))
51
+ return { repository: `https://github.com/${source}.git`, ...(ref ? { ref } : {}) };
52
+ if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
53
+ return { repository: source, ...(ref ? { ref } : {}) };
54
+ const url = new URL(source);
55
+ if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password || (url.username && url.protocol !== 'ssh:') || url.search || url.hash)
56
+ throw new Error('Skill source is not a supported Git repository');
57
+ if (url.hostname === 'github.com' && !url.port) {
58
+ const parts = url.pathname.split('/').filter(Boolean);
59
+ if (parts.length >= 2)
60
+ return { repository: `https://github.com/${parts[0]}/${parts[1].replace(/\.git$/, '')}.git`, ...(ref || (['tree', 'blob'].includes(parts[2]) && parts[3]) ? { ref: ref || decodeURIComponent(parts[3]) } : {}), ...(['tree', 'blob'].includes(parts[2]) && parts.length > 4 ? { subdirectory: decodeURIComponent(parts.slice(4).join('/')).replace(/\/SKILL\.md$/, '') } : {}) };
61
+ }
62
+ return { repository: url.href, ...(ref ? { ref } : {}) };
63
+ }
64
+ /** Compare the manager's existing source receipt; no network access or second lockfile. */
65
+ export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
66
+ const location = (source, ref, sourceType) => {
67
+ const value = normalize(source, sourceType);
68
+ if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
69
+ throw new Error('Invalid normalized Skill source');
70
+ const hash = value.indexOf('#');
71
+ const fragment = hash < 0 ? undefined : value.slice(hash + 1);
72
+ const plain = hash < 0 ? value : value.slice(0, hash);
73
+ const parsed = isAbsolute(plain) ? { repository: plain, ref: ref ?? fragment } : gitSource(plain, ref ?? fragment);
74
+ let repository = parsed.repository;
75
+ if (repository.startsWith('file:'))
76
+ repository = fileURLToPath(repository);
77
+ if (isAbsolute(repository))
78
+ repository = resolve(repository);
79
+ else {
80
+ const ssh = repository.match(/^[\w.-]+@([\w.-]+):(.+)$/);
81
+ const url = new URL(ssh ? `ssh://${ssh[1]}/${ssh[2]}` : repository);
82
+ // Authentication transport is independent of the repository's identity.
83
+ repository = `${url.hostname.toLowerCase()}${url.port ? ':' + url.port : ''}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`;
84
+ }
85
+ return { ...parsed, repository, ref: parsed.ref || undefined };
86
+ };
87
+ try {
88
+ const expected = location(requested);
89
+ const actual = location(tracked.source, tracked.ref, tracked.sourceType);
90
+ if (expected.repository !== actual.repository || expected.ref !== actual.ref)
91
+ return false;
92
+ if (!expected.subdirectory)
93
+ return true;
94
+ const path = (tracked.skillPath ?? actual.subdirectory ?? '').replace(/(?:^|\/)SKILL\.md$/, '').replace(/\/+$/, '');
95
+ return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
96
+ }
97
+ catch {
98
+ return normalize(requested) === normalize(tracked.source, tracked.sourceType) && !tracked.ref;
99
+ }
100
+ }
101
+ function skillName(text) {
102
+ return text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1].match(/^name:\s*["']?([^"'\r\n]+?)["']?\s*$/m)?.[1]?.trim();
103
+ }
104
+ async function findSkillDirectory(root, name, skillPath) {
105
+ if (skillPath) {
106
+ const directory = resolve(root, skillPath.replace(/(?:^|\/)SKILL\.md$/, ''));
107
+ const resolvedRoot = await realpath(root);
108
+ const candidate = await realpath(directory);
109
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + sep))
110
+ throw new Error('Skill source path escapes its repository');
111
+ if (skillName(await readFile(join(candidate, 'SKILL.md'), 'utf8')) === name)
112
+ return candidate;
113
+ }
114
+ const matches = [];
115
+ let scanned = 0;
116
+ const visit = async (directory, depth) => {
117
+ if (++scanned > 10_000 || depth > 32)
118
+ throw new Error('Skill source discovery exceeds its limit');
119
+ const entries = await readdir(directory, { withFileTypes: true });
120
+ const manifest = entries.find(entry => entry.name === 'SKILL.md' && entry.isFile());
121
+ if (manifest && skillName(await readFile(join(directory, 'SKILL.md'), 'utf8')) === name)
122
+ matches.push(directory);
123
+ for (const entry of entries)
124
+ if (entry.isDirectory() && !['.git', 'node_modules'].includes(entry.name))
125
+ await visit(join(directory, entry.name), depth + 1);
126
+ };
127
+ await visit(root, 0);
128
+ if (matches.length !== 1)
129
+ throw new Error(`Cannot uniquely locate Skill source: ${name}`);
130
+ return matches[0];
131
+ }
132
+ /** Matches the public Skills folder hash without writing to the installed Skill. */
133
+ export async function hashSkillDirectory(directory, ignoreRuntimeFiles = false) {
134
+ const files = [];
135
+ let total = 0;
136
+ const visit = async (current) => {
137
+ for (const entry of await readdir(current, { withFileTypes: true })) {
138
+ if (['.git', 'node_modules'].includes(entry.name) || (ignoreRuntimeFiles && ['__pycache__', '.DS_Store'].includes(entry.name)))
139
+ continue;
140
+ const file = join(current, entry.name);
141
+ if (entry.isDirectory())
142
+ await visit(file);
143
+ else if (entry.isFile()) {
144
+ const info = await stat(file);
145
+ total += info.size;
146
+ if (total > 64 * 1024 * 1024 || files.length >= 10_000)
147
+ throw new Error('Skill source exceeds the comparison limit');
148
+ files.push({ path: relative(directory, file).split(sep).join('/').replace(ignoreRuntimeFiles ? /^SKILL\.md\.mearl-disabled$/ : /$^/, 'SKILL.md'), bytes: await readFile(file) });
149
+ }
150
+ }
151
+ };
152
+ await visit(directory);
153
+ const hash = createHash('sha256');
154
+ for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
155
+ hash.update(file.path);
156
+ hash.update(file.bytes);
157
+ }
158
+ return hash.digest('hex');
159
+ }
160
+ /** Checks source content in a temporary checkout; never invokes the mutating Skills `check` alias. */
161
+ export async function checkTrackedSkillUpdates(options = {}) {
162
+ const sources = [...readTrackedSkillSources('global', undefined, options), ...(options.cwd ? readTrackedSkillSources('project', options.cwd, options) : [])];
163
+ if (sources.some(source => !isPublicSkillSource(source)))
164
+ throw new Error('Installed Skills require an external manager. Enable its preset.');
165
+ if (sources.length > 500)
166
+ throw new Error('Too many tracked Skills to inspect at once');
167
+ const temporary = await mkdtemp(join(tmpdir(), 'mearl-skill-sources-'));
168
+ const checkouts = new Map();
169
+ const runner = options.runCommand ?? runCommand;
170
+ const updates = [];
171
+ try {
172
+ for (const source of sources) {
173
+ const local = isAbsolute(source.source) ? source.source : undefined;
174
+ const location = local ? { repository: local } : gitSource(source.source, source.ref);
175
+ const key = JSON.stringify(location);
176
+ let checkout = checkouts.get(key);
177
+ if (!checkout) {
178
+ const target = join(temporary, String(checkouts.size));
179
+ checkout = local ? Promise.resolve(local) : (async () => {
180
+ const result = await runner('git', ['clone', '--depth', '1', '--filter=blob:none', ...(location.ref ? ['--branch', location.ref] : []), '--', location.repository, target], { capture: true, env: { ...(options.env ?? process.env), GIT_TERMINAL_PROMPT: '0' }, timeoutMs: 120_000 });
181
+ if (result.code !== 0)
182
+ throw new Error(`Unable to inspect Skill source: ${source.name}`);
183
+ return target;
184
+ })();
185
+ checkouts.set(key, checkout);
186
+ }
187
+ const root = await realpath(await checkout);
188
+ const directory = await findSkillDirectory(root, source.name, source.skillPath ?? location.subdirectory);
189
+ let changed;
190
+ if (source.expectedHash && source.hashKind === 'git-tree' && !local) {
191
+ const folder = relative(root, directory).split(sep).join('/');
192
+ const result = await runner('git', ['rev-parse', `HEAD${folder ? ':' + folder : '^{tree}'}`], { cwd: root, capture: true, timeoutMs: 30_000 });
193
+ if (result.code !== 0 || !/^[a-f\d]{40,64}$/.test(result.stdout.trim()))
194
+ throw new Error(`Unable to compare Skill revision: ${source.name}`);
195
+ changed = result.stdout.trim() !== source.expectedHash;
196
+ }
197
+ else if (source.expectedHash && source.hashKind === 'folder')
198
+ changed = await hashSkillDirectory(directory) !== source.expectedHash;
199
+ else {
200
+ const installed = join(source.scope === 'global' ? options.homeDirectory ?? homedir() : options.cwd, '.agents', 'skills', source.name);
201
+ changed = await hashSkillDirectory(directory, true) !== await hashSkillDirectory(installed, true);
202
+ }
203
+ if (changed)
204
+ updates.push({ name: source.name, source: source.source, scope: source.scope });
205
+ }
206
+ return updates;
207
+ }
208
+ finally {
209
+ await rm(temporary, { recursive: true, force: true });
210
+ }
211
+ }
212
+ /** CLI and Agent readiness use the same source, manifest and registration evidence. */
213
+ export function inspectGlobalSkill(source, name, version, options = {}) {
214
+ if (!source.trim() || !name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
215
+ throw new Error('Invalid Skill identity');
216
+ const directory = join(homedir(), '.agents', 'skills', name);
217
+ if (existsSync(join(directory, 'SKILL.md')) && existsSync(join(directory, DISABLED_SKILL_MANIFEST)))
218
+ return { state: 'conflict', reason: 'SKILL_MANIFEST_CONFLICT' };
219
+ const tracked = (options.readSources ?? readTrackedSkillSources)('global').find(entry => entry.name === name);
220
+ if (tracked && ((!options.externalManager && !isPublicSkillSource(tracked)) || !matchesTrackedSkillSource(source, tracked, options.normalizeSource)))
221
+ return { state: 'conflict', reason: 'SKILL_SOURCE_CONFLICT' };
222
+ const metadata = readInstalledSkillMetadata(directory);
223
+ if (!metadata)
224
+ return { state: 'missing', reason: 'SKILL_NOT_INSTALLED' };
225
+ if (!tracked)
226
+ return { state: 'conflict', reason: 'SKILL_SOURCE_UNKNOWN' };
227
+ if (metadata.name !== name)
228
+ return { state: 'conflict', reason: 'SKILL_MANIFEST_IDENTITY' };
229
+ if (version && (!metadata.version || !valid(metadata.version) || !satisfies(metadata.version, version)))
230
+ return { state: 'version', reason: 'SKILL_VERSION_REQUIRED' };
231
+ const entry = findPluginDirectoryEntry(directory);
232
+ const names = listPluginNames().filter(name => {
233
+ const registered = findPluginRegistration(name)?.entry;
234
+ return registered !== undefined && resolve(dirname(dirname(registered))) === resolve(directory);
235
+ });
236
+ const managedNames = names.filter(name => findPluginRegistration(name)?.managedBy === OFFICIAL_PLUGIN_MANAGER);
237
+ // User aliases may share a managed Skill without participating in its readiness or cleanup.
238
+ const requiredNames = managedNames.length ? managedNames : names;
239
+ if (entry && (requiredNames.length !== 1 || !requiredNames.some(name => {
240
+ const registration = findPluginRegistration(name);
241
+ return resolve(registration.entry) === resolve(entry) && !pluginRegistrationNeedsRefresh(registration, true);
242
+ })))
243
+ return { state: 'registration', reason: 'SKILL_PLUGIN_REGISTRATION_REQUIRED' };
244
+ if (!entry && names.length)
245
+ return { state: 'registration', reason: 'SKILL_PLUGIN_REGISTRATION_REQUIRED' };
246
+ return { state: 'ready' };
247
+ }
@@ -0,0 +1,45 @@
1
+ import { inspectGlobalSkill } from './skillSources.js';
2
+ import type { PluginCatalogSelection } from '@mearl/client/plugin';
3
+ import type { ResolvedComponent } from '@mearl/preset';
4
+ import { type CommandRunner, type CommandResult, type RunCommandOptions } from '@mearl/daemon-core';
5
+ export declare const SKILLS_CLI_PACKAGE = "skills@1.5.0";
6
+ export declare const MEARL_SKILL_SOURCE: string;
7
+ export interface ResolveSkillsOptions {
8
+ env?: NodeJS.ProcessEnv;
9
+ existsSync?: (filePath: string) => boolean;
10
+ platform?: NodeJS.Platform;
11
+ processExecPath?: string;
12
+ }
13
+ export declare function stripAnsi(value: string): string;
14
+ export declare function resolveSkillsExecutable(options?: ResolveSkillsOptions): string | null;
15
+ interface SkillsInvocation {
16
+ command: string;
17
+ argsPrefix: string[];
18
+ env: NodeJS.ProcessEnv;
19
+ }
20
+ export declare function resolveSkillsInvocation(options?: ResolveSkillsOptions): SkillsInvocation | null;
21
+ export interface SkillsCli {
22
+ id: string;
23
+ managedSkills: Readonly<Record<string, ResolvedComponent>>;
24
+ selection?: PluginCatalogSelection;
25
+ inspectGlobal: typeof inspectGlobalSkill;
26
+ run(args: string[], options?: Pick<RunCommandOptions, 'capture' | 'tee' | 'cwd' | 'timeoutMs'>): Promise<CommandResult>;
27
+ }
28
+ /** Only the selected manager module is needed; browser and Provider factories stay in their hosts. */
29
+ export declare function captureSkillsCli(options?: ResolveSkillsOptions & {
30
+ runCommand?: CommandRunner;
31
+ }): Promise<SkillsCli | undefined>;
32
+ export interface InstalledSkill {
33
+ name: string;
34
+ path: string;
35
+ scope: 'global' | 'project';
36
+ }
37
+ export declare function parseInstalledSkills(output: string): InstalledSkill[];
38
+ export declare function parseGlobalMearlSkillPaths(output: string): string[];
39
+ export type MearlSkillOperation = 'install' | 'update' | 'uninstall';
40
+ export declare function runGlobalMearlSkillOperation(operation: MearlSkillOperation, options?: ResolveSkillsOptions & {
41
+ runCommand?: CommandRunner;
42
+ yes?: boolean;
43
+ writeLine?: (line: string) => void;
44
+ }): Promise<boolean>;
45
+ export {};
package/dist/skills.js ADDED
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import { SETUP_VERSION } from './setupVersion.js';
3
+ import { homedir } from 'node:os';
4
+ import { NodeCapabilityRegistry } from '@mearl/daemon-core/capabilities';
5
+ import { inspectGlobalSkill, readTrackedSkillSources } from './skillSources.js';
6
+ import { PresetInstallationStore } from './presetStore.js';
7
+ import { findExecutable, isExecutable, prependCommandPath, prependNodePath, runCommand, } from '@mearl/daemon-core';
8
+ export const SKILLS_CLI_PACKAGE = 'skills@1.5.0';
9
+ export const MEARL_SKILL_SOURCE = SETUP_VERSION.includes('-beta.')
10
+ ? `https://github.com/F-loat/mearl/tree/v${SETUP_VERSION}`
11
+ : 'https://github.com/F-loat/mearl';
12
+ const COMMAND_TIMEOUT_MS = 120_000;
13
+ const ANSI_ESCAPE_PATTERN = new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, 'g');
14
+ export function stripAnsi(value) {
15
+ return value.replace(ANSI_ESCAPE_PATTERN, '');
16
+ }
17
+ export function resolveSkillsExecutable(options = {}) {
18
+ const candidate = (options.env ?? process.env).MEARL_SKILLS_PATH;
19
+ const exists = options.existsSync ?? (file => isExecutable(file, options.platform ?? process.platform));
20
+ return candidate && exists(candidate) ? candidate : null;
21
+ }
22
+ export function resolveSkillsInvocation(options = {}) {
23
+ const env = options.env ?? process.env;
24
+ const executable = resolveSkillsExecutable(options);
25
+ if (executable) {
26
+ return {
27
+ command: executable,
28
+ argsPrefix: [],
29
+ env: prependNodePath(prependCommandPath(executable, env)),
30
+ };
31
+ }
32
+ const processExecPath = options.processExecPath ?? process.execPath;
33
+ const npx = findExecutable('npx', {
34
+ env,
35
+ existsSync: options.existsSync,
36
+ platform: options.platform,
37
+ extraDirectories: [path.dirname(processExecPath)],
38
+ });
39
+ if (!npx)
40
+ return null;
41
+ return {
42
+ command: npx,
43
+ argsPrefix: ['-y', SKILLS_CLI_PACKAGE],
44
+ env: prependNodePath(prependCommandPath(npx, env), processExecPath),
45
+ };
46
+ }
47
+ /** Only the selected manager module is needed; browser and Provider factories stay in their hosts. */
48
+ export async function captureSkillsCli(options = {}) {
49
+ const env = options.env ?? process.env;
50
+ const runner = options.runCommand ?? runCommand;
51
+ const directory = env.MEARL_CAPABILITY_DIR || path.join(homedir(), '.mearl', 'capabilities');
52
+ const current = await new PresetInstallationStore({ directory }).current({ verifyResources: false });
53
+ const registry = new NodeCapabilityRegistry({ env: Object.freeze({ ...env }), runCommand: runner }, directory, { hooks: ['skills.manager'] });
54
+ await registry.refresh();
55
+ if (current.activation?.revision !== registry.revision || current.activation?.installationRevision !== registry.installationRevision)
56
+ throw new Error('SKILL_MANAGER_CHANGED');
57
+ const readSources = (scope, cwd) => readTrackedSkillSources(scope, cwd, { env });
58
+ const selection = current.rawHash ? { path: path.resolve(directory, 'active-node.json'), sha256: current.rawHash } : undefined;
59
+ const managedSkills = Object.fromEntries(Object.entries(current.preset?.skills ?? {}).filter(([, entry]) => entry.enabled !== false));
60
+ const backend = registry.get('skills.manager')?.();
61
+ if (backend !== undefined) {
62
+ if (!backend || typeof backend.id !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(backend.id) || typeof backend.cli !== 'function')
63
+ throw new Error('The selected Skill manager requires a preset update for CLI operations');
64
+ return { id: backend.id, managedSkills, selection, inspectGlobal: (source, name, version) => inspectGlobalSkill(source, name, version, { externalManager: true, normalizeSource: backend.normalizeSource, readSources }), run: async (args, settings) => {
65
+ const result = await backend.cli([...args], settings);
66
+ if (!result || !Number.isInteger(result.code) || typeof result.stdout !== 'string' || typeof result.stderr !== 'string')
67
+ throw new Error('Invalid Skill manager CLI result');
68
+ return result;
69
+ } };
70
+ }
71
+ const invocation = resolveSkillsInvocation(options);
72
+ if (!invocation)
73
+ return undefined;
74
+ return { id: 'skills', managedSkills, selection, inspectGlobal: (source, name, version) => inspectGlobalSkill(source, name, version, { readSources }), run: (args, settings) => runner(invocation.command, [...invocation.argsPrefix, ...args], { ...settings, env: invocation.env }) };
75
+ }
76
+ export function parseInstalledSkills(output) {
77
+ const value = JSON.parse(stripAnsi(output).trim());
78
+ if (!Array.isArray(value) || value.some(item => !item || typeof item.name !== 'string' || typeof item.path !== 'string' || !['global', 'project'].includes(item.scope)))
79
+ throw new Error('Skills returned an invalid skill list');
80
+ return value;
81
+ }
82
+ export function parseGlobalMearlSkillPaths(output) {
83
+ const parsed = parseInstalledSkills(output);
84
+ return [
85
+ ...new Set(parsed.flatMap((skill) => skill.name === 'mearl' &&
86
+ skill.scope === 'global' &&
87
+ typeof skill.path === 'string' &&
88
+ skill.path.trim()
89
+ ? [skill.path.trim()]
90
+ : [])),
91
+ ];
92
+ }
93
+ export async function runGlobalMearlSkillOperation(operation, options = {}) {
94
+ const runner = options.runCommand ?? runCommand;
95
+ const writeLine = options.writeLine ?? (line => console.error(line));
96
+ let cli;
97
+ try {
98
+ cli = await captureSkillsCli({ ...options, runCommand: runner });
99
+ }
100
+ catch (error) {
101
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}`);
102
+ return false;
103
+ }
104
+ if (!cli) {
105
+ writeLine('Skill: skills and npx are both unavailable.');
106
+ return false;
107
+ }
108
+ if (operation !== 'uninstall' && cli.managedSkills.mearl) {
109
+ const owned = cli.managedSkills.mearl;
110
+ let status;
111
+ try {
112
+ status = cli.inspectGlobal(owned.source, 'mearl', owned.version);
113
+ }
114
+ catch (error) {
115
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}`);
116
+ return false;
117
+ }
118
+ if (status.state !== 'ready') {
119
+ writeLine(`Skill: preset-managed Mearl requires preset apply/update (${status.reason}).`);
120
+ return false;
121
+ }
122
+ writeLine('Skill: retained the Mearl Skill managed by the selected preset.');
123
+ return true;
124
+ }
125
+ const run = async (args, capture) => {
126
+ try {
127
+ return await cli.run(args, {
128
+ capture,
129
+ timeoutMs: COMMAND_TIMEOUT_MS,
130
+ });
131
+ }
132
+ catch (error) {
133
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}`);
134
+ return null;
135
+ }
136
+ };
137
+ const listMearlPaths = async () => {
138
+ const listed = await run(['list', '--global', '--json'], true);
139
+ if (!listed)
140
+ return null;
141
+ if (listed.code !== 0) {
142
+ writeLine(`Skill: unable to list global skills (exit ${listed.code}).`);
143
+ const detail = stripAnsi(listed.stderr || listed.stdout).trim();
144
+ if (detail)
145
+ writeLine(detail);
146
+ return null;
147
+ }
148
+ try {
149
+ return parseGlobalMearlSkillPaths(listed.stdout);
150
+ }
151
+ catch (error) {
152
+ writeLine(`Skill: ${error instanceof Error ? error.message : String(error)}.`);
153
+ return null;
154
+ }
155
+ };
156
+ if (operation === 'install') {
157
+ const installed = await run([
158
+ 'add',
159
+ MEARL_SKILL_SOURCE,
160
+ '--skill',
161
+ 'mearl',
162
+ '--global',
163
+ ...(options.yes ? ['--yes'] : []),
164
+ ], false);
165
+ if (!installed)
166
+ return false;
167
+ if (installed.code !== 0) {
168
+ writeLine(`Skill: install failed (exit ${installed.code}).`);
169
+ return false;
170
+ }
171
+ const installedPaths = await listMearlPaths();
172
+ if (!installedPaths || installedPaths.length === 0) {
173
+ writeLine('Skill: installation finished without a tracked global mearl skill.');
174
+ return false;
175
+ }
176
+ writeLine('Skill: installed.');
177
+ return true;
178
+ }
179
+ const paths = await listMearlPaths();
180
+ if (!paths)
181
+ return false;
182
+ if (paths.length === 0) {
183
+ writeLine('Skill: no tracked global mearl skill found; skipped.');
184
+ return true;
185
+ }
186
+ writeLine('Skill installation:');
187
+ paths.forEach(skillPath => writeLine(` ${skillPath}`));
188
+ const args = operation === 'update'
189
+ ? ['add', MEARL_SKILL_SOURCE, '--skill', 'mearl', '--global', '--yes']
190
+ : ['remove', 'mearl', '--global', '--yes'];
191
+ const result = await run(args, true);
192
+ if (!result)
193
+ return false;
194
+ if (result.code !== 0) {
195
+ writeLine(`Skill: ${operation} failed (exit ${result.code}).`);
196
+ return false;
197
+ }
198
+ const detail = stripAnsi(`${result.stdout}
199
+ ${result.stderr}`).trim();
200
+ if (detail)
201
+ writeLine(detail);
202
+ if (/Failed to (?:update|remove)\s+[1-9]\d*\s+skills?/i.test(detail) || (operation === 'update' && !/\bInstalled [1-9]\d* skills?\b/.test(detail))) {
203
+ writeLine(`Skill: ${operation} failed.`);
204
+ return false;
205
+ }
206
+ const remainingPaths = await listMearlPaths();
207
+ if (!remainingPaths || (operation === 'uninstall' ? remainingPaths.length !== 0 : remainingPaths.length === 0)) {
208
+ writeLine(`Skill: ${operation} did not reach the expected installation state.`);
209
+ return false;
210
+ }
211
+ writeLine(`Skill: ${operation === 'update' ? 'updated' : 'uninstalled'}.`);
212
+ return true;
213
+ }
package/dist/version.d.ts CHANGED
@@ -20,3 +20,5 @@ export declare function mearlReleaseLineForVersion(version: string): string;
20
20
  export declare function isMearlReleaseVersionInLine(version: string, releaseLine: string): boolean;
21
21
  export declare function compareMearlReleaseLines(leftVersion: string, rightVersion: string): -1 | 0 | 1;
22
22
  export declare function compareMearlReleaseVersions(leftVersion: string, rightVersion: string): -1 | 0 | 1;
23
+ /** Explicit release lines win; beta setup keeps all runtime packages on its own release. */
24
+ export declare function mearlPackageSelector(setupVersion: string, targetLine?: string): string;
package/dist/version.js CHANGED
@@ -97,3 +97,9 @@ export function compareMearlReleaseVersions(leftVersion, rightVersion) {
97
97
  return 1;
98
98
  return 0;
99
99
  }
100
+ /** Explicit release lines win; beta setup keeps all runtime packages on its own release. */
101
+ export function mearlPackageSelector(setupVersion, targetLine) {
102
+ if (targetLine)
103
+ return targetLine;
104
+ return parseMearlReleaseVersion(setupVersion)?.channel === 'beta' ? setupVersion : 'latest';
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "2.21.0",
3
+ "version": "3.0.0-beta.0",
4
4
  "description": "One-shot environment setup and configuration for Mearl",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,10 +14,6 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "import": "./dist/index.js"
16
16
  },
17
- "./ali-skills": {
18
- "types": "./dist/aliSkills.d.ts",
19
- "import": "./dist/aliSkills.js"
20
- },
21
17
  "./package-manager": {
22
18
  "types": "./dist/packageManager.d.ts",
23
19
  "import": "./dist/packageManager.js"
@@ -37,6 +33,18 @@
37
33
  "./browser-extension": {
38
34
  "types": "./dist/browserExtension.d.ts",
39
35
  "import": "./dist/browserExtension.js"
36
+ },
37
+ "./skills": {
38
+ "types": "./dist/skills.d.ts",
39
+ "import": "./dist/skills.js"
40
+ },
41
+ "./skill-sources": {
42
+ "types": "./dist/skillSources.d.ts",
43
+ "import": "./dist/skillSources.js"
44
+ },
45
+ "./preset": {
46
+ "types": "./dist/preset.d.ts",
47
+ "import": "./dist/preset.js"
40
48
  }
41
49
  },
42
50
  "bin": {
@@ -54,7 +62,7 @@
54
62
  "update",
55
63
  "uninstall"
56
64
  ],
57
- "license": "ISC",
65
+ "license": "Apache-2.0",
58
66
  "engines": {
59
67
  "node": ">=22.12.0"
60
68
  },
@@ -65,14 +73,18 @@
65
73
  "commander": "^15.0.0",
66
74
  "extract-zip": "^2.0.1",
67
75
  "ws": "^8.18.3",
68
- "@mearl/client": "2.21.0",
69
- "@mearl/provider": "2.21.0",
70
- "@mearl/daemon-core": "2.21.0"
76
+ "semver": "^7.8.5",
77
+ "@mearl/client": "3.0.0-beta.0",
78
+ "@mearl/provider": "3.0.0-beta.0",
79
+ "@mearl/daemon-core": "3.0.0-beta.0",
80
+ "@mearl/preset": "3.0.0-beta.0",
81
+ "@mearl/browser-core": "3.0.0-beta.0"
71
82
  },
72
83
  "devDependencies": {
73
84
  "@types/node": "^24.9.1",
74
85
  "@types/ws": "^8.18.1",
75
- "typescript": "^5.8.3"
86
+ "typescript": "^5.8.3",
87
+ "@types/semver": "^7.7.1"
76
88
  },
77
89
  "scripts": {
78
90
  "build": "rm -rf dist/ && tsc && chmod +x dist/bin.js",
@@ -1,32 +0,0 @@
1
- import fs from 'node:fs';
2
- import { type CommandRunner } from '@mearl/daemon-core';
3
- export interface AliSkillsCliOutput {
4
- stdout: string;
5
- stderr: string;
6
- }
7
- export type AliSkillsCliRunner = (executable: string, args: string[], cwd?: string) => Promise<AliSkillsCliOutput>;
8
- export interface ResolveAliSkillsOptions {
9
- env?: NodeJS.ProcessEnv;
10
- existsSync?: (filePath: string) => boolean;
11
- homedir?: () => string;
12
- platform?: NodeJS.Platform;
13
- processExecPath?: string;
14
- readdirSync?: typeof fs.readdirSync;
15
- }
16
- export declare function stripAnsi(value: string): string;
17
- export declare function resolveAliSkillsExecutable(options?: ResolveAliSkillsOptions): string | null;
18
- export declare const runAliSkillsCli: AliSkillsCliRunner;
19
- interface AliSkillsInvocation {
20
- command: string;
21
- argsPrefix: string[];
22
- env: NodeJS.ProcessEnv;
23
- }
24
- export declare function resolveAliSkillsInvocation(options?: ResolveAliSkillsOptions): AliSkillsInvocation | null;
25
- export declare function parseGlobalMearlSkillPaths(output: string): string[];
26
- export type MearlSkillOperation = 'install' | 'update' | 'uninstall';
27
- export declare function runGlobalMearlSkillOperation(operation: MearlSkillOperation, options?: ResolveAliSkillsOptions & {
28
- runCommand?: CommandRunner;
29
- yes?: boolean;
30
- writeLine?: (line: string) => void;
31
- }): Promise<boolean>;
32
- export {};