openmeld 0.3.159 → 0.3.160
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/{add-me-membership-jQsB5MBL.js → add-me-membership-DIlgLZLR.js} +2 -2
- package/dist/{add-me-membership-jQsB5MBL.js.map → add-me-membership-DIlgLZLR.js.map} +1 -1
- package/dist/{api-client-foundation-D3vgN2Jh.js → api-client-foundation-UKDtNrdW.js} +1 -1
- package/dist/{api-client-foundation-CoFjLc3o.js → api-client-foundation-i0M0ZIrQ.js} +3 -3
- package/dist/{api-client-foundation-CoFjLc3o.js.map → api-client-foundation-i0M0ZIrQ.js.map} +1 -1
- package/dist/auth-session-DMovqZYd.js +4 -0
- package/dist/{auth-session-Da7OgTZk.js → auth-session-DnS-Axyz.js} +2 -2
- package/dist/{auth-session-Da7OgTZk.js.map → auth-session-DnS-Axyz.js.map} +1 -1
- package/dist/{command-CcqgRB4J.js → command-D9xEikCY.js} +81 -904
- package/dist/command-D9xEikCY.js.map +1 -0
- package/dist/{dist-KezFWl5t.js → dist-CqYwEcEG.js} +24 -7
- package/dist/dist-CqYwEcEG.js.map +1 -0
- package/dist/{dist-VKaBpS9J.js → dist-sCZhNX4o.js} +1 -1
- package/dist/{elapsed-time-BI7-NmU1.js → elapsed-time-Dd6Ecbev.js} +3 -3
- package/dist/{elapsed-time-BI7-NmU1.js.map → elapsed-time-Dd6Ecbev.js.map} +1 -1
- package/dist/installer-CLPF4DHx.js +1100 -0
- package/dist/installer-CLPF4DHx.js.map +1 -0
- package/dist/{openmeld-DsT7zl0R.js → openmeld-DR8Bn-1b.js} +337 -68
- package/dist/openmeld-DR8Bn-1b.js.map +1 -0
- package/dist/openmeld-maintenance-worker-Bj1ZAvBN.js +78 -0
- package/dist/openmeld-maintenance-worker-Bj1ZAvBN.js.map +1 -0
- package/dist/{openmeld-tools-update-coordinator-H16PNoPX.js → openmeld-tools-update-coordinator-C7JyBdik.js} +11 -11
- package/dist/{openmeld-tools-update-coordinator-H16PNoPX.js.map → openmeld-tools-update-coordinator-C7JyBdik.js.map} +1 -1
- package/dist/openmeld.js +3 -3
- package/dist/{runtime-transport-D6j3RJ5o.js → runtime-transport-BsvsUfwb.js} +7 -1
- package/dist/runtime-transport-BsvsUfwb.js.map +1 -0
- package/dist/upgrade-CDPK-ayC.js +4 -0
- package/dist/{upgrade-BcmVKRpO.js → upgrade-VYwQsaiC.js} +4 -4
- package/dist/{upgrade-BcmVKRpO.js.map → upgrade-VYwQsaiC.js.map} +1 -1
- package/package.json +1 -1
- package/skills/openmeld-cli/SKILL.md +0 -9
- package/skills/openmeld-cli/playbooks/space-ops.md +8 -0
- package/skills/openmeld-cli/references/commands.md +26 -0
- package/dist/auth-session-DNXann3J.js +0 -4
- package/dist/command-CcqgRB4J.js.map +0 -1
- package/dist/dist-KezFWl5t.js.map +0 -1
- package/dist/openmeld-DsT7zl0R.js.map +0 -1
- package/dist/openmeld-maintenance-worker-Bmc0QSqk.js +0 -49
- package/dist/openmeld-maintenance-worker-Bmc0QSqk.js.map +0 -1
- package/dist/runtime-transport-D6j3RJ5o.js.map +0 -1
- package/dist/upgrade-xohSdsR-.js +0 -4
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { lt as resolveOpenMeldEnvPreset, mt as skillsDir, pt as runtimeLogsDir, tt as openMeldRootDir } from "./lock-VfUd0-tH.js";
|
|
4
|
+
import { An as canManageRegistryAgentSkillTargets, Mn as resolveSkillsAgentRegistry, Xa as OPENMELD_VERSION, jn as detectInstalledSkillsAgents, qo as compareSemver } from "./openmeld-tools-update-coordinator-C7JyBdik.js";
|
|
5
|
+
import { v as isBinaryDistribution } from "./auth-session-DnS-Axyz.js";
|
|
6
|
+
import { T as updateConfiguredSkillsConfig, g as getConfiguredSkillsConfig, y as resolveRuntimeSkillsCustomTargetId } from "./base-url-CgF_v0Ue.js";
|
|
7
|
+
import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
|
+
import { createHash, randomUUID, verify } from "node:crypto";
|
|
9
|
+
import { cp, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import * as z$1 from "zod/v4";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import process$1 from "node:process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
+
const OFFICIAL_CLI_SKILL_URL = "https://openmeld.ai/skills/openmeld-cli/SKILL.md";
|
|
16
|
+
const OFFICIAL_CLI_SKILL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
17
|
+
MCowBQYDK2VwAyEAFZjFHrQ/QLsJQpHkAbKBRG57fEazhm7zc0dMa54ilWQ=
|
|
18
|
+
-----END PUBLIC KEY-----
|
|
19
|
+
`;
|
|
20
|
+
const officialCliSkillPayloadSchema = z$1.object({
|
|
21
|
+
schemaVersion: z$1.literal(1),
|
|
22
|
+
revision: z$1.number().int().positive(),
|
|
23
|
+
minimumCliVersion: z$1.string().regex(/^\d+\.\d+\.\d+$/),
|
|
24
|
+
contractVersion: z$1.string().min(1).max(128),
|
|
25
|
+
files: z$1.array(z$1.object({
|
|
26
|
+
path: z$1.string().regex(/^(?:SKILL\.md|(?:playbooks|references)\/[a-z0-9-]+\.md)$/),
|
|
27
|
+
content: z$1.string().min(1).max(131072)
|
|
28
|
+
}).strict()).min(1).max(32)
|
|
29
|
+
}).strict().superRefine((value, context) => {
|
|
30
|
+
const paths = value.files.map((file) => file.path);
|
|
31
|
+
if (!paths.includes("SKILL.md") || new Set(paths).size !== paths.length) context.addIssue({
|
|
32
|
+
code: "custom",
|
|
33
|
+
message: "Skill entry is required and paths must be unique"
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const officialCliSkillReleaseSchema = z$1.object({
|
|
37
|
+
payload: z$1.string().min(1).max(524288),
|
|
38
|
+
signature: z$1.string().regex(/^[A-Za-z0-9+/]{86}==$/)
|
|
39
|
+
}).strict();
|
|
40
|
+
const officialCliSkillChannelSchema = z$1.object({
|
|
41
|
+
schemaVersion: z$1.literal(1),
|
|
42
|
+
releases: z$1.array(officialCliSkillReleaseSchema).min(1).max(16)
|
|
43
|
+
}).strict();
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/skills/content-hash.ts
|
|
46
|
+
async function computeDirectoryContentHash(input) {
|
|
47
|
+
const ignoredNames = new Set(input.ignoredNames ?? []);
|
|
48
|
+
const files = await collectFiles({
|
|
49
|
+
dir: input.rootDir,
|
|
50
|
+
rootDir: input.rootDir,
|
|
51
|
+
ignoredNames
|
|
52
|
+
});
|
|
53
|
+
const hash = createHash("sha256");
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
hash.update(`file:${file.relativePath}\n`);
|
|
56
|
+
hash.update(file.content);
|
|
57
|
+
hash.update("\n");
|
|
58
|
+
}
|
|
59
|
+
return hash.digest("hex");
|
|
60
|
+
}
|
|
61
|
+
async function collectFiles(input) {
|
|
62
|
+
const entries = await readdir(input.dir, { withFileTypes: true }).catch((error) => {
|
|
63
|
+
if (getErrorCode$1(error) === "ENOENT") return [];
|
|
64
|
+
warnDirectoryReadError(input.dir, error);
|
|
65
|
+
throw error;
|
|
66
|
+
});
|
|
67
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
const files = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (input.ignoredNames.has(entry.name)) continue;
|
|
71
|
+
const absolutePath = join(input.dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
files.push(...await collectFiles({
|
|
74
|
+
dir: absolutePath,
|
|
75
|
+
rootDir: input.rootDir,
|
|
76
|
+
ignoredNames: input.ignoredNames
|
|
77
|
+
}));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!entry.isFile()) continue;
|
|
81
|
+
const content = await readFile(absolutePath);
|
|
82
|
+
files.push({
|
|
83
|
+
relativePath: normalizeRelativePath(relative(input.rootDir, absolutePath)),
|
|
84
|
+
content
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
function normalizeRelativePath(value) {
|
|
90
|
+
return value.split("\\").join("/");
|
|
91
|
+
}
|
|
92
|
+
function getErrorCode$1(error) {
|
|
93
|
+
if (!error || typeof error !== "object") return;
|
|
94
|
+
const code = error.code;
|
|
95
|
+
return typeof code === "string" ? code : void 0;
|
|
96
|
+
}
|
|
97
|
+
function warnDirectoryReadError(dir, error) {
|
|
98
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
99
|
+
process.stderr.write(`[openmeld] failed to read directory while hashing skills content (${dir}): ${reason}\n`);
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/skills/bundled.ts
|
|
103
|
+
const CLI_PACKAGE_JSON = "package.json";
|
|
104
|
+
/**
|
|
105
|
+
* Locate the on-disk bundled skills directory for the npm distribution by
|
|
106
|
+
* walking up from this module to the CLI package root (a directory that has
|
|
107
|
+
* both package.json and skills/). Returns null under the binary distribution,
|
|
108
|
+
* where import.meta.url is a virtual path and the skills are embedded instead —
|
|
109
|
+
* callers in that mode go through bundled-assets.ts, not this function.
|
|
110
|
+
*/
|
|
111
|
+
function resolveBundledSkillsLocation() {
|
|
112
|
+
const cliRootDir = resolveCliRootDir(path.dirname(fileURLToPath(import.meta.url)));
|
|
113
|
+
if (!cliRootDir) return null;
|
|
114
|
+
return {
|
|
115
|
+
cliRootDir,
|
|
116
|
+
skillsRootDir: path.join(cliRootDir, "skills")
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveCliRootDir(fromDir) {
|
|
120
|
+
let current = fromDir;
|
|
121
|
+
for (;;) {
|
|
122
|
+
const hasPackageJson = existsSync(path.join(current, CLI_PACKAGE_JSON));
|
|
123
|
+
const hasSkillsDir = existsSync(path.join(current, "skills"));
|
|
124
|
+
if (hasPackageJson && hasSkillsDir) return current;
|
|
125
|
+
const parent = path.dirname(current);
|
|
126
|
+
if (parent === current) return null;
|
|
127
|
+
current = parent;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/skills/bundled-assets-fs.ts
|
|
132
|
+
/**
|
|
133
|
+
* npm-distribution source: read the bundled skills from the on-disk `skills/`
|
|
134
|
+
* directory next to the CLI package. Assets are sorted by relative path so the
|
|
135
|
+
* order (and therefore any content hash computed from them) is stable and
|
|
136
|
+
* matches the embedded distribution.
|
|
137
|
+
*/
|
|
138
|
+
async function loadFsBundledSkillAssets() {
|
|
139
|
+
const location = resolveBundledSkillsLocation();
|
|
140
|
+
if (!location) throw new Error("unable to resolve bundled OpenMeld skills directory");
|
|
141
|
+
return sortBundledSkillAssets(await collectSkillAssets(location.skillsRootDir, location.skillsRootDir));
|
|
142
|
+
}
|
|
143
|
+
function sortBundledSkillAssets(assets) {
|
|
144
|
+
return [...assets].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
145
|
+
}
|
|
146
|
+
async function collectSkillAssets(dir, rootDir) {
|
|
147
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
148
|
+
const assets = [];
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const absolutePath = join(dir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
assets.push(...await collectSkillAssets(absolutePath, rootDir));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!entry.isFile()) continue;
|
|
156
|
+
assets.push({
|
|
157
|
+
relativePath: toPosixRelativePath(relative(rootDir, absolutePath)),
|
|
158
|
+
content: await readFile(absolutePath)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return assets;
|
|
162
|
+
}
|
|
163
|
+
function toPosixRelativePath(value) {
|
|
164
|
+
return value.split("\\").join("/");
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/skills/bundled-assets.ts
|
|
168
|
+
/**
|
|
169
|
+
* Load the OpenMeld-managed skill files that ship with this CLI build.
|
|
170
|
+
*
|
|
171
|
+
* - npm distribution: read from the on-disk `skills/` directory.
|
|
172
|
+
* - binary distribution: read from files embedded into the single binary at
|
|
173
|
+
* compile time. The embedded module is a Bun-only island resolved only by
|
|
174
|
+
* `bun build --compile`; the npm build never reaches this branch and marks
|
|
175
|
+
* the module external, so its `with { type: "file" }` imports are never
|
|
176
|
+
* parsed by tsdown.
|
|
177
|
+
*/
|
|
178
|
+
async function loadBundledSkillAssets() {
|
|
179
|
+
if (isBinaryDistribution()) return (await import("./skills/bundled-assets-embedded")).loadEmbeddedBundledSkillAssets();
|
|
180
|
+
return loadFsBundledSkillAssets();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Content hash over an ordered list of skill assets. Byte-for-byte compatible
|
|
184
|
+
* with `computeDirectoryContentHash` (same `file:<relativePath>\n<content>\n`
|
|
185
|
+
* framing) so the embedded and on-disk sources produce identical hashes as long
|
|
186
|
+
* as `assets` is sorted by relative path (as `loadBundledSkillAssets` returns
|
|
187
|
+
* it). This is the single skills-hash algorithm for both distribution forms.
|
|
188
|
+
*/
|
|
189
|
+
function hashBundledSkillAssets(assets) {
|
|
190
|
+
const hash = createHash("sha256");
|
|
191
|
+
for (const asset of assets) {
|
|
192
|
+
hash.update(`file:${asset.relativePath}\n`);
|
|
193
|
+
hash.update(asset.content);
|
|
194
|
+
hash.update("\n");
|
|
195
|
+
}
|
|
196
|
+
return hash.digest("hex");
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/skills/install-lock.ts
|
|
200
|
+
const heldLock = new AsyncLocalStorage();
|
|
201
|
+
const pendingLocks = /* @__PURE__ */ new Map();
|
|
202
|
+
function withSkillsInstallLock(run) {
|
|
203
|
+
if (heldLock.getStore()) return run();
|
|
204
|
+
const key = runtimeLogsDir();
|
|
205
|
+
const current = (pendingLocks.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => acquireSkillsInstallLock(() => heldLock.run(true, run)));
|
|
206
|
+
pendingLocks.set(key, current);
|
|
207
|
+
return current.finally(() => {
|
|
208
|
+
if (pendingLocks.get(key) === current) pendingLocks.delete(key);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const LOCK_FILE_NAME = "skills-install.lock";
|
|
212
|
+
const LOCK_ACQUIRE_MAX_ATTEMPTS = 3;
|
|
213
|
+
async function acquireSkillsInstallLock(run) {
|
|
214
|
+
const logsDir = runtimeLogsDir();
|
|
215
|
+
await mkdir(logsDir, { recursive: true });
|
|
216
|
+
const lockPath = join(logsDir, LOCK_FILE_NAME);
|
|
217
|
+
let acquired = false;
|
|
218
|
+
const lockContents = `${JSON.stringify({
|
|
219
|
+
pid: process.pid,
|
|
220
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
221
|
+
})}\n`;
|
|
222
|
+
for (let attempt = 0; attempt < LOCK_ACQUIRE_MAX_ATTEMPTS; attempt += 1) try {
|
|
223
|
+
await writeFile(lockPath, lockContents, {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
flag: "wx"
|
|
226
|
+
});
|
|
227
|
+
acquired = true;
|
|
228
|
+
break;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (getErrorCode(error) !== "EEXIST") throw error;
|
|
231
|
+
if (!await recoverStaleSkillsInstallLock(lockPath)) throw new Error("another skills installation is already running; retry after it finishes");
|
|
232
|
+
try {
|
|
233
|
+
await writeFile(lockPath, lockContents, {
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
flag: "wx"
|
|
236
|
+
});
|
|
237
|
+
acquired = true;
|
|
238
|
+
break;
|
|
239
|
+
} catch (retryError) {
|
|
240
|
+
if (getErrorCode(retryError) !== "EEXIST") throw retryError;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!acquired) throw new Error("another skills installation is already running; retry after it finishes");
|
|
244
|
+
try {
|
|
245
|
+
return await run();
|
|
246
|
+
} finally {
|
|
247
|
+
await rm(lockPath, { force: true }).catch(() => void 0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function recoverStaleSkillsInstallLock(lockPath) {
|
|
251
|
+
const lockRaw = await readFile(lockPath, "utf8").catch((error) => {
|
|
252
|
+
if (getErrorCode(error) === "ENOENT") return "";
|
|
253
|
+
throw error;
|
|
254
|
+
});
|
|
255
|
+
if (!lockRaw) return true;
|
|
256
|
+
const pid = resolveLockPid(parseLockMeta(lockRaw));
|
|
257
|
+
if (!pid) {
|
|
258
|
+
const metadata = await stat(lockPath).catch(() => null);
|
|
259
|
+
if (metadata && Date.now() - metadata.mtimeMs < 3e4) return false;
|
|
260
|
+
}
|
|
261
|
+
if (pid && isPidAlive(pid)) return false;
|
|
262
|
+
const latestLockRaw = await readFile(lockPath, "utf8").catch((error) => {
|
|
263
|
+
if (getErrorCode(error) === "ENOENT") return "";
|
|
264
|
+
throw error;
|
|
265
|
+
});
|
|
266
|
+
if (!latestLockRaw) return true;
|
|
267
|
+
if (!isSameLockSnapshot({
|
|
268
|
+
beforeRaw: lockRaw,
|
|
269
|
+
beforePid: pid,
|
|
270
|
+
afterRaw: latestLockRaw,
|
|
271
|
+
afterPid: resolveLockPid(parseLockMeta(latestLockRaw))
|
|
272
|
+
})) return false;
|
|
273
|
+
await rm(lockPath, { force: true }).catch((error) => {
|
|
274
|
+
if (getErrorCode(error) === "ENOENT") return;
|
|
275
|
+
throw error;
|
|
276
|
+
});
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
function parseLockMeta(value) {
|
|
280
|
+
try {
|
|
281
|
+
return JSON.parse(value);
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function resolveLockPid(meta) {
|
|
287
|
+
if (!meta || typeof meta !== "object") return null;
|
|
288
|
+
const pid = meta.pid;
|
|
289
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
290
|
+
}
|
|
291
|
+
function isPidAlive(pid) {
|
|
292
|
+
try {
|
|
293
|
+
process.kill(pid, 0);
|
|
294
|
+
return true;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return getErrorCode(error) !== "ESRCH";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function getErrorCode(error) {
|
|
300
|
+
if (!error || typeof error !== "object") return;
|
|
301
|
+
const code = error.code;
|
|
302
|
+
return typeof code === "string" ? code : void 0;
|
|
303
|
+
}
|
|
304
|
+
function isSameLockSnapshot(input) {
|
|
305
|
+
if (input.beforeRaw !== input.afterRaw) return false;
|
|
306
|
+
return input.beforePid === input.afterPid;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/skills/official-release.ts
|
|
310
|
+
const CACHE_FILE = "official-cli-skill.json";
|
|
311
|
+
function verifyOfficialSkillRelease(value) {
|
|
312
|
+
const envelope = officialCliSkillReleaseSchema.safeParse(value);
|
|
313
|
+
if (!(envelope.success && verify(null, Buffer.from(envelope.data.payload), OFFICIAL_CLI_SKILL_PUBLIC_KEY, Buffer.from(envelope.data.signature, "base64")))) throw new Error("OpenMeld Skill signature is invalid");
|
|
314
|
+
const payload = officialCliSkillPayloadSchema.safeParse(JSON.parse(envelope.data.payload));
|
|
315
|
+
if (!payload.success) throw new Error("OpenMeld Skill content is invalid");
|
|
316
|
+
return {
|
|
317
|
+
release: envelope.data,
|
|
318
|
+
payload: payload.data
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function isCompatible(payload, cliVersion = OPENMELD_VERSION) {
|
|
322
|
+
return payload.contractVersion === "openmeld-skills-contract-2026-08-tool-owned-actions-v2" && compareSemver(cliVersion, payload.minimumCliVersion) >= 0;
|
|
323
|
+
}
|
|
324
|
+
function selectOfficialSkillRelease(value, cliVersion = OPENMELD_VERSION) {
|
|
325
|
+
const channel = officialCliSkillChannelSchema.safeParse(value);
|
|
326
|
+
if (!channel.success) throw new Error("OpenMeld Skill channel is invalid");
|
|
327
|
+
return channel.data.releases.map(verifyOfficialSkillRelease).filter(({ payload }) => isCompatible(payload, cliVersion)).sort((a, b) => b.payload.revision - a.payload.revision)[0] ?? null;
|
|
328
|
+
}
|
|
329
|
+
async function readCachedRelease() {
|
|
330
|
+
const raw = await readFile(join(openMeldRootDir(), CACHE_FILE), "utf8").catch((error) => {
|
|
331
|
+
if (error.code === "ENOENT") return null;
|
|
332
|
+
throw error;
|
|
333
|
+
});
|
|
334
|
+
if (!raw) return null;
|
|
335
|
+
try {
|
|
336
|
+
return verifyOfficialSkillRelease(JSON.parse(raw));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
process.stderr.write(`[openmeld] cached Skill rejected: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function loadSelectedSkillAssets() {
|
|
343
|
+
const cached = await readCachedRelease();
|
|
344
|
+
if (!(cached && isCompatible(cached.payload))) return {
|
|
345
|
+
assets: await loadBundledSkillAssets(),
|
|
346
|
+
version: OPENMELD_VERSION
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
assets: [...cached.payload.files.map((file) => ({
|
|
350
|
+
relativePath: `openmeld-cli/${file.path}`,
|
|
351
|
+
content: Buffer.from(file.content)
|
|
352
|
+
}))].sort((a, b) => a.relativePath.localeCompare(b.relativePath)),
|
|
353
|
+
version: `skill-r${cached.payload.revision}`,
|
|
354
|
+
contentRelease: {
|
|
355
|
+
revision: cached.payload.revision,
|
|
356
|
+
minimumCliVersion: cached.payload.minimumCliVersion
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async function readSelectedSkillDocument(path) {
|
|
361
|
+
const { assets } = await loadSelectedSkillAssets();
|
|
362
|
+
const asset = assets.find((file) => file.relativePath === `openmeld-cli/${path}`);
|
|
363
|
+
if (!asset) throw new Error(`OpenMeld Skill document is missing: ${path}`);
|
|
364
|
+
return asset.content.toString("utf8");
|
|
365
|
+
}
|
|
366
|
+
function resolveOfficialSkillChannelUrl() {
|
|
367
|
+
if (resolveOpenMeldEnvPreset() === "prod") return "https://openmeld.ai/skills/openmeld-cli/channel.json";
|
|
368
|
+
const override = process.env.OPENMELD_CLI_SKILL_CHANNEL_URL;
|
|
369
|
+
if (!(override && process.env.OPENMELD_HOME)) return null;
|
|
370
|
+
const url = new URL(override);
|
|
371
|
+
if (url.protocol !== "http:" || ![
|
|
372
|
+
"localhost",
|
|
373
|
+
"127.0.0.1",
|
|
374
|
+
"[::1]"
|
|
375
|
+
].includes(url.hostname) || url.username || url.password) throw new Error("Isolated Skill updates require an explicit localhost URL");
|
|
376
|
+
return url.href;
|
|
377
|
+
}
|
|
378
|
+
async function downloadOfficialSkillUpdate() {
|
|
379
|
+
const url = resolveOfficialSkillChannelUrl();
|
|
380
|
+
if (!url) return "lane_isolated";
|
|
381
|
+
const response = await fetch(url, {
|
|
382
|
+
signal: AbortSignal.timeout(5e3),
|
|
383
|
+
redirect: "error"
|
|
384
|
+
});
|
|
385
|
+
if (!(response.ok && response.body)) throw new Error(`OpenMeld Skill download failed (HTTP ${response.status})`);
|
|
386
|
+
const chunks = [];
|
|
387
|
+
let size = 0;
|
|
388
|
+
for await (const chunk of response.body) {
|
|
389
|
+
size += chunk.byteLength;
|
|
390
|
+
if (size > 1048576) throw new Error("OpenMeld Skill download exceeds the size limit");
|
|
391
|
+
chunks.push(chunk);
|
|
392
|
+
}
|
|
393
|
+
const selected = selectOfficialSkillRelease(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
394
|
+
if (!selected) return "incompatible";
|
|
395
|
+
return withSkillsInstallLock(async () => {
|
|
396
|
+
const cached = await readCachedRelease();
|
|
397
|
+
if (cached && cached.payload.revision >= selected.payload.revision) return "current";
|
|
398
|
+
await mkdir(openMeldRootDir(), { recursive: true });
|
|
399
|
+
const cache = join(openMeldRootDir(), CACHE_FILE);
|
|
400
|
+
const staged = `${cache}.${randomUUID()}.tmp`;
|
|
401
|
+
try {
|
|
402
|
+
await writeFile(staged, JSON.stringify(selected.release), {
|
|
403
|
+
flag: "wx",
|
|
404
|
+
mode: 384
|
|
405
|
+
});
|
|
406
|
+
await rename(staged, cache);
|
|
407
|
+
} finally {
|
|
408
|
+
await rm(staged, { force: true });
|
|
409
|
+
}
|
|
410
|
+
return "updated";
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/skills/replace-directory.ts
|
|
415
|
+
async function recoverSkillReplacement(target) {
|
|
416
|
+
const previous = join(dirname(target), `.${basename(target)}.openmeld-previous`);
|
|
417
|
+
if (!await exists(previous)) return;
|
|
418
|
+
if (!await isManagedReplacement(previous, basename(target))) throw new Error("OpenMeld Skill backup conflicts with an unmanaged path");
|
|
419
|
+
if (await exists(target)) {
|
|
420
|
+
if (!await isManagedReplacement(target, basename(target))) throw new Error("OpenMeld Skill recovery would affect an unmanaged path");
|
|
421
|
+
await rm(previous, {
|
|
422
|
+
recursive: true,
|
|
423
|
+
force: true
|
|
424
|
+
});
|
|
425
|
+
} else await rename(previous, target);
|
|
426
|
+
}
|
|
427
|
+
async function replaceSkillDirectory(target, prepare) {
|
|
428
|
+
await recoverSkillReplacement(target);
|
|
429
|
+
const staged = join(dirname(target), `.${basename(target)}.openmeld-staged-${randomUUID()}`);
|
|
430
|
+
const previous = join(dirname(target), `.${basename(target)}.openmeld-previous`);
|
|
431
|
+
try {
|
|
432
|
+
await prepare(staged);
|
|
433
|
+
const hadPrevious = await exists(target);
|
|
434
|
+
if (hadPrevious) await rename(target, previous);
|
|
435
|
+
try {
|
|
436
|
+
await rename(staged, target);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (hadPrevious) await rename(previous, target);
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
await rm(previous, {
|
|
442
|
+
recursive: true,
|
|
443
|
+
force: true
|
|
444
|
+
}).catch(() => void 0);
|
|
445
|
+
} finally {
|
|
446
|
+
await rm(staged, {
|
|
447
|
+
recursive: true,
|
|
448
|
+
force: true
|
|
449
|
+
}).catch(() => void 0);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function isManagedReplacement(path, skillId) {
|
|
453
|
+
const raw = await readFile(join(path, ".openmeld-skill.json"), "utf8").catch(() => null);
|
|
454
|
+
if (raw) try {
|
|
455
|
+
const marker = JSON.parse(raw);
|
|
456
|
+
return marker.managedBy === "openmeld" && marker.skillId === skillId;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if ((await lstat(path)).isSymbolicLink()) {
|
|
461
|
+
const destination = resolve(dirname(path), await readlink(path));
|
|
462
|
+
return basename(destination) === skillId && !await exists(destination);
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
function exists(path) {
|
|
467
|
+
return lstat(path).then(() => true, (error) => {
|
|
468
|
+
if (error.code === "ENOENT") return false;
|
|
469
|
+
throw error;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/skills/store.ts
|
|
474
|
+
const OPENMELD_SKILL_MARKER_FILE = ".openmeld-skill.json";
|
|
475
|
+
const OPENMELD_CLI_SKILL_FILE = "SKILL.md";
|
|
476
|
+
const SKILL_NAME_REGEX = /^name:\s*(.+)$/m;
|
|
477
|
+
function ensureLocalSkills() {
|
|
478
|
+
return withSkillsInstallLock(ensureLocalSkillsLocked);
|
|
479
|
+
}
|
|
480
|
+
async function ensureLocalSkillsLocked() {
|
|
481
|
+
const storePath = skillsDir();
|
|
482
|
+
await mkdir(storePath, { recursive: true });
|
|
483
|
+
const { assets, version: bundledVersion } = await loadSelectedSkillAssets();
|
|
484
|
+
if (assets.length === 0) return listLocalSkills();
|
|
485
|
+
const skillGroups = groupAssetsBySkill(assets);
|
|
486
|
+
for (const [skillId, skillAssets] of skillGroups) {
|
|
487
|
+
const targetDir = join(storePath, skillId);
|
|
488
|
+
await recoverSkillReplacement(targetDir);
|
|
489
|
+
const sourceContentHash = hashBundledSkillAssets(skillAssets);
|
|
490
|
+
const existingMarker = await readOpenMeldSkillMarker(targetDir);
|
|
491
|
+
const existing = await lstat(targetDir).catch((error) => {
|
|
492
|
+
if (error.code === "ENOENT") return null;
|
|
493
|
+
throw error;
|
|
494
|
+
});
|
|
495
|
+
if (existing && (!existingMarker || existing.isSymbolicLink())) throw new Error(`OpenMeld Skill source conflicts with an unmanaged path: ${targetDir}`);
|
|
496
|
+
if (existingMarker?.managedBy === "openmeld" && existingMarker.skillId === skillId && existingMarker.mode === "source" && existingMarker.contentHash === sourceContentHash && existingMarker.version === bundledVersion && await computeDirectoryContentHash({
|
|
497
|
+
rootDir: targetDir,
|
|
498
|
+
ignoredNames: [".openmeld-skill.json"]
|
|
499
|
+
}) === sourceContentHash) continue;
|
|
500
|
+
await replaceSkillDirectory(targetDir, async (stagedDir) => {
|
|
501
|
+
for (const asset of skillAssets) {
|
|
502
|
+
const filePath = join(stagedDir, asset.relativePath);
|
|
503
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
504
|
+
await writeFile(filePath, asset.content);
|
|
505
|
+
}
|
|
506
|
+
await writeOpenMeldSkillMarker({
|
|
507
|
+
skillDir: stagedDir,
|
|
508
|
+
skillId,
|
|
509
|
+
mode: "source",
|
|
510
|
+
contentHash: sourceContentHash,
|
|
511
|
+
version: bundledVersion
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return listLocalSkills();
|
|
516
|
+
}
|
|
517
|
+
function groupAssetsBySkill(assets) {
|
|
518
|
+
const groups = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const asset of assets) {
|
|
520
|
+
const slashIndex = asset.relativePath.indexOf("/");
|
|
521
|
+
if (slashIndex < 0) continue;
|
|
522
|
+
const skillId = asset.relativePath.slice(0, slashIndex);
|
|
523
|
+
const skillRelativePath = asset.relativePath.slice(slashIndex + 1);
|
|
524
|
+
const group = groups.get(skillId) ?? [];
|
|
525
|
+
group.push({
|
|
526
|
+
relativePath: skillRelativePath,
|
|
527
|
+
content: asset.content
|
|
528
|
+
});
|
|
529
|
+
groups.set(skillId, group);
|
|
530
|
+
}
|
|
531
|
+
for (const [skillId, group] of [...groups.entries()]) if (group.some((asset) => asset.relativePath === OPENMELD_CLI_SKILL_FILE)) groups.set(skillId, sortBundledSkillAssets(group));
|
|
532
|
+
else groups.delete(skillId);
|
|
533
|
+
return groups;
|
|
534
|
+
}
|
|
535
|
+
async function listLocalSkills() {
|
|
536
|
+
const storePath = skillsDir();
|
|
537
|
+
const skillDirs = await discoverSkillDirs(storePath, 0, 6);
|
|
538
|
+
const skills = [];
|
|
539
|
+
for (const skillDir of skillDirs) {
|
|
540
|
+
const raw = await readFile(join(skillDir, "SKILL.md"), "utf8").catch(() => "");
|
|
541
|
+
if (!raw) continue;
|
|
542
|
+
const skillId = path.basename(skillDir);
|
|
543
|
+
const skillName = readSkillName(raw) ?? skillId;
|
|
544
|
+
const marker = await readOpenMeldSkillMarker(skillDir);
|
|
545
|
+
skills.push({
|
|
546
|
+
id: skillId,
|
|
547
|
+
name: skillName,
|
|
548
|
+
absolutePath: skillDir,
|
|
549
|
+
relativePath: relative(storePath, skillDir) || skillId,
|
|
550
|
+
...marker?.contentHash ? { contentHash: marker.contentHash } : {},
|
|
551
|
+
...marker?.version ? { version: marker.version } : {}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
skills.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
555
|
+
return skills;
|
|
556
|
+
}
|
|
557
|
+
async function writeOpenMeldSkillMarker(input) {
|
|
558
|
+
const markerPath = join(input.skillDir, OPENMELD_SKILL_MARKER_FILE);
|
|
559
|
+
const marker = {
|
|
560
|
+
managedBy: "openmeld",
|
|
561
|
+
skillId: input.skillId,
|
|
562
|
+
mode: input.mode,
|
|
563
|
+
...input.contentHash ? { contentHash: input.contentHash } : {},
|
|
564
|
+
...input.version ? { version: input.version } : {},
|
|
565
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
566
|
+
};
|
|
567
|
+
await writeFile(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
568
|
+
}
|
|
569
|
+
async function isOpenMeldManagedSkillDir(skillDir) {
|
|
570
|
+
return (await readOpenMeldSkillMarker(skillDir))?.managedBy === "openmeld";
|
|
571
|
+
}
|
|
572
|
+
async function readOpenMeldSkillMarker(skillDir) {
|
|
573
|
+
const raw = await readFile(join(skillDir, OPENMELD_SKILL_MARKER_FILE), "utf8").catch(() => null);
|
|
574
|
+
if (!raw) return null;
|
|
575
|
+
try {
|
|
576
|
+
const parsed = JSON.parse(raw);
|
|
577
|
+
if (parsed.managedBy !== "openmeld") return null;
|
|
578
|
+
if (typeof parsed.skillId !== "string" || parsed.skillId.trim().length === 0) return null;
|
|
579
|
+
if (parsed.mode !== "source" && parsed.mode !== "symlink" && parsed.mode !== "copy") return null;
|
|
580
|
+
return {
|
|
581
|
+
managedBy: "openmeld",
|
|
582
|
+
skillId: parsed.skillId.trim(),
|
|
583
|
+
mode: parsed.mode,
|
|
584
|
+
...typeof parsed.contentHash === "string" && parsed.contentHash.trim().length > 0 ? { contentHash: parsed.contentHash.trim() } : {},
|
|
585
|
+
...typeof parsed.version === "string" && parsed.version.trim().length > 0 ? { version: parsed.version.trim() } : {},
|
|
586
|
+
updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.trim().length > 0 ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
587
|
+
};
|
|
588
|
+
} catch {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function discoverSkillDirs(dir, depth, maxDepth) {
|
|
593
|
+
if (depth > maxDepth) return [];
|
|
594
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
595
|
+
if (entries.length === 0) return [];
|
|
596
|
+
if (entries.some((entry) => entry.isFile() && entry.name === "SKILL.md")) return [dir];
|
|
597
|
+
return (await Promise.all(entries.filter((entry) => entry.isDirectory() && !entry.name.includes(".openmeld-")).map((entry) => discoverSkillDirs(join(dir, entry.name), depth + 1, maxDepth)))).flat();
|
|
598
|
+
}
|
|
599
|
+
function readSkillName(rawSkillFile) {
|
|
600
|
+
const match = rawSkillFile.match(SKILL_NAME_REGEX);
|
|
601
|
+
if (!match) return;
|
|
602
|
+
const candidate = match[1]?.trim();
|
|
603
|
+
return candidate && candidate.length > 0 ? candidate : void 0;
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/skills/state.ts
|
|
607
|
+
async function resolveSkillsInstallStates(input) {
|
|
608
|
+
const states = [];
|
|
609
|
+
for (const target of input.targets) for (const skill of input.skills) states.push(await resolveSkillInstallStateForTarget({
|
|
610
|
+
skill,
|
|
611
|
+
target
|
|
612
|
+
}));
|
|
613
|
+
return states;
|
|
614
|
+
}
|
|
615
|
+
async function resolveSkillInstallStateForTarget(input) {
|
|
616
|
+
const base = createBaseResult(input);
|
|
617
|
+
const source = await resolveSourceSnapshot(input.skill);
|
|
618
|
+
const target = await resolveTargetSnapshot(base.skillDir);
|
|
619
|
+
if (!target.exists) return createStateResult({
|
|
620
|
+
base,
|
|
621
|
+
source,
|
|
622
|
+
state: "missing"
|
|
623
|
+
});
|
|
624
|
+
if (target.marker?.managedBy !== "openmeld") {
|
|
625
|
+
if (isLegacyOpenMeldSkillSymlink({
|
|
626
|
+
skillId: input.skill.id,
|
|
627
|
+
target
|
|
628
|
+
})) return createStateResult({
|
|
629
|
+
base,
|
|
630
|
+
source,
|
|
631
|
+
state: "outdated",
|
|
632
|
+
targetMode: "symlink",
|
|
633
|
+
reason: "legacy OpenMeld skill link found; update required"
|
|
634
|
+
});
|
|
635
|
+
return createStateResult({
|
|
636
|
+
base,
|
|
637
|
+
source,
|
|
638
|
+
state: "external_conflict",
|
|
639
|
+
reason: "existing non-OpenMeld skill detected; skipped to avoid overwrite"
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (!target.isSymlink && target.marker.contentHash) {
|
|
643
|
+
if (!(await lstat(join(base.skillDir, "SKILL.md")).catch(() => null))?.isFile()) return createStateResult({
|
|
644
|
+
base,
|
|
645
|
+
source,
|
|
646
|
+
state: "outdated",
|
|
647
|
+
targetMarker: target.marker,
|
|
648
|
+
reason: "OpenMeld Skill entry is missing"
|
|
649
|
+
});
|
|
650
|
+
if (await computeDirectoryContentHash({
|
|
651
|
+
rootDir: base.skillDir,
|
|
652
|
+
ignoredNames: [".openmeld-skill.json"]
|
|
653
|
+
}) !== target.marker.contentHash) return createStateResult({
|
|
654
|
+
base,
|
|
655
|
+
source,
|
|
656
|
+
state: "external_conflict",
|
|
657
|
+
targetMarker: target.marker,
|
|
658
|
+
reason: "OpenMeld Skill files were modified locally; kept your changes"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (isUpToDate({
|
|
662
|
+
source,
|
|
663
|
+
targetMarker: target.marker,
|
|
664
|
+
isSymlink: target.isSymlink,
|
|
665
|
+
symlinkTargetPath: target.symlinkTargetPath,
|
|
666
|
+
sourceSkillDir: input.skill.absolutePath
|
|
667
|
+
})) return createStateResult({
|
|
668
|
+
base,
|
|
669
|
+
source,
|
|
670
|
+
targetMarker: target.marker,
|
|
671
|
+
state: "up_to_date",
|
|
672
|
+
reason: "already up-to-date"
|
|
673
|
+
});
|
|
674
|
+
return createStateResult({
|
|
675
|
+
base,
|
|
676
|
+
source,
|
|
677
|
+
targetMarker: target.marker,
|
|
678
|
+
state: "outdated",
|
|
679
|
+
reason: target.isSymlink ? "OpenMeld skill link points to a different OpenMeld skills store; update required" : "OpenMeld skill content changed; update required"
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function createBaseResult(input) {
|
|
683
|
+
const skillDir = join(input.target.dir, input.skill.id);
|
|
684
|
+
return {
|
|
685
|
+
targetId: input.target.id,
|
|
686
|
+
targetDir: input.target.dir,
|
|
687
|
+
targetType: input.target.type,
|
|
688
|
+
skillId: input.skill.id,
|
|
689
|
+
skillDir
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function resolveSourceSnapshot(skill) {
|
|
693
|
+
const sourceMarker = await readOpenMeldSkillMarker(skill.absolutePath);
|
|
694
|
+
return {
|
|
695
|
+
...skill.contentHash ?? sourceMarker?.contentHash ? { hash: skill.contentHash ?? sourceMarker?.contentHash } : {},
|
|
696
|
+
...skill.version ?? sourceMarker?.version ? { version: skill.version ?? sourceMarker?.version } : {}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function resolveTargetSnapshot(skillDir) {
|
|
700
|
+
const stats = await lstat(skillDir).catch(() => null);
|
|
701
|
+
if (!stats) return { exists: false };
|
|
702
|
+
let marker = await readOpenMeldSkillMarker(skillDir);
|
|
703
|
+
let symlinkTargetPath = "";
|
|
704
|
+
if (stats.isSymbolicLink()) {
|
|
705
|
+
symlinkTargetPath = await resolveSymlinkTarget(skillDir).catch(() => "");
|
|
706
|
+
if (!marker && symlinkTargetPath) marker = await readOpenMeldSkillMarker(symlinkTargetPath);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
exists: true,
|
|
710
|
+
isSymlink: stats.isSymbolicLink(),
|
|
711
|
+
marker,
|
|
712
|
+
symlinkTargetPath,
|
|
713
|
+
destinationExists: symlinkTargetPath ? Boolean(await lstat(symlinkTargetPath).catch(() => null)) : true
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function isLegacyOpenMeldSkillSymlink(input) {
|
|
717
|
+
if (!(input.target.isSymlink && input.target.symlinkTargetPath) || input.target.destinationExists) return false;
|
|
718
|
+
return basename(input.target.symlinkTargetPath) === input.skillId;
|
|
719
|
+
}
|
|
720
|
+
function isUpToDate(input) {
|
|
721
|
+
if (input.isSymlink) return input.symlinkTargetPath.length > 0 && pathsEqual(input.symlinkTargetPath, input.sourceSkillDir);
|
|
722
|
+
return hasMatchingHashAndVersion(input.source, input.targetMarker);
|
|
723
|
+
}
|
|
724
|
+
function hasMatchingHashAndVersion(source, targetMarker) {
|
|
725
|
+
if (!(source.hash && targetMarker.contentHash && source.hash === targetMarker.contentHash)) return false;
|
|
726
|
+
return !(source.version && targetMarker.version) || source.version === targetMarker.version;
|
|
727
|
+
}
|
|
728
|
+
function createStateResult(input) {
|
|
729
|
+
return {
|
|
730
|
+
...input.base,
|
|
731
|
+
state: input.state,
|
|
732
|
+
...input.source.hash ? { sourceHash: input.source.hash } : {},
|
|
733
|
+
...input.source.version ? { sourceVersion: input.source.version } : {},
|
|
734
|
+
...input.targetMarker?.contentHash ? { targetHash: input.targetMarker.contentHash } : {},
|
|
735
|
+
...input.targetMarker?.mode ? { targetMode: input.targetMarker.mode } : {},
|
|
736
|
+
...input.targetMode ? { targetMode: input.targetMode } : {},
|
|
737
|
+
...input.targetMarker?.version ? { targetVersion: input.targetMarker.version } : {},
|
|
738
|
+
...input.reason ? { reason: input.reason } : {}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
async function resolveSymlinkTarget(linkPath) {
|
|
742
|
+
const target = await readlink(linkPath);
|
|
743
|
+
return resolve(dirname(linkPath), target);
|
|
744
|
+
}
|
|
745
|
+
function pathsEqual(left, right) {
|
|
746
|
+
return resolve(left) === resolve(right);
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/skills/installation-inventory.ts
|
|
750
|
+
async function collectSkillsReadinessSnapshot() {
|
|
751
|
+
const skillsConfig = await getConfiguredSkillsConfig();
|
|
752
|
+
const registry = resolveSkillsAgentRegistry(skillsConfig);
|
|
753
|
+
const installedAgents = await detectInstalledSkillsAgents(registry);
|
|
754
|
+
const localSkills = await ensureLocalSkills();
|
|
755
|
+
const checkTargets = mergeTargetsByDir({
|
|
756
|
+
primary: buildAgentTargets(installedAgents, registry),
|
|
757
|
+
secondary: await resolveTrackedInstallTargets({
|
|
758
|
+
skillsConfig,
|
|
759
|
+
registry,
|
|
760
|
+
localSkills,
|
|
761
|
+
onlyOpenMeldInstalledTargets: false
|
|
762
|
+
})
|
|
763
|
+
});
|
|
764
|
+
const states = await resolveSkillsInstallStates({
|
|
765
|
+
skills: localSkills,
|
|
766
|
+
targets: checkTargets
|
|
767
|
+
});
|
|
768
|
+
await persistDetectedInstallStateFromCheck({ states });
|
|
769
|
+
const summary = summarizeSkillState(states);
|
|
770
|
+
const hasIssues = summary.outdated > 0 || summary.missing > 0 || summary.externalConflict > 0;
|
|
771
|
+
let status = "up_to_date";
|
|
772
|
+
if (checkTargets.length === 0) status = "no_targets";
|
|
773
|
+
else if (hasIssues) status = "action_required";
|
|
774
|
+
return {
|
|
775
|
+
status,
|
|
776
|
+
targetCount: checkTargets.length,
|
|
777
|
+
summary
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function buildAgentTargets(agents, registry) {
|
|
781
|
+
return agents.flatMap((agentName) => {
|
|
782
|
+
const config = registry[agentName];
|
|
783
|
+
if (!config?.globalSkillsDir || config.skillsInstallable !== true) return [];
|
|
784
|
+
return [{
|
|
785
|
+
id: agentName,
|
|
786
|
+
dir: config.globalSkillsDir,
|
|
787
|
+
type: "agent"
|
|
788
|
+
}];
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function isLikelyCustomTargetId(value) {
|
|
792
|
+
return value.startsWith("custom-");
|
|
793
|
+
}
|
|
794
|
+
function resolveTrackedInstallTargets(input) {
|
|
795
|
+
const installs = input.skillsConfig?.installs ?? [];
|
|
796
|
+
const customTargetByDir = buildCustomTargetByDir(input.skillsConfig?.customTargets ?? []);
|
|
797
|
+
const byTargetDir = /* @__PURE__ */ new Map();
|
|
798
|
+
seedTrackedTargetsFromCustomTargets({
|
|
799
|
+
byTargetDir,
|
|
800
|
+
customTargetByDir
|
|
801
|
+
});
|
|
802
|
+
seedTrackedTargetsFromInstallRecords({
|
|
803
|
+
byTargetDir,
|
|
804
|
+
installs,
|
|
805
|
+
registry: input.registry,
|
|
806
|
+
customTargetByDir
|
|
807
|
+
});
|
|
808
|
+
const candidates = sortTrackedTargets([...byTargetDir.values()]);
|
|
809
|
+
return filterTrackedTargetsByState({
|
|
810
|
+
candidates: canManageRegistryAgentSkillTargets() ? candidates : candidates.filter((target) => target.type === "custom" || input.registry[target.id]?.globalSkillsDirSource === "config-override"),
|
|
811
|
+
localSkills: input.localSkills,
|
|
812
|
+
onlyOpenMeldInstalledTargets: input.onlyOpenMeldInstalledTargets
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function buildCustomTargetByDir(customTargets) {
|
|
816
|
+
const customTargetByDir = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const customTarget of customTargets) {
|
|
818
|
+
const id = resolveRuntimeSkillsCustomTargetId({
|
|
819
|
+
dir: customTarget.dir,
|
|
820
|
+
...customTarget.id ? { id: customTarget.id } : {}
|
|
821
|
+
});
|
|
822
|
+
customTargetByDir.set(customTarget.dir, {
|
|
823
|
+
id,
|
|
824
|
+
name: customTarget.name,
|
|
825
|
+
dir: customTarget.dir
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return customTargetByDir;
|
|
829
|
+
}
|
|
830
|
+
function seedTrackedTargetsFromCustomTargets(input) {
|
|
831
|
+
for (const customTarget of input.customTargetByDir.values()) input.byTargetDir.set(customTarget.dir, {
|
|
832
|
+
id: customTarget.id ?? resolveRuntimeSkillsCustomTargetId({ dir: customTarget.dir }),
|
|
833
|
+
dir: customTarget.dir,
|
|
834
|
+
type: "custom"
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function seedTrackedTargetsFromInstallRecords(input) {
|
|
838
|
+
for (const install of input.installs) {
|
|
839
|
+
const trackedTarget = buildTrackedTargetFromInstallRecord({
|
|
840
|
+
install,
|
|
841
|
+
registry: input.registry,
|
|
842
|
+
customTargetByDir: input.customTargetByDir
|
|
843
|
+
});
|
|
844
|
+
input.byTargetDir.set(install.targetDir, trackedTarget);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function buildTrackedTargetFromInstallRecord(input) {
|
|
848
|
+
const knownAgentDir = input.registry[input.install.targetId]?.globalSkillsDir;
|
|
849
|
+
if ((knownAgentDir && knownAgentDir === input.install.targetDir ? "agent" : "custom") === "agent") return {
|
|
850
|
+
id: input.install.targetId,
|
|
851
|
+
dir: input.install.targetDir,
|
|
852
|
+
type: "agent"
|
|
853
|
+
};
|
|
854
|
+
const customTarget = input.customTargetByDir.get(input.install.targetDir);
|
|
855
|
+
const fallbackCustomId = isLikelyCustomTargetId(input.install.targetId) ? input.install.targetId : void 0;
|
|
856
|
+
const resolvedCustomId = customTarget?.id ?? fallbackCustomId;
|
|
857
|
+
return {
|
|
858
|
+
id: resolveRuntimeSkillsCustomTargetId({
|
|
859
|
+
dir: input.install.targetDir,
|
|
860
|
+
...resolvedCustomId ? { id: resolvedCustomId } : {}
|
|
861
|
+
}),
|
|
862
|
+
dir: input.install.targetDir,
|
|
863
|
+
type: "custom"
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function sortTrackedTargets(targets) {
|
|
867
|
+
return [...targets].sort((a, b) => `${a.id}:${a.dir}`.localeCompare(`${b.id}:${b.dir}`));
|
|
868
|
+
}
|
|
869
|
+
function mergeTargetsByDir(input) {
|
|
870
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const target of input.secondary) byDir.set(target.dir, target);
|
|
872
|
+
for (const target of input.primary) byDir.set(target.dir, target);
|
|
873
|
+
return sortTrackedTargets([...byDir.values()]);
|
|
874
|
+
}
|
|
875
|
+
async function filterInstalledTrackedTargets(input) {
|
|
876
|
+
const installedOnly = [];
|
|
877
|
+
for (const candidate of input.candidates) {
|
|
878
|
+
if (!await targetHasInstalledOpenMeldSkill({
|
|
879
|
+
target: candidate,
|
|
880
|
+
localSkills: input.localSkills
|
|
881
|
+
})) continue;
|
|
882
|
+
installedOnly.push(candidate);
|
|
883
|
+
}
|
|
884
|
+
return installedOnly;
|
|
885
|
+
}
|
|
886
|
+
async function filterTargetsWithInstalledOpenMeldSkills(input) {
|
|
887
|
+
return filterInstalledTrackedTargets({
|
|
888
|
+
candidates: await filterExistingTargetDirs(input.targets),
|
|
889
|
+
localSkills: input.localSkills
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
async function filterTrackedTargetsByState(input) {
|
|
893
|
+
const existingTargets = await filterExistingTargetDirs(input.candidates);
|
|
894
|
+
if (!input.onlyOpenMeldInstalledTargets) return existingTargets;
|
|
895
|
+
return filterInstalledTrackedTargets({
|
|
896
|
+
candidates: existingTargets,
|
|
897
|
+
localSkills: input.localSkills
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
async function filterExistingTargetDirs(candidates) {
|
|
901
|
+
const existing = [];
|
|
902
|
+
for (const candidate of candidates) {
|
|
903
|
+
if (!(await stat(candidate.dir).catch(() => null))?.isDirectory()) continue;
|
|
904
|
+
existing.push(candidate);
|
|
905
|
+
}
|
|
906
|
+
return existing;
|
|
907
|
+
}
|
|
908
|
+
function summarizeSkillState(states) {
|
|
909
|
+
return {
|
|
910
|
+
upToDate: states.filter((state) => state.state === "up_to_date").length,
|
|
911
|
+
outdated: states.filter((state) => state.state === "outdated").length,
|
|
912
|
+
missing: states.filter((state) => state.state === "missing").length,
|
|
913
|
+
externalConflict: states.filter((state) => state.state === "external_conflict").length
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async function targetHasInstalledOpenMeldSkill(input) {
|
|
917
|
+
if (!(await stat(input.target.dir).catch(() => null))?.isDirectory()) return false;
|
|
918
|
+
for (const skill of input.localSkills) {
|
|
919
|
+
const state = await resolveSkillInstallStateForTarget({
|
|
920
|
+
skill,
|
|
921
|
+
target: input.target
|
|
922
|
+
});
|
|
923
|
+
if (state.state === "up_to_date" || state.state === "outdated") return true;
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
async function persistDetectedInstallStateFromCheck(input) {
|
|
928
|
+
const discoveredByTargetDir = /* @__PURE__ */ new Map();
|
|
929
|
+
for (const state of input.states) {
|
|
930
|
+
if (!(state.state === "up_to_date" || state.state === "outdated")) continue;
|
|
931
|
+
const list = discoveredByTargetDir.get(state.targetDir) ?? [];
|
|
932
|
+
list.push(state);
|
|
933
|
+
discoveredByTargetDir.set(state.targetDir, list);
|
|
934
|
+
}
|
|
935
|
+
if (discoveredByTargetDir.size === 0) return;
|
|
936
|
+
await updateConfiguredSkillsConfig((currentSkillsConfig) => {
|
|
937
|
+
const byTargetDir = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const record of currentSkillsConfig?.installs ?? []) byTargetDir.set(record.targetDir, record);
|
|
939
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
940
|
+
for (const [targetDir, group] of discoveredByTargetDir) {
|
|
941
|
+
const first = group[0];
|
|
942
|
+
if (!first) continue;
|
|
943
|
+
const mode = group.some((state) => state.targetMode === "copy") ? "copy" : "symlink";
|
|
944
|
+
const skillIds = Array.from(new Set(group.map((state) => state.skillId))).sort((a, b) => a.localeCompare(b));
|
|
945
|
+
byTargetDir.set(targetDir, {
|
|
946
|
+
targetId: first.targetId,
|
|
947
|
+
targetDir,
|
|
948
|
+
mode,
|
|
949
|
+
status: "installed",
|
|
950
|
+
skillIds,
|
|
951
|
+
updatedAt: now
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const nextInstalls = [...byTargetDir.values()].sort(compareInstallRecords);
|
|
955
|
+
const previousInstalls = [...currentSkillsConfig?.installs ?? []].sort(compareInstallRecords).map(normalizeInstallRecordForCompare);
|
|
956
|
+
const nextComparable = nextInstalls.map(normalizeInstallRecordForCompare);
|
|
957
|
+
if (JSON.stringify(previousInstalls) === JSON.stringify(nextComparable)) return currentSkillsConfig;
|
|
958
|
+
return {
|
|
959
|
+
storeDir: currentSkillsConfig?.storeDir ?? skillsDir(),
|
|
960
|
+
...currentSkillsConfig?.agents ? { agents: currentSkillsConfig.agents } : {},
|
|
961
|
+
...currentSkillsConfig?.customTargets ? { customTargets: currentSkillsConfig.customTargets } : {},
|
|
962
|
+
installs: nextInstalls
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
function compareInstallRecords(left, right) {
|
|
967
|
+
const byTargetId = left.targetId.localeCompare(right.targetId);
|
|
968
|
+
if (byTargetId !== 0) return byTargetId;
|
|
969
|
+
return left.targetDir.localeCompare(right.targetDir);
|
|
970
|
+
}
|
|
971
|
+
function normalizeInstallRecordForCompare(input) {
|
|
972
|
+
return {
|
|
973
|
+
targetId: input.targetId,
|
|
974
|
+
targetDir: input.targetDir,
|
|
975
|
+
mode: input.mode,
|
|
976
|
+
status: input.status,
|
|
977
|
+
skillIds: [...input.skillIds].sort((a, b) => a.localeCompare(b)),
|
|
978
|
+
...input.reason ? { reason: input.reason } : {}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/skills/installer.ts
|
|
983
|
+
function installSkillsToTargets(input) {
|
|
984
|
+
return withSkillsInstallLock(() => installSkillsToTargetsLocked(input));
|
|
985
|
+
}
|
|
986
|
+
async function installSkillsToTargetsLocked(input) {
|
|
987
|
+
const results = [];
|
|
988
|
+
for (const target of input.targets) for (const skill of input.skills) {
|
|
989
|
+
const result = await installOneSkillToTarget({
|
|
990
|
+
skill,
|
|
991
|
+
target
|
|
992
|
+
});
|
|
993
|
+
results.push(result);
|
|
994
|
+
}
|
|
995
|
+
return results;
|
|
996
|
+
}
|
|
997
|
+
async function installOneSkillToTarget(input) {
|
|
998
|
+
const targetDir = input.target.dir;
|
|
999
|
+
const failInstall = (reason) => ({
|
|
1000
|
+
targetId: input.target.id,
|
|
1001
|
+
targetDir,
|
|
1002
|
+
targetType: input.target.type,
|
|
1003
|
+
skillId: input.skill.id,
|
|
1004
|
+
status: "failed",
|
|
1005
|
+
mode: "unknown",
|
|
1006
|
+
degraded: false,
|
|
1007
|
+
reason
|
|
1008
|
+
});
|
|
1009
|
+
if (!isAbsolute(targetDir)) return failInstall("target dir must be an absolute path");
|
|
1010
|
+
const skillId = input.skill.id;
|
|
1011
|
+
if (skillId.trim().length === 0 || isAbsolute(skillId) || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\")) return failInstall("skill id would escape target dir");
|
|
1012
|
+
const resolvedTargetDir = resolve(targetDir);
|
|
1013
|
+
const resolvedSkillDir = resolve(resolvedTargetDir, skillId);
|
|
1014
|
+
const skillRelativePath = relative(resolvedTargetDir, resolvedSkillDir);
|
|
1015
|
+
if (skillRelativePath === ".." || skillRelativePath.startsWith("../") || skillRelativePath.startsWith("..\\")) return failInstall("skill id would escape target dir");
|
|
1016
|
+
const skillTargetDir = resolvedSkillDir;
|
|
1017
|
+
await mkdir(targetDir, { recursive: true });
|
|
1018
|
+
await recoverSkillReplacement(skillTargetDir);
|
|
1019
|
+
const state = await resolveSkillInstallStateForTarget({
|
|
1020
|
+
skill: input.skill,
|
|
1021
|
+
target: input.target
|
|
1022
|
+
});
|
|
1023
|
+
if (state.state === "external_conflict") return {
|
|
1024
|
+
targetId: input.target.id,
|
|
1025
|
+
targetDir,
|
|
1026
|
+
targetType: input.target.type,
|
|
1027
|
+
skillId: input.skill.id,
|
|
1028
|
+
status: "skipped",
|
|
1029
|
+
mode: resolveSkippedInstallMode(state),
|
|
1030
|
+
degraded: false,
|
|
1031
|
+
reason: state.reason
|
|
1032
|
+
};
|
|
1033
|
+
if (state.state === "up_to_date") return {
|
|
1034
|
+
targetId: input.target.id,
|
|
1035
|
+
targetDir,
|
|
1036
|
+
targetType: input.target.type,
|
|
1037
|
+
skillId: input.skill.id,
|
|
1038
|
+
status: "skipped",
|
|
1039
|
+
mode: resolveSkippedInstallMode(state),
|
|
1040
|
+
degraded: false,
|
|
1041
|
+
reason: state.reason
|
|
1042
|
+
};
|
|
1043
|
+
const installation = { mode: "symlink" };
|
|
1044
|
+
try {
|
|
1045
|
+
await readFile(join(input.skill.absolutePath, "SKILL.md"), "utf8");
|
|
1046
|
+
await replaceSkillDirectory(skillTargetDir, async (stagedDir) => {
|
|
1047
|
+
if (await tryCreateSymlink({
|
|
1048
|
+
sourceDir: input.skill.absolutePath,
|
|
1049
|
+
targetDir: stagedDir
|
|
1050
|
+
})) return;
|
|
1051
|
+
installation.mode = "copy";
|
|
1052
|
+
await cp(input.skill.absolutePath, stagedDir, {
|
|
1053
|
+
recursive: true,
|
|
1054
|
+
errorOnExist: true,
|
|
1055
|
+
force: false
|
|
1056
|
+
});
|
|
1057
|
+
await writeOpenMeldSkillMarker({
|
|
1058
|
+
skillDir: stagedDir,
|
|
1059
|
+
skillId: input.skill.id,
|
|
1060
|
+
mode: "copy",
|
|
1061
|
+
...input.skill.contentHash ? { contentHash: input.skill.contentHash } : {},
|
|
1062
|
+
...input.skill.version ? { version: input.skill.version } : {}
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
return {
|
|
1066
|
+
targetId: input.target.id,
|
|
1067
|
+
targetDir,
|
|
1068
|
+
targetType: input.target.type,
|
|
1069
|
+
skillId: input.skill.id,
|
|
1070
|
+
status: "installed",
|
|
1071
|
+
mode: installation.mode,
|
|
1072
|
+
degraded: installation.mode === "copy",
|
|
1073
|
+
...installation.mode === "copy" ? { reason: "symlink failed; fell back to copy" } : {}
|
|
1074
|
+
};
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return failInstall(error instanceof Error ? error.message : "failed to install skill to target");
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function resolveSkippedInstallMode(state) {
|
|
1080
|
+
if (state.targetMode === "copy") return "copy";
|
|
1081
|
+
if (state.targetMode === "symlink") return "symlink";
|
|
1082
|
+
return "unknown";
|
|
1083
|
+
}
|
|
1084
|
+
async function tryCreateSymlink(input) {
|
|
1085
|
+
try {
|
|
1086
|
+
await mkdir(dirname(input.targetDir), { recursive: true });
|
|
1087
|
+
const targetParentDir = dirname(input.targetDir);
|
|
1088
|
+
const resolvedTargetParentDir = await realpath(targetParentDir).catch(() => targetParentDir);
|
|
1089
|
+
const resolvedSourceDir = await realpath(input.sourceDir).catch(() => input.sourceDir);
|
|
1090
|
+
const relativeTarget = relative(resolvedTargetParentDir, resolvedSourceDir);
|
|
1091
|
+
await symlink(process$1.platform === "win32" ? resolvedSourceDir : relativeTarget, input.targetDir, process$1.platform === "win32" ? "junction" : void 0);
|
|
1092
|
+
return true;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
//#endregion
|
|
1098
|
+
export { readSelectedSkillDocument as _, filterTargetsWithInstalledOpenMeldSkills as a, computeDirectoryContentHash as b, resolveTrackedInstallTargets as c, resolveSkillsInstallStates as d, ensureLocalSkills as f, loadSelectedSkillAssets as g, downloadOfficialSkillUpdate as h, compareInstallRecords as i, summarizeSkillState as l, readOpenMeldSkillMarker as m, buildAgentTargets as n, mergeTargetsByDir as o, isOpenMeldManagedSkillDir as p, collectSkillsReadinessSnapshot as r, persistDetectedInstallStateFromCheck as s, installSkillsToTargets as t, resolveSkillInstallStateForTarget as u, withSkillsInstallLock as v, OFFICIAL_CLI_SKILL_URL as x, hashBundledSkillAssets as y };
|
|
1099
|
+
|
|
1100
|
+
//# sourceMappingURL=installer-CLPF4DHx.js.map
|