yeka-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/cli.js +1735 -0
- package/package.json +48 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands.ts
|
|
7
|
+
import { chmod as chmod2, lstat as lstat6, mkdir as mkdir4, mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
|
|
8
|
+
import os6 from "os";
|
|
9
|
+
import path7 from "path";
|
|
10
|
+
|
|
11
|
+
// ../contracts/dist/index.js
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var skillNamePattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
14
|
+
var opaqueShareIdPattern = /^[A-Za-z0-9_-]{22}$/;
|
|
15
|
+
var SkillNameSchema = z.string().regex(skillNamePattern);
|
|
16
|
+
var OpaqueShareIdSchema = z.string().regex(opaqueShareIdPattern);
|
|
17
|
+
var SkillLaneSchema = z.enum(["shared", "codex", "claude"]);
|
|
18
|
+
var RuntimeSchema = z.enum(["codex", "claude"]);
|
|
19
|
+
var SkillSourceSchema = z.object({
|
|
20
|
+
repository: z.string().min(1),
|
|
21
|
+
commit: z.string().regex(/^[0-9a-f]{40}$/),
|
|
22
|
+
path: z.string().min(1),
|
|
23
|
+
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
24
|
+
updatedAt: z.iso.datetime({ offset: true })
|
|
25
|
+
});
|
|
26
|
+
var SkillArtifactSchema = z.object({
|
|
27
|
+
key: z.string().startsWith("registry/v1/artifacts/"),
|
|
28
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
29
|
+
sizeBytes: z.number().int().positive().max(25 * 1024 * 1024)
|
|
30
|
+
});
|
|
31
|
+
var RegistrySkillSchema = z.object({
|
|
32
|
+
name: SkillNameSchema,
|
|
33
|
+
opaqueId: OpaqueShareIdSchema,
|
|
34
|
+
sourceDescription: z.string().min(1).max(4e3),
|
|
35
|
+
lane: SkillLaneSchema,
|
|
36
|
+
runtimes: z.array(RuntimeSchema).min(1).max(2),
|
|
37
|
+
source: SkillSourceSchema,
|
|
38
|
+
artifact: SkillArtifactSchema
|
|
39
|
+
});
|
|
40
|
+
var ShareIdMapSchema = z.object({
|
|
41
|
+
schemaVersion: z.literal(1),
|
|
42
|
+
skills: z.record(SkillNameSchema, OpaqueShareIdSchema)
|
|
43
|
+
});
|
|
44
|
+
var RegistryManifestSchema = z.object({
|
|
45
|
+
schemaVersion: z.literal(1),
|
|
46
|
+
generatedAt: z.iso.datetime({ offset: true }),
|
|
47
|
+
sourceRepository: z.string().min(1),
|
|
48
|
+
sourceCommit: z.string().regex(/^[0-9a-f]{40}$/),
|
|
49
|
+
skills: z.array(RegistrySkillSchema).min(1)
|
|
50
|
+
});
|
|
51
|
+
var InstallReceiptSchema = z.object({
|
|
52
|
+
schemaVersion: z.literal(1),
|
|
53
|
+
skillName: SkillNameSchema,
|
|
54
|
+
runtime: RuntimeSchema,
|
|
55
|
+
targetRoot: z.string().min(1),
|
|
56
|
+
installedPath: z.string().min(1),
|
|
57
|
+
sourceCommit: z.string().regex(/^[0-9a-f]{40}$/),
|
|
58
|
+
sourceHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
59
|
+
artifactSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
60
|
+
installedHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
61
|
+
installedAt: z.iso.datetime({ offset: true })
|
|
62
|
+
});
|
|
63
|
+
var TokenResponseSchema = z.object({
|
|
64
|
+
accessToken: z.string().min(32),
|
|
65
|
+
tokenType: z.literal("Bearer"),
|
|
66
|
+
expiresIn: z.number().int().positive().max(28800)
|
|
67
|
+
});
|
|
68
|
+
var ApiErrorSchema = z.object({
|
|
69
|
+
error: z.object({
|
|
70
|
+
code: z.string().min(1),
|
|
71
|
+
message: z.string().min(1),
|
|
72
|
+
requestId: z.string().uuid()
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// src/config.ts
|
|
77
|
+
import os from "os";
|
|
78
|
+
import path from "path";
|
|
79
|
+
var REGISTRY_ORIGIN = "https://registry.yekaskills.com";
|
|
80
|
+
var KEYCHAIN_SERVICE = "com.gemography.yeka-skills.cli";
|
|
81
|
+
var KEYCHAIN_ACCOUNT = "registry-session";
|
|
82
|
+
var localPaths = (home = os.homedir()) => {
|
|
83
|
+
const applicationSupport = path.join(home, "Library", "Application Support", "Yeka Skills");
|
|
84
|
+
return {
|
|
85
|
+
applicationSupport,
|
|
86
|
+
backups: path.join(applicationSupport, "backups"),
|
|
87
|
+
cache: path.join(home, "Library", "Caches", "Yeka Skills"),
|
|
88
|
+
receipts: path.join(applicationSupport, "receipts")
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/errors.ts
|
|
93
|
+
var CliError = class extends Error {
|
|
94
|
+
code;
|
|
95
|
+
exitCode;
|
|
96
|
+
constructor(code, message, options) {
|
|
97
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
98
|
+
this.name = "CliError";
|
|
99
|
+
this.code = code;
|
|
100
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var AuthenticationError = class extends CliError {
|
|
104
|
+
constructor(message = "Your Yeka Skills session has expired.") {
|
|
105
|
+
super("authentication_required", message);
|
|
106
|
+
this.name = "AuthenticationError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/installer.ts
|
|
111
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
112
|
+
import {
|
|
113
|
+
chmod,
|
|
114
|
+
cp,
|
|
115
|
+
lstat as lstat3,
|
|
116
|
+
mkdir as mkdir3,
|
|
117
|
+
mkdtemp,
|
|
118
|
+
rename as rename2,
|
|
119
|
+
rm as rm2,
|
|
120
|
+
stat
|
|
121
|
+
} from "fs/promises";
|
|
122
|
+
import os3 from "os";
|
|
123
|
+
import path5 from "path";
|
|
124
|
+
|
|
125
|
+
// ../integrity/dist/index.js
|
|
126
|
+
import { createHash } from "crypto";
|
|
127
|
+
import { createReadStream } from "fs";
|
|
128
|
+
import { lstat, readdir } from "fs/promises";
|
|
129
|
+
import path2 from "path";
|
|
130
|
+
var MAX_SKILL_FILE_BYTES = 10 * 1024 * 1024;
|
|
131
|
+
var MAX_SKILL_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
132
|
+
var MAX_SKILL_FILE_COUNT = 2e3;
|
|
133
|
+
var hashFile = async (filePath) => {
|
|
134
|
+
const hash = createHash("sha256");
|
|
135
|
+
const stream = createReadStream(filePath);
|
|
136
|
+
for await (const chunk of stream) {
|
|
137
|
+
hash.update(chunk);
|
|
138
|
+
}
|
|
139
|
+
return hash.digest("hex");
|
|
140
|
+
};
|
|
141
|
+
var assertSafeRelativePath = (value) => {
|
|
142
|
+
if (value.length === 0 || value.includes("\0") || value.includes("\\")) {
|
|
143
|
+
throw new Error(`Unsafe relative path: ${JSON.stringify(value)}`);
|
|
144
|
+
}
|
|
145
|
+
const normalized = path2.posix.normalize(value);
|
|
146
|
+
const segments = value.split("/");
|
|
147
|
+
if (path2.posix.isAbsolute(value) || normalized !== value || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
148
|
+
throw new Error(`Unsafe relative path: ${JSON.stringify(value)}`);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
};
|
|
152
|
+
var collectFiles = async (root, current, entries) => {
|
|
153
|
+
const directoryEntries = await readdir(current, { withFileTypes: true });
|
|
154
|
+
directoryEntries.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
|
155
|
+
let totalBytes = 0;
|
|
156
|
+
for (const directoryEntry of directoryEntries) {
|
|
157
|
+
const absolutePath = path2.join(current, directoryEntry.name);
|
|
158
|
+
const relativePath = assertSafeRelativePath(path2.relative(root, absolutePath).split(path2.sep).join("/"));
|
|
159
|
+
const fileStat = await lstat(absolutePath);
|
|
160
|
+
if (fileStat.isSymbolicLink()) {
|
|
161
|
+
throw new Error(`Symbolic links are not allowed in skill artifacts: ${relativePath}`);
|
|
162
|
+
}
|
|
163
|
+
if (fileStat.isDirectory()) {
|
|
164
|
+
totalBytes += await collectFiles(root, absolutePath, entries);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!fileStat.isFile()) {
|
|
168
|
+
throw new Error(`Unsupported filesystem entry in skill artifact: ${relativePath}`);
|
|
169
|
+
}
|
|
170
|
+
if (fileStat.size > MAX_SKILL_FILE_BYTES) {
|
|
171
|
+
throw new Error(`Skill file exceeds the ${MAX_SKILL_FILE_BYTES} byte limit: ${relativePath}`);
|
|
172
|
+
}
|
|
173
|
+
entries.push({
|
|
174
|
+
path: relativePath,
|
|
175
|
+
sizeBytes: fileStat.size,
|
|
176
|
+
executable: (fileStat.mode & 73) !== 0,
|
|
177
|
+
sha256: await hashFile(absolutePath)
|
|
178
|
+
});
|
|
179
|
+
if (entries.length > MAX_SKILL_FILE_COUNT) {
|
|
180
|
+
throw new Error(`Skill contains more than ${MAX_SKILL_FILE_COUNT} files`);
|
|
181
|
+
}
|
|
182
|
+
totalBytes += fileStat.size;
|
|
183
|
+
if (totalBytes > MAX_SKILL_TOTAL_BYTES) {
|
|
184
|
+
throw new Error(`Skill content exceeds the ${MAX_SKILL_TOTAL_BYTES} byte limit`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return totalBytes;
|
|
188
|
+
};
|
|
189
|
+
var hashDirectory = async (root) => {
|
|
190
|
+
const rootStat = await lstat(root);
|
|
191
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
192
|
+
throw new Error(`Skill root must be a real directory: ${root}`);
|
|
193
|
+
}
|
|
194
|
+
const entries = [];
|
|
195
|
+
const totalBytes = await collectFiles(root, root, entries);
|
|
196
|
+
const hash = createHash("sha256");
|
|
197
|
+
hash.update("yeka-skill-tree-v1\0");
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
hash.update(`${JSON.stringify(entry)}
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
contentHash: hash.digest("hex"),
|
|
204
|
+
entries,
|
|
205
|
+
totalBytes
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// src/archive.ts
|
|
210
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
211
|
+
import { lstat as lstat2, mkdir, rm } from "fs/promises";
|
|
212
|
+
import path3 from "path";
|
|
213
|
+
import * as tar from "tar";
|
|
214
|
+
var MAX_ARCHIVE_ENTRY_COUNT = MAX_SKILL_FILE_COUNT * 2;
|
|
215
|
+
var MAX_ARCHIVE_DEPTH = 64;
|
|
216
|
+
var MAX_ARCHIVE_PATH_BYTES = 4096;
|
|
217
|
+
var normalizeEntryPath = (entry) => {
|
|
218
|
+
const value = entry.type === "Directory" ? entry.path.replace(/\/+$/, "") : entry.path;
|
|
219
|
+
if (value.length === 0) {
|
|
220
|
+
throw new CliError("unsafe_archive", "The skill archive contains an empty path.");
|
|
221
|
+
}
|
|
222
|
+
if (Buffer.byteLength(value, "utf8") > MAX_ARCHIVE_PATH_BYTES) {
|
|
223
|
+
throw new CliError("unsafe_archive", "The skill archive contains a path that is too long.");
|
|
224
|
+
}
|
|
225
|
+
if (/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
226
|
+
throw new CliError("unsafe_archive", "The skill archive contains a path with control characters.");
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
assertSafeRelativePath(value);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw new CliError("unsafe_archive", `The skill archive contains an unsafe path: ${value}`, {
|
|
232
|
+
cause: error
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (value.split("/").length > MAX_ARCHIVE_DEPTH) {
|
|
236
|
+
throw new CliError("unsafe_archive", "The skill archive contains a path that is too deep.");
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
};
|
|
240
|
+
var inspectEntry = (entry) => {
|
|
241
|
+
if (entry.type !== "File" && entry.type !== "Directory") {
|
|
242
|
+
throw new CliError(
|
|
243
|
+
"unsafe_archive",
|
|
244
|
+
`The skill archive contains an unsupported ${entry.type} entry.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const archivePath = normalizeEntryPath(entry);
|
|
248
|
+
if (entry.linkpath !== void 0 && entry.linkpath.length > 0) {
|
|
249
|
+
throw new CliError("unsafe_archive", `The skill archive contains a link at ${archivePath}.`);
|
|
250
|
+
}
|
|
251
|
+
if (entry.mode !== void 0 && (entry.mode & 3584) !== 0) {
|
|
252
|
+
throw new CliError("unsafe_archive", `The skill archive contains unsafe mode bits at ${archivePath}.`);
|
|
253
|
+
}
|
|
254
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
|
|
255
|
+
throw new CliError("unsafe_archive", `The skill archive contains an invalid size at ${archivePath}.`);
|
|
256
|
+
}
|
|
257
|
+
if (entry.type === "Directory" && entry.size !== 0) {
|
|
258
|
+
throw new CliError("unsafe_archive", `The skill archive contains an invalid directory at ${archivePath}.`);
|
|
259
|
+
}
|
|
260
|
+
if (entry.type === "File" && entry.size > MAX_SKILL_FILE_BYTES) {
|
|
261
|
+
throw new CliError("unsafe_archive", `The skill archive contains an oversized file at ${archivePath}.`);
|
|
262
|
+
}
|
|
263
|
+
return { path: archivePath, type: entry.type, sizeBytes: entry.size };
|
|
264
|
+
};
|
|
265
|
+
var assertNoPathConflicts = (entries) => {
|
|
266
|
+
const files = new Set(entries.filter((entry) => entry.type === "File").map((entry) => entry.path));
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const segments = entry.path.split("/");
|
|
269
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
270
|
+
if (files.has(segments.slice(0, index).join("/"))) {
|
|
271
|
+
throw new CliError("unsafe_archive", "The skill archive contains conflicting paths.");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (entry.type === "Directory" && files.has(entry.path)) {
|
|
275
|
+
throw new CliError("unsafe_archive", "The skill archive contains conflicting paths.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var inspectArchive = async (archivePath) => {
|
|
280
|
+
const entries = [];
|
|
281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
282
|
+
let fileCount = 0;
|
|
283
|
+
let totalBytes = 0;
|
|
284
|
+
try {
|
|
285
|
+
await new Promise((resolve, reject) => {
|
|
286
|
+
const input = createReadStream2(archivePath);
|
|
287
|
+
const parser = new tar.Parser({
|
|
288
|
+
strict: true,
|
|
289
|
+
maxDecompressionRatio: 1e3
|
|
290
|
+
});
|
|
291
|
+
let settled = false;
|
|
292
|
+
const fail = (error) => {
|
|
293
|
+
if (settled) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
settled = true;
|
|
297
|
+
input.destroy();
|
|
298
|
+
reject(error);
|
|
299
|
+
};
|
|
300
|
+
parser.on("error", fail);
|
|
301
|
+
input.on("error", fail);
|
|
302
|
+
parser.on("entry", (rawEntry) => {
|
|
303
|
+
try {
|
|
304
|
+
const entry = inspectEntry(rawEntry);
|
|
305
|
+
if (seen.has(entry.path)) {
|
|
306
|
+
throw new CliError("unsafe_archive", `The skill archive repeats ${entry.path}.`);
|
|
307
|
+
}
|
|
308
|
+
seen.add(entry.path);
|
|
309
|
+
entries.push(entry);
|
|
310
|
+
if (entries.length > MAX_ARCHIVE_ENTRY_COUNT) {
|
|
311
|
+
throw new CliError("unsafe_archive", "The skill archive contains too many entries.");
|
|
312
|
+
}
|
|
313
|
+
if (entry.type === "File") {
|
|
314
|
+
fileCount += 1;
|
|
315
|
+
totalBytes += entry.sizeBytes;
|
|
316
|
+
}
|
|
317
|
+
if (fileCount > MAX_SKILL_FILE_COUNT || totalBytes > MAX_SKILL_TOTAL_BYTES) {
|
|
318
|
+
throw new CliError("unsafe_archive", "The skill archive exceeds its content limits.");
|
|
319
|
+
}
|
|
320
|
+
rawEntry.resume();
|
|
321
|
+
} catch (error) {
|
|
322
|
+
fail(error);
|
|
323
|
+
parser.abort(error instanceof Error ? error : new Error(String(error)));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
parser.on("end", () => {
|
|
327
|
+
if (!settled) {
|
|
328
|
+
settled = true;
|
|
329
|
+
resolve();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
input.pipe(parser);
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error instanceof CliError) {
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
throw new CliError("invalid_archive", "The downloaded skill archive is invalid.", { cause: error });
|
|
339
|
+
}
|
|
340
|
+
if (fileCount === 0 || !seen.has("SKILL.md")) {
|
|
341
|
+
throw new CliError("invalid_archive", "The skill archive does not contain a SKILL.md file.");
|
|
342
|
+
}
|
|
343
|
+
assertNoPathConflicts(entries);
|
|
344
|
+
return { entries, fileCount, totalBytes };
|
|
345
|
+
};
|
|
346
|
+
var assertDestinationMissing = async (destination) => {
|
|
347
|
+
try {
|
|
348
|
+
await lstat2(destination);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
throw new CliError("staging_conflict", `The private staging path already exists: ${destination}`);
|
|
356
|
+
};
|
|
357
|
+
var extractVerifiedArchive = async (archivePath, destination, expectedContentHash) => {
|
|
358
|
+
await inspectArchive(archivePath);
|
|
359
|
+
await assertDestinationMissing(destination);
|
|
360
|
+
await mkdir(path3.dirname(destination), { recursive: true, mode: 448 });
|
|
361
|
+
await mkdir(destination, { mode: 448 });
|
|
362
|
+
try {
|
|
363
|
+
let extractionError;
|
|
364
|
+
await tar.x({
|
|
365
|
+
cwd: destination,
|
|
366
|
+
file: archivePath,
|
|
367
|
+
strict: true,
|
|
368
|
+
preserveOwner: false,
|
|
369
|
+
preservePaths: false,
|
|
370
|
+
noMtime: true,
|
|
371
|
+
unlink: true,
|
|
372
|
+
chmod: true,
|
|
373
|
+
processUmask: 18,
|
|
374
|
+
maxDepth: MAX_ARCHIVE_DEPTH,
|
|
375
|
+
maxDecompressionRatio: 1e3,
|
|
376
|
+
filter: (_archivePath, rawEntry) => {
|
|
377
|
+
try {
|
|
378
|
+
inspectEntry(rawEntry);
|
|
379
|
+
return true;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
extractionError = error instanceof CliError ? error : new CliError("unsafe_archive", "The skill archive changed during extraction.", {
|
|
382
|
+
cause: error
|
|
383
|
+
});
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
if (extractionError !== void 0) {
|
|
389
|
+
throw extractionError;
|
|
390
|
+
}
|
|
391
|
+
const integrity = await hashDirectory(destination);
|
|
392
|
+
if (integrity.contentHash !== expectedContentHash) {
|
|
393
|
+
throw new CliError(
|
|
394
|
+
"content_integrity_failed",
|
|
395
|
+
"The extracted skill content does not match its signed manifest."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
await rm(destination, { recursive: true, force: true });
|
|
400
|
+
if (error instanceof CliError) {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
throw new CliError("archive_extract_failed", "The CLI could not extract the skill archive.", {
|
|
404
|
+
cause: error
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/receipts.ts
|
|
410
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
411
|
+
import { mkdir as mkdir2, readFile, readdir as readdir2, rename, unlink, writeFile } from "fs/promises";
|
|
412
|
+
import os2 from "os";
|
|
413
|
+
import path4 from "path";
|
|
414
|
+
var rootIdentifier = (targetRoot) => createHash2("sha256").update(targetRoot).digest("hex").slice(0, 16);
|
|
415
|
+
var receiptPath = (targetRoot, skillName, home = os2.homedir()) => path4.join(localPaths(home).receipts, rootIdentifier(targetRoot), `${skillName}.json`);
|
|
416
|
+
var writeReceipt = async (receipt, home = os2.homedir()) => {
|
|
417
|
+
const validated = InstallReceiptSchema.parse(receipt);
|
|
418
|
+
const destination = receiptPath(receipt.targetRoot, receipt.skillName, home);
|
|
419
|
+
const directory = path4.dirname(destination);
|
|
420
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
421
|
+
const temporary = path4.join(directory, `.${receipt.skillName}.${randomUUID()}.tmp`);
|
|
422
|
+
await writeFile(temporary, `${JSON.stringify(validated, null, 2)}
|
|
423
|
+
`, {
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
flag: "wx",
|
|
426
|
+
mode: 384
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
await rename(temporary, destination);
|
|
430
|
+
} finally {
|
|
431
|
+
await unlink(temporary).catch(() => void 0);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
var deleteReceipt = async (targetRoot, skillName, home = os2.homedir()) => {
|
|
435
|
+
await unlink(receiptPath(targetRoot, skillName, home)).catch((error) => {
|
|
436
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
var readReceipt = async (targetRoot, skillName, home = os2.homedir()) => {
|
|
443
|
+
try {
|
|
444
|
+
return InstallReceiptSchema.parse(
|
|
445
|
+
JSON.parse(await readFile(receiptPath(targetRoot, skillName, home), "utf8"))
|
|
446
|
+
);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
throw new CliError("invalid_receipt", `The local receipt for ${skillName} is invalid.`, {
|
|
452
|
+
cause: error
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
var listReceipts = async (home = os2.homedir()) => {
|
|
457
|
+
const root = localPaths(home).receipts;
|
|
458
|
+
let rootEntries;
|
|
459
|
+
try {
|
|
460
|
+
rootEntries = await readdir2(root, { withFileTypes: true });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
const receipts = [];
|
|
468
|
+
for (const rootEntry of rootEntries) {
|
|
469
|
+
if (!rootEntry.isDirectory()) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const files = await readdir2(path4.join(root, rootEntry.name), { withFileTypes: true });
|
|
473
|
+
for (const file of files) {
|
|
474
|
+
if (!file.isFile() || !file.name.endsWith(".json")) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
receipts.push(
|
|
479
|
+
InstallReceiptSchema.parse(
|
|
480
|
+
JSON.parse(await readFile(path4.join(root, rootEntry.name, file.name), "utf8"))
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw new CliError("invalid_receipt", `The local receipt ${file.name} is invalid.`, {
|
|
485
|
+
cause: error
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return receipts.sort(
|
|
491
|
+
(left, right) => `${left.skillName}:${left.runtime}`.localeCompare(`${right.skillName}:${right.runtime}`, "en")
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// src/installer.ts
|
|
496
|
+
var isNotFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
497
|
+
var fingerprint = async (entryPath) => {
|
|
498
|
+
try {
|
|
499
|
+
const entry = await lstat3(entryPath, { bigint: true });
|
|
500
|
+
return {
|
|
501
|
+
dev: entry.dev,
|
|
502
|
+
ino: entry.ino,
|
|
503
|
+
mode: Number(entry.mode),
|
|
504
|
+
mtimeNs: entry.mtimeNs,
|
|
505
|
+
size: entry.size
|
|
506
|
+
};
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (isNotFound(error)) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var fingerprintsMatch = (left, right) => left !== null && right !== void 0 && left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.size === right.size;
|
|
515
|
+
var allowedTargetRoots = (runtime, home) => runtime === "codex" ? [path5.join(home, ".agents", "skills"), path5.join(home, ".codex", "skills")] : [path5.join(home, ".claude", "skills")];
|
|
516
|
+
var assertSafeTarget = async (target, skillName, home) => {
|
|
517
|
+
const resolvedRoot = path5.resolve(target.targetRoot);
|
|
518
|
+
if (!allowedTargetRoots(target.runtime, home).some((root) => path5.resolve(root) === resolvedRoot)) {
|
|
519
|
+
throw new CliError("unsafe_target", `The ${target.runtime} installation target is not supported.`);
|
|
520
|
+
}
|
|
521
|
+
const parent = path5.dirname(resolvedRoot);
|
|
522
|
+
let parentStat;
|
|
523
|
+
try {
|
|
524
|
+
parentStat = await lstat3(parent);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (isNotFound(error)) {
|
|
527
|
+
throw new CliError("runtime_not_found", `The ${target.runtime} configuration directory is missing.`);
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
|
|
532
|
+
throw new CliError("unsafe_target", `The ${target.runtime} configuration directory is not a real directory.`);
|
|
533
|
+
}
|
|
534
|
+
const rootFingerprint = await fingerprint(resolvedRoot);
|
|
535
|
+
if (rootFingerprint !== null) {
|
|
536
|
+
const rootStat = await lstat3(resolvedRoot);
|
|
537
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
538
|
+
throw new CliError("unsafe_target", `The ${target.runtime} skills directory is not a real directory.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const installedPath = path5.resolve(resolvedRoot, skillName);
|
|
542
|
+
if (path5.dirname(installedPath) !== resolvedRoot) {
|
|
543
|
+
throw new CliError("unsafe_target", "The skill installation path is unsafe.");
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var targetsFromReceipts = (receipts, skillName, home = os3.homedir()) => {
|
|
547
|
+
const targets = [];
|
|
548
|
+
const seen = /* @__PURE__ */ new Set();
|
|
549
|
+
for (const receipt of receipts) {
|
|
550
|
+
if (receipt.skillName !== skillName) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const allowed = allowedTargetRoots(receipt.runtime, home).map((root2) => path5.resolve(root2));
|
|
554
|
+
const root = path5.resolve(receipt.targetRoot);
|
|
555
|
+
const installed = path5.resolve(receipt.installedPath);
|
|
556
|
+
if (!allowed.includes(root) || installed !== path5.join(root, skillName)) {
|
|
557
|
+
throw new CliError("invalid_receipt_target", `The local receipt for ${skillName} has an unsafe target.`);
|
|
558
|
+
}
|
|
559
|
+
const key = `${receipt.runtime}:${root}`;
|
|
560
|
+
if (!seen.has(key)) {
|
|
561
|
+
targets.push({
|
|
562
|
+
runtime: receipt.runtime,
|
|
563
|
+
targetRoot: root,
|
|
564
|
+
legacy: root === path5.resolve(home, ".codex", "skills")
|
|
565
|
+
});
|
|
566
|
+
seen.add(key);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return targets;
|
|
570
|
+
};
|
|
571
|
+
var preflightTarget = async (target, skill, home) => {
|
|
572
|
+
await assertSafeTarget(target, skill.name, home);
|
|
573
|
+
const targetRoot = path5.resolve(target.targetRoot);
|
|
574
|
+
const installedPath = path5.join(targetRoot, skill.name);
|
|
575
|
+
const oldReceipt = await readReceipt(targetRoot, skill.name, home);
|
|
576
|
+
if (oldReceipt !== null && (oldReceipt.runtime !== target.runtime || path5.resolve(oldReceipt.targetRoot) !== targetRoot || path5.resolve(oldReceipt.installedPath) !== installedPath)) {
|
|
577
|
+
throw new CliError("invalid_receipt_target", `The local receipt for ${skill.name} has an invalid target.`);
|
|
578
|
+
}
|
|
579
|
+
const entryFingerprint = await fingerprint(installedPath);
|
|
580
|
+
if (entryFingerprint === null) {
|
|
581
|
+
return {
|
|
582
|
+
action: "install",
|
|
583
|
+
runtime: target.runtime,
|
|
584
|
+
targetRoot,
|
|
585
|
+
installedPath,
|
|
586
|
+
existed: false,
|
|
587
|
+
oldReceipt
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (oldReceipt === null) {
|
|
591
|
+
return {
|
|
592
|
+
action: "replace-unmanaged",
|
|
593
|
+
runtime: target.runtime,
|
|
594
|
+
targetRoot,
|
|
595
|
+
installedPath,
|
|
596
|
+
existed: true,
|
|
597
|
+
fingerprint: entryFingerprint,
|
|
598
|
+
oldReceipt
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const installedStat = await lstat3(installedPath);
|
|
602
|
+
if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
|
|
603
|
+
throw new CliError(
|
|
604
|
+
"local_changes",
|
|
605
|
+
`The managed ${skill.name} installation for ${target.runtime} is no longer a real directory. It was not changed.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
let currentHash;
|
|
609
|
+
try {
|
|
610
|
+
currentHash = (await hashDirectory(installedPath)).contentHash;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
throw new CliError(
|
|
613
|
+
"local_changes",
|
|
614
|
+
`The managed ${skill.name} installation for ${target.runtime} contains unsupported local changes. It was not changed.`,
|
|
615
|
+
{ cause: error }
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (currentHash !== oldReceipt.installedHash) {
|
|
619
|
+
throw new CliError(
|
|
620
|
+
"local_changes",
|
|
621
|
+
`The managed ${skill.name} installation for ${target.runtime} has local changes. It was not changed.`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const current = oldReceipt.sourceHash === skill.source.contentHash && oldReceipt.artifactSha256 === skill.artifact.sha256 && currentHash === skill.source.contentHash;
|
|
625
|
+
return {
|
|
626
|
+
action: current ? "unchanged" : "update",
|
|
627
|
+
runtime: target.runtime,
|
|
628
|
+
targetRoot,
|
|
629
|
+
installedPath,
|
|
630
|
+
existed: true,
|
|
631
|
+
fingerprint: entryFingerprint,
|
|
632
|
+
oldReceipt
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
var ensureTargetRoot = async (targetRoot) => {
|
|
636
|
+
try {
|
|
637
|
+
await mkdir3(targetRoot, { mode: 493 });
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const rootStat = await lstat3(targetRoot);
|
|
644
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
645
|
+
throw new CliError("unsafe_target", `The skills target is not a real directory: ${targetRoot}`);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
var assertNoTargetRace = async (target) => {
|
|
649
|
+
const current = await fingerprint(target.installedPath);
|
|
650
|
+
if (target.existed ? !fingerprintsMatch(current, target.fingerprint) : current !== null) {
|
|
651
|
+
throw new CliError(
|
|
652
|
+
"target_changed",
|
|
653
|
+
`The ${target.runtime} installation changed during the operation. Nothing was overwritten.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (target.oldReceipt !== null && target.existed) {
|
|
657
|
+
const currentHash = (await hashDirectory(target.installedPath)).contentHash;
|
|
658
|
+
if (currentHash !== target.oldReceipt.installedHash) {
|
|
659
|
+
throw new CliError(
|
|
660
|
+
"target_changed",
|
|
661
|
+
`The ${target.runtime} installation changed during the operation. Nothing was overwritten.`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
var backupDestination = (home, now, batchId, target) => {
|
|
667
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-");
|
|
668
|
+
const targetId = createHash3("sha256").update(target.targetRoot).digest("hex").slice(0, 16);
|
|
669
|
+
return path5.join(
|
|
670
|
+
localPaths(home).backups,
|
|
671
|
+
`${timestamp}-${batchId}`,
|
|
672
|
+
target.runtime,
|
|
673
|
+
targetId,
|
|
674
|
+
path5.basename(target.installedPath)
|
|
675
|
+
);
|
|
676
|
+
};
|
|
677
|
+
var rollbackMutations = async (mutations) => {
|
|
678
|
+
const failures = [];
|
|
679
|
+
for (const mutation of [...mutations].reverse()) {
|
|
680
|
+
try {
|
|
681
|
+
if (mutation.installedNew) {
|
|
682
|
+
await rm2(mutation.target.installedPath, { recursive: true, force: true });
|
|
683
|
+
}
|
|
684
|
+
if (mutation.backedUp && mutation.target.backupPath !== void 0) {
|
|
685
|
+
await rename2(mutation.target.backupPath, mutation.target.installedPath);
|
|
686
|
+
}
|
|
687
|
+
} catch (error) {
|
|
688
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return failures;
|
|
692
|
+
};
|
|
693
|
+
var restoreReceipts = async (targets, home) => {
|
|
694
|
+
const failures = [];
|
|
695
|
+
for (const target of targets) {
|
|
696
|
+
try {
|
|
697
|
+
if (target.oldReceipt === null) {
|
|
698
|
+
await deleteReceipt(target.targetRoot, path5.basename(target.installedPath), home);
|
|
699
|
+
} else {
|
|
700
|
+
await writeReceipt(target.oldReceipt, home);
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return failures;
|
|
707
|
+
};
|
|
708
|
+
var newReceipt = (target, skill, installedAt) => ({
|
|
709
|
+
schemaVersion: 1,
|
|
710
|
+
skillName: skill.name,
|
|
711
|
+
runtime: target.runtime,
|
|
712
|
+
targetRoot: target.targetRoot,
|
|
713
|
+
installedPath: target.installedPath,
|
|
714
|
+
sourceCommit: skill.source.commit,
|
|
715
|
+
sourceHash: skill.source.contentHash,
|
|
716
|
+
artifactSha256: skill.artifact.sha256,
|
|
717
|
+
installedHash: skill.source.contentHash,
|
|
718
|
+
installedAt
|
|
719
|
+
});
|
|
720
|
+
var installSkillFromArchive = async (options) => {
|
|
721
|
+
const home = path5.resolve(options.home ?? os3.homedir());
|
|
722
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
723
|
+
if (options.targets.length === 0) {
|
|
724
|
+
throw new CliError("runtime_not_found", "There is no supported target for this skill.");
|
|
725
|
+
}
|
|
726
|
+
const uniqueTargets = /* @__PURE__ */ new Set();
|
|
727
|
+
for (const target of options.targets) {
|
|
728
|
+
const key = `${target.runtime}:${path5.resolve(target.targetRoot)}`;
|
|
729
|
+
if (uniqueTargets.has(key)) {
|
|
730
|
+
throw new CliError("duplicate_target", "The installation target list contains a duplicate.");
|
|
731
|
+
}
|
|
732
|
+
uniqueTargets.add(key);
|
|
733
|
+
}
|
|
734
|
+
const operationCache = localPaths(home).cache;
|
|
735
|
+
await mkdir3(operationCache, { recursive: true, mode: 448 });
|
|
736
|
+
const cacheStat = await lstat3(operationCache);
|
|
737
|
+
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
738
|
+
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
739
|
+
}
|
|
740
|
+
const operationRoot = await mkdtemp(path5.join(operationCache, "install-"));
|
|
741
|
+
await chmod(operationRoot, 448);
|
|
742
|
+
const extractedPath = path5.join(operationRoot, "content");
|
|
743
|
+
try {
|
|
744
|
+
await extractVerifiedArchive(options.archivePath, extractedPath, options.skill.source.contentHash);
|
|
745
|
+
const preflight = await Promise.all(
|
|
746
|
+
options.targets.map((target) => preflightTarget(target, options.skill, home))
|
|
747
|
+
);
|
|
748
|
+
if (options.dryRun === true || preflight.every((target) => target.action === "unchanged")) {
|
|
749
|
+
return preflight.map(({ action, runtime, targetRoot, installedPath }) => ({
|
|
750
|
+
action,
|
|
751
|
+
runtime,
|
|
752
|
+
targetRoot,
|
|
753
|
+
installedPath
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
const mutableTargets = preflight.filter((target) => target.action !== "unchanged");
|
|
757
|
+
try {
|
|
758
|
+
for (const target of mutableTargets) {
|
|
759
|
+
await ensureTargetRoot(target.targetRoot);
|
|
760
|
+
target.stagedPath = path5.join(
|
|
761
|
+
target.targetRoot,
|
|
762
|
+
`.yeka-stage-${options.skill.name}-${randomUUID2()}`
|
|
763
|
+
);
|
|
764
|
+
await cp(extractedPath, target.stagedPath, {
|
|
765
|
+
recursive: true,
|
|
766
|
+
force: false,
|
|
767
|
+
errorOnExist: true,
|
|
768
|
+
preserveTimestamps: false
|
|
769
|
+
});
|
|
770
|
+
const stagedHash = (await hashDirectory(target.stagedPath)).contentHash;
|
|
771
|
+
if (stagedHash !== options.skill.source.contentHash) {
|
|
772
|
+
throw new CliError("staging_integrity_failed", "A staged skill copy failed its integrity check.");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const batchId = randomUUID2();
|
|
776
|
+
const mutations = [];
|
|
777
|
+
const writtenReceipts = [];
|
|
778
|
+
try {
|
|
779
|
+
for (const target of mutableTargets) {
|
|
780
|
+
await assertNoTargetRace(target);
|
|
781
|
+
const mutation = { target, backedUp: false, installedNew: false };
|
|
782
|
+
mutations.push(mutation);
|
|
783
|
+
if (target.existed) {
|
|
784
|
+
target.backupPath = backupDestination(home, now, batchId, target);
|
|
785
|
+
await mkdir3(path5.dirname(target.backupPath), { recursive: true, mode: 448 });
|
|
786
|
+
const backupParent = await stat(path5.dirname(target.backupPath));
|
|
787
|
+
if (!backupParent.isDirectory()) {
|
|
788
|
+
throw new CliError("backup_failed", "The CLI could not create its private backup directory.");
|
|
789
|
+
}
|
|
790
|
+
await rename2(target.installedPath, target.backupPath);
|
|
791
|
+
mutation.backedUp = true;
|
|
792
|
+
}
|
|
793
|
+
if (target.stagedPath === void 0) {
|
|
794
|
+
throw new CliError("staging_failed", "The CLI lost a staged skill copy.");
|
|
795
|
+
}
|
|
796
|
+
await rename2(target.stagedPath, target.installedPath);
|
|
797
|
+
mutation.installedNew = true;
|
|
798
|
+
const installedHash = (await hashDirectory(target.installedPath)).contentHash;
|
|
799
|
+
if (installedHash !== options.skill.source.contentHash) {
|
|
800
|
+
throw new CliError("install_integrity_failed", "An installed skill failed its final integrity check.");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const installedAt = now.toISOString();
|
|
804
|
+
for (const target of mutableTargets) {
|
|
805
|
+
await writeReceipt(newReceipt(target, options.skill, installedAt), home);
|
|
806
|
+
writtenReceipts.push(target);
|
|
807
|
+
}
|
|
808
|
+
} catch (error) {
|
|
809
|
+
const receiptFailures = await restoreReceipts(writtenReceipts, home);
|
|
810
|
+
const rollbackFailures = await rollbackMutations(mutations);
|
|
811
|
+
if (receiptFailures.length > 0 || rollbackFailures.length > 0) {
|
|
812
|
+
throw new CliError(
|
|
813
|
+
"rollback_failed",
|
|
814
|
+
"The installation failed, and automatic rollback was incomplete. The backups were preserved.",
|
|
815
|
+
{ cause: error }
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
return preflight.map(({ action, runtime, targetRoot, installedPath, backupPath }) => ({
|
|
821
|
+
action,
|
|
822
|
+
runtime,
|
|
823
|
+
targetRoot,
|
|
824
|
+
installedPath,
|
|
825
|
+
...backupPath === void 0 ? {} : { backupPath }
|
|
826
|
+
}));
|
|
827
|
+
} finally {
|
|
828
|
+
for (const target of mutableTargets) {
|
|
829
|
+
if (target.stagedPath !== void 0) {
|
|
830
|
+
await rm2(target.stagedPath, { recursive: true, force: true });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} finally {
|
|
835
|
+
await rm2(operationRoot, { recursive: true, force: true });
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// src/keychain.ts
|
|
840
|
+
import { spawn } from "child_process";
|
|
841
|
+
import { createHash as createHash4 } from "crypto";
|
|
842
|
+
var KEYCHAIN_CHUNK_CHARACTERS = 96;
|
|
843
|
+
var MAX_SESSION_CHARACTERS = 4096;
|
|
844
|
+
var MAX_SESSION_CHUNKS = Math.ceil(MAX_SESSION_CHARACTERS / KEYCHAIN_CHUNK_CHARACTERS);
|
|
845
|
+
var MANIFEST_PATTERN = /^v1:([1-9][0-9]*):([0-9a-f]{64})$/;
|
|
846
|
+
var sessionDigest = (value) => createHash4("sha256").update(value).digest("hex");
|
|
847
|
+
var keychainPasswordInput = (password) => {
|
|
848
|
+
if (password.length === 0 || password.includes("\n") || password.includes("\r")) {
|
|
849
|
+
throw new CliError(
|
|
850
|
+
"invalid_keychain_value",
|
|
851
|
+
"The registry session cannot be saved safely in macOS Keychain."
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return `${password}
|
|
855
|
+
${password}
|
|
856
|
+
`;
|
|
857
|
+
};
|
|
858
|
+
var securitySpawnOptions = {
|
|
859
|
+
// Without a new session, `security` reads from the user's controlling
|
|
860
|
+
// terminal instead of the private stdin pipe and leaves the CLI waiting.
|
|
861
|
+
detached: true,
|
|
862
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
863
|
+
windowsHide: true
|
|
864
|
+
};
|
|
865
|
+
var encodeSessionForKeychain = (token) => {
|
|
866
|
+
keychainPasswordInput(token);
|
|
867
|
+
if (token.length > MAX_SESSION_CHARACTERS) {
|
|
868
|
+
throw new CliError(
|
|
869
|
+
"invalid_keychain_value",
|
|
870
|
+
"The registry session cannot be saved safely in macOS Keychain."
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const chunks = [];
|
|
874
|
+
for (let offset = 0; offset < token.length; offset += KEYCHAIN_CHUNK_CHARACTERS) {
|
|
875
|
+
chunks.push(token.slice(offset, offset + KEYCHAIN_CHUNK_CHARACTERS));
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
chunks,
|
|
879
|
+
manifest: `v1:${chunks.length}:${sessionDigest(token)}`
|
|
880
|
+
};
|
|
881
|
+
};
|
|
882
|
+
var parseManifest = (manifest) => {
|
|
883
|
+
const match = MANIFEST_PATTERN.exec(manifest);
|
|
884
|
+
if (match === null) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const count = Number(match[1]);
|
|
888
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > MAX_SESSION_CHUNKS) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
return { count, digest: match[2] };
|
|
892
|
+
};
|
|
893
|
+
var decodeSessionFromKeychain = (manifest, chunks) => {
|
|
894
|
+
const parsed = parseManifest(manifest);
|
|
895
|
+
if (parsed === null || chunks.length !== parsed.count) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const token = chunks.join("");
|
|
899
|
+
if (token.length === 0 || token.length > MAX_SESSION_CHARACTERS || token.includes("\n") || token.includes("\r") || sessionDigest(token) !== parsed.digest) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
return token;
|
|
903
|
+
};
|
|
904
|
+
var chunkAccount = (index) => `${KEYCHAIN_ACCOUNT}.${index}`;
|
|
905
|
+
var runSecurity = (args, input) => new Promise((resolve, reject) => {
|
|
906
|
+
const child = spawn("/usr/bin/security", args, securitySpawnOptions);
|
|
907
|
+
const stdout = [];
|
|
908
|
+
const stderr = [];
|
|
909
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
910
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
911
|
+
child.on("error", reject);
|
|
912
|
+
child.on("close", (code) => {
|
|
913
|
+
resolve({
|
|
914
|
+
code: code ?? 1,
|
|
915
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
916
|
+
stderr: Buffer.concat(stderr).toString("utf8")
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
if (input !== void 0) {
|
|
920
|
+
child.stdin.end(keychainPasswordInput(input));
|
|
921
|
+
} else {
|
|
922
|
+
child.stdin.end();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
var readPassword = async (account) => {
|
|
926
|
+
const result = await runSecurity([
|
|
927
|
+
"find-generic-password",
|
|
928
|
+
"-a",
|
|
929
|
+
account,
|
|
930
|
+
"-s",
|
|
931
|
+
KEYCHAIN_SERVICE,
|
|
932
|
+
"-w"
|
|
933
|
+
]);
|
|
934
|
+
if (result.code === 44) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
if (result.code !== 0) {
|
|
938
|
+
throw new CliError("keychain_read_failed", "The CLI could not read its macOS Keychain session.");
|
|
939
|
+
}
|
|
940
|
+
const value = result.stdout.trim();
|
|
941
|
+
return value.length === 0 ? null : value;
|
|
942
|
+
};
|
|
943
|
+
var writePassword = async (account, label, value) => {
|
|
944
|
+
const result = await runSecurity(
|
|
945
|
+
[
|
|
946
|
+
"add-generic-password",
|
|
947
|
+
"-U",
|
|
948
|
+
"-a",
|
|
949
|
+
account,
|
|
950
|
+
"-s",
|
|
951
|
+
KEYCHAIN_SERVICE,
|
|
952
|
+
"-l",
|
|
953
|
+
label,
|
|
954
|
+
"-w"
|
|
955
|
+
],
|
|
956
|
+
value
|
|
957
|
+
);
|
|
958
|
+
if (result.code !== 0) {
|
|
959
|
+
throw new CliError("keychain_write_failed", "The CLI could not save its macOS Keychain session.");
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
var deletePassword = async (account) => {
|
|
963
|
+
const result = await runSecurity([
|
|
964
|
+
"delete-generic-password",
|
|
965
|
+
"-a",
|
|
966
|
+
account,
|
|
967
|
+
"-s",
|
|
968
|
+
KEYCHAIN_SERVICE
|
|
969
|
+
]);
|
|
970
|
+
if (result.code === 44) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
if (result.code !== 0) {
|
|
974
|
+
throw new CliError("keychain_delete_failed", "The CLI could not remove its macOS Keychain session.");
|
|
975
|
+
}
|
|
976
|
+
return true;
|
|
977
|
+
};
|
|
978
|
+
var readSession = async () => {
|
|
979
|
+
const manifest = await readPassword(KEYCHAIN_ACCOUNT);
|
|
980
|
+
if (manifest === null) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
const parsed = parseManifest(manifest);
|
|
984
|
+
if (parsed === null) {
|
|
985
|
+
await deletePassword(KEYCHAIN_ACCOUNT);
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const chunks = await Promise.all(
|
|
989
|
+
Array.from({ length: parsed.count }, (_value, index) => readPassword(chunkAccount(index)))
|
|
990
|
+
);
|
|
991
|
+
if (chunks.some((chunk) => chunk === null)) {
|
|
992
|
+
await deleteSession();
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
const token = decodeSessionFromKeychain(manifest, chunks);
|
|
996
|
+
if (token === null) {
|
|
997
|
+
await deleteSession();
|
|
998
|
+
}
|
|
999
|
+
return token;
|
|
1000
|
+
};
|
|
1001
|
+
var writeSession = async (token) => {
|
|
1002
|
+
const encoded = encodeSessionForKeychain(token);
|
|
1003
|
+
await deleteSession();
|
|
1004
|
+
let writtenChunks = 0;
|
|
1005
|
+
try {
|
|
1006
|
+
await writePassword(KEYCHAIN_ACCOUNT, "Yeka Skills CLI session", encoded.manifest);
|
|
1007
|
+
for (const [index, chunk] of encoded.chunks.entries()) {
|
|
1008
|
+
await writePassword(
|
|
1009
|
+
chunkAccount(index),
|
|
1010
|
+
`Yeka Skills CLI session part ${index + 1}`,
|
|
1011
|
+
chunk
|
|
1012
|
+
);
|
|
1013
|
+
writtenChunks += 1;
|
|
1014
|
+
}
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
await deletePassword(KEYCHAIN_ACCOUNT).catch(() => false);
|
|
1017
|
+
await Promise.all(
|
|
1018
|
+
Array.from(
|
|
1019
|
+
{ length: writtenChunks },
|
|
1020
|
+
(_value, index) => deletePassword(chunkAccount(index)).catch(() => false)
|
|
1021
|
+
)
|
|
1022
|
+
);
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
var deleteSession = async () => {
|
|
1027
|
+
const manifest = await readPassword(KEYCHAIN_ACCOUNT);
|
|
1028
|
+
const count = manifest === null ? 0 : parseManifest(manifest)?.count ?? 0;
|
|
1029
|
+
const deletedManifest = await deletePassword(KEYCHAIN_ACCOUNT);
|
|
1030
|
+
const deletedChunks = await Promise.all(
|
|
1031
|
+
Array.from({ length: count }, (_value, index) => deletePassword(chunkAccount(index)))
|
|
1032
|
+
);
|
|
1033
|
+
return deletedManifest || deletedChunks.some(Boolean);
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/local-status.ts
|
|
1037
|
+
import { lstat as lstat4 } from "fs/promises";
|
|
1038
|
+
import os4 from "os";
|
|
1039
|
+
var isNotFound2 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1040
|
+
var inspectLocalStatus = async (receipt, remote, home = os4.homedir()) => {
|
|
1041
|
+
targetsFromReceipts([receipt], receipt.skillName, home);
|
|
1042
|
+
let installedStat;
|
|
1043
|
+
try {
|
|
1044
|
+
installedStat = await lstat4(receipt.installedPath);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (isNotFound2(error)) {
|
|
1047
|
+
return { kind: "missing", receipt, ...remote === void 0 ? {} : { remote } };
|
|
1048
|
+
}
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
|
|
1052
|
+
return { kind: "modified", receipt, ...remote === void 0 ? {} : { remote } };
|
|
1053
|
+
}
|
|
1054
|
+
let currentHash;
|
|
1055
|
+
try {
|
|
1056
|
+
currentHash = (await hashDirectory(receipt.installedPath)).contentHash;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return { kind: "modified", receipt, ...remote === void 0 ? {} : { remote } };
|
|
1059
|
+
}
|
|
1060
|
+
if (currentHash !== receipt.installedHash) {
|
|
1061
|
+
return { kind: "modified", receipt, ...remote === void 0 ? {} : { remote } };
|
|
1062
|
+
}
|
|
1063
|
+
if (remote === void 0) {
|
|
1064
|
+
return { kind: "unavailable", receipt };
|
|
1065
|
+
}
|
|
1066
|
+
if (receipt.sourceHash !== remote.source.contentHash || receipt.artifactSha256 !== remote.artifact.sha256) {
|
|
1067
|
+
return { kind: "update-available", receipt, remote };
|
|
1068
|
+
}
|
|
1069
|
+
return { kind: "current", receipt, remote };
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/login.ts
|
|
1073
|
+
import { createHash as createHash5, randomBytes, timingSafeEqual } from "crypto";
|
|
1074
|
+
import { execFile } from "child_process";
|
|
1075
|
+
import http from "http";
|
|
1076
|
+
import { promisify } from "util";
|
|
1077
|
+
var execFileAsync = promisify(execFile);
|
|
1078
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1079
|
+
var MAX_TOKEN_RESPONSE_BYTES = 16 * 1024;
|
|
1080
|
+
var AUTHORIZATION_CODE_PATTERN = /^[A-Za-z0-9._-]{32,4096}$/;
|
|
1081
|
+
var readBoundedJson = async (response) => {
|
|
1082
|
+
const declared = response.headers.get("Content-Length");
|
|
1083
|
+
if (declared !== null && (!/^(?:0|[1-9][0-9]*)$/.test(declared) || Number(declared) > MAX_TOKEN_RESPONSE_BYTES)) {
|
|
1084
|
+
await response.body?.cancel("token response exceeds limit");
|
|
1085
|
+
throw new CliError("token_response_too_large", "The registry token response is larger than allowed.");
|
|
1086
|
+
}
|
|
1087
|
+
if (response.body === null) {
|
|
1088
|
+
throw new CliError("empty_token_response", "The registry returned an empty token response.");
|
|
1089
|
+
}
|
|
1090
|
+
const reader = response.body.getReader();
|
|
1091
|
+
const chunks = [];
|
|
1092
|
+
let total = 0;
|
|
1093
|
+
while (true) {
|
|
1094
|
+
const result = await reader.read();
|
|
1095
|
+
if (result.done) {
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
total += result.value.byteLength;
|
|
1099
|
+
if (total > MAX_TOKEN_RESPONSE_BYTES) {
|
|
1100
|
+
await reader.cancel("token response exceeds limit");
|
|
1101
|
+
throw new CliError("token_response_too_large", "The registry token response is larger than allowed.");
|
|
1102
|
+
}
|
|
1103
|
+
chunks.push(result.value);
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new CliError("invalid_token_response", "The registry returned an invalid token response.", {
|
|
1109
|
+
cause: error
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
var constantTimeEqual = (left, right) => {
|
|
1114
|
+
const leftHash = createHash5("sha256").update(left).digest();
|
|
1115
|
+
const rightHash = createHash5("sha256").update(right).digest();
|
|
1116
|
+
return timingSafeEqual(leftHash, rightHash);
|
|
1117
|
+
};
|
|
1118
|
+
var successPage = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><title>Yeka Skills login complete</title><style>html{color-scheme:dark;background:#151515;color:#f3f0e8;font:18px/1.5 system-ui,sans-serif}body{display:grid;min-height:100vh;margin:0;place-items:center}main{max-width:32rem;padding:2rem}</style></head><body><main><h1>Login complete</h1><p>You can close this page and return to your terminal.</p></main></body></html>`;
|
|
1119
|
+
var waitForCallback = async (expectedState) => {
|
|
1120
|
+
let resolveCallback;
|
|
1121
|
+
let rejectCallback;
|
|
1122
|
+
const callback = new Promise((resolve, reject) => {
|
|
1123
|
+
resolveCallback = resolve;
|
|
1124
|
+
rejectCallback = reject;
|
|
1125
|
+
});
|
|
1126
|
+
const server = http.createServer((request, response) => {
|
|
1127
|
+
const address2 = server.address();
|
|
1128
|
+
if (typeof address2 === "string" || address2 === null) {
|
|
1129
|
+
response.writeHead(500).end();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const redirectUri = `http://127.0.0.1:${address2.port}/callback`;
|
|
1133
|
+
const url = new URL(request.url ?? "/", redirectUri);
|
|
1134
|
+
const code = url.searchParams.get("code");
|
|
1135
|
+
const state = url.searchParams.get("state");
|
|
1136
|
+
if (request.method !== "GET" || url.origin !== new URL(redirectUri).origin || url.pathname !== "/callback" || code === null || !AUTHORIZATION_CODE_PATTERN.test(code) || state === null || !constantTimeEqual(state, expectedState)) {
|
|
1137
|
+
response.writeHead(400, {
|
|
1138
|
+
"Cache-Control": "no-store",
|
|
1139
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1140
|
+
"X-Content-Type-Options": "nosniff"
|
|
1141
|
+
});
|
|
1142
|
+
response.end("The Yeka Skills login response is invalid.");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
response.writeHead(200, {
|
|
1146
|
+
"Cache-Control": "no-store",
|
|
1147
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
1148
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1149
|
+
"Referrer-Policy": "no-referrer",
|
|
1150
|
+
"X-Content-Type-Options": "nosniff"
|
|
1151
|
+
});
|
|
1152
|
+
response.end(successPage);
|
|
1153
|
+
resolveCallback?.({ code, redirectUri });
|
|
1154
|
+
});
|
|
1155
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
1156
|
+
await new Promise((resolve, reject) => {
|
|
1157
|
+
server.once("error", reject);
|
|
1158
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
1159
|
+
});
|
|
1160
|
+
const address = server.address();
|
|
1161
|
+
if (typeof address === "string" || address === null) {
|
|
1162
|
+
server.close();
|
|
1163
|
+
throw new CliError("login_listener_failed", "The CLI could not start its private login listener.");
|
|
1164
|
+
}
|
|
1165
|
+
const timeout = setTimeout(() => {
|
|
1166
|
+
rejectCallback?.(new CliError("login_timeout", "The browser login did not finish within five minutes."));
|
|
1167
|
+
}, LOGIN_TIMEOUT_MS);
|
|
1168
|
+
timeout.unref();
|
|
1169
|
+
return {
|
|
1170
|
+
callback,
|
|
1171
|
+
authorizationUrl: `http://127.0.0.1:${address.port}/callback`,
|
|
1172
|
+
close: async () => {
|
|
1173
|
+
clearTimeout(timeout);
|
|
1174
|
+
if (!server.listening) {
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
await new Promise(
|
|
1178
|
+
(resolve, reject) => server.close((error) => error === void 0 ? resolve() : reject(error))
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
var openBrowser = async (url) => {
|
|
1184
|
+
try {
|
|
1185
|
+
await execFileAsync("/usr/bin/open", [url]);
|
|
1186
|
+
return true;
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
void error;
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var login = async () => {
|
|
1193
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
1194
|
+
const challenge = createHash5("sha256").update(verifier).digest("base64url");
|
|
1195
|
+
const state = randomBytes(32).toString("base64url");
|
|
1196
|
+
const listener = await waitForCallback(state);
|
|
1197
|
+
const authorizationUrl = new URL("/authorize", REGISTRY_ORIGIN);
|
|
1198
|
+
authorizationUrl.searchParams.set("code_challenge", challenge);
|
|
1199
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
1200
|
+
authorizationUrl.searchParams.set("redirect_uri", listener.authorizationUrl);
|
|
1201
|
+
authorizationUrl.searchParams.set("state", state);
|
|
1202
|
+
process.stdout.write("Your browser will open for Gemography email verification.\n");
|
|
1203
|
+
try {
|
|
1204
|
+
if (!await openBrowser(authorizationUrl.toString())) {
|
|
1205
|
+
process.stdout.write(`Open this address in your browser:
|
|
1206
|
+
${authorizationUrl.toString()}
|
|
1207
|
+
`);
|
|
1208
|
+
}
|
|
1209
|
+
const callback = await listener.callback;
|
|
1210
|
+
const response = await fetch(new URL("/v1/auth/token", REGISTRY_ORIGIN), {
|
|
1211
|
+
method: "POST",
|
|
1212
|
+
headers: { "Content-Type": "application/json" },
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
code: callback.code,
|
|
1215
|
+
codeVerifier: verifier,
|
|
1216
|
+
redirectUri: callback.redirectUri
|
|
1217
|
+
}),
|
|
1218
|
+
redirect: "error",
|
|
1219
|
+
signal: AbortSignal.timeout(15e3)
|
|
1220
|
+
});
|
|
1221
|
+
if (!response.ok) {
|
|
1222
|
+
throw new CliError("token_exchange_failed", "The registry did not accept the browser login.");
|
|
1223
|
+
}
|
|
1224
|
+
const token = TokenResponseSchema.parse(await readBoundedJson(response)).accessToken;
|
|
1225
|
+
await writeSession(token);
|
|
1226
|
+
process.stdout.write("Login complete. The short session is stored in macOS Keychain.\n");
|
|
1227
|
+
return token;
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (error instanceof CliError) {
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
throw new CliError("login_failed", "The browser login did not complete.", { cause: error });
|
|
1233
|
+
} finally {
|
|
1234
|
+
await listener.close();
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// src/registry-client.ts
|
|
1239
|
+
import { createHash as createHash6 } from "crypto";
|
|
1240
|
+
import { open, unlink as unlink2 } from "fs/promises";
|
|
1241
|
+
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
1242
|
+
var parseContentLength = (value) => {
|
|
1243
|
+
if (value === null) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(value)) {
|
|
1247
|
+
throw new CliError("invalid_content_length", "The registry returned an invalid content length.");
|
|
1248
|
+
}
|
|
1249
|
+
const parsed = Number(value);
|
|
1250
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
1251
|
+
throw new CliError("invalid_content_length", "The registry returned an invalid content length.");
|
|
1252
|
+
}
|
|
1253
|
+
return parsed;
|
|
1254
|
+
};
|
|
1255
|
+
var readBoundedText = async (response, limit) => {
|
|
1256
|
+
const declared = parseContentLength(response.headers.get("Content-Length"));
|
|
1257
|
+
if (declared !== null && declared > limit) {
|
|
1258
|
+
throw new CliError("response_too_large", "The registry response is larger than allowed.");
|
|
1259
|
+
}
|
|
1260
|
+
if (response.body === null) {
|
|
1261
|
+
throw new CliError("empty_response", "The registry returned an empty response.");
|
|
1262
|
+
}
|
|
1263
|
+
const reader = response.body.getReader();
|
|
1264
|
+
const chunks = [];
|
|
1265
|
+
let total = 0;
|
|
1266
|
+
while (true) {
|
|
1267
|
+
const result = await reader.read();
|
|
1268
|
+
if (result.done) {
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
total += result.value.byteLength;
|
|
1272
|
+
if (total > limit) {
|
|
1273
|
+
await reader.cancel("response exceeds limit");
|
|
1274
|
+
throw new CliError("response_too_large", "The registry response is larger than allowed.");
|
|
1275
|
+
}
|
|
1276
|
+
chunks.push(result.value);
|
|
1277
|
+
}
|
|
1278
|
+
const bytes = new Uint8Array(total);
|
|
1279
|
+
let offset = 0;
|
|
1280
|
+
for (const chunk of chunks) {
|
|
1281
|
+
bytes.set(chunk, offset);
|
|
1282
|
+
offset += chunk.byteLength;
|
|
1283
|
+
}
|
|
1284
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1285
|
+
};
|
|
1286
|
+
var assertRegistryResponse = async (response) => {
|
|
1287
|
+
if (response.status === 401 || response.status === 403) {
|
|
1288
|
+
await response.body?.cancel("authentication failed");
|
|
1289
|
+
throw new AuthenticationError();
|
|
1290
|
+
}
|
|
1291
|
+
if (!response.ok) {
|
|
1292
|
+
await response.body?.cancel("registry request failed");
|
|
1293
|
+
throw new CliError(
|
|
1294
|
+
"registry_request_failed",
|
|
1295
|
+
`The registry request failed with status ${response.status}.`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
var RegistryClient = class {
|
|
1300
|
+
origin;
|
|
1301
|
+
constructor(origin = REGISTRY_ORIGIN) {
|
|
1302
|
+
this.origin = new URL(origin);
|
|
1303
|
+
if (this.origin.protocol !== "https:" && this.origin.hostname !== "127.0.0.1") {
|
|
1304
|
+
throw new CliError("unsafe_registry_origin", "The registry address must use HTTPS.");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async getManifest(token) {
|
|
1308
|
+
const response = await fetch(new URL("/v1/manifest", this.origin), {
|
|
1309
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1310
|
+
redirect: "error",
|
|
1311
|
+
signal: AbortSignal.timeout(3e4)
|
|
1312
|
+
});
|
|
1313
|
+
await assertRegistryResponse(response);
|
|
1314
|
+
try {
|
|
1315
|
+
return RegistryManifestSchema.parse(
|
|
1316
|
+
JSON.parse(await readBoundedText(response, MAX_MANIFEST_BYTES))
|
|
1317
|
+
);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (error instanceof CliError) {
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
throw new CliError("invalid_manifest", "The registry manifest is invalid.", { cause: error });
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async downloadArtifact(skill, token, destination) {
|
|
1326
|
+
const response = await fetch(
|
|
1327
|
+
new URL(`/v1/skills/${encodeURIComponent(skill.name)}/archive`, this.origin),
|
|
1328
|
+
{
|
|
1329
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1330
|
+
redirect: "error",
|
|
1331
|
+
signal: AbortSignal.timeout(2 * 60 * 1e3)
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
await assertRegistryResponse(response);
|
|
1335
|
+
if (response.body === null) {
|
|
1336
|
+
throw new CliError("empty_artifact", "The registry returned an empty skill artifact.");
|
|
1337
|
+
}
|
|
1338
|
+
const declaredLength = parseContentLength(response.headers.get("Content-Length"));
|
|
1339
|
+
if (declaredLength !== null && declaredLength !== skill.artifact.sizeBytes) {
|
|
1340
|
+
await response.body.cancel("artifact size does not match manifest");
|
|
1341
|
+
throw new CliError("artifact_size_mismatch", "The skill artifact size does not match its manifest.");
|
|
1342
|
+
}
|
|
1343
|
+
const file = await open(destination, "wx", 384);
|
|
1344
|
+
const hash = createHash6("sha256");
|
|
1345
|
+
const reader = response.body.getReader();
|
|
1346
|
+
let bytesWritten = 0;
|
|
1347
|
+
try {
|
|
1348
|
+
while (true) {
|
|
1349
|
+
const result = await reader.read();
|
|
1350
|
+
if (result.done) {
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
bytesWritten += result.value.byteLength;
|
|
1354
|
+
if (bytesWritten > skill.artifact.sizeBytes) {
|
|
1355
|
+
await reader.cancel("artifact exceeds manifest size");
|
|
1356
|
+
throw new CliError(
|
|
1357
|
+
"artifact_size_mismatch",
|
|
1358
|
+
"The skill artifact is larger than its manifest allows."
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
hash.update(result.value);
|
|
1362
|
+
let offset = 0;
|
|
1363
|
+
while (offset < result.value.byteLength) {
|
|
1364
|
+
const writeResult = await file.write(
|
|
1365
|
+
result.value,
|
|
1366
|
+
offset,
|
|
1367
|
+
result.value.byteLength - offset,
|
|
1368
|
+
null
|
|
1369
|
+
);
|
|
1370
|
+
if (writeResult.bytesWritten === 0) {
|
|
1371
|
+
throw new CliError("artifact_write_failed", "The CLI could not save the skill artifact.");
|
|
1372
|
+
}
|
|
1373
|
+
offset += writeResult.bytesWritten;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
await file.close();
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
await file.close().catch(() => void 0);
|
|
1379
|
+
await unlink2(destination).catch(() => void 0);
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
const digest = hash.digest("hex");
|
|
1383
|
+
const responseDigest = response.headers.get("X-Yeka-Artifact-Sha256");
|
|
1384
|
+
if (bytesWritten !== skill.artifact.sizeBytes || digest !== skill.artifact.sha256 || responseDigest !== null && responseDigest !== skill.artifact.sha256) {
|
|
1385
|
+
await unlink2(destination).catch(() => void 0);
|
|
1386
|
+
throw new CliError(
|
|
1387
|
+
"artifact_integrity_failed",
|
|
1388
|
+
"The downloaded skill artifact failed its integrity check."
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/session.ts
|
|
1395
|
+
var defaultDependencies = { deleteSession, login, readSession };
|
|
1396
|
+
var withRegistrySession = async (operation, dependencies = defaultDependencies) => {
|
|
1397
|
+
let token = await dependencies.readSession();
|
|
1398
|
+
if (token === null) {
|
|
1399
|
+
token = await dependencies.login();
|
|
1400
|
+
}
|
|
1401
|
+
try {
|
|
1402
|
+
return await operation(token);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
if (!(error instanceof AuthenticationError)) {
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
await dependencies.deleteSession();
|
|
1408
|
+
const renewedToken = await dependencies.login();
|
|
1409
|
+
return operation(renewedToken);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
// src/targets.ts
|
|
1414
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
1415
|
+
import os5 from "os";
|
|
1416
|
+
import path6 from "path";
|
|
1417
|
+
var isRealDirectory = async (candidate) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const entry = await lstat5(candidate);
|
|
1420
|
+
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
var pathExists = async (candidate) => {
|
|
1429
|
+
try {
|
|
1430
|
+
await lstat5(candidate);
|
|
1431
|
+
return true;
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
var resolveCodexTarget = async (home, skillName) => {
|
|
1440
|
+
const currentRoot = path6.join(home, ".agents", "skills");
|
|
1441
|
+
const currentParent = path6.join(home, ".agents");
|
|
1442
|
+
const codexConfig = path6.join(home, ".codex");
|
|
1443
|
+
const legacyRoot = path6.join(codexConfig, "skills");
|
|
1444
|
+
const legacyActive = await isRealDirectory(legacyRoot);
|
|
1445
|
+
const currentActive = await isRealDirectory(currentRoot) || await isRealDirectory(currentParent) || await isRealDirectory(codexConfig) && !legacyActive;
|
|
1446
|
+
if (currentActive) {
|
|
1447
|
+
if (legacyActive && await pathExists(path6.join(legacyRoot, skillName))) {
|
|
1448
|
+
throw new CliError(
|
|
1449
|
+
"legacy_codex_conflict",
|
|
1450
|
+
`Codex can see a legacy copy at ${path6.join(legacyRoot, skillName)}. Remove or archive that copy before installation.`
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
return { runtime: "codex", targetRoot: currentRoot, legacy: false };
|
|
1454
|
+
}
|
|
1455
|
+
if (legacyActive) {
|
|
1456
|
+
return { runtime: "codex", targetRoot: legacyRoot, legacy: true };
|
|
1457
|
+
}
|
|
1458
|
+
return null;
|
|
1459
|
+
};
|
|
1460
|
+
var resolveClaudeTarget = async (home) => {
|
|
1461
|
+
const claudeConfig = path6.join(home, ".claude");
|
|
1462
|
+
if (!await isRealDirectory(claudeConfig)) {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
runtime: "claude",
|
|
1467
|
+
targetRoot: path6.join(claudeConfig, "skills"),
|
|
1468
|
+
legacy: false
|
|
1469
|
+
};
|
|
1470
|
+
};
|
|
1471
|
+
var resolveInstallTargets = async (skill, home = os5.homedir()) => {
|
|
1472
|
+
const targets = [];
|
|
1473
|
+
if (skill.runtimes.includes("codex")) {
|
|
1474
|
+
const codex = await resolveCodexTarget(home, skill.name);
|
|
1475
|
+
if (codex !== null) {
|
|
1476
|
+
targets.push(codex);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (skill.runtimes.includes("claude")) {
|
|
1480
|
+
const claude = await resolveClaudeTarget(home);
|
|
1481
|
+
if (claude !== null) {
|
|
1482
|
+
targets.push(claude);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (targets.length === 0) {
|
|
1486
|
+
throw new CliError(
|
|
1487
|
+
"runtime_not_found",
|
|
1488
|
+
"The CLI could not find a supported local ChatGPT, Codex, or Claude skill folder on this Mac."
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return targets;
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/commands.ts
|
|
1495
|
+
var output = (context, message) => {
|
|
1496
|
+
(context.stdout ?? process.stdout).write(`${message}
|
|
1497
|
+
`);
|
|
1498
|
+
};
|
|
1499
|
+
var validatedSkillName = (value) => {
|
|
1500
|
+
const result = SkillNameSchema.safeParse(value);
|
|
1501
|
+
if (!result.success) {
|
|
1502
|
+
throw new CliError("invalid_skill_name", "Use the exact lowercase skill name from the catalog.");
|
|
1503
|
+
}
|
|
1504
|
+
return result.data;
|
|
1505
|
+
};
|
|
1506
|
+
var findSkill = (manifest, name) => {
|
|
1507
|
+
const skill = manifest.skills.find((candidate) => candidate.name === name);
|
|
1508
|
+
if (skill === void 0) {
|
|
1509
|
+
throw new CliError("skill_not_found", `The private registry does not contain ${name}.`);
|
|
1510
|
+
}
|
|
1511
|
+
return skill;
|
|
1512
|
+
};
|
|
1513
|
+
var ensurePrivateCache = async (home) => {
|
|
1514
|
+
const cache = localPaths(home).cache;
|
|
1515
|
+
await mkdir4(cache, { recursive: true, mode: 448 });
|
|
1516
|
+
const cacheStat = await lstat6(cache);
|
|
1517
|
+
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
1518
|
+
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
1519
|
+
}
|
|
1520
|
+
return cache;
|
|
1521
|
+
};
|
|
1522
|
+
var downloadAndInstall = async (client, token, skill, targets, options, home) => {
|
|
1523
|
+
const cache = await ensurePrivateCache(home);
|
|
1524
|
+
const downloadRoot = await mkdtemp2(path7.join(cache, "download-"));
|
|
1525
|
+
await chmod2(downloadRoot, 448);
|
|
1526
|
+
const archivePath = path7.join(downloadRoot, `${skill.name}.tgz`);
|
|
1527
|
+
try {
|
|
1528
|
+
await client.downloadArtifact(skill, token, archivePath);
|
|
1529
|
+
return await installSkillFromArchive({
|
|
1530
|
+
skill,
|
|
1531
|
+
archivePath,
|
|
1532
|
+
targets,
|
|
1533
|
+
...options.dryRun === void 0 ? {} : { dryRun: options.dryRun },
|
|
1534
|
+
home
|
|
1535
|
+
});
|
|
1536
|
+
} finally {
|
|
1537
|
+
await rm3(downloadRoot, { recursive: true, force: true });
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
var actionMessage = (skillName, action, dryRun) => {
|
|
1541
|
+
switch (action.action) {
|
|
1542
|
+
case "install":
|
|
1543
|
+
return dryRun ? `Would install ${skillName} for ${action.runtime} at ${action.installedPath}.` : `Installed ${skillName} for ${action.runtime} at ${action.installedPath}.`;
|
|
1544
|
+
case "update":
|
|
1545
|
+
return dryRun ? `Would update ${skillName} for ${action.runtime} at ${action.installedPath}.` : `Updated ${skillName} for ${action.runtime} at ${action.installedPath}.`;
|
|
1546
|
+
case "replace-unmanaged":
|
|
1547
|
+
return dryRun ? `Would back up the existing ${action.runtime} copy and install ${skillName}.` : `Backed up the existing ${action.runtime} copy and installed ${skillName}.`;
|
|
1548
|
+
case "unchanged":
|
|
1549
|
+
return `${skillName} is current for ${action.runtime}.`;
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
var printActions = (context, skillName, actions, dryRun) => {
|
|
1553
|
+
for (const action of actions) {
|
|
1554
|
+
output(context, actionMessage(skillName, action, dryRun));
|
|
1555
|
+
if (!dryRun && action.backupPath !== void 0) {
|
|
1556
|
+
output(context, `Backup: ${action.backupPath}`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
var addCommand = async (skillNameInput, options, context = {}) => {
|
|
1561
|
+
const skillName = validatedSkillName(skillNameInput);
|
|
1562
|
+
const home = path7.resolve(context.home ?? os6.homedir());
|
|
1563
|
+
const client = context.client ?? new RegistryClient();
|
|
1564
|
+
const actions = await withRegistrySession(async (token) => {
|
|
1565
|
+
const manifest = await client.getManifest(token);
|
|
1566
|
+
const skill = findSkill(manifest, skillName);
|
|
1567
|
+
const targets = await resolveInstallTargets(skill, home);
|
|
1568
|
+
return downloadAndInstall(client, token, skill, targets, options, home);
|
|
1569
|
+
});
|
|
1570
|
+
printActions(context, skillName, actions, options.dryRun === true);
|
|
1571
|
+
};
|
|
1572
|
+
var receiptGroups = (receipts) => {
|
|
1573
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1574
|
+
for (const receipt of receipts) {
|
|
1575
|
+
const group = groups.get(receipt.skillName) ?? [];
|
|
1576
|
+
group.push(receipt);
|
|
1577
|
+
groups.set(receipt.skillName, group);
|
|
1578
|
+
}
|
|
1579
|
+
return groups;
|
|
1580
|
+
};
|
|
1581
|
+
var statusLabel = (status) => {
|
|
1582
|
+
switch (status.kind) {
|
|
1583
|
+
case "current":
|
|
1584
|
+
return "current";
|
|
1585
|
+
case "update-available":
|
|
1586
|
+
return "update available";
|
|
1587
|
+
case "modified":
|
|
1588
|
+
return "local changes";
|
|
1589
|
+
case "missing":
|
|
1590
|
+
return "missing";
|
|
1591
|
+
case "unavailable":
|
|
1592
|
+
return "not in registry";
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
var listCommand = async (context = {}) => {
|
|
1596
|
+
const home = path7.resolve(context.home ?? os6.homedir());
|
|
1597
|
+
const receipts = await listReceipts(home);
|
|
1598
|
+
if (receipts.length === 0) {
|
|
1599
|
+
output(context, "No managed Yeka skills are installed.");
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
for (const receipt of receipts) {
|
|
1603
|
+
output(context, `${receipt.skillName} ${receipt.runtime} ${receipt.installedPath}`);
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
var statusCommand = async (context = {}) => {
|
|
1607
|
+
const home = path7.resolve(context.home ?? os6.homedir());
|
|
1608
|
+
const receipts = await listReceipts(home);
|
|
1609
|
+
if (receipts.length === 0) {
|
|
1610
|
+
output(context, "No managed Yeka skills are installed.");
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const client = context.client ?? new RegistryClient();
|
|
1614
|
+
const statuses = await withRegistrySession(async (token) => {
|
|
1615
|
+
const manifest = await client.getManifest(token);
|
|
1616
|
+
const remoteByName = new Map(manifest.skills.map((skill) => [skill.name, skill]));
|
|
1617
|
+
return Promise.all(
|
|
1618
|
+
receipts.map((receipt) => inspectLocalStatus(receipt, remoteByName.get(receipt.skillName), home))
|
|
1619
|
+
);
|
|
1620
|
+
});
|
|
1621
|
+
for (const status of statuses) {
|
|
1622
|
+
output(
|
|
1623
|
+
context,
|
|
1624
|
+
`${status.receipt.skillName} ${status.receipt.runtime} ${statusLabel(status)}`
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
var assertUpdatableStatuses = (skillName, statuses) => {
|
|
1629
|
+
const modified = statuses.find((status) => status.kind === "modified");
|
|
1630
|
+
if (modified !== void 0) {
|
|
1631
|
+
throw new CliError(
|
|
1632
|
+
"local_changes",
|
|
1633
|
+
`${skillName} has local changes for ${modified.receipt.runtime}. It was not changed.`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
if (statuses.some((status) => status.kind === "unavailable")) {
|
|
1637
|
+
throw new CliError("skill_unavailable", `${skillName} is no longer available in the private registry.`);
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
var updateCommand = async (skillNameInput, options, context = {}) => {
|
|
1641
|
+
const selectedName = skillNameInput === void 0 ? void 0 : validatedSkillName(skillNameInput);
|
|
1642
|
+
const home = path7.resolve(context.home ?? os6.homedir());
|
|
1643
|
+
const allReceipts = await listReceipts(home);
|
|
1644
|
+
const groups = receiptGroups(allReceipts);
|
|
1645
|
+
if (selectedName !== void 0 && !groups.has(selectedName)) {
|
|
1646
|
+
throw new CliError("not_installed", `${selectedName} is not a managed installation.`);
|
|
1647
|
+
}
|
|
1648
|
+
if (groups.size === 0) {
|
|
1649
|
+
output(context, "No managed Yeka skills are installed.");
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const client = context.client ?? new RegistryClient();
|
|
1653
|
+
const results = await withRegistrySession(async (token) => {
|
|
1654
|
+
const manifest = await client.getManifest(token);
|
|
1655
|
+
const remoteByName = new Map(manifest.skills.map((skill) => [skill.name, skill]));
|
|
1656
|
+
const selectedGroups = [...groups.entries()].filter(([name]) => selectedName === void 0 || name === selectedName);
|
|
1657
|
+
const prepared = [];
|
|
1658
|
+
for (const [name, receipts] of selectedGroups) {
|
|
1659
|
+
const remote = remoteByName.get(name);
|
|
1660
|
+
const statuses = await Promise.all(
|
|
1661
|
+
receipts.map((receipt) => inspectLocalStatus(receipt, remote, home))
|
|
1662
|
+
);
|
|
1663
|
+
assertUpdatableStatuses(name, statuses);
|
|
1664
|
+
if (remote === void 0) {
|
|
1665
|
+
throw new CliError("skill_unavailable", `${name} is no longer available in the private registry.`);
|
|
1666
|
+
}
|
|
1667
|
+
prepared.push({ name, receipts, skill: remote, statuses });
|
|
1668
|
+
}
|
|
1669
|
+
const updated = [];
|
|
1670
|
+
for (const item of prepared) {
|
|
1671
|
+
if (item.statuses.every((status) => status.kind === "current")) {
|
|
1672
|
+
updated.push({
|
|
1673
|
+
name: item.name,
|
|
1674
|
+
actions: item.receipts.map((receipt) => ({
|
|
1675
|
+
action: "unchanged",
|
|
1676
|
+
runtime: receipt.runtime,
|
|
1677
|
+
targetRoot: receipt.targetRoot,
|
|
1678
|
+
installedPath: receipt.installedPath
|
|
1679
|
+
}))
|
|
1680
|
+
});
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
const targets = targetsFromReceipts(item.receipts, item.name, home);
|
|
1684
|
+
updated.push({
|
|
1685
|
+
name: item.name,
|
|
1686
|
+
actions: await downloadAndInstall(client, token, item.skill, targets, options, home)
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return updated;
|
|
1690
|
+
});
|
|
1691
|
+
for (const result of results) {
|
|
1692
|
+
printActions(context, result.name, result.actions, options.dryRun === true);
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
var loginCommand = async (context = {}) => {
|
|
1696
|
+
await deleteSession();
|
|
1697
|
+
await login();
|
|
1698
|
+
output(context, "Yeka Skills is ready.");
|
|
1699
|
+
};
|
|
1700
|
+
var logoutCommand = async (context = {}) => {
|
|
1701
|
+
const removed = await deleteSession();
|
|
1702
|
+
output(context, removed ? "The Yeka Skills session was removed." : "There was no saved Yeka Skills session.");
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/cli.ts
|
|
1706
|
+
var program = new Command();
|
|
1707
|
+
program.name("yeka-skills").description("Install and update private Gemography agent skills").version("0.1.0").showHelpAfterError();
|
|
1708
|
+
program.command("add").description("Install one private skill").argument("<skill>", "exact skill name").option("--dry-run", "verify and show changes without installation").action(async (skill, options) => addCommand(skill, options));
|
|
1709
|
+
program.command("list").description("List managed local skills without network access").action(async () => listCommand());
|
|
1710
|
+
program.command("status").description("Check local skills and available updates").action(async () => statusCommand());
|
|
1711
|
+
program.command("update").description("Update one managed skill or all managed skills").argument("[skill]", "exact skill name").option("--dry-run", "verify and show changes without installation").action(
|
|
1712
|
+
async (skill, options) => updateCommand(skill, options)
|
|
1713
|
+
);
|
|
1714
|
+
program.command("login").description("Start a new private registry session").action(async () => loginCommand());
|
|
1715
|
+
program.command("logout").description("Remove the saved private registry session").action(async () => logoutCommand());
|
|
1716
|
+
var main = async () => {
|
|
1717
|
+
if (process.platform !== "darwin") {
|
|
1718
|
+
throw new CliError("unsupported_platform", "Yeka Skills currently supports macOS only.");
|
|
1719
|
+
}
|
|
1720
|
+
await program.parseAsync(process.argv);
|
|
1721
|
+
};
|
|
1722
|
+
main().catch((error) => {
|
|
1723
|
+
if (error instanceof CliError) {
|
|
1724
|
+
process.stderr.write(`Error: ${error.message}
|
|
1725
|
+
`);
|
|
1726
|
+
process.exitCode = error.exitCode;
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
process.stderr.write("Error: Yeka Skills stopped because of an unexpected error.\n");
|
|
1730
|
+
if (process.env.YEKA_SKILLS_DEBUG === "1" && error instanceof Error) {
|
|
1731
|
+
process.stderr.write(`${error.stack ?? error.message}
|
|
1732
|
+
`);
|
|
1733
|
+
}
|
|
1734
|
+
process.exitCode = 1;
|
|
1735
|
+
});
|