@mearl/setup 3.0.0-beta.1 → 3.0.0-beta.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/dist/plugin.js +32 -8
- package/dist/skillSourceReceipts.d.ts +25 -0
- package/dist/skillSourceReceipts.js +94 -0
- package/dist/skillSources.d.ts +7 -22
- package/dist/skillSources.js +43 -96
- package/dist/skills.js +4 -2
- package/package.json +10 -6
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readlinkSync, realpathSync, rmdirSync, statSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { assertPluginName, findPluginDirectoryEntry, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
4
|
+
import { assertPluginName, findPluginDirectoryEntry, skillManifestPath, readOfficialPluginCatalog, findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, OFFICIAL_PLUGIN_MANAGER, registerPlugin, resolvePluginEntry, unregisterPlugin, } from '@mearl/client/plugin';
|
|
5
|
+
import { preserveSkillDisabled } from './skillSources.js';
|
|
5
6
|
import { claimPresetInstallation } from './presetInstallationLease.js';
|
|
6
7
|
import { ensurePluginRuntime } from './pluginRuntime.js';
|
|
7
8
|
import { captureSkillsCli, parseInstalledSkills, stripAnsi } from './skills.js';
|
|
@@ -52,8 +53,22 @@ export async function ensureOfficialPlugin(name, runner = runCommand) {
|
|
|
52
53
|
const definition = accepted?.plugins[name];
|
|
53
54
|
if (!definition)
|
|
54
55
|
throw new Error(`未知官方插件: ${name}`);
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const existing = findPluginRegistration(name);
|
|
57
|
+
if (existing) {
|
|
58
|
+
if (!accepted.selection || existing.managedBy !== OFFICIAL_PLUGIN_MANAGER)
|
|
59
|
+
return 'existing';
|
|
60
|
+
const cli = await captureSkillsCli({ runCommand: runner });
|
|
61
|
+
if (!cli || cli.selection?.path !== accepted.selection.path || cli.selection.sha256 !== accepted.selection.sha256)
|
|
62
|
+
throw new Error('PLUGIN_CATALOG_SELECTION_CHANGED');
|
|
63
|
+
const declaration = cli.managedSkills[definition.skill];
|
|
64
|
+
if (!declaration?.source || declaration.source !== definition.installArgs[0])
|
|
65
|
+
throw new Error('PLUGIN_CATALOG_SKILL_UNDECLARED');
|
|
66
|
+
const status = cli.inspectGlobal(declaration.source, definition.skill, declaration.version, { migrateFrom: declaration.migrateFrom });
|
|
67
|
+
if (status.state === 'ready')
|
|
68
|
+
return 'existing';
|
|
69
|
+
if (status.state === 'conflict')
|
|
70
|
+
throw new Error(`${status.reason}: ${definition.skill}`);
|
|
71
|
+
}
|
|
57
72
|
const lease = claimPresetInstallation();
|
|
58
73
|
try {
|
|
59
74
|
return await withOwnedCommands(lease.lifecycle, async () => {
|
|
@@ -276,13 +291,22 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
276
291
|
if (status.state !== 'ready' && (registered || status.state !== 'registration'))
|
|
277
292
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
278
293
|
};
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
const assertSource = () => {
|
|
295
|
+
if (!options.expectedSkill || !declaration)
|
|
296
|
+
return;
|
|
297
|
+
const status = cli.inspectGlobal(declaration.source, options.expectedSkill, declaration.version, {
|
|
298
|
+
migrateFrom: options.registrationManagedBy === OFFICIAL_PLUGIN_MANAGER ? declaration.migrateFrom : undefined,
|
|
299
|
+
});
|
|
281
300
|
if (status.state === 'conflict')
|
|
282
301
|
throw new Error(`${status.reason}: ${options.expectedSkill}`);
|
|
283
|
-
}
|
|
302
|
+
};
|
|
303
|
+
assertSource();
|
|
284
304
|
await options.confirm?.();
|
|
285
|
-
|
|
305
|
+
assertSource();
|
|
306
|
+
const runInstall = () => cli.run(['add', ...args, '--global'], { tee: true });
|
|
307
|
+
const installed = await (options.expectedSkill
|
|
308
|
+
? preserveSkillDisabled(join(homedir(), '.agents', 'skills', options.expectedSkill), runInstall)
|
|
309
|
+
: runInstall());
|
|
286
310
|
if (installed.code !== 0)
|
|
287
311
|
return installed.code;
|
|
288
312
|
await options.confirm?.();
|
|
@@ -307,7 +331,7 @@ export async function installPlugins(args, runner = runCommand, options = {}) {
|
|
|
307
331
|
}
|
|
308
332
|
const plugins = new Map();
|
|
309
333
|
for (const skillPath of paths) {
|
|
310
|
-
if (!statSync(join(skillPath, 'SKILL.md')).isFile())
|
|
334
|
+
if (!statSync(skillManifestPath(skillPath) ?? join(skillPath, 'SKILL.md')).isFile())
|
|
311
335
|
throw new Error(`安装结果缺少 SKILL.md: ${skillPath}`);
|
|
312
336
|
const entry = findPluginDirectoryEntry(skillPath);
|
|
313
337
|
if (!entry)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type SkillSourceScope = 'global' | 'project';
|
|
2
|
+
export interface TrackedSkillSource {
|
|
3
|
+
name: string;
|
|
4
|
+
source: string;
|
|
5
|
+
scope: SkillSourceScope;
|
|
6
|
+
ref?: string;
|
|
7
|
+
skillPath?: string;
|
|
8
|
+
sourceType?: string;
|
|
9
|
+
expectedHash?: string;
|
|
10
|
+
hashKind: 'git-tree' | 'folder';
|
|
11
|
+
}
|
|
12
|
+
export interface SourceOptions {
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
homeDirectory?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function readTrackedSkillSources(scope: SkillSourceScope, cwd?: string, options?: SourceOptions): TrackedSkillSource[];
|
|
17
|
+
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
18
|
+
export declare function isPublicSkillSource(source: TrackedSkillSource): boolean;
|
|
19
|
+
export declare function gitSource(source: string, ref?: string): {
|
|
20
|
+
repository: string;
|
|
21
|
+
ref?: string;
|
|
22
|
+
subdirectory?: string;
|
|
23
|
+
};
|
|
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;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
function lockFile(scope, cwd, options) {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
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');
|
|
8
|
+
}
|
|
9
|
+
export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
10
|
+
const file = lockFile(scope, cwd, options);
|
|
11
|
+
let text;
|
|
12
|
+
try {
|
|
13
|
+
if (statSync(file).size > 4 * 1024 * 1024)
|
|
14
|
+
throw new Error('Skill source lock exceeds 4 MiB');
|
|
15
|
+
text = readFileSync(file, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT')
|
|
19
|
+
return [];
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const lock = JSON.parse(text);
|
|
23
|
+
if (!lock || !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills))
|
|
24
|
+
throw new Error('Invalid Skills source lock');
|
|
25
|
+
return Object.entries(lock.skills).flatMap(([name, entry]) => {
|
|
26
|
+
if (!entry || typeof entry !== 'object' || entry.sourceType === 'node_modules')
|
|
27
|
+
return [];
|
|
28
|
+
if (!name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
29
|
+
throw new Error('Invalid tracked Skill name');
|
|
30
|
+
const source = typeof entry.sourceUrl === 'string' ? entry.sourceUrl : entry.source;
|
|
31
|
+
if (typeof source !== 'string' || !source.trim())
|
|
32
|
+
return [];
|
|
33
|
+
if (source.length > 4096 || /[\0\r\n]/.test(source))
|
|
34
|
+
throw new Error('Invalid tracked Skill source');
|
|
35
|
+
const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
|
|
36
|
+
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' }];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
40
|
+
export function isPublicSkillSource(source) {
|
|
41
|
+
return source.sourceType === undefined || ['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType);
|
|
42
|
+
}
|
|
43
|
+
export function gitSource(source, ref) {
|
|
44
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(source))
|
|
45
|
+
return { repository: `https://github.com/${source}.git`, ...(ref ? { ref } : {}) };
|
|
46
|
+
if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
|
|
47
|
+
return { repository: source, ...(ref ? { ref } : {}) };
|
|
48
|
+
const url = new URL(source);
|
|
49
|
+
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password || (url.username && url.protocol !== 'ssh:') || url.search || url.hash)
|
|
50
|
+
throw new Error('Skill source is not a supported Git repository');
|
|
51
|
+
if (url.hostname === 'github.com' && !url.port) {
|
|
52
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
53
|
+
if (parts.length >= 2)
|
|
54
|
+
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$/, '') } : {}) };
|
|
55
|
+
}
|
|
56
|
+
return { repository: url.href, ...(ref ? { ref } : {}) };
|
|
57
|
+
}
|
|
58
|
+
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
59
|
+
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
|
|
60
|
+
const location = (source, ref, sourceType) => {
|
|
61
|
+
const value = normalize(source, sourceType);
|
|
62
|
+
if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
|
|
63
|
+
throw new Error('Invalid normalized Skill source');
|
|
64
|
+
const hash = value.indexOf('#');
|
|
65
|
+
const fragment = hash < 0 ? undefined : value.slice(hash + 1);
|
|
66
|
+
const plain = hash < 0 ? value : value.slice(0, hash);
|
|
67
|
+
const parsed = isAbsolute(plain) ? { repository: plain, ref: ref ?? fragment } : gitSource(plain, ref ?? fragment);
|
|
68
|
+
let repository = parsed.repository;
|
|
69
|
+
if (repository.startsWith('file:'))
|
|
70
|
+
repository = fileURLToPath(repository);
|
|
71
|
+
if (isAbsolute(repository))
|
|
72
|
+
repository = resolve(repository);
|
|
73
|
+
else {
|
|
74
|
+
const ssh = repository.match(/^[\w.-]+@([\w.-]+):(.+)$/);
|
|
75
|
+
const url = new URL(ssh ? `ssh://${ssh[1]}/${ssh[2]}` : repository);
|
|
76
|
+
// Authentication transport is independent of the repository's identity.
|
|
77
|
+
repository = `${url.hostname.toLowerCase()}${url.port ? ':' + url.port : ''}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`;
|
|
78
|
+
}
|
|
79
|
+
return { ...parsed, repository, ref: parsed.ref || undefined };
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const expected = location(requested);
|
|
83
|
+
const actual = location(tracked.source, tracked.ref, tracked.sourceType);
|
|
84
|
+
if (expected.repository !== actual.repository || expected.ref !== actual.ref)
|
|
85
|
+
return false;
|
|
86
|
+
if (!expected.subdirectory)
|
|
87
|
+
return true;
|
|
88
|
+
const path = (tracked.skillPath ?? actual.subdirectory ?? '').replace(/(?:^|\/)SKILL\.md$/, '').replace(/\/+$/, '');
|
|
89
|
+
return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return normalize(requested) === normalize(tracked.source, tracked.sourceType) && !tracked.ref;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/skillSources.d.ts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import { type CommandRunner } from '@mearl/daemon-core';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
source: string;
|
|
6
|
-
scope: SkillSourceScope;
|
|
7
|
-
ref?: string;
|
|
8
|
-
skillPath?: string;
|
|
9
|
-
sourceType?: string;
|
|
10
|
-
expectedHash?: string;
|
|
11
|
-
hashKind: 'git-tree' | 'folder';
|
|
12
|
-
}
|
|
13
|
-
interface SourceOptions {
|
|
14
|
-
env?: NodeJS.ProcessEnv;
|
|
15
|
-
homeDirectory?: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function readTrackedSkillSources(scope: SkillSourceScope, cwd?: string, options?: SourceOptions): TrackedSkillSource[];
|
|
18
|
-
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
19
|
-
export declare function isPublicSkillSource(source: TrackedSkillSource): boolean;
|
|
20
|
-
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
21
|
-
export declare function matchesTrackedSkillSource(requested: string, tracked: TrackedSkillSource, normalize?: (source: string, sourceType?: string) => string): boolean;
|
|
2
|
+
import { readTrackedSkillSources, type SourceOptions, type SkillSourceScope, type TrackedSkillSource } from './skillSourceReceipts.js';
|
|
3
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
4
|
+
export type { SkillSourceScope, TrackedSkillSource } from './skillSourceReceipts.js';
|
|
22
5
|
/** Matches the public Skills folder hash without writing to the installed Skill. */
|
|
23
6
|
export declare function hashSkillDirectory(directory: string, ignoreRuntimeFiles?: boolean): Promise<string>;
|
|
24
7
|
/** Checks source content in a temporary checkout; never invokes the mutating Skills `check` alias. */
|
|
@@ -31,16 +14,18 @@ export declare function checkTrackedSkillUpdates(options?: SourceOptions & {
|
|
|
31
14
|
scope: SkillSourceScope;
|
|
32
15
|
}>>;
|
|
33
16
|
export interface SkillInstallationStatus {
|
|
34
|
-
state: 'ready' | 'missing' | 'version' | 'registration' | 'conflict';
|
|
17
|
+
state: 'ready' | 'missing' | 'version' | 'registration' | 'migration' | 'conflict';
|
|
35
18
|
reason?: string;
|
|
36
19
|
}
|
|
37
20
|
/** Developer links keep their Git checkout as the source receipt without changing a manager's lock. */
|
|
38
21
|
export declare function readLinkedSkillSource(directory: string, name: string): TrackedSkillSource | undefined;
|
|
22
|
+
/** Keep the user's disabled state when an installer replaces the Skill directory. */
|
|
23
|
+
export declare function preserveSkillDisabled<T>(directory: string, operation: () => Promise<T>): Promise<T>;
|
|
39
24
|
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
40
25
|
export declare function inspectGlobalSkill(source: string, name: string, version?: string, options?: {
|
|
41
26
|
homeDirectory?: string;
|
|
42
27
|
readSources?: typeof readTrackedSkillSources;
|
|
43
28
|
externalManager?: boolean;
|
|
29
|
+
migrateFrom?: readonly string[];
|
|
44
30
|
normalizeSource?: (source: string, sourceType?: string) => string;
|
|
45
31
|
}): SkillInstallationStatus;
|
|
46
|
-
export {};
|
package/dist/skillSources.js
CHANGED
|
@@ -1,104 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdtemp, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { lstatSync, realpathSync, renameSync } from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { homedir, tmpdir } from 'node:os';
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
7
|
import { runCommand } from '@mearl/daemon-core';
|
|
9
8
|
import { satisfies, valid } from 'semver';
|
|
10
|
-
import { DISABLED_SKILL_MANIFEST, 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';
|
|
11
10
|
import { existsSync } from 'node:fs';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
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');
|
|
15
|
-
}
|
|
16
|
-
export function readTrackedSkillSources(scope, cwd, options = {}) {
|
|
17
|
-
const file = lockFile(scope, cwd, options);
|
|
18
|
-
let text;
|
|
19
|
-
try {
|
|
20
|
-
if (statSync(file).size > 4 * 1024 * 1024)
|
|
21
|
-
throw new Error('Skill source lock exceeds 4 MiB');
|
|
22
|
-
text = readFileSync(file, 'utf8');
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error.code === 'ENOENT')
|
|
26
|
-
return [];
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
const lock = JSON.parse(text);
|
|
30
|
-
if (!lock || !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills))
|
|
31
|
-
throw new Error('Invalid Skills source lock');
|
|
32
|
-
return Object.entries(lock.skills).flatMap(([name, entry]) => {
|
|
33
|
-
if (!entry || typeof entry !== 'object' || entry.sourceType === 'node_modules')
|
|
34
|
-
return [];
|
|
35
|
-
if (!name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
36
|
-
throw new Error('Invalid tracked Skill name');
|
|
37
|
-
const source = typeof entry.sourceUrl === 'string' ? entry.sourceUrl : entry.source;
|
|
38
|
-
if (typeof source !== 'string' || !source.trim())
|
|
39
|
-
return [];
|
|
40
|
-
if (source.length > 4096 || /[\0\r\n]/.test(source))
|
|
41
|
-
throw new Error('Invalid tracked Skill source');
|
|
42
|
-
const expectedHash = scope === 'global' ? entry.skillFolderHash : entry.computedHash;
|
|
43
|
-
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' }];
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
/** Custom source kinds require their installed backend, even if they use owner/repo syntax. */
|
|
47
|
-
export function isPublicSkillSource(source) {
|
|
48
|
-
return source.sourceType === undefined || ['github', 'gitlab', 'git', 'local', 'well-known'].includes(source.sourceType);
|
|
49
|
-
}
|
|
50
|
-
function gitSource(source, ref) {
|
|
51
|
-
if (/^[\w.-]+\/[\w.-]+$/.test(source))
|
|
52
|
-
return { repository: `https://github.com/${source}.git`, ...(ref ? { ref } : {}) };
|
|
53
|
-
if (/^[\w.-]+@[\w.-]+:[^\s]+$/.test(source))
|
|
54
|
-
return { repository: source, ...(ref ? { ref } : {}) };
|
|
55
|
-
const url = new URL(source);
|
|
56
|
-
if (!['https:', 'http:', 'ssh:', 'git:', 'file:'].includes(url.protocol) || url.password || (url.username && url.protocol !== 'ssh:') || url.search || url.hash)
|
|
57
|
-
throw new Error('Skill source is not a supported Git repository');
|
|
58
|
-
if (url.hostname === 'github.com' && !url.port) {
|
|
59
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
60
|
-
if (parts.length >= 2)
|
|
61
|
-
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$/, '') } : {}) };
|
|
62
|
-
}
|
|
63
|
-
return { repository: url.href, ...(ref ? { ref } : {}) };
|
|
64
|
-
}
|
|
65
|
-
/** Compare the manager's existing source receipt; no network access or second lockfile. */
|
|
66
|
-
export function matchesTrackedSkillSource(requested, tracked, normalize = source => source) {
|
|
67
|
-
const location = (source, ref, sourceType) => {
|
|
68
|
-
const value = normalize(source, sourceType);
|
|
69
|
-
if (typeof value !== 'string' || !value || value.length > 8192 || /[\0\r\n]/.test(value))
|
|
70
|
-
throw new Error('Invalid normalized Skill source');
|
|
71
|
-
const hash = value.indexOf('#');
|
|
72
|
-
const fragment = hash < 0 ? undefined : value.slice(hash + 1);
|
|
73
|
-
const plain = hash < 0 ? value : value.slice(0, hash);
|
|
74
|
-
const parsed = isAbsolute(plain) ? { repository: plain, ref: ref ?? fragment } : gitSource(plain, ref ?? fragment);
|
|
75
|
-
let repository = parsed.repository;
|
|
76
|
-
if (repository.startsWith('file:'))
|
|
77
|
-
repository = fileURLToPath(repository);
|
|
78
|
-
if (isAbsolute(repository))
|
|
79
|
-
repository = resolve(repository);
|
|
80
|
-
else {
|
|
81
|
-
const ssh = repository.match(/^[\w.-]+@([\w.-]+):(.+)$/);
|
|
82
|
-
const url = new URL(ssh ? `ssh://${ssh[1]}/${ssh[2]}` : repository);
|
|
83
|
-
// Authentication transport is independent of the repository's identity.
|
|
84
|
-
repository = `${url.hostname.toLowerCase()}${url.port ? ':' + url.port : ''}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`;
|
|
85
|
-
}
|
|
86
|
-
return { ...parsed, repository, ref: parsed.ref || undefined };
|
|
87
|
-
};
|
|
88
|
-
try {
|
|
89
|
-
const expected = location(requested);
|
|
90
|
-
const actual = location(tracked.source, tracked.ref, tracked.sourceType);
|
|
91
|
-
if (expected.repository !== actual.repository || expected.ref !== actual.ref)
|
|
92
|
-
return false;
|
|
93
|
-
if (!expected.subdirectory)
|
|
94
|
-
return true;
|
|
95
|
-
const path = (tracked.skillPath ?? actual.subdirectory ?? '').replace(/(?:^|\/)SKILL\.md$/, '').replace(/\/+$/, '');
|
|
96
|
-
return path === expected.subdirectory || path.startsWith(expected.subdirectory + '/');
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return normalize(requested) === normalize(tracked.source, tracked.sourceType) && !tracked.ref;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
11
|
+
import { gitSource, isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
12
|
+
export { isPublicSkillSource, matchesTrackedSkillSource, readTrackedSkillSources } from './skillSourceReceipts.js';
|
|
102
13
|
function skillName(text) {
|
|
103
14
|
return text.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1].match(/^name:\s*["']?([^"'\r\n]+?)["']?\s*$/m)?.[1]?.trim();
|
|
104
15
|
}
|
|
@@ -237,6 +148,35 @@ export function readLinkedSkillSource(directory, name) {
|
|
|
237
148
|
return undefined;
|
|
238
149
|
}
|
|
239
150
|
}
|
|
151
|
+
/** Keep the user's disabled state when an installer replaces the Skill directory. */
|
|
152
|
+
export async function preserveSkillDisabled(directory, operation) {
|
|
153
|
+
const disabled = getSkillEnabled(directory) === false;
|
|
154
|
+
try {
|
|
155
|
+
return await operation();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (disabled && existsSync(join(directory, 'SKILL.md')))
|
|
159
|
+
renameSync(join(directory, 'SKILL.md'), join(directory, DISABLED_SKILL_MANIFEST));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function canonicalPath(path) {
|
|
163
|
+
const absolute = resolve(path);
|
|
164
|
+
try {
|
|
165
|
+
return realpathSync(absolute);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
const parent = dirname(absolute);
|
|
169
|
+
return parent === absolute ? absolute : join(canonicalPath(parent), basename(absolute));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function hasOnlySetupRegistrations(directory) {
|
|
173
|
+
const contains = (root, entry) => {
|
|
174
|
+
const path = relative(root, entry);
|
|
175
|
+
return path !== '' && path !== '..' && !path.startsWith('..' + sep) && !isAbsolute(path);
|
|
176
|
+
};
|
|
177
|
+
const registrations = listPluginNames().flatMap(name => findPluginRegistration(name) ?? []).filter(registration => contains(resolve(directory), resolve(registration.entry)) || contains(canonicalPath(directory), canonicalPath(registration.entry)));
|
|
178
|
+
return registrations.length > 0 && registrations.every(registration => registration.managedBy === OFFICIAL_PLUGIN_MANAGER);
|
|
179
|
+
}
|
|
240
180
|
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
241
181
|
export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
242
182
|
if (!source.trim() || !name || name === '.' || name === '..' || /[\\/\0\r\n]/.test(name))
|
|
@@ -244,10 +184,17 @@ export function inspectGlobalSkill(source, name, version, options = {}) {
|
|
|
244
184
|
const directory = join(options.homeDirectory ?? homedir(), '.agents', 'skills', name);
|
|
245
185
|
if (existsSync(join(directory, 'SKILL.md')) && existsSync(join(directory, DISABLED_SKILL_MANIFEST)))
|
|
246
186
|
return { state: 'conflict', reason: 'SKILL_MANIFEST_CONFLICT' };
|
|
247
|
-
const
|
|
187
|
+
const receipt = (options.readSources ?? readTrackedSkillSources)('global', undefined, { homeDirectory: options.homeDirectory }).find(entry => entry.name === name);
|
|
188
|
+
const tracked = receipt
|
|
248
189
|
?? (!/(?:#|\/(?:tree|blob)\/)/.test(source) ? readLinkedSkillSource(directory, name) : undefined);
|
|
249
|
-
if (tracked && ((!options.externalManager && !isPublicSkillSource(tracked)) || !matchesTrackedSkillSource(source, tracked, options.normalizeSource)))
|
|
190
|
+
if (tracked && ((!options.externalManager && !isPublicSkillSource(tracked)) || !matchesTrackedSkillSource(source, tracked, options.normalizeSource))) {
|
|
191
|
+
const metadata = readInstalledSkillMetadata(directory);
|
|
192
|
+
if (receipt && (options.externalManager || isPublicSkillSource(receipt)) && metadata?.name === name &&
|
|
193
|
+
options.migrateFrom?.some(previous => matchesTrackedSkillSource(previous, receipt, options.normalizeSource)) && hasOnlySetupRegistrations(directory)) {
|
|
194
|
+
return { state: 'migration', reason: 'SKILL_SOURCE_MIGRATION_REQUIRED' };
|
|
195
|
+
}
|
|
250
196
|
return { state: 'conflict', reason: 'SKILL_SOURCE_CONFLICT' };
|
|
197
|
+
}
|
|
251
198
|
const metadata = readInstalledSkillMetadata(directory);
|
|
252
199
|
if (!metadata)
|
|
253
200
|
return { state: 'missing', reason: 'SKILL_NOT_INSTALLED' };
|
package/dist/skills.js
CHANGED
|
@@ -59,9 +59,11 @@ export async function captureSkillsCli(options = {}) {
|
|
|
59
59
|
const managedSkills = Object.fromEntries(Object.entries(current.preset?.skills ?? {}).filter(([, entry]) => entry.enabled !== false));
|
|
60
60
|
const backend = registry.get('skills.manager')?.();
|
|
61
61
|
if (backend !== undefined) {
|
|
62
|
+
if (backend?.readSources !== undefined && typeof backend.readSources !== 'function')
|
|
63
|
+
throw new Error('Invalid Skill source reader');
|
|
62
64
|
if (!backend || typeof backend.id !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(backend.id) || typeof backend.cli !== 'function')
|
|
63
65
|
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) => {
|
|
66
|
+
return { id: backend.id, managedSkills, selection, inspectGlobal: (source, name, version, inspection) => inspectGlobalSkill(source, name, version, { migrateFrom: inspection?.migrateFrom, externalManager: true, normalizeSource: backend.normalizeSource, readSources: backend.readSources ?? readSources }), run: async (args, settings) => {
|
|
65
67
|
const result = await backend.cli([...args], settings);
|
|
66
68
|
if (!result || !Number.isInteger(result.code) || typeof result.stdout !== 'string' || typeof result.stderr !== 'string')
|
|
67
69
|
throw new Error('Invalid Skill manager CLI result');
|
|
@@ -71,7 +73,7 @@ export async function captureSkillsCli(options = {}) {
|
|
|
71
73
|
const invocation = resolveSkillsInvocation(options);
|
|
72
74
|
if (!invocation)
|
|
73
75
|
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 }) };
|
|
76
|
+
return { id: 'skills', managedSkills, selection, inspectGlobal: (source, name, version, inspection) => inspectGlobalSkill(source, name, version, { migrateFrom: inspection?.migrateFrom, readSources }), run: (args, settings) => runner(invocation.command, [...invocation.argsPrefix, ...args], { ...settings, env: invocation.env }) };
|
|
75
77
|
}
|
|
76
78
|
export function parseInstalledSkills(output) {
|
|
77
79
|
const value = JSON.parse(stripAnsi(output).trim());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/setup",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.2",
|
|
4
4
|
"description": "One-shot environment setup and configuration for Mearl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"./preset": {
|
|
46
46
|
"types": "./dist/preset.d.ts",
|
|
47
47
|
"import": "./dist/preset.js"
|
|
48
|
+
},
|
|
49
|
+
"./skill-source-receipts": {
|
|
50
|
+
"types": "./dist/skillSourceReceipts.d.ts",
|
|
51
|
+
"import": "./dist/skillSourceReceipts.js"
|
|
48
52
|
}
|
|
49
53
|
},
|
|
50
54
|
"bin": {
|
|
@@ -74,11 +78,11 @@
|
|
|
74
78
|
"extract-zip": "^2.0.1",
|
|
75
79
|
"ws": "^8.18.3",
|
|
76
80
|
"semver": "^7.8.5",
|
|
77
|
-
"@mearl/
|
|
78
|
-
"@mearl/
|
|
79
|
-
"@mearl/
|
|
80
|
-
"@mearl/
|
|
81
|
-
"@mearl/
|
|
81
|
+
"@mearl/client": "3.0.0-beta.2",
|
|
82
|
+
"@mearl/provider": "3.0.0-beta.2",
|
|
83
|
+
"@mearl/daemon-core": "3.0.0-beta.2",
|
|
84
|
+
"@mearl/browser-core": "3.0.0-beta.2",
|
|
85
|
+
"@mearl/preset": "3.0.0-beta.2"
|
|
82
86
|
},
|
|
83
87
|
"devDependencies": {
|
|
84
88
|
"@types/node": "^24.9.1",
|