openmeld 0.3.159 → 0.3.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{add-me-membership-jQsB5MBL.js → add-me-membership-zhgufZnd.js} +2 -2
  2. package/dist/{add-me-membership-jQsB5MBL.js.map → add-me-membership-zhgufZnd.js.map} +1 -1
  3. package/dist/{api-client-foundation-CoFjLc3o.js → api-client-foundation-C2Ji0552.js} +3 -3
  4. package/dist/{api-client-foundation-CoFjLc3o.js.map → api-client-foundation-C2Ji0552.js.map} +1 -1
  5. package/dist/{api-client-foundation-D3vgN2Jh.js → api-client-foundation-DoUewyi_.js} +1 -1
  6. package/dist/auth-session-DMovqZYd.js +4 -0
  7. package/dist/{auth-session-Da7OgTZk.js → auth-session-DnS-Axyz.js} +2 -2
  8. package/dist/{auth-session-Da7OgTZk.js.map → auth-session-DnS-Axyz.js.map} +1 -1
  9. package/dist/{command-CcqgRB4J.js → command-cgFedSaF.js} +81 -904
  10. package/dist/command-cgFedSaF.js.map +1 -0
  11. package/dist/{dist-KezFWl5t.js → dist-BVrzz3du.js} +36 -11
  12. package/dist/dist-BVrzz3du.js.map +1 -0
  13. package/dist/{dist-VKaBpS9J.js → dist-CGL0GrR6.js} +1 -1
  14. package/dist/{elapsed-time-BI7-NmU1.js → elapsed-time-0R9PAYtt.js} +3 -3
  15. package/dist/{elapsed-time-BI7-NmU1.js.map → elapsed-time-0R9PAYtt.js.map} +1 -1
  16. package/dist/installer-C3AuWZ_h.js +1100 -0
  17. package/dist/installer-C3AuWZ_h.js.map +1 -0
  18. package/dist/{openmeld-DsT7zl0R.js → openmeld-DmYjQa7n.js} +337 -68
  19. package/dist/openmeld-DmYjQa7n.js.map +1 -0
  20. package/dist/openmeld-maintenance-worker-Bfj_RQbe.js +78 -0
  21. package/dist/openmeld-maintenance-worker-Bfj_RQbe.js.map +1 -0
  22. package/dist/{openmeld-tools-update-coordinator-H16PNoPX.js → openmeld-tools-update-coordinator-CwIVD1Fx.js} +12 -12
  23. package/dist/{openmeld-tools-update-coordinator-H16PNoPX.js.map → openmeld-tools-update-coordinator-CwIVD1Fx.js.map} +1 -1
  24. package/dist/openmeld.js +3 -3
  25. package/dist/{runtime-transport-D6j3RJ5o.js → runtime-transport-BsvsUfwb.js} +7 -1
  26. package/dist/runtime-transport-BsvsUfwb.js.map +1 -0
  27. package/dist/upgrade-D-iPQ9qO.js +4 -0
  28. package/dist/{upgrade-BcmVKRpO.js → upgrade-hBZIXIcH.js} +4 -4
  29. package/dist/{upgrade-BcmVKRpO.js.map → upgrade-hBZIXIcH.js.map} +1 -1
  30. package/package.json +2 -2
  31. package/skills/openmeld-cli/SKILL.md +0 -9
  32. package/skills/openmeld-cli/playbooks/space-ops.md +8 -0
  33. package/skills/openmeld-cli/references/commands.md +26 -0
  34. package/dist/auth-session-DNXann3J.js +0 -4
  35. package/dist/command-CcqgRB4J.js.map +0 -1
  36. package/dist/dist-KezFWl5t.js.map +0 -1
  37. package/dist/openmeld-DsT7zl0R.js.map +0 -1
  38. package/dist/openmeld-maintenance-worker-Bmc0QSqk.js +0 -49
  39. package/dist/openmeld-maintenance-worker-Bmc0QSqk.js.map +0 -1
  40. package/dist/runtime-transport-D6j3RJ5o.js.map +0 -1
  41. 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-CwIVD1Fx.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-C3AuWZ_h.js.map