@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.
- package/LICENSE +201 -0
- package/README.md +59 -59
- package/dist/browserExtension.d.ts +2 -2
- package/dist/browserExtension.js +25 -19
- package/dist/cli.d.ts +8 -3
- package/dist/cli.js +74 -17
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/lifecycle.d.ts +11 -4
- package/dist/lifecycle.js +48 -15
- package/dist/managedPlugins.d.ts +21 -0
- package/dist/managedPlugins.js +76 -0
- package/dist/npmRegistry.d.ts +1 -1
- package/dist/npmRegistry.js +4 -16
- package/dist/plugin.d.ts +13 -3
- package/dist/plugin.js +106 -44
- package/dist/preset.d.ts +94 -0
- package/dist/preset.js +275 -0
- package/dist/presetBrowserTransfer.d.ts +11 -0
- package/dist/presetBrowserTransfer.js +72 -0
- package/dist/presetInstallationLease.d.ts +6 -0
- package/dist/presetInstallationLease.js +66 -0
- package/dist/presetStore.d.ts +48 -0
- package/dist/presetStore.js +252 -0
- package/dist/presetUpgrade.d.ts +13 -0
- package/dist/presetUpgrade.js +42 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +24 -2
- package/dist/runtimeCleanup.js +1 -8
- package/dist/setupVersion.d.ts +2 -0
- package/dist/setupVersion.js +4 -0
- package/dist/skillSources.d.ts +43 -0
- package/dist/skillSources.js +247 -0
- package/dist/skills.d.ts +45 -0
- package/dist/skills.js +213 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +6 -0
- package/package.json +22 -10
- package/dist/aliSkills.d.ts +0 -32
- package/dist/aliSkills.js +0 -212
- package/dist/internalPlugins.d.ts +0 -26
- package/dist/internalPlugins.js +0 -197
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PresetTransferRequest } from '@mearl/preset/bundles';
|
|
2
|
+
import { PresetInstallationStore } from './presetStore.js';
|
|
3
|
+
/** One bounded, expiring transfer belongs to the host's attached extension connection. */
|
|
4
|
+
export declare class PresetBrowserTransfer {
|
|
5
|
+
private readonly store;
|
|
6
|
+
private readonly now;
|
|
7
|
+
private session?;
|
|
8
|
+
constructor(store?: PresetInstallationStore, now?: () => number);
|
|
9
|
+
selectedRevision(acceptedNodeRevision: string | null | undefined): Promise<string | null>;
|
|
10
|
+
handle(input: PresetTransferRequest): Promise<unknown>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { BundleError, MAX_PRESET_TRANSFER_HEADER_BYTES, PRESET_TRANSFER_CHUNK_BYTES } from '@mearl/preset/bundles';
|
|
3
|
+
import { PresetInstallationStore } from './presetStore.js';
|
|
4
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
5
|
+
/** One bounded, expiring transfer belongs to the host's attached extension connection. */
|
|
6
|
+
export class PresetBrowserTransfer {
|
|
7
|
+
store;
|
|
8
|
+
now;
|
|
9
|
+
session;
|
|
10
|
+
constructor(store = new PresetInstallationStore(), now = Date.now) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
this.now = now;
|
|
13
|
+
}
|
|
14
|
+
async selectedRevision(acceptedNodeRevision) {
|
|
15
|
+
const current = await this.store.current({ verifyResources: false });
|
|
16
|
+
return current.receipt?.browserSyncRequired && current.receipt.revision === acceptedNodeRevision ? current.receipt.revision : null;
|
|
17
|
+
}
|
|
18
|
+
async handle(input) {
|
|
19
|
+
if (!input || typeof input !== 'object')
|
|
20
|
+
throw new BundleError('BUNDLE_INVALID', 'Expected a browser transfer operation');
|
|
21
|
+
if (this.session && this.now() - this.session.touched > 120000)
|
|
22
|
+
this.session = undefined;
|
|
23
|
+
if (input.operation === 'open') {
|
|
24
|
+
if (this.session)
|
|
25
|
+
throw new BundleError('BUNDLE_CONFLICT', 'A browser preset transfer is already running');
|
|
26
|
+
if (typeof input.installationRevision !== 'string' || !/^[a-f0-9]{64}$/.test(input.installationRevision))
|
|
27
|
+
throw new BundleError('BUNDLE_INVALID', 'Expected an installation revision');
|
|
28
|
+
const current = await this.store.current();
|
|
29
|
+
if (!current.receipt || !current.prepared || current.receipt.revision !== input.installationRevision)
|
|
30
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed before browser transfer');
|
|
31
|
+
const header = {
|
|
32
|
+
schemaVersion: 1, installationRevision: current.receipt.revision, snapshot: current.prepared.browser.snapshot,
|
|
33
|
+
bundles: current.prepared.browser.bundles.map(({ source, revision, manifest }) => ({ source, revision, manifest })),
|
|
34
|
+
overrides: current.receipt.overrides,
|
|
35
|
+
};
|
|
36
|
+
const bytes = Buffer.from(JSON.stringify(header));
|
|
37
|
+
if (bytes.byteLength > MAX_PRESET_TRANSFER_HEADER_BYTES)
|
|
38
|
+
throw new BundleError('BUNDLE_QUOTA', 'Browser transfer metadata exceeds 16 MiB');
|
|
39
|
+
const sha256 = digest(bytes);
|
|
40
|
+
const resources = new Map([[sha256, bytes]]);
|
|
41
|
+
for (const bundle of current.prepared.browser.bundles)
|
|
42
|
+
for (const [name, entry] of Object.entries(bundle.manifest.resources))
|
|
43
|
+
resources.set(entry.sha256, bundle.resources[name]);
|
|
44
|
+
// A second open may finish validation while the first one is already streaming.
|
|
45
|
+
if (this.session)
|
|
46
|
+
throw new BundleError('BUNDLE_CONFLICT', 'A browser preset transfer is already running');
|
|
47
|
+
this.session = { id: randomUUID(), revision: current.receipt.revision, touched: this.now(), resources };
|
|
48
|
+
return { session: this.session.id, sha256, byteLength: bytes.byteLength };
|
|
49
|
+
}
|
|
50
|
+
const session = this.session;
|
|
51
|
+
if (!session || input.session !== session.id)
|
|
52
|
+
throw new BundleError('BUNDLE_CONFLICT', 'Browser preset transfer expired or changed');
|
|
53
|
+
session.touched = this.now();
|
|
54
|
+
if (input.operation === 'release') {
|
|
55
|
+
this.session = undefined;
|
|
56
|
+
return { released: true };
|
|
57
|
+
}
|
|
58
|
+
if (input.operation === 'confirm') {
|
|
59
|
+
const current = await this.store.current({ verifyResources: false });
|
|
60
|
+
if (current.receipt?.revision !== session.revision)
|
|
61
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The selected preset changed during browser transfer');
|
|
62
|
+
return { installationRevision: session.revision };
|
|
63
|
+
}
|
|
64
|
+
if (input.operation === 'read') {
|
|
65
|
+
const bytes = session.resources.get(input.sha256);
|
|
66
|
+
if (!bytes || !Number.isSafeInteger(input.offset) || input.offset < 0 || input.offset >= bytes.byteLength || input.offset % PRESET_TRANSFER_CHUNK_BYTES !== 0)
|
|
67
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid browser transfer resource or offset');
|
|
68
|
+
return { data: Buffer.from(bytes.subarray(input.offset, input.offset + PRESET_TRANSFER_CHUNK_BYTES)).toString('base64') };
|
|
69
|
+
}
|
|
70
|
+
throw new BundleError('BUNDLE_INVALID', 'Unsupported browser preset transfer operation');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CommandLifecycle } from '@mearl/daemon-core';
|
|
2
|
+
/** The owner covers the shared global Skills directory, including hosts with different presets. */
|
|
3
|
+
export declare function claimPresetInstallation(file?: string): {
|
|
4
|
+
release(): void;
|
|
5
|
+
lifecycle: CommandLifecycle;
|
|
6
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { isCommandProcessAlive, isProcessOwnerAlive, processIdentity, readRecord, withRecordLock } from '@mearl/daemon-core';
|
|
6
|
+
/** The owner covers the shared global Skills directory, including hosts with different presets. */
|
|
7
|
+
export function claimPresetInstallation(file = join(homedir(), '.agents', '.mearl-preset-install.json')) {
|
|
8
|
+
const owner = { pid: process.pid, token: randomUUID(), processIdentity: processIdentity(process.pid), active: true, children: [] };
|
|
9
|
+
const read = () => {
|
|
10
|
+
try {
|
|
11
|
+
return statSync(file).size <= 65536 ? readRecord(file) : null;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT')
|
|
15
|
+
return null;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const write = (value) => {
|
|
20
|
+
const temporary = `${file}.${owner.token}.tmp`;
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
|
|
23
|
+
renameSync(temporary, file);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
rmSync(temporary, { force: true });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const owned = (value) => value?.token === owner.token && value.pid === owner.pid && value.processIdentity === owner.processIdentity;
|
|
30
|
+
withRecordLock(file, () => {
|
|
31
|
+
const previous = read();
|
|
32
|
+
if (previous && ((previous.active !== false && isProcessOwnerAlive(previous)) || previous.children?.some(isCommandProcessAlive)))
|
|
33
|
+
throw new Error('PRESET_INSTALL_BUSY');
|
|
34
|
+
write(owner);
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
lifecycle: {
|
|
38
|
+
started(child) {
|
|
39
|
+
withRecordLock(file, () => {
|
|
40
|
+
const current = read();
|
|
41
|
+
if (!owned(current) || !current.active || current.children.length >= 256)
|
|
42
|
+
throw new Error('PRESET_INSTALL_OWNER_CHANGED');
|
|
43
|
+
write({ ...current, children: [...current.children, child] });
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
finished(child) {
|
|
47
|
+
withRecordLock(file, () => {
|
|
48
|
+
const current = read();
|
|
49
|
+
if (!owned(current))
|
|
50
|
+
return;
|
|
51
|
+
write({ ...current, children: current.children.filter(entry => entry.pid !== child.pid || entry.processIdentity !== child.processIdentity) });
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
release: () => withRecordLock(file, () => {
|
|
56
|
+
const current = read();
|
|
57
|
+
if (!owned(current))
|
|
58
|
+
return;
|
|
59
|
+
const children = current.children.filter(isCommandProcessAlive);
|
|
60
|
+
if (children.length)
|
|
61
|
+
write({ ...current, active: false, children });
|
|
62
|
+
else
|
|
63
|
+
rmSync(file, { force: true });
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type JsonObject, type NodeCapabilityActivation, type ResolvedPreset } from '@mearl/preset';
|
|
2
|
+
import { type PreparedBundle, type PreparedPreset, type PresetSnapshot } from '@mearl/preset/bundles';
|
|
3
|
+
export interface PresetInstallReceipt {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
snapshot: PresetSnapshot;
|
|
6
|
+
bundles: Array<Omit<PreparedBundle, 'resources'>>;
|
|
7
|
+
browserRevision: string;
|
|
8
|
+
nodeRevision: string;
|
|
9
|
+
managedModules: string[];
|
|
10
|
+
nodeSyncRequired: boolean;
|
|
11
|
+
browserSyncRequired?: boolean;
|
|
12
|
+
nodeConfig: Record<string, JsonObject>;
|
|
13
|
+
overrides: JsonObject;
|
|
14
|
+
revision: string;
|
|
15
|
+
}
|
|
16
|
+
interface SelectionState {
|
|
17
|
+
rawHash: string | null;
|
|
18
|
+
activation?: NodeCapabilityActivation;
|
|
19
|
+
receipt?: PresetInstallReceipt;
|
|
20
|
+
prepared?: PreparedPreset;
|
|
21
|
+
preset?: ResolvedPreset;
|
|
22
|
+
}
|
|
23
|
+
/** One pointer binds the desired preset, user overrides and exact Node selection. */
|
|
24
|
+
export declare class PresetInstallationStore {
|
|
25
|
+
readonly directory: string;
|
|
26
|
+
readonly mearlApiVersion: string;
|
|
27
|
+
private readonly pointer;
|
|
28
|
+
constructor(options?: {
|
|
29
|
+
directory?: string;
|
|
30
|
+
mearlApiVersion?: string;
|
|
31
|
+
});
|
|
32
|
+
private assertCurrent;
|
|
33
|
+
current(options?: {
|
|
34
|
+
verifyResources?: boolean;
|
|
35
|
+
}): Promise<SelectionState>;
|
|
36
|
+
apply(input: Pick<PreparedPreset, 'snapshot' | 'bundles'>, options: {
|
|
37
|
+
expectedRevision: string | null;
|
|
38
|
+
overrides?: JsonObject;
|
|
39
|
+
nodeConfig?: Record<string, JsonObject>;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
confirm?: () => Promise<void>;
|
|
42
|
+
}): Promise<PresetInstallReceipt>;
|
|
43
|
+
rollback(expectedRevision: string): Promise<void>;
|
|
44
|
+
collectGarbage(): Promise<void>;
|
|
45
|
+
private collectLocked;
|
|
46
|
+
}
|
|
47
|
+
export declare function presetSelectionRevision(state: SelectionState): string | null;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants, closeSync, fstatSync, lstatSync, linkSync, mkdirSync, openSync, readSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { withRecordLock } from '@mearl/daemon-core';
|
|
6
|
+
import { mergePresetDefaults, prepareNodeBundleSnapshot, prepareNodeCapabilitySnapshot, verifyNodeCapabilityActivation, validateNodeModuleConfig, resolvePresetModuleConfig, MAX_NODE_SELECTION_BYTES } from '@mearl/preset';
|
|
7
|
+
import { BundleError, MAX_BROWSER_STORAGE_BYTES, presetRequiresBrowser, validateBundleManifest, verifyPreset, verifyBrowserPreset, verifyPreparedPreset } from '@mearl/preset/bundles';
|
|
8
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
10
|
+
const MAX_RECEIPT_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const MAX_OVERRIDES_BYTES = 262_144;
|
|
12
|
+
const POINTER_BYTES = MAX_NODE_SELECTION_BYTES + 128;
|
|
13
|
+
function readBounded(file, limit) {
|
|
14
|
+
const before = lstatSync(file);
|
|
15
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > limit)
|
|
16
|
+
throw new Error('Invalid preset resource file or size');
|
|
17
|
+
const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
18
|
+
try {
|
|
19
|
+
const stat = fstatSync(descriptor);
|
|
20
|
+
if (!stat.isFile() || stat.size > limit)
|
|
21
|
+
throw new Error('Invalid preset resource file or size');
|
|
22
|
+
const bytes = Buffer.alloc(Math.min(stat.size, limit) + 1);
|
|
23
|
+
let length = 0;
|
|
24
|
+
while (length < bytes.byteLength) {
|
|
25
|
+
const count = readSync(descriptor, bytes, length, bytes.byteLength - length, null);
|
|
26
|
+
if (count === 0)
|
|
27
|
+
break;
|
|
28
|
+
length += count;
|
|
29
|
+
}
|
|
30
|
+
if (length > stat.size)
|
|
31
|
+
throw new Error('Preset resource changed while reading');
|
|
32
|
+
return bytes.subarray(0, length);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
closeSync(descriptor);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function optionalFile(file, limit) {
|
|
39
|
+
try {
|
|
40
|
+
return readBounded(file, limit);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === 'ENOENT')
|
|
44
|
+
return undefined;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function atomicWrite(file, bytes) {
|
|
49
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(temporary, bytes, { flag: 'wx', mode: 0o600 });
|
|
52
|
+
renameSync(temporary, file);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
rmSync(temporary, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function revision(state) {
|
|
59
|
+
return state.activation?.installationRevision ?? (state.activation ? `node:${state.activation.revision}` : null);
|
|
60
|
+
}
|
|
61
|
+
function overrides(value) {
|
|
62
|
+
const accepted = mergePresetDefaults({}, value);
|
|
63
|
+
if (Buffer.byteLength(JSON.stringify(accepted)) > MAX_OVERRIDES_BYTES)
|
|
64
|
+
throw new BundleError('BUNDLE_QUOTA', 'User overrides exceed 256 KiB');
|
|
65
|
+
return accepted;
|
|
66
|
+
}
|
|
67
|
+
/** One pointer binds the desired preset, user overrides and exact Node selection. */
|
|
68
|
+
export class PresetInstallationStore {
|
|
69
|
+
directory;
|
|
70
|
+
mearlApiVersion;
|
|
71
|
+
pointer;
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.directory = resolve(options.directory ?? (process.env.MEARL_CAPABILITY_DIR || join(homedir(), '.mearl/capabilities')));
|
|
74
|
+
this.mearlApiVersion = options.mearlApiVersion ?? '1.0.0';
|
|
75
|
+
this.pointer = join(this.directory, 'active-node.json');
|
|
76
|
+
}
|
|
77
|
+
assertCurrent(expected) {
|
|
78
|
+
const bytes = optionalFile(this.pointer, POINTER_BYTES);
|
|
79
|
+
if ((bytes ? digest(bytes) : null) !== expected)
|
|
80
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed during preparation');
|
|
81
|
+
}
|
|
82
|
+
async current(options = {}) {
|
|
83
|
+
const raw = optionalFile(this.pointer, POINTER_BYTES);
|
|
84
|
+
if (!raw)
|
|
85
|
+
return { rawHash: null };
|
|
86
|
+
const rawHash = digest(raw);
|
|
87
|
+
const activation = await verifyNodeCapabilityActivation(JSON.parse(raw.toString('utf8')));
|
|
88
|
+
if (!activation.installationRevision)
|
|
89
|
+
return { rawHash, activation };
|
|
90
|
+
const captured = withRecordLock(this.pointer, () => {
|
|
91
|
+
this.assertCurrent(rawHash);
|
|
92
|
+
const receiptBytes = readBounded(join(this.directory, 'installations', `${activation.installationRevision}.json`), MAX_RECEIPT_BYTES);
|
|
93
|
+
const receipt = JSON.parse(receiptBytes.toString('utf8'));
|
|
94
|
+
const { revision: receiptRevision, ...data } = receipt;
|
|
95
|
+
if (receiptRevision !== activation.installationRevision || digest(JSON.stringify(data)) !== receiptRevision || data.schemaVersion !== 1 ||
|
|
96
|
+
Object.keys(data).some(key => !['schemaVersion', 'snapshot', 'bundles', 'browserRevision', 'nodeRevision', 'managedModules', 'nodeSyncRequired', 'browserSyncRequired', 'nodeConfig', 'overrides'].includes(key)) || typeof data.nodeSyncRequired !== 'boolean' || (data.browserSyncRequired !== undefined && typeof data.browserSyncRequired !== 'boolean') ||
|
|
97
|
+
data.nodeRevision !== activation.revision || !Array.isArray(data.bundles) || data.bundles.length > 64 ||
|
|
98
|
+
!Array.isArray(data.managedModules) || new Set(data.managedModules).size !== data.managedModules.length || data.managedModules.some(id => typeof id !== 'string' || !Object.hasOwn(activation.modules, id))) {
|
|
99
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Installed preset receipt does not match its selection');
|
|
100
|
+
}
|
|
101
|
+
const acceptedOverrides = overrides(data.overrides);
|
|
102
|
+
const nodeConfig = validateNodeModuleConfig(data.nodeConfig);
|
|
103
|
+
let total = 0;
|
|
104
|
+
const metadata = data.bundles.map(bundle => {
|
|
105
|
+
if (Object.keys(bundle).some(key => !['source', 'manifest', 'revision'].includes(key)))
|
|
106
|
+
throw new BundleError('BUNDLE_INVALID', 'Invalid installed package metadata');
|
|
107
|
+
const manifest = validateBundleManifest(bundle.manifest, this.mearlApiVersion);
|
|
108
|
+
total += Object.values(manifest.resources).reduce((sum, entry) => sum + entry.byteLength, 0);
|
|
109
|
+
return { ...bundle, manifest };
|
|
110
|
+
});
|
|
111
|
+
if (total > MAX_BROWSER_STORAGE_BYTES)
|
|
112
|
+
throw new BundleError('BUNDLE_QUOTA', 'Installed preset resources exceed 64 MiB');
|
|
113
|
+
const bundles = options.verifyResources === false ? [] : metadata.map(bundle => ({ ...bundle, resources: Object.fromEntries(Object.entries(bundle.manifest.resources).map(([name, entry]) => [name, readBounded(join(this.directory, 'blobs', `${entry.sha256}.bin`), entry.byteLength)])) }));
|
|
114
|
+
for (const entry of options.verifyResources === false ? [] : Object.values(activation.modules)) {
|
|
115
|
+
const bytes = readBounded(join(this.directory, 'objects', `${entry.sha256}.cjs`), entry.byteLength);
|
|
116
|
+
if (bytes.byteLength !== entry.byteLength || digest(bytes) !== entry.sha256)
|
|
117
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Installed Node object is missing or corrupt');
|
|
118
|
+
}
|
|
119
|
+
return { receipt: { ...receipt, overrides: acceptedOverrides, nodeConfig }, bundles };
|
|
120
|
+
});
|
|
121
|
+
if (options.verifyResources === false) {
|
|
122
|
+
const graph = await verifyPreset(captured.receipt.snapshot, this.mearlApiVersion);
|
|
123
|
+
await verifyBrowserPreset({ source: graph.snapshot.source, manifests: graph.snapshot.manifests,
|
|
124
|
+
bundleRevisions: Object.fromEntries(Object.entries(graph.snapshot.bundleRevisions).filter(([id]) => graph.preset.packages[id].runtimes?.includes('browser'))), revision: captured.receipt.browserRevision }, this.mearlApiVersion);
|
|
125
|
+
return { rawHash, activation, receipt: captured.receipt, preset: graph.preset };
|
|
126
|
+
}
|
|
127
|
+
const prepared = await verifyPreparedPreset({ snapshot: captured.receipt.snapshot, bundles: captured.bundles }, this.mearlApiVersion);
|
|
128
|
+
if (prepared.browser.snapshot.revision !== captured.receipt.browserRevision)
|
|
129
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Installed browser projection does not match');
|
|
130
|
+
const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].runtimes?.includes('node')), 'native-host', this.mearlApiVersion, resolvePresetModuleConfig(mergePresetDefaults(prepared.preset.defaults, captured.receipt.overrides), captured.receipt.nodeConfig));
|
|
131
|
+
if (Object.keys(selected.snapshot.modules).length !== captured.receipt.managedModules.length || captured.receipt.managedModules.some(id => !Object.hasOwn(selected.snapshot.modules, id)) ||
|
|
132
|
+
Object.entries(selected.snapshot.modules).some(([id, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.modules[id])) ||
|
|
133
|
+
Object.entries(selected.snapshot.bindings).some(([hook, entry]) => JSON.stringify(entry) !== JSON.stringify(activation.bindings[hook])) ||
|
|
134
|
+
Object.values(activation.bindings).filter(entry => captured.receipt.managedModules.includes(entry.module)).length !== Object.keys(selected.snapshot.bindings).length) {
|
|
135
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Installed managed Node selection does not match its packages');
|
|
136
|
+
}
|
|
137
|
+
return { rawHash, activation, prepared, receipt: captured.receipt, preset: prepared.preset };
|
|
138
|
+
}
|
|
139
|
+
async apply(input, options) {
|
|
140
|
+
options.signal?.throwIfAborted();
|
|
141
|
+
const acceptedOverrides = options.overrides === undefined ? undefined : overrides(options.overrides);
|
|
142
|
+
const nodeConfig = options.nodeConfig === undefined ? undefined : validateNodeModuleConfig(options.nodeConfig);
|
|
143
|
+
const [current, prepared] = await Promise.all([this.current({ verifyResources: false }), verifyPreparedPreset(input, this.mearlApiVersion)]);
|
|
144
|
+
if (revision(current) !== options.expectedRevision)
|
|
145
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The installed preset changed');
|
|
146
|
+
const configured = nodeConfig ?? current.receipt?.nodeConfig ?? Object.fromEntries(Object.entries(current.activation?.modules ?? {}).filter(([, entry]) => entry.config !== undefined).map(([id, entry]) => [id, entry.config]));
|
|
147
|
+
const selected = await prepareNodeBundleSnapshot(prepared.bundles.filter(bundle => prepared.preset.packages[bundle.manifest.id].runtimes?.includes('node')), 'native-host', this.mearlApiVersion, resolvePresetModuleConfig(mergePresetDefaults(prepared.preset.defaults, acceptedOverrides ?? current.receipt?.overrides ?? {}), configured));
|
|
148
|
+
const previousManaged = new Set(current.receipt?.managedModules ?? []);
|
|
149
|
+
const unmanaged = Object.fromEntries(Object.entries(current.activation?.modules ?? {}).filter(([id]) => !previousManaged.has(id)));
|
|
150
|
+
for (const entry of Object.values(unmanaged)) {
|
|
151
|
+
const bytes = readBounded(join(this.directory, 'objects', `${entry.sha256}.cjs`), entry.byteLength);
|
|
152
|
+
if (bytes.byteLength !== entry.byteLength || digest(bytes) !== entry.sha256)
|
|
153
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'An independent Node module is corrupt');
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(selected.snapshot.modules).some(id => Object.hasOwn(unmanaged, id)))
|
|
156
|
+
throw new BundleError('BUNDLE_CONFLICT', 'A preset module conflicts with an independently installed Node module');
|
|
157
|
+
const bindings = Object.fromEntries(Object.entries(current.activation?.bindings ?? {}).filter(([, entry]) => Object.hasOwn(unmanaged, entry.module)));
|
|
158
|
+
if (Object.keys(selected.snapshot.bindings).some(hook => Object.hasOwn(bindings, hook)))
|
|
159
|
+
throw new BundleError('BUNDLE_CONFLICT', 'A preset hook conflicts with an independently installed Node module');
|
|
160
|
+
const snapshot = await prepareNodeCapabilitySnapshot({ schemaVersion: 1, modules: { ...unmanaged, ...selected.snapshot.modules }, bindings: { ...bindings, ...selected.snapshot.bindings } });
|
|
161
|
+
const data = {
|
|
162
|
+
schemaVersion: 1, snapshot: prepared.snapshot,
|
|
163
|
+
bundles: prepared.bundles.map(({ source, revision, manifest }) => ({ source, revision, manifest })),
|
|
164
|
+
browserRevision: prepared.browser.snapshot.revision, nodeRevision: snapshot.revision,
|
|
165
|
+
managedModules: Object.keys(selected.snapshot.modules), overrides: acceptedOverrides ?? current.receipt?.overrides ?? {},
|
|
166
|
+
nodeSyncRequired: Boolean(current.receipt?.nodeSyncRequired || current.receipt?.managedModules.length || Object.values(prepared.preset.packages).some(entry => entry.enabled !== false && entry.runtimes?.includes('node'))),
|
|
167
|
+
browserSyncRequired: Boolean(current.receipt?.browserSyncRequired || presetRequiresBrowser(prepared.preset)),
|
|
168
|
+
nodeConfig: configured,
|
|
169
|
+
};
|
|
170
|
+
const receipt = { ...data, revision: digest(JSON.stringify(data)) };
|
|
171
|
+
const receiptBytes = JSON.stringify(receipt);
|
|
172
|
+
if (Buffer.byteLength(receiptBytes) > MAX_RECEIPT_BYTES)
|
|
173
|
+
throw new BundleError('BUNDLE_QUOTA', 'Preset installation receipt exceeds 16 MiB');
|
|
174
|
+
const activation = { ...snapshot, installationRevision: receipt.revision };
|
|
175
|
+
await options.confirm?.();
|
|
176
|
+
withRecordLock(this.pointer, () => {
|
|
177
|
+
this.assertCurrent(current.rawHash);
|
|
178
|
+
options.signal?.throwIfAborted();
|
|
179
|
+
for (const child of ['blobs', 'objects', 'installations'])
|
|
180
|
+
mkdirSync(join(this.directory, child), { recursive: true, mode: 0o700 });
|
|
181
|
+
for (const bundle of prepared.bundles)
|
|
182
|
+
for (const [name, entry] of Object.entries(bundle.manifest.resources)) {
|
|
183
|
+
atomicWrite(join(this.directory, 'blobs', `${entry.sha256}.bin`), bundle.resources[name]);
|
|
184
|
+
}
|
|
185
|
+
for (const entry of Object.values(selected.snapshot.modules)) {
|
|
186
|
+
const destination = join(this.directory, 'objects', `${entry.sha256}.cjs`);
|
|
187
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
188
|
+
try {
|
|
189
|
+
linkSync(join(this.directory, 'blobs', `${entry.sha256}.bin`), temporary);
|
|
190
|
+
renameSync(temporary, destination);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
rmSync(temporary, { force: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
atomicWrite(join(this.directory, 'installations', `${receipt.revision}.json`), receiptBytes);
|
|
197
|
+
if (current.receipt?.revision !== receipt.revision)
|
|
198
|
+
atomicWrite(join(this.directory, 'rollback.json'), JSON.stringify({ candidate: receipt.revision, previous: current.activation ?? null }));
|
|
199
|
+
atomicWrite(this.pointer, JSON.stringify(activation));
|
|
200
|
+
});
|
|
201
|
+
return receipt;
|
|
202
|
+
}
|
|
203
|
+
async rollback(expectedRevision) {
|
|
204
|
+
const current = await this.current({ verifyResources: false });
|
|
205
|
+
if (current.activation?.installationRevision !== expectedRevision)
|
|
206
|
+
throw new BundleError('BUNDLE_CONFLICT', 'A newer preset installation cannot be rolled back');
|
|
207
|
+
const record = JSON.parse(readBounded(join(this.directory, 'rollback.json'), POINTER_BYTES + 256).toString('utf8'));
|
|
208
|
+
if (record.candidate !== expectedRevision)
|
|
209
|
+
throw new BundleError('BUNDLE_CONFLICT', 'The preset rollback record has changed');
|
|
210
|
+
const previous = record.previous === null ? undefined : await verifyNodeCapabilityActivation(record.previous);
|
|
211
|
+
withRecordLock(this.pointer, () => {
|
|
212
|
+
this.assertCurrent(current.rawHash);
|
|
213
|
+
if (previous)
|
|
214
|
+
atomicWrite(this.pointer, JSON.stringify(previous));
|
|
215
|
+
else
|
|
216
|
+
rmSync(this.pointer, { force: true });
|
|
217
|
+
rmSync(join(this.directory, 'rollback.json'), { force: true });
|
|
218
|
+
this.collectLocked(previous);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async collectGarbage() {
|
|
222
|
+
const current = await this.current({ verifyResources: false });
|
|
223
|
+
const raw = optionalFile(join(this.directory, 'rollback.json'), POINTER_BYTES + 256);
|
|
224
|
+
const record = raw && JSON.parse(raw.toString('utf8'));
|
|
225
|
+
const previous = record?.candidate === current.activation?.installationRevision && record.previous !== null ? await verifyNodeCapabilityActivation(record.previous) : undefined;
|
|
226
|
+
withRecordLock(this.pointer, () => {
|
|
227
|
+
this.assertCurrent(current.rawHash);
|
|
228
|
+
for (const child of ['blobs', 'objects', 'installations'])
|
|
229
|
+
mkdirSync(join(this.directory, child), { recursive: true, mode: 0o700 });
|
|
230
|
+
this.collectLocked(current.activation, previous);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
collectLocked(active, previous) {
|
|
234
|
+
const receipts = new Set([active?.installationRevision, previous?.installationRevision].filter((value) => Boolean(value)));
|
|
235
|
+
const blobs = new Set();
|
|
236
|
+
const modules = new Set([active, previous].flatMap(selection => Object.values(selection?.modules ?? {}).map(entry => entry.sha256)));
|
|
237
|
+
for (const id of receipts) {
|
|
238
|
+
const record = JSON.parse(readBounded(join(this.directory, 'installations', `${id}.json`), MAX_RECEIPT_BYTES).toString('utf8'));
|
|
239
|
+
for (const bundle of record.bundles)
|
|
240
|
+
for (const entry of Object.values(bundle.manifest.resources))
|
|
241
|
+
blobs.add(entry.sha256);
|
|
242
|
+
}
|
|
243
|
+
for (const [directory, extension, retained] of [['blobs', '.bin', blobs], ['objects', '.cjs', modules], ['installations', '.json', receipts]]) {
|
|
244
|
+
for (const name of readdirSync(join(this.directory, directory))) {
|
|
245
|
+
const id = name.slice(0, -extension.length);
|
|
246
|
+
if (name.endsWith(extension) && hashPattern.test(id) && !retained.has(id))
|
|
247
|
+
rmSync(join(this.directory, directory, name), { force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function presetSelectionRevision(state) { return revision(state); }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type InstallationConfig } from '@mearl/preset/installation';
|
|
2
|
+
import { PresetInstaller, type StagedPresetInstallation } from './preset.js';
|
|
3
|
+
import { type DetectedGlobalPackage } from './packageManager.js';
|
|
4
|
+
export declare const MEARL_INSTALLATION_CONFIG_URL = "https://f-loat.github.io/mearl/installation.json";
|
|
5
|
+
/** Reuse the installation receipt as the retry marker; organization defaults come from deployment data. */
|
|
6
|
+
export declare function prepareSetupPreset(options: {
|
|
7
|
+
source?: string;
|
|
8
|
+
installed?: Array<Pick<DetectedGlobalPackage, 'packageName' | 'packageRoot'>>;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}, dependencies?: {
|
|
11
|
+
installer?: PresetInstaller;
|
|
12
|
+
configuration?: () => Promise<InstallationConfig>;
|
|
13
|
+
}): Promise<StagedPresetInstallation | undefined>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadInstallationConfig } from '@mearl/preset/installation';
|
|
4
|
+
import { PresetInstaller } from './preset.js';
|
|
5
|
+
import { detectGlobalPackage } from './packageManager.js';
|
|
6
|
+
import { detectMearlProfiles } from './profiles.js';
|
|
7
|
+
export const MEARL_INSTALLATION_CONFIG_URL = 'https://f-loat.github.io/mearl/installation.json';
|
|
8
|
+
function isLegacyPackage(installation) {
|
|
9
|
+
try {
|
|
10
|
+
const manifest = JSON.parse(readFileSync(join(installation.packageRoot, 'package.json'), 'utf8'));
|
|
11
|
+
return manifest.name === installation.packageName && typeof manifest.version === 'string' && /^[0-2]\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(manifest.version);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Reuse the installation receipt as the retry marker; organization defaults come from deployment data. */
|
|
18
|
+
export async function prepareSetupPreset(options, dependencies = {}) {
|
|
19
|
+
const env = options.env ?? process.env;
|
|
20
|
+
const installer = dependencies.installer ?? new PresetInstaller({ directory: env.MEARL_CAPABILITY_DIR });
|
|
21
|
+
if (options.source)
|
|
22
|
+
return installer.stage(options.source);
|
|
23
|
+
const current = await installer.store.current();
|
|
24
|
+
if (current.receipt)
|
|
25
|
+
return { revision: current.receipt.revision, activate: () => installer.resume(current.receipt.revision) };
|
|
26
|
+
if (current.activation)
|
|
27
|
+
return;
|
|
28
|
+
const client = options.installed === undefined ? detectGlobalPackage('@mearl/client', ['mearl'], { env }) : null;
|
|
29
|
+
const installed = options.installed ?? [...detectMearlProfiles({ env }).values(), ...(client ? [client] : [])];
|
|
30
|
+
if (!installed.some(isLegacyPackage))
|
|
31
|
+
return;
|
|
32
|
+
const configuration = dependencies.configuration ?? (() => {
|
|
33
|
+
const url = new URL(env.MEARL_INSTALLATION_CONFIG_URL || MEARL_INSTALLATION_CONFIG_URL);
|
|
34
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))
|
|
35
|
+
throw new Error('Installation configuration requires HTTPS or loopback HTTP');
|
|
36
|
+
return loadInstallationConfig(url, url.origin, AbortSignal.timeout(5_000));
|
|
37
|
+
});
|
|
38
|
+
const config = await configuration();
|
|
39
|
+
if (!config.legacyPreset)
|
|
40
|
+
throw new Error('Legacy upgrade preset is unavailable. Retry when the installation catalog is available, or select a preset with --preset.');
|
|
41
|
+
return installer.stage(config.presets[config.legacyPreset].source, { expectedRevision: null, expectedId: config.legacyPreset });
|
|
42
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface ProviderSetupResult {
|
|
|
20
20
|
manifest: InstalledProviderManifest;
|
|
21
21
|
manager: PackageManagerInstallation['manager'];
|
|
22
22
|
}
|
|
23
|
+
/** A registry record alone does not prove that the package still exists. */
|
|
24
|
+
export declare function inspectProviderPackage(manifest: InstalledProviderManifest): boolean;
|
|
23
25
|
export declare function parseProviderPackageSpec(value: string): ProviderPackageSpec;
|
|
24
26
|
export declare function installProviderPackage(packageSpec: string, dependencies?: ProviderSetupDependencies): Promise<ProviderSetupResult>;
|
|
25
27
|
export declare function updateProviderPackage(selector: string, dependencies?: ProviderSetupDependencies): Promise<ProviderSetupResult>;
|
package/dist/provider.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { listInstalledProviders, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from '@mearl/provider/host';
|
|
3
|
+
import { listInstalledProviders, validateInstalledProviderManifest, registerProviderPackage, stopProvider, readLiveProvider, readProviderStart, uninstallProvider, } from '@mearl/provider/host';
|
|
4
4
|
import { buildGlobalPackageCommand, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
|
|
5
5
|
import { packageRegistryEnvironment } from './npmRegistry.js';
|
|
6
6
|
import { isProcessOwnerAlive, processIdentity, prependCommandPath, runCommand, } from '@mearl/daemon-core';
|
|
7
|
+
/** A registry record alone does not prove that the package still exists. */
|
|
8
|
+
export function inspectProviderPackage(manifest) {
|
|
9
|
+
if (validateInstalledProviderManifest(manifest))
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
const file = join(manifest.packageRoot, 'package.json');
|
|
13
|
+
const stat = statSync(file);
|
|
14
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
15
|
+
return false;
|
|
16
|
+
const metadata = JSON.parse(readFileSync(file, 'utf8'));
|
|
17
|
+
const provider = metadata.mearl?.provider;
|
|
18
|
+
return metadata.name === manifest.packageName && metadata.version === manifest.version &&
|
|
19
|
+
provider?.providerId === manifest.providerId && provider.protocolVersion === manifest.protocolVersion &&
|
|
20
|
+
typeof provider.entry === 'string' && resolve(manifest.packageRoot, provider.entry) === manifest.entryPath;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
async function stopProviderBeforePackageChange(providerId) {
|
|
8
27
|
if (readProviderStart(providerId))
|
|
9
28
|
throw new Error(`Provider ${providerId} is still starting`);
|
|
@@ -96,6 +115,9 @@ export async function installProviderPackage(packageSpec, dependencies = {}) {
|
|
|
96
115
|
const env = process.env;
|
|
97
116
|
const runner = dependencies.runCommand ?? runCommand;
|
|
98
117
|
const installation = providerManager(env, dependencies);
|
|
118
|
+
const previous = (dependencies.listProviders ?? listInstalledProviders)().filter(provider => provider.packageName === packageName);
|
|
119
|
+
for (const provider of previous)
|
|
120
|
+
await (dependencies.stopProvider ?? stopProviderBeforePackageChange)(provider.providerId);
|
|
99
121
|
await runPackageCommand(installation, 'install', packageName, target, runner, env);
|
|
100
122
|
const packageRoot = await (dependencies.resolvePackageRoot ?? resolveGlobalProviderPackageRoot)(installation, packageName, runner, env);
|
|
101
123
|
if (!(dependencies.existsSync ?? existsSync)(join(packageRoot, 'package.json'))) {
|
package/dist/runtimeCleanup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { basename, join, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { cleanupPluginRuntime, pluginDirectory } from '@mearl/client/plugin';
|
|
@@ -18,13 +18,6 @@ export async function cleanupUninstalledRuntime() {
|
|
|
18
18
|
}))
|
|
19
19
|
throw new Error('Providers still use runtime caches; caches were retained');
|
|
20
20
|
}
|
|
21
|
-
for (const directory of [
|
|
22
|
-
'authx-sdk',
|
|
23
|
-
'design-overlay/objects',
|
|
24
|
-
'design-overlay/uploads',
|
|
25
|
-
'design-overlay/pages',
|
|
26
|
-
])
|
|
27
|
-
rmSync(join(root, directory), { recursive: true, force: true });
|
|
28
21
|
pruneOrphanLogs(join(root, 'managed-browsers'), 0);
|
|
29
22
|
pruneOrphanLogs(join(root, 'providers/logs'), 0);
|
|
30
23
|
let entries;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
export const SETUP_VERSION = require('../package.json').version;
|
|
4
|
+
export const SETUP_COMMAND = `npx @mearl/setup${SETUP_VERSION.includes('-beta.') ? '@' + SETUP_VERSION : ''}`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CommandRunner } from '@mearl/daemon-core';
|
|
2
|
+
export type SkillSourceScope = 'global' | 'project';
|
|
3
|
+
export interface TrackedSkillSource {
|
|
4
|
+
name: string;
|
|
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;
|
|
22
|
+
/** Matches the public Skills folder hash without writing to the installed Skill. */
|
|
23
|
+
export declare function hashSkillDirectory(directory: string, ignoreRuntimeFiles?: boolean): Promise<string>;
|
|
24
|
+
/** Checks source content in a temporary checkout; never invokes the mutating Skills `check` alias. */
|
|
25
|
+
export declare function checkTrackedSkillUpdates(options?: SourceOptions & {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
runCommand?: CommandRunner;
|
|
28
|
+
}): Promise<Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
source: string;
|
|
31
|
+
scope: SkillSourceScope;
|
|
32
|
+
}>>;
|
|
33
|
+
export interface SkillInstallationStatus {
|
|
34
|
+
state: 'ready' | 'missing' | 'version' | 'registration' | 'conflict';
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
/** CLI and Agent readiness use the same source, manifest and registration evidence. */
|
|
38
|
+
export declare function inspectGlobalSkill(source: string, name: string, version?: string, options?: {
|
|
39
|
+
readSources?: typeof readTrackedSkillSources;
|
|
40
|
+
externalManager?: boolean;
|
|
41
|
+
normalizeSource?: (source: string, sourceType?: string) => string;
|
|
42
|
+
}): SkillInstallationStatus;
|
|
43
|
+
export {};
|