yeka-skills 0.1.0 → 0.2.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 +31 -3
- package/dist/cli.js +911 -70
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# yeka-skills
|
|
2
2
|
|
|
3
|
-
Install and
|
|
3
|
+
Install, update, and remove private Gemography agent skills from Yeka Skills.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx yeka-skills add ste100-session
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
The CLI
|
|
9
|
+
The CLI tries to open Cloudflare Access in your browser when it needs a session. It always prints
|
|
10
|
+
the same one-time address in the terminal, so you can open it yourself if the browser does not
|
|
11
|
+
continue. The address expires after five minutes. Run the same command again to get a new address.
|
|
12
|
+
The CLI stores the short session in macOS Keychain.
|
|
10
13
|
|
|
11
14
|
It detects the shared local skill folders used by ChatGPT desktop, Codex, Claude Desktop, Cowork, and Claude Code. A shared skill installs into each active compatible folder.
|
|
12
15
|
|
|
@@ -17,13 +20,38 @@ Commands:
|
|
|
17
20
|
```text
|
|
18
21
|
yeka-skills add <skill>
|
|
19
22
|
yeka-skills list
|
|
23
|
+
yeka-skills share <skill>
|
|
20
24
|
yeka-skills status
|
|
21
25
|
yeka-skills update [skill]
|
|
26
|
+
yeka-skills remove <skill>
|
|
22
27
|
yeka-skills login
|
|
23
28
|
yeka-skills logout
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
## Share a local skill with the team
|
|
32
|
+
|
|
33
|
+
`yeka-skills share <skill>` packages one of your local skill folders and sends it to the private
|
|
34
|
+
submission inbox, where a review bot turns it into a team-repo pull request. Nothing is published
|
|
35
|
+
by a share — a person reviews every submission first.
|
|
36
|
+
|
|
37
|
+
- The CLI looks for the folder in the same local roots the installer knows. If a skill exists in
|
|
38
|
+
several of them, it picks the one it would install into first (the current Codex folder, then
|
|
39
|
+
the Claude folder, then the legacy Codex folder) and tells you which it chose.
|
|
40
|
+
- `yeka-skills share <skill> --check` looks the folder over without network access or a session:
|
|
41
|
+
SKILL.md present with a matching `name`, folder under the 5 MB share limit, and plain-language
|
|
42
|
+
warnings about anything reviewers will look at (web addresses, credentials paths, `curl | sh`,
|
|
43
|
+
long base64 tokens). Warnings never block a share — the team's server-side checks decide.
|
|
44
|
+
- `--notes-file <path>` attaches a small UTF-8 text file (up to 8 KB) with your interview answers.
|
|
45
|
+
The file rides inside the archive as `_yeka/notes.txt`, always the first entry, namespaced so
|
|
46
|
+
the review bot can find it, read it, and strip it before the skill content is unpacked. Do not
|
|
47
|
+
create a `_yeka` folder inside your skill yourself.
|
|
48
|
+
- A successful share answers with one sentence: you'll hear back in #yeka-skills, and updates
|
|
49
|
+
usually go live within the hour.
|
|
50
|
+
|
|
51
|
+
Use `--dry-run` with `add`, `update`, or `remove` to verify and report changes without mutating the
|
|
52
|
+
managed installation. Removal is fully offline and uses local receipts. It refuses locally edited
|
|
53
|
+
copies unless `--force` is present; forced removal moves edited content into a private backup and
|
|
54
|
+
prints its path.
|
|
27
55
|
|
|
28
56
|
Managed installation receipts and backups stay in `~/Library/Application Support/Yeka Skills/`.
|
|
29
57
|
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands.ts
|
|
7
|
-
import { chmod as
|
|
8
|
-
import
|
|
9
|
-
import
|
|
7
|
+
import { chmod as chmod3, lstat as lstat9, mkdir as mkdir6, mkdtemp as mkdtemp3, readFile as readFile4, rm as rm5 } from "fs/promises";
|
|
8
|
+
import os7 from "os";
|
|
9
|
+
import path9 from "path";
|
|
10
10
|
|
|
11
11
|
// ../contracts/dist/index.js
|
|
12
12
|
import { z } from "zod";
|
|
@@ -41,6 +41,10 @@ var ShareIdMapSchema = z.object({
|
|
|
41
41
|
schemaVersion: z.literal(1),
|
|
42
42
|
skills: z.record(SkillNameSchema, OpaqueShareIdSchema)
|
|
43
43
|
});
|
|
44
|
+
var PublishedSkillsSchema = z.object({
|
|
45
|
+
schemaVersion: z.literal(1),
|
|
46
|
+
published: z.array(SkillNameSchema).min(1)
|
|
47
|
+
});
|
|
44
48
|
var RegistryManifestSchema = z.object({
|
|
45
49
|
schemaVersion: z.literal(1),
|
|
46
50
|
generatedAt: z.iso.datetime({ offset: true }),
|
|
@@ -112,7 +116,7 @@ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
|
112
116
|
import {
|
|
113
117
|
chmod,
|
|
114
118
|
cp,
|
|
115
|
-
lstat as
|
|
119
|
+
lstat as lstat4,
|
|
116
120
|
mkdir as mkdir3,
|
|
117
121
|
mkdtemp,
|
|
118
122
|
rename as rename2,
|
|
@@ -408,11 +412,45 @@ var extractVerifiedArchive = async (archivePath, destination, expectedContentHas
|
|
|
408
412
|
|
|
409
413
|
// src/receipts.ts
|
|
410
414
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
411
|
-
import { mkdir as mkdir2, readFile, readdir as readdir2, rename, unlink, writeFile } from "fs/promises";
|
|
415
|
+
import { lstat as lstat3, mkdir as mkdir2, readFile, readdir as readdir2, rename, unlink, writeFile } from "fs/promises";
|
|
412
416
|
import os2 from "os";
|
|
413
417
|
import path4 from "path";
|
|
414
418
|
var rootIdentifier = (targetRoot) => createHash2("sha256").update(targetRoot).digest("hex").slice(0, 16);
|
|
415
419
|
var receiptPath = (targetRoot, skillName, home = os2.homedir()) => path4.join(localPaths(home).receipts, rootIdentifier(targetRoot), `${skillName}.json`);
|
|
420
|
+
var receiptStoreExists = async (home, root) => {
|
|
421
|
+
const resolvedHome = path4.resolve(home);
|
|
422
|
+
const relativeRoot = path4.relative(resolvedHome, root);
|
|
423
|
+
if (relativeRoot.startsWith("..") || path4.isAbsolute(relativeRoot)) {
|
|
424
|
+
throw new CliError("unsafe_receipts", "The Yeka Skills receipt location is unsafe.");
|
|
425
|
+
}
|
|
426
|
+
let current = resolvedHome;
|
|
427
|
+
const homeEntry = await lstat3(current);
|
|
428
|
+
if (!homeEntry.isDirectory() || homeEntry.isSymbolicLink()) {
|
|
429
|
+
throw new CliError(
|
|
430
|
+
"unsafe_receipts",
|
|
431
|
+
`The Yeka Skills receipt location is not a real directory: ${current}.`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
for (const segment of relativeRoot.split(path4.sep)) {
|
|
435
|
+
current = path4.join(current, segment);
|
|
436
|
+
let entry;
|
|
437
|
+
try {
|
|
438
|
+
entry = await lstat3(current);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
446
|
+
throw new CliError(
|
|
447
|
+
"unsafe_receipts",
|
|
448
|
+
`The Yeka Skills receipt location is not a real directory: ${current}.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
};
|
|
416
454
|
var writeReceipt = async (receipt, home = os2.homedir()) => {
|
|
417
455
|
const validated = InstallReceiptSchema.parse(receipt);
|
|
418
456
|
const destination = receiptPath(receipt.targetRoot, receipt.skillName, home);
|
|
@@ -453,50 +491,76 @@ var readReceipt = async (targetRoot, skillName, home = os2.homedir()) => {
|
|
|
453
491
|
});
|
|
454
492
|
}
|
|
455
493
|
};
|
|
456
|
-
var
|
|
494
|
+
var scanReceiptRecords = async (home = os2.homedir()) => {
|
|
457
495
|
const root = localPaths(home).receipts;
|
|
496
|
+
if (!await receiptStoreExists(home, root)) {
|
|
497
|
+
return { records: [], invalidFiles: [] };
|
|
498
|
+
}
|
|
458
499
|
let rootEntries;
|
|
459
500
|
try {
|
|
460
501
|
rootEntries = await readdir2(root, { withFileTypes: true });
|
|
461
502
|
} catch (error) {
|
|
462
503
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
463
|
-
return [];
|
|
504
|
+
return { records: [], invalidFiles: [] };
|
|
464
505
|
}
|
|
465
506
|
throw error;
|
|
466
507
|
}
|
|
467
|
-
const
|
|
508
|
+
const records = [];
|
|
509
|
+
const invalidFiles = [];
|
|
468
510
|
for (const rootEntry of rootEntries) {
|
|
469
511
|
if (!rootEntry.isDirectory()) {
|
|
512
|
+
if (/^[0-9a-f]{16}$/.test(rootEntry.name)) {
|
|
513
|
+
invalidFiles.push(path4.join(root, rootEntry.name));
|
|
514
|
+
}
|
|
470
515
|
continue;
|
|
471
516
|
}
|
|
472
517
|
const files = await readdir2(path4.join(root, rootEntry.name), { withFileTypes: true });
|
|
473
518
|
for (const file of files) {
|
|
474
|
-
if (!file.
|
|
519
|
+
if (!file.name.endsWith(".json")) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const filePath = path4.join(root, rootEntry.name, file.name);
|
|
523
|
+
if (!file.isFile()) {
|
|
524
|
+
invalidFiles.push(filePath);
|
|
475
525
|
continue;
|
|
476
526
|
}
|
|
477
527
|
try {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
} catch (error) {
|
|
484
|
-
throw new CliError("invalid_receipt", `The local receipt ${file.name} is invalid.`, {
|
|
485
|
-
cause: error
|
|
528
|
+
const serialized = await readFile(filePath, "utf8");
|
|
529
|
+
records.push({
|
|
530
|
+
filePath,
|
|
531
|
+
contentHash: createHash2("sha256").update(serialized).digest("hex"),
|
|
532
|
+
receipt: InstallReceiptSchema.parse(JSON.parse(serialized))
|
|
486
533
|
});
|
|
534
|
+
} catch {
|
|
535
|
+
invalidFiles.push(filePath);
|
|
487
536
|
}
|
|
488
537
|
}
|
|
489
538
|
}
|
|
490
|
-
|
|
491
|
-
(left, right) => `${left.skillName}:${left.runtime}
|
|
539
|
+
records.sort(
|
|
540
|
+
(left, right) => `${left.receipt.skillName}:${left.receipt.runtime}:${left.filePath}`.localeCompare(
|
|
541
|
+
`${right.receipt.skillName}:${right.receipt.runtime}:${right.filePath}`,
|
|
542
|
+
"en"
|
|
543
|
+
)
|
|
492
544
|
);
|
|
545
|
+
invalidFiles.sort((left, right) => left.localeCompare(right, "en"));
|
|
546
|
+
return { records, invalidFiles };
|
|
547
|
+
};
|
|
548
|
+
var listReceipts = async (home = os2.homedir()) => {
|
|
549
|
+
const scan = await scanReceiptRecords(home);
|
|
550
|
+
if (scan.invalidFiles.length > 0) {
|
|
551
|
+
throw new CliError(
|
|
552
|
+
"invalid_receipt",
|
|
553
|
+
`The local receipt file is invalid: ${scan.invalidFiles[0]}.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return scan.records.map((record) => record.receipt);
|
|
493
557
|
};
|
|
494
558
|
|
|
495
559
|
// src/installer.ts
|
|
496
560
|
var isNotFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
497
561
|
var fingerprint = async (entryPath) => {
|
|
498
562
|
try {
|
|
499
|
-
const entry = await
|
|
563
|
+
const entry = await lstat4(entryPath, { bigint: true });
|
|
500
564
|
return {
|
|
501
565
|
dev: entry.dev,
|
|
502
566
|
ino: entry.ino,
|
|
@@ -521,7 +585,7 @@ var assertSafeTarget = async (target, skillName, home) => {
|
|
|
521
585
|
const parent = path5.dirname(resolvedRoot);
|
|
522
586
|
let parentStat;
|
|
523
587
|
try {
|
|
524
|
-
parentStat = await
|
|
588
|
+
parentStat = await lstat4(parent);
|
|
525
589
|
} catch (error) {
|
|
526
590
|
if (isNotFound(error)) {
|
|
527
591
|
throw new CliError("runtime_not_found", `The ${target.runtime} configuration directory is missing.`);
|
|
@@ -533,7 +597,7 @@ var assertSafeTarget = async (target, skillName, home) => {
|
|
|
533
597
|
}
|
|
534
598
|
const rootFingerprint = await fingerprint(resolvedRoot);
|
|
535
599
|
if (rootFingerprint !== null) {
|
|
536
|
-
const rootStat = await
|
|
600
|
+
const rootStat = await lstat4(resolvedRoot);
|
|
537
601
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
538
602
|
throw new CliError("unsafe_target", `The ${target.runtime} skills directory is not a real directory.`);
|
|
539
603
|
}
|
|
@@ -576,8 +640,8 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
576
640
|
if (oldReceipt !== null && (oldReceipt.runtime !== target.runtime || path5.resolve(oldReceipt.targetRoot) !== targetRoot || path5.resolve(oldReceipt.installedPath) !== installedPath)) {
|
|
577
641
|
throw new CliError("invalid_receipt_target", `The local receipt for ${skill.name} has an invalid target.`);
|
|
578
642
|
}
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
643
|
+
const entryFingerprint2 = await fingerprint(installedPath);
|
|
644
|
+
if (entryFingerprint2 === null) {
|
|
581
645
|
return {
|
|
582
646
|
action: "install",
|
|
583
647
|
runtime: target.runtime,
|
|
@@ -594,11 +658,11 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
594
658
|
targetRoot,
|
|
595
659
|
installedPath,
|
|
596
660
|
existed: true,
|
|
597
|
-
fingerprint:
|
|
661
|
+
fingerprint: entryFingerprint2,
|
|
598
662
|
oldReceipt
|
|
599
663
|
};
|
|
600
664
|
}
|
|
601
|
-
const installedStat = await
|
|
665
|
+
const installedStat = await lstat4(installedPath);
|
|
602
666
|
if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
|
|
603
667
|
throw new CliError(
|
|
604
668
|
"local_changes",
|
|
@@ -628,7 +692,7 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
628
692
|
targetRoot,
|
|
629
693
|
installedPath,
|
|
630
694
|
existed: true,
|
|
631
|
-
fingerprint:
|
|
695
|
+
fingerprint: entryFingerprint2,
|
|
632
696
|
oldReceipt
|
|
633
697
|
};
|
|
634
698
|
};
|
|
@@ -640,7 +704,7 @@ var ensureTargetRoot = async (targetRoot) => {
|
|
|
640
704
|
throw error;
|
|
641
705
|
}
|
|
642
706
|
}
|
|
643
|
-
const rootStat = await
|
|
707
|
+
const rootStat = await lstat4(targetRoot);
|
|
644
708
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
645
709
|
throw new CliError("unsafe_target", `The skills target is not a real directory: ${targetRoot}`);
|
|
646
710
|
}
|
|
@@ -733,7 +797,7 @@ var installSkillFromArchive = async (options) => {
|
|
|
733
797
|
}
|
|
734
798
|
const operationCache = localPaths(home).cache;
|
|
735
799
|
await mkdir3(operationCache, { recursive: true, mode: 448 });
|
|
736
|
-
const cacheStat = await
|
|
800
|
+
const cacheStat = await lstat4(operationCache);
|
|
737
801
|
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
738
802
|
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
739
803
|
}
|
|
@@ -1034,14 +1098,14 @@ var deleteSession = async () => {
|
|
|
1034
1098
|
};
|
|
1035
1099
|
|
|
1036
1100
|
// src/local-status.ts
|
|
1037
|
-
import { lstat as
|
|
1101
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
1038
1102
|
import os4 from "os";
|
|
1039
1103
|
var isNotFound2 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1040
1104
|
var inspectLocalStatus = async (receipt, remote, home = os4.homedir()) => {
|
|
1041
1105
|
targetsFromReceipts([receipt], receipt.skillName, home);
|
|
1042
1106
|
let installedStat;
|
|
1043
1107
|
try {
|
|
1044
|
-
installedStat = await
|
|
1108
|
+
installedStat = await lstat5(receipt.installedPath);
|
|
1045
1109
|
} catch (error) {
|
|
1046
1110
|
if (isNotFound2(error)) {
|
|
1047
1111
|
return { kind: "missing", receipt, ...remote === void 0 ? {} : { remote } };
|
|
@@ -1076,6 +1140,7 @@ import http from "http";
|
|
|
1076
1140
|
import { promisify } from "util";
|
|
1077
1141
|
var execFileAsync = promisify(execFile);
|
|
1078
1142
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1143
|
+
var LOGIN_RETRY = "run the same Yeka Skills command again to get a new address.";
|
|
1079
1144
|
var MAX_TOKEN_RESPONSE_BYTES = 16 * 1024;
|
|
1080
1145
|
var AUTHORIZATION_CODE_PATTERN = /^[A-Za-z0-9._-]{32,4096}$/;
|
|
1081
1146
|
var readBoundedJson = async (response) => {
|
|
@@ -1163,7 +1228,10 @@ var waitForCallback = async (expectedState) => {
|
|
|
1163
1228
|
throw new CliError("login_listener_failed", "The CLI could not start its private login listener.");
|
|
1164
1229
|
}
|
|
1165
1230
|
const timeout = setTimeout(() => {
|
|
1166
|
-
rejectCallback?.(new CliError(
|
|
1231
|
+
rejectCallback?.(new CliError(
|
|
1232
|
+
"login_timeout",
|
|
1233
|
+
"The one-time browser address expired after five minutes. Run the same Yeka Skills command again to get a new address."
|
|
1234
|
+
));
|
|
1167
1235
|
}, LOGIN_TIMEOUT_MS);
|
|
1168
1236
|
timeout.unref();
|
|
1169
1237
|
return {
|
|
@@ -1183,12 +1251,23 @@ var waitForCallback = async (expectedState) => {
|
|
|
1183
1251
|
var openBrowser = async (url) => {
|
|
1184
1252
|
try {
|
|
1185
1253
|
await execFileAsync("/usr/bin/open", [url]);
|
|
1186
|
-
return true;
|
|
1187
1254
|
} catch (error) {
|
|
1188
1255
|
void error;
|
|
1189
|
-
return false;
|
|
1190
1256
|
}
|
|
1191
1257
|
};
|
|
1258
|
+
var launchAuthorization = async (url, dependencies = {
|
|
1259
|
+
openBrowser,
|
|
1260
|
+
write: (message) => process.stdout.write(message)
|
|
1261
|
+
}) => {
|
|
1262
|
+
dependencies.write([
|
|
1263
|
+
"Your browser will try to open for Gemography email verification.",
|
|
1264
|
+
"If it does not continue, open this one-time address:",
|
|
1265
|
+
url,
|
|
1266
|
+
`This address expires in five minutes. If it expires, ${LOGIN_RETRY}`,
|
|
1267
|
+
""
|
|
1268
|
+
].join("\n"));
|
|
1269
|
+
await dependencies.openBrowser(url);
|
|
1270
|
+
};
|
|
1192
1271
|
var login = async () => {
|
|
1193
1272
|
const verifier = randomBytes(32).toString("base64url");
|
|
1194
1273
|
const challenge = createHash5("sha256").update(verifier).digest("base64url");
|
|
@@ -1199,13 +1278,8 @@ var login = async () => {
|
|
|
1199
1278
|
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
1200
1279
|
authorizationUrl.searchParams.set("redirect_uri", listener.authorizationUrl);
|
|
1201
1280
|
authorizationUrl.searchParams.set("state", state);
|
|
1202
|
-
process.stdout.write("Your browser will open for Gemography email verification.\n");
|
|
1203
1281
|
try {
|
|
1204
|
-
|
|
1205
|
-
process.stdout.write(`Open this address in your browser:
|
|
1206
|
-
${authorizationUrl.toString()}
|
|
1207
|
-
`);
|
|
1208
|
-
}
|
|
1282
|
+
await launchAuthorization(authorizationUrl.toString());
|
|
1209
1283
|
const callback = await listener.callback;
|
|
1210
1284
|
const response = await fetch(new URL("/v1/auth/token", REGISTRY_ORIGIN), {
|
|
1211
1285
|
method: "POST",
|
|
@@ -1235,8 +1309,346 @@ ${authorizationUrl.toString()}
|
|
|
1235
1309
|
}
|
|
1236
1310
|
};
|
|
1237
1311
|
|
|
1312
|
+
// src/remover.ts
|
|
1313
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
1314
|
+
import { chmod as chmod2, lstat as lstat6, mkdir as mkdir4, readFile as readFile2, rename as rename3, rm as rm3 } from "fs/promises";
|
|
1315
|
+
import os5 from "os";
|
|
1316
|
+
import path6 from "path";
|
|
1317
|
+
var isNotFound3 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1318
|
+
var entryFingerprint = async (entryPath) => {
|
|
1319
|
+
try {
|
|
1320
|
+
const entry = await lstat6(entryPath, { bigint: true });
|
|
1321
|
+
return {
|
|
1322
|
+
dev: entry.dev,
|
|
1323
|
+
ino: entry.ino,
|
|
1324
|
+
mode: Number(entry.mode),
|
|
1325
|
+
mtimeNs: entry.mtimeNs,
|
|
1326
|
+
size: entry.size
|
|
1327
|
+
};
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (isNotFound3(error)) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
var fingerprintsMatch2 = (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;
|
|
1336
|
+
var agentLabel = (runtime) => runtime === "codex" ? "Codex" : "Claude";
|
|
1337
|
+
var targetIdentifier = (targetRoot) => createHash6("sha256").update(targetRoot).digest("hex").slice(0, 16);
|
|
1338
|
+
var receiptIssue = (record, message) => `${record.filePath}: ${message}`;
|
|
1339
|
+
var assertRealDirectory = async (directory) => {
|
|
1340
|
+
const fingerprint2 = await entryFingerprint(directory);
|
|
1341
|
+
if (fingerprint2 === null) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
const entry = await lstat6(directory);
|
|
1345
|
+
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
1346
|
+
};
|
|
1347
|
+
var validateReceiptTarget = (record, skillName, home) => {
|
|
1348
|
+
const { receipt } = record;
|
|
1349
|
+
try {
|
|
1350
|
+
targetsFromReceipts([receipt], skillName, home);
|
|
1351
|
+
} catch {
|
|
1352
|
+
return receiptIssue(record, "the target is outside the supported local skill folders");
|
|
1353
|
+
}
|
|
1354
|
+
const canonicalRoot = path6.resolve(receipt.targetRoot);
|
|
1355
|
+
const canonicalInstalledPath = path6.join(canonicalRoot, skillName);
|
|
1356
|
+
if (receipt.targetRoot !== canonicalRoot || receipt.installedPath !== canonicalInstalledPath || path6.dirname(receipt.installedPath) !== canonicalRoot) {
|
|
1357
|
+
return receiptIssue(record, "the installed path is not the canonical path inside its target root");
|
|
1358
|
+
}
|
|
1359
|
+
if (receiptPath(receipt.targetRoot, receipt.skillName, home) !== record.filePath) {
|
|
1360
|
+
return receiptIssue(record, "the file is stored under the wrong target identifier");
|
|
1361
|
+
}
|
|
1362
|
+
return null;
|
|
1363
|
+
};
|
|
1364
|
+
var preflightRecord = async (record, skillName, home, force, batchRoot) => {
|
|
1365
|
+
const issues = [];
|
|
1366
|
+
const targetIssue = validateReceiptTarget(record, skillName, home);
|
|
1367
|
+
if (targetIssue !== null) {
|
|
1368
|
+
return { issues: [targetIssue] };
|
|
1369
|
+
}
|
|
1370
|
+
const receiptFingerprint = await entryFingerprint(record.filePath);
|
|
1371
|
+
if (receiptFingerprint === null) {
|
|
1372
|
+
return { issues: [receiptIssue(record, "the receipt disappeared during preflight")] };
|
|
1373
|
+
}
|
|
1374
|
+
const receiptStat = await lstat6(record.filePath);
|
|
1375
|
+
if (!receiptStat.isFile() || receiptStat.isSymbolicLink()) {
|
|
1376
|
+
return { issues: [receiptIssue(record, "the receipt is not a real file")] };
|
|
1377
|
+
}
|
|
1378
|
+
const { receipt } = record;
|
|
1379
|
+
const installedFingerprint = await entryFingerprint(receipt.installedPath);
|
|
1380
|
+
if (installedFingerprint === null) {
|
|
1381
|
+
return {
|
|
1382
|
+
target: {
|
|
1383
|
+
action: "clean-receipt",
|
|
1384
|
+
runtime: receipt.runtime,
|
|
1385
|
+
targetRoot: receipt.targetRoot,
|
|
1386
|
+
installedPath: receipt.installedPath,
|
|
1387
|
+
record,
|
|
1388
|
+
receiptFingerprint
|
|
1389
|
+
},
|
|
1390
|
+
issues
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (!await assertRealDirectory(path6.dirname(receipt.targetRoot))) {
|
|
1394
|
+
issues.push(receiptIssue(record, "the agent configuration directory is not a real directory"));
|
|
1395
|
+
}
|
|
1396
|
+
if (!await assertRealDirectory(receipt.targetRoot)) {
|
|
1397
|
+
issues.push(receiptIssue(record, "the target root is not a real directory"));
|
|
1398
|
+
}
|
|
1399
|
+
const installedStat = await lstat6(receipt.installedPath);
|
|
1400
|
+
if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
|
|
1401
|
+
issues.push(
|
|
1402
|
+
`${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} is not a real directory.`
|
|
1403
|
+
);
|
|
1404
|
+
return { issues };
|
|
1405
|
+
}
|
|
1406
|
+
let installedHash;
|
|
1407
|
+
try {
|
|
1408
|
+
installedHash = (await hashDirectory(receipt.installedPath)).contentHash;
|
|
1409
|
+
} catch {
|
|
1410
|
+
issues.push(
|
|
1411
|
+
`${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} contains unsupported local changes.`
|
|
1412
|
+
);
|
|
1413
|
+
return { issues };
|
|
1414
|
+
}
|
|
1415
|
+
const modified = installedHash !== receipt.installedHash;
|
|
1416
|
+
if (modified && !force) {
|
|
1417
|
+
issues.push(
|
|
1418
|
+
`${agentLabel(receipt.runtime)} copy at ${receipt.installedPath} was edited after installation; re-run with --force to preserve it in a private backup.`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
const backupPath = path6.join(
|
|
1422
|
+
batchRoot,
|
|
1423
|
+
"content",
|
|
1424
|
+
receipt.runtime,
|
|
1425
|
+
targetIdentifier(receipt.targetRoot),
|
|
1426
|
+
skillName
|
|
1427
|
+
);
|
|
1428
|
+
return {
|
|
1429
|
+
target: {
|
|
1430
|
+
action: modified ? "backup-modified" : "remove",
|
|
1431
|
+
runtime: receipt.runtime,
|
|
1432
|
+
targetRoot: receipt.targetRoot,
|
|
1433
|
+
installedPath: receipt.installedPath,
|
|
1434
|
+
backupPath,
|
|
1435
|
+
record,
|
|
1436
|
+
receiptFingerprint,
|
|
1437
|
+
installedFingerprint,
|
|
1438
|
+
installedHash
|
|
1439
|
+
},
|
|
1440
|
+
issues
|
|
1441
|
+
};
|
|
1442
|
+
};
|
|
1443
|
+
var assertInstalledUnchanged = async (target) => {
|
|
1444
|
+
const currentFingerprint = await entryFingerprint(target.installedPath);
|
|
1445
|
+
if (target.action === "clean-receipt") {
|
|
1446
|
+
if (currentFingerprint !== null) {
|
|
1447
|
+
throw new CliError(
|
|
1448
|
+
"target_changed",
|
|
1449
|
+
`The ${target.runtime} installation appeared during removal preflight.`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (!fingerprintsMatch2(currentFingerprint, target.installedFingerprint)) {
|
|
1455
|
+
throw new CliError(
|
|
1456
|
+
"target_changed",
|
|
1457
|
+
`The ${target.runtime} installation changed during removal preflight.`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const currentHash = (await hashDirectory(target.installedPath)).contentHash;
|
|
1461
|
+
if (currentHash !== target.installedHash) {
|
|
1462
|
+
throw new CliError(
|
|
1463
|
+
"target_changed",
|
|
1464
|
+
`The ${target.runtime} installation changed during removal preflight.`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
var assertReceiptUnchanged = async (target) => {
|
|
1469
|
+
const currentFingerprint = await entryFingerprint(target.record.filePath);
|
|
1470
|
+
if (!fingerprintsMatch2(currentFingerprint, target.receiptFingerprint)) {
|
|
1471
|
+
throw new CliError(
|
|
1472
|
+
"receipt_changed",
|
|
1473
|
+
`The receipt changed during removal: ${target.record.filePath}.`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
const serialized = await readFile2(target.record.filePath, "utf8");
|
|
1477
|
+
const contentHash = createHash6("sha256").update(serialized).digest("hex");
|
|
1478
|
+
if (contentHash !== target.record.contentHash) {
|
|
1479
|
+
throw new CliError(
|
|
1480
|
+
"receipt_changed",
|
|
1481
|
+
`The receipt changed during removal: ${target.record.filePath}.`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
var createPrivateBatchRoot = async (batchRoot, home) => {
|
|
1486
|
+
const backups = localPaths(home).backups;
|
|
1487
|
+
await mkdir4(backups, { recursive: true, mode: 448 });
|
|
1488
|
+
const backupsStat = await lstat6(backups);
|
|
1489
|
+
if (!backupsStat.isDirectory() || backupsStat.isSymbolicLink()) {
|
|
1490
|
+
throw new CliError("unsafe_backup", "The Yeka Skills backup location is not a real directory.");
|
|
1491
|
+
}
|
|
1492
|
+
await mkdir4(batchRoot, { mode: 448 });
|
|
1493
|
+
await chmod2(batchRoot, 448);
|
|
1494
|
+
};
|
|
1495
|
+
var moveToBatch = async (source, destination) => {
|
|
1496
|
+
await mkdir4(path6.dirname(destination), { recursive: true, mode: 448 });
|
|
1497
|
+
await rename3(source, destination);
|
|
1498
|
+
return { source, destination };
|
|
1499
|
+
};
|
|
1500
|
+
var rollbackMoves = async (moves) => {
|
|
1501
|
+
const failures = [];
|
|
1502
|
+
for (const move of [...moves].reverse()) {
|
|
1503
|
+
try {
|
|
1504
|
+
await rename3(move.destination, move.source);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return failures;
|
|
1510
|
+
};
|
|
1511
|
+
var restoreReceipts2 = async (targets, home) => {
|
|
1512
|
+
const failures = [];
|
|
1513
|
+
for (const target of [...targets].reverse()) {
|
|
1514
|
+
try {
|
|
1515
|
+
await writeReceipt(target.record.receipt, home);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
failures.push(error instanceof Error ? error : new Error(String(error)));
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return failures;
|
|
1521
|
+
};
|
|
1522
|
+
var publicActions = (targets) => targets.map(({ action, runtime, targetRoot, installedPath, backupPath }) => ({
|
|
1523
|
+
action,
|
|
1524
|
+
runtime,
|
|
1525
|
+
targetRoot,
|
|
1526
|
+
installedPath,
|
|
1527
|
+
...backupPath === void 0 ? {} : { backupPath }
|
|
1528
|
+
}));
|
|
1529
|
+
var removableSkillNames = async (home = os5.homedir()) => {
|
|
1530
|
+
const scan = await scanReceiptRecords(home);
|
|
1531
|
+
return [...new Set(scan.records.map((record) => record.receipt.skillName))].sort(
|
|
1532
|
+
(left, right) => left.localeCompare(right, "en")
|
|
1533
|
+
);
|
|
1534
|
+
};
|
|
1535
|
+
var removeSkill = async (skillName, options = {}) => {
|
|
1536
|
+
const home = path6.resolve(options.home ?? os5.homedir());
|
|
1537
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1538
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-");
|
|
1539
|
+
const batchRoot = path6.join(
|
|
1540
|
+
localPaths(home).backups,
|
|
1541
|
+
`${timestamp}-remove-${randomUUID3()}`
|
|
1542
|
+
);
|
|
1543
|
+
const scan = await scanReceiptRecords(home);
|
|
1544
|
+
const matchingRecords = scan.records.filter((record) => record.receipt.skillName === skillName);
|
|
1545
|
+
if (matchingRecords.length === 0 && scan.invalidFiles.length === 0) {
|
|
1546
|
+
throw new CliError("not_installed", `${skillName} is not a managed installation.`);
|
|
1547
|
+
}
|
|
1548
|
+
const issues = scan.invalidFiles.map((filePath) => `${filePath}: the receipt is invalid or corrupt`);
|
|
1549
|
+
const targets = [];
|
|
1550
|
+
for (const record of matchingRecords) {
|
|
1551
|
+
try {
|
|
1552
|
+
const result = await preflightRecord(
|
|
1553
|
+
record,
|
|
1554
|
+
skillName,
|
|
1555
|
+
home,
|
|
1556
|
+
options.force === true,
|
|
1557
|
+
batchRoot
|
|
1558
|
+
);
|
|
1559
|
+
issues.push(...result.issues);
|
|
1560
|
+
if (result.target !== void 0) {
|
|
1561
|
+
targets.push(result.target);
|
|
1562
|
+
}
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
issues.push(
|
|
1565
|
+
receiptIssue(
|
|
1566
|
+
record,
|
|
1567
|
+
error instanceof Error ? error.message : "preflight could not inspect this target"
|
|
1568
|
+
)
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const liveInstallsByRuntime = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const target of targets) {
|
|
1574
|
+
if (target.backupPath !== void 0) {
|
|
1575
|
+
const live = liveInstallsByRuntime.get(target.runtime) ?? [];
|
|
1576
|
+
live.push(target);
|
|
1577
|
+
liveInstallsByRuntime.set(target.runtime, live);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
for (const [runtime, liveTargets] of liveInstallsByRuntime) {
|
|
1581
|
+
if (liveTargets.length > 1) {
|
|
1582
|
+
for (const target of liveTargets) {
|
|
1583
|
+
issues.push(
|
|
1584
|
+
receiptIssue(
|
|
1585
|
+
target.record,
|
|
1586
|
+
`there is more than one ${runtime} receipt with an existing installation`
|
|
1587
|
+
)
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (issues.length > 0) {
|
|
1593
|
+
throw new CliError(
|
|
1594
|
+
"remove_preflight_failed",
|
|
1595
|
+
`Nothing was removed because preflight found a problem:
|
|
1596
|
+
${issues.map((issue) => `- ${issue}`).join("\n")}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (targets.length === 0) {
|
|
1600
|
+
throw new CliError("not_installed", `${skillName} is not a managed installation.`);
|
|
1601
|
+
}
|
|
1602
|
+
const actions = publicActions(targets);
|
|
1603
|
+
if (options.dryRun === true) {
|
|
1604
|
+
return actions;
|
|
1605
|
+
}
|
|
1606
|
+
const hasDirectoryMoves = targets.some((target) => target.backupPath !== void 0);
|
|
1607
|
+
if (hasDirectoryMoves) {
|
|
1608
|
+
await createPrivateBatchRoot(batchRoot, home);
|
|
1609
|
+
}
|
|
1610
|
+
const directoryMoves = [];
|
|
1611
|
+
const deletedReceipts = [];
|
|
1612
|
+
try {
|
|
1613
|
+
for (const target of targets) {
|
|
1614
|
+
await assertInstalledUnchanged(target);
|
|
1615
|
+
if (target.backupPath !== void 0) {
|
|
1616
|
+
directoryMoves.push(await moveToBatch(target.installedPath, target.backupPath));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
for (const target of targets) {
|
|
1620
|
+
await assertReceiptUnchanged(target);
|
|
1621
|
+
await deleteReceipt(target.targetRoot, skillName, home);
|
|
1622
|
+
deletedReceipts.push(target);
|
|
1623
|
+
}
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
const receiptFailures = await restoreReceipts2(deletedReceipts, home);
|
|
1626
|
+
const directoryFailures = await rollbackMoves(directoryMoves);
|
|
1627
|
+
if (receiptFailures.length > 0 || directoryFailures.length > 0) {
|
|
1628
|
+
throw new CliError(
|
|
1629
|
+
"rollback_failed",
|
|
1630
|
+
hasDirectoryMoves ? `Removal failed and automatic rollback was incomplete. Recovery data remains at ${batchRoot}.` : "Removal failed and automatic rollback was incomplete.",
|
|
1631
|
+
{ cause: error }
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (hasDirectoryMoves) {
|
|
1635
|
+
await rm3(batchRoot, { recursive: true, force: true });
|
|
1636
|
+
}
|
|
1637
|
+
throw new CliError(
|
|
1638
|
+
"remove_failed",
|
|
1639
|
+
`Nothing was removed. ${error instanceof Error ? error.message : "The removal batch failed."}`,
|
|
1640
|
+
{ cause: error }
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const retainsModifiedCopy = targets.some((target) => target.action === "backup-modified");
|
|
1644
|
+
if (hasDirectoryMoves && !retainsModifiedCopy) {
|
|
1645
|
+
await rm3(batchRoot, { recursive: true, force: true });
|
|
1646
|
+
}
|
|
1647
|
+
return actions;
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1238
1650
|
// src/registry-client.ts
|
|
1239
|
-
import { createHash as
|
|
1651
|
+
import { createHash as createHash7 } from "crypto";
|
|
1240
1652
|
import { open, unlink as unlink2 } from "fs/promises";
|
|
1241
1653
|
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
1242
1654
|
var parseContentLength = (value) => {
|
|
@@ -1341,7 +1753,7 @@ var RegistryClient = class {
|
|
|
1341
1753
|
throw new CliError("artifact_size_mismatch", "The skill artifact size does not match its manifest.");
|
|
1342
1754
|
}
|
|
1343
1755
|
const file = await open(destination, "wx", 384);
|
|
1344
|
-
const hash =
|
|
1756
|
+
const hash = createHash7("sha256");
|
|
1345
1757
|
const reader = response.body.getReader();
|
|
1346
1758
|
let bytesWritten = 0;
|
|
1347
1759
|
try {
|
|
@@ -1389,6 +1801,60 @@ var RegistryClient = class {
|
|
|
1389
1801
|
);
|
|
1390
1802
|
}
|
|
1391
1803
|
}
|
|
1804
|
+
async uploadSubmission(skillName, archiveSha256, body, token) {
|
|
1805
|
+
const response = await fetch(new URL("/v1/submissions", this.origin), {
|
|
1806
|
+
method: "POST",
|
|
1807
|
+
headers: {
|
|
1808
|
+
Authorization: `Bearer ${token}`,
|
|
1809
|
+
"X-Yeka-Skill-Name": skillName,
|
|
1810
|
+
"X-Yeka-Archive-Sha256": archiveSha256,
|
|
1811
|
+
"Content-Type": "application/gzip"
|
|
1812
|
+
},
|
|
1813
|
+
body,
|
|
1814
|
+
redirect: "error",
|
|
1815
|
+
signal: AbortSignal.timeout(2 * 60 * 1e3)
|
|
1816
|
+
});
|
|
1817
|
+
if (response.status === 201) {
|
|
1818
|
+
const payload = await response.json();
|
|
1819
|
+
if (typeof payload.submissionId !== "string" || payload.submissionId.length === 0) {
|
|
1820
|
+
throw new CliError("invalid_submission_response", "The registry response was incomplete.");
|
|
1821
|
+
}
|
|
1822
|
+
return payload.submissionId;
|
|
1823
|
+
}
|
|
1824
|
+
if (response.status === 401 || response.status === 403) {
|
|
1825
|
+
await response.body?.cancel("authentication failed");
|
|
1826
|
+
throw new AuthenticationError();
|
|
1827
|
+
}
|
|
1828
|
+
await response.body?.cancel("submission rejected");
|
|
1829
|
+
if (response.status === 415) {
|
|
1830
|
+
throw new CliError(
|
|
1831
|
+
"submission_not_gzip",
|
|
1832
|
+
"The registry did not accept the archive as gzip. Run `yeka-skills share " + skillName + " --check` and share the error with the team channel if it looks fine."
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
if (response.status === 413) {
|
|
1836
|
+
throw new CliError(
|
|
1837
|
+
"submission_too_large",
|
|
1838
|
+
"The skill is over the 5 MB share limit. Move anything heavy out of the folder, then run `yeka-skills share " + skillName + "` again."
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
if (response.status === 429) {
|
|
1842
|
+
throw new CliError(
|
|
1843
|
+
"submission_rate_limited",
|
|
1844
|
+
"You have shared the daily maximum of 20 skills. Try again tomorrow."
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
if (response.status === 400) {
|
|
1848
|
+
throw new CliError(
|
|
1849
|
+
"submission_rejected",
|
|
1850
|
+
"The registry could not accept this share. Run `yeka-skills share " + skillName + " --check` to look the folder over, then try again."
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
throw new CliError(
|
|
1854
|
+
"registry_request_failed",
|
|
1855
|
+
`The registry could not accept the share right now (status ${response.status}). Try again in a moment.`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1392
1858
|
};
|
|
1393
1859
|
|
|
1394
1860
|
// src/session.ts
|
|
@@ -1410,13 +1876,253 @@ var withRegistrySession = async (operation, dependencies = defaultDependencies)
|
|
|
1410
1876
|
}
|
|
1411
1877
|
};
|
|
1412
1878
|
|
|
1879
|
+
// src/share.ts
|
|
1880
|
+
import { createHash as createHash8 } from "crypto";
|
|
1881
|
+
import { lstat as lstat7, mkdir as mkdir5, mkdtemp as mkdtemp2, readdir as readdir3, readFile as readFile3, realpath, rm as rm4, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1882
|
+
import { tmpdir } from "os";
|
|
1883
|
+
import path7 from "path";
|
|
1884
|
+
import * as tar2 from "tar";
|
|
1885
|
+
import { gzipSync } from "zlib";
|
|
1886
|
+
var MAX_PACKAGED_BYTES = 5 * 1024 * 1024;
|
|
1887
|
+
var MAX_NOTES_BYTES = 8 * 1024;
|
|
1888
|
+
var RESERVED_ENTRY = "_yeka";
|
|
1889
|
+
var NOTES_ARCHIVE_PATH = "_yeka/notes.txt";
|
|
1890
|
+
var SKILL_MANIFEST_PATH = "SKILL.md";
|
|
1891
|
+
var isSkillFolder = async (candidate) => {
|
|
1892
|
+
try {
|
|
1893
|
+
const entry = await lstat7(candidate);
|
|
1894
|
+
if (entry.isSymbolicLink()) {
|
|
1895
|
+
const resolved = await stat2(await realpath(candidate));
|
|
1896
|
+
return resolved.isDirectory();
|
|
1897
|
+
}
|
|
1898
|
+
return entry.isDirectory();
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
throw error;
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
var candidateRoots = (home) => [
|
|
1907
|
+
{ runtime: "codex", targetRoot: path7.join(home, ".agents", "skills") },
|
|
1908
|
+
{ runtime: "claude", targetRoot: path7.join(home, ".claude", "skills") },
|
|
1909
|
+
{ runtime: "codex", targetRoot: path7.join(home, ".codex", "skills") }
|
|
1910
|
+
];
|
|
1911
|
+
var resolveSkillFolder = async (skillName, home) => {
|
|
1912
|
+
const parsedName = SkillNameSchema.safeParse(skillName);
|
|
1913
|
+
if (!parsedName.success) {
|
|
1914
|
+
throw new CliError("invalid_skill_name", "Use the exact lowercase skill name from the catalog.");
|
|
1915
|
+
}
|
|
1916
|
+
const found = [];
|
|
1917
|
+
for (const candidate of candidateRoots(home)) {
|
|
1918
|
+
const requestedPath = path7.join(candidate.targetRoot, skillName);
|
|
1919
|
+
if (await isSkillFolder(requestedPath)) {
|
|
1920
|
+
found.push({
|
|
1921
|
+
...candidate,
|
|
1922
|
+
requestedPath,
|
|
1923
|
+
folderPath: await realpath(requestedPath)
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (found.length === 0) {
|
|
1928
|
+
throw new CliError(
|
|
1929
|
+
"skill_folder_not_found",
|
|
1930
|
+
`No local skill folder named ${skillName} was found in your Codex or Claude skills folders.`
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
return found[0];
|
|
1934
|
+
};
|
|
1935
|
+
var enumerateSkillFiles = async (folderPath) => {
|
|
1936
|
+
const entries = [];
|
|
1937
|
+
const walk = async (relativePrefix) => {
|
|
1938
|
+
const absoluteDir = relativePrefix === "" ? folderPath : path7.join(folderPath, relativePrefix);
|
|
1939
|
+
for (const entry of await readdir3(absoluteDir, { withFileTypes: true })) {
|
|
1940
|
+
const relativePath = relativePrefix === "" ? entry.name : `${relativePrefix}/${entry.name}`;
|
|
1941
|
+
if (relativePrefix === "" && entry.name === RESERVED_ENTRY) {
|
|
1942
|
+
throw new CliError(
|
|
1943
|
+
"reserved_folder_name",
|
|
1944
|
+
"This skill contains a reserved `_yeka` folder. Rename it \u2014 Yeka Skills uses `_yeka` inside the archive for its own metadata."
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
if (entry.isSymbolicLink()) {
|
|
1948
|
+
throw new CliError(
|
|
1949
|
+
"unsafe_skill_folder",
|
|
1950
|
+
`${relativePath} is a symlink. Replace it with a real file or folder before sharing.`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
if (entry.isDirectory()) {
|
|
1954
|
+
await walk(relativePath);
|
|
1955
|
+
} else if (entry.isFile()) {
|
|
1956
|
+
entries.push({ relativePath, sizeBytes: (await lstat7(path7.join(absoluteDir, entry.name))).size });
|
|
1957
|
+
} else {
|
|
1958
|
+
throw new CliError(
|
|
1959
|
+
"unsafe_skill_folder",
|
|
1960
|
+
`${relativePath} is not a regular file. Remove it before sharing.`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
await walk("");
|
|
1966
|
+
entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath, "en"));
|
|
1967
|
+
if (!entries.some((entry) => entry.relativePath === SKILL_MANIFEST_PATH)) {
|
|
1968
|
+
throw new CliError(
|
|
1969
|
+
"skill_md_missing",
|
|
1970
|
+
`The folder has no SKILL.md file. Add one at ${path7.join(folderPath, SKILL_MANIFEST_PATH)} so reviewers know what the skill does.`
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
return {
|
|
1974
|
+
entries,
|
|
1975
|
+
totalBytes: entries.reduce((total, entry) => total + entry.sizeBytes, 0)
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
var CREDENTIAL_PATH_PATTERN = /(\.ssh[/\\]|\.aws[/\\]|\.netrc|\.git-credentials|id_rsa|\.env\b|credentials\.json)/i;
|
|
1979
|
+
var CURL_PIPE_SHELL_PATTERN = /(?:curl|wget)\s[^|]*\|\s*(?:ba|z)?sh\b/i;
|
|
1980
|
+
var LONG_BASE64_PATTERN = /[A-Za-z0-9+/]{80,}={0,2}/;
|
|
1981
|
+
var URL_PATTERN = /https?:\/\/[^\s"'`>)\]]+/g;
|
|
1982
|
+
var lintWarningsFor = async (folderPath, entries) => {
|
|
1983
|
+
const warnings = [];
|
|
1984
|
+
const urls = /* @__PURE__ */ new Set();
|
|
1985
|
+
for (const { relativePath } of entries) {
|
|
1986
|
+
const text = (await readFile3(path7.join(folderPath, relativePath))).toString("utf8");
|
|
1987
|
+
for (const match of text.matchAll(URL_PATTERN)) {
|
|
1988
|
+
urls.add(match[0]);
|
|
1989
|
+
}
|
|
1990
|
+
if (CREDENTIAL_PATH_PATTERN.test(text)) {
|
|
1991
|
+
warnings.push(
|
|
1992
|
+
`${relativePath} mentions a credentials path (like .ssh, .aws, or a .env file). Check that it only points at them and never contains the secrets themselves.`
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
if (CURL_PIPE_SHELL_PATTERN.test(text)) {
|
|
1996
|
+
warnings.push(
|
|
1997
|
+
`${relativePath} pipes curl or wget straight into a shell. Reviewers will look at this closely, so make sure the command is safe.`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
if (LONG_BASE64_PATTERN.test(text)) {
|
|
2001
|
+
warnings.push(
|
|
2002
|
+
`${relativePath} contains a long base64 token. Make sure it is not a real secret before you share.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (urls.size > 0) {
|
|
2007
|
+
warnings.push(
|
|
2008
|
+
`The skill mentions these web addresses, just so you know: ${[...urls].slice(0, 5).join(", ")}.`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
return warnings;
|
|
2012
|
+
};
|
|
2013
|
+
var packageSkillFolder = async (folderPath, notes, maxBytes = MAX_PACKAGED_BYTES) => {
|
|
2014
|
+
if (notes !== void 0 && Buffer.byteLength(notes.text, "utf8") > MAX_NOTES_BYTES) {
|
|
2015
|
+
throw new CliError(
|
|
2016
|
+
"notes_too_large",
|
|
2017
|
+
"The notes file is over 8 KB. Trim it down and try again."
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const skillFiles = await enumerateSkillFiles(folderPath);
|
|
2021
|
+
const staging = await mkdtemp2(path7.join(tmpdir(), "yeka-share-"));
|
|
2022
|
+
let archiveBytes;
|
|
2023
|
+
try {
|
|
2024
|
+
const tarPath = path7.join(staging, "skill.tar");
|
|
2025
|
+
if (notes === void 0) {
|
|
2026
|
+
await tar2.c(
|
|
2027
|
+
{ cwd: folderPath, file: tarPath, portable: true, noMtime: true, strict: true },
|
|
2028
|
+
skillFiles.entries.map((entry) => entry.relativePath)
|
|
2029
|
+
);
|
|
2030
|
+
} else {
|
|
2031
|
+
const notesStagingPath = path7.join(staging, NOTES_ARCHIVE_PATH);
|
|
2032
|
+
await mkdir5(path7.dirname(notesStagingPath), { recursive: true });
|
|
2033
|
+
await writeFile2(notesStagingPath, notes.text, { encoding: "utf8", mode: 384 });
|
|
2034
|
+
await tar2.c(
|
|
2035
|
+
{ cwd: staging, file: tarPath, portable: true, noMtime: true, strict: true },
|
|
2036
|
+
[NOTES_ARCHIVE_PATH]
|
|
2037
|
+
);
|
|
2038
|
+
await tar2.r(
|
|
2039
|
+
{ cwd: folderPath, file: tarPath, portable: true, noMtime: true, strict: true },
|
|
2040
|
+
skillFiles.entries.map((entry) => entry.relativePath)
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
const archivePaths = [];
|
|
2044
|
+
await tar2.t({
|
|
2045
|
+
file: tarPath,
|
|
2046
|
+
strict: true,
|
|
2047
|
+
onReadEntry: (entry) => {
|
|
2048
|
+
const entryPath = entry.path.replace(/\/+$/, "");
|
|
2049
|
+
if (archivePaths.includes(entryPath)) {
|
|
2050
|
+
throw new CliError(
|
|
2051
|
+
"reserved_folder_name",
|
|
2052
|
+
`The archive repeats ${entryPath}. Remove the duplicate before sharing.`
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
archivePaths.push(entryPath);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
const expectedPaths = new Set(skillFiles.entries.map((entry) => entry.relativePath));
|
|
2059
|
+
if (notes !== void 0) {
|
|
2060
|
+
expectedPaths.add(NOTES_ARCHIVE_PATH);
|
|
2061
|
+
}
|
|
2062
|
+
if (archivePaths.length !== expectedPaths.size || archivePaths.some((p) => !expectedPaths.has(p))) {
|
|
2063
|
+
throw new CliError(
|
|
2064
|
+
"unsafe_skill_folder",
|
|
2065
|
+
"The folder changed while the archive was being built. Nothing was shared; run the share again."
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
const recheck = await enumerateSkillFiles(folderPath);
|
|
2069
|
+
if (recheck.entries.length !== skillFiles.entries.length || recheck.entries.some(
|
|
2070
|
+
(entry, index) => entry.relativePath !== skillFiles.entries[index].relativePath || entry.sizeBytes !== skillFiles.entries[index].sizeBytes
|
|
2071
|
+
)) {
|
|
2072
|
+
throw new CliError(
|
|
2073
|
+
"skill_folder_changed",
|
|
2074
|
+
"The folder changed while it was being packaged. Nothing was shared; run the share again."
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
const tarBytes = new Uint8Array(await readFile3(tarPath));
|
|
2078
|
+
const bytes = new Uint8Array(gzipSync(tarBytes));
|
|
2079
|
+
archiveBytes = bytes.byteLength;
|
|
2080
|
+
if (archiveBytes > maxBytes) {
|
|
2081
|
+
throw new CliError(
|
|
2082
|
+
"skill_too_large",
|
|
2083
|
+
`The packaged skill is ${(archiveBytes / (1024 * 1024)).toFixed(1)} MB, over the 5 MB share limit. Move anything heavy out of the folder and try again.`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
const sha256 = createHash8("sha256").update(bytes).digest("hex");
|
|
2087
|
+
return {
|
|
2088
|
+
bytes,
|
|
2089
|
+
sha256,
|
|
2090
|
+
fileCount: skillFiles.entries.length,
|
|
2091
|
+
packagedBytes: skillFiles.totalBytes,
|
|
2092
|
+
archiveBytes
|
|
2093
|
+
};
|
|
2094
|
+
} finally {
|
|
2095
|
+
await rm4(staging, { recursive: true, force: true });
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
var checkSkillFolder = async (folderPath, skillName, maxBytes = MAX_PACKAGED_BYTES, notes) => {
|
|
2099
|
+
const skillFiles = await enumerateSkillFiles(folderPath);
|
|
2100
|
+
const skillMd = await readFile3(path7.join(folderPath, SKILL_MANIFEST_PATH), "utf8");
|
|
2101
|
+
const frontmatterMatch = skillMd.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2102
|
+
const frontmatter = frontmatterMatch?.[1] ?? "";
|
|
2103
|
+
const declaredName = frontmatter.match(/^name:\s*(.+)\s*$/m)?.[1]?.trim() ?? "";
|
|
2104
|
+
if (declaredName !== skillName || !SkillNameSchema.safeParse(declaredName).success) {
|
|
2105
|
+
throw new CliError(
|
|
2106
|
+
"skill_name_mismatch",
|
|
2107
|
+
declaredName.length === 0 ? `The SKILL.md frontmatter has no name line. Add \`name: ${skillName}\` at the top.` : `The SKILL.md frontmatter says name: ${declaredName}, but the folder is ${skillName}. Make them match, using only lowercase letters, numbers, and dashes.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
const packaged = await packageSkillFolder(folderPath, notes, maxBytes);
|
|
2111
|
+
return {
|
|
2112
|
+
fileCount: packaged.fileCount,
|
|
2113
|
+
rawBytes: skillFiles.totalBytes,
|
|
2114
|
+
archiveBytes: packaged.archiveBytes,
|
|
2115
|
+
warnings: await lintWarningsFor(folderPath, skillFiles.entries)
|
|
2116
|
+
};
|
|
2117
|
+
};
|
|
2118
|
+
|
|
1413
2119
|
// src/targets.ts
|
|
1414
|
-
import { lstat as
|
|
1415
|
-
import
|
|
1416
|
-
import
|
|
2120
|
+
import { lstat as lstat8 } from "fs/promises";
|
|
2121
|
+
import os6 from "os";
|
|
2122
|
+
import path8 from "path";
|
|
1417
2123
|
var isRealDirectory = async (candidate) => {
|
|
1418
2124
|
try {
|
|
1419
|
-
const entry = await
|
|
2125
|
+
const entry = await lstat8(candidate);
|
|
1420
2126
|
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
1421
2127
|
} catch (error) {
|
|
1422
2128
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1427,7 +2133,7 @@ var isRealDirectory = async (candidate) => {
|
|
|
1427
2133
|
};
|
|
1428
2134
|
var pathExists = async (candidate) => {
|
|
1429
2135
|
try {
|
|
1430
|
-
await
|
|
2136
|
+
await lstat8(candidate);
|
|
1431
2137
|
return true;
|
|
1432
2138
|
} catch (error) {
|
|
1433
2139
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1437,17 +2143,17 @@ var pathExists = async (candidate) => {
|
|
|
1437
2143
|
}
|
|
1438
2144
|
};
|
|
1439
2145
|
var resolveCodexTarget = async (home, skillName) => {
|
|
1440
|
-
const currentRoot =
|
|
1441
|
-
const currentParent =
|
|
1442
|
-
const codexConfig =
|
|
1443
|
-
const legacyRoot =
|
|
2146
|
+
const currentRoot = path8.join(home, ".agents", "skills");
|
|
2147
|
+
const currentParent = path8.join(home, ".agents");
|
|
2148
|
+
const codexConfig = path8.join(home, ".codex");
|
|
2149
|
+
const legacyRoot = path8.join(codexConfig, "skills");
|
|
1444
2150
|
const legacyActive = await isRealDirectory(legacyRoot);
|
|
1445
2151
|
const currentActive = await isRealDirectory(currentRoot) || await isRealDirectory(currentParent) || await isRealDirectory(codexConfig) && !legacyActive;
|
|
1446
2152
|
if (currentActive) {
|
|
1447
|
-
if (legacyActive && await pathExists(
|
|
2153
|
+
if (legacyActive && await pathExists(path8.join(legacyRoot, skillName))) {
|
|
1448
2154
|
throw new CliError(
|
|
1449
2155
|
"legacy_codex_conflict",
|
|
1450
|
-
`Codex can see a legacy copy at ${
|
|
2156
|
+
`Codex can see a legacy copy at ${path8.join(legacyRoot, skillName)}. Remove or archive that copy before installation.`
|
|
1451
2157
|
);
|
|
1452
2158
|
}
|
|
1453
2159
|
return { runtime: "codex", targetRoot: currentRoot, legacy: false };
|
|
@@ -1458,17 +2164,17 @@ var resolveCodexTarget = async (home, skillName) => {
|
|
|
1458
2164
|
return null;
|
|
1459
2165
|
};
|
|
1460
2166
|
var resolveClaudeTarget = async (home) => {
|
|
1461
|
-
const claudeConfig =
|
|
2167
|
+
const claudeConfig = path8.join(home, ".claude");
|
|
1462
2168
|
if (!await isRealDirectory(claudeConfig)) {
|
|
1463
2169
|
return null;
|
|
1464
2170
|
}
|
|
1465
2171
|
return {
|
|
1466
2172
|
runtime: "claude",
|
|
1467
|
-
targetRoot:
|
|
2173
|
+
targetRoot: path8.join(claudeConfig, "skills"),
|
|
1468
2174
|
legacy: false
|
|
1469
2175
|
};
|
|
1470
2176
|
};
|
|
1471
|
-
var resolveInstallTargets = async (skill, home =
|
|
2177
|
+
var resolveInstallTargets = async (skill, home = os6.homedir()) => {
|
|
1472
2178
|
const targets = [];
|
|
1473
2179
|
if (skill.runtimes.includes("codex")) {
|
|
1474
2180
|
const codex = await resolveCodexTarget(home, skill.name);
|
|
@@ -1512,8 +2218,8 @@ var findSkill = (manifest, name) => {
|
|
|
1512
2218
|
};
|
|
1513
2219
|
var ensurePrivateCache = async (home) => {
|
|
1514
2220
|
const cache = localPaths(home).cache;
|
|
1515
|
-
await
|
|
1516
|
-
const cacheStat = await
|
|
2221
|
+
await mkdir6(cache, { recursive: true, mode: 448 });
|
|
2222
|
+
const cacheStat = await lstat9(cache);
|
|
1517
2223
|
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
1518
2224
|
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
1519
2225
|
}
|
|
@@ -1521,9 +2227,9 @@ var ensurePrivateCache = async (home) => {
|
|
|
1521
2227
|
};
|
|
1522
2228
|
var downloadAndInstall = async (client, token, skill, targets, options, home) => {
|
|
1523
2229
|
const cache = await ensurePrivateCache(home);
|
|
1524
|
-
const downloadRoot = await
|
|
1525
|
-
await
|
|
1526
|
-
const archivePath =
|
|
2230
|
+
const downloadRoot = await mkdtemp3(path9.join(cache, "download-"));
|
|
2231
|
+
await chmod3(downloadRoot, 448);
|
|
2232
|
+
const archivePath = path9.join(downloadRoot, `${skill.name}.tgz`);
|
|
1527
2233
|
try {
|
|
1528
2234
|
await client.downloadArtifact(skill, token, archivePath);
|
|
1529
2235
|
return await installSkillFromArchive({
|
|
@@ -1534,7 +2240,7 @@ var downloadAndInstall = async (client, token, skill, targets, options, home) =>
|
|
|
1534
2240
|
home
|
|
1535
2241
|
});
|
|
1536
2242
|
} finally {
|
|
1537
|
-
await
|
|
2243
|
+
await rm5(downloadRoot, { recursive: true, force: true });
|
|
1538
2244
|
}
|
|
1539
2245
|
};
|
|
1540
2246
|
var actionMessage = (skillName, action, dryRun) => {
|
|
@@ -1557,17 +2263,52 @@ var printActions = (context, skillName, actions, dryRun) => {
|
|
|
1557
2263
|
}
|
|
1558
2264
|
}
|
|
1559
2265
|
};
|
|
2266
|
+
var COMPANION_SKILL_NAME = "yeka-skills";
|
|
1560
2267
|
var addCommand = async (skillNameInput, options, context = {}) => {
|
|
1561
2268
|
const skillName = validatedSkillName(skillNameInput);
|
|
1562
|
-
const home =
|
|
2269
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1563
2270
|
const client = context.client ?? new RegistryClient();
|
|
1564
|
-
const actions = await withRegistrySession(async (token) => {
|
|
2271
|
+
const { actions, companionInstalled } = await withRegistrySession(async (token) => {
|
|
1565
2272
|
const manifest = await client.getManifest(token);
|
|
1566
2273
|
const skill = findSkill(manifest, skillName);
|
|
1567
2274
|
const targets = await resolveInstallTargets(skill, home);
|
|
1568
|
-
|
|
1569
|
-
|
|
2275
|
+
const primaryActions = await downloadAndInstall(client, token, skill, targets, options, home);
|
|
2276
|
+
let companion = "skipped";
|
|
2277
|
+
if (options.dryRun !== true && skillName !== COMPANION_SKILL_NAME) {
|
|
2278
|
+
let hasCompanionReceipt = false;
|
|
2279
|
+
try {
|
|
2280
|
+
const receipts = await listReceipts(home);
|
|
2281
|
+
hasCompanionReceipt = receipts.some((r2) => r2.skillName === COMPANION_SKILL_NAME);
|
|
2282
|
+
} catch {
|
|
2283
|
+
hasCompanionReceipt = false;
|
|
2284
|
+
}
|
|
2285
|
+
if (hasCompanionReceipt) {
|
|
2286
|
+
companion = "present";
|
|
2287
|
+
} else {
|
|
2288
|
+
try {
|
|
2289
|
+
const companionSkill = findSkill(manifest, COMPANION_SKILL_NAME);
|
|
2290
|
+
const companionTargets = await resolveInstallTargets(companionSkill, home);
|
|
2291
|
+
await downloadAndInstall(client, token, companionSkill, companionTargets, options, home);
|
|
2292
|
+
companion = "installed";
|
|
2293
|
+
} catch {
|
|
2294
|
+
companion = "failed";
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return { actions: primaryActions, companionInstalled: companion };
|
|
2299
|
+
}, context.session);
|
|
1570
2300
|
printActions(context, skillName, actions, options.dryRun === true);
|
|
2301
|
+
if (companionInstalled === "installed") {
|
|
2302
|
+
output(
|
|
2303
|
+
context,
|
|
2304
|
+
"Also installed the yeka-skills assistant so your agent knows how to update and share skills."
|
|
2305
|
+
);
|
|
2306
|
+
} else if (companionInstalled === "failed") {
|
|
2307
|
+
output(
|
|
2308
|
+
context,
|
|
2309
|
+
"(could not auto-install the assistant; run `npx yeka-skills add yeka-skills` yourself)"
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
1571
2312
|
};
|
|
1572
2313
|
var receiptGroups = (receipts) => {
|
|
1573
2314
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -1593,7 +2334,7 @@ var statusLabel = (status) => {
|
|
|
1593
2334
|
}
|
|
1594
2335
|
};
|
|
1595
2336
|
var listCommand = async (context = {}) => {
|
|
1596
|
-
const home =
|
|
2337
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1597
2338
|
const receipts = await listReceipts(home);
|
|
1598
2339
|
if (receipts.length === 0) {
|
|
1599
2340
|
output(context, "No managed Yeka skills are installed.");
|
|
@@ -1604,7 +2345,7 @@ var listCommand = async (context = {}) => {
|
|
|
1604
2345
|
}
|
|
1605
2346
|
};
|
|
1606
2347
|
var statusCommand = async (context = {}) => {
|
|
1607
|
-
const home =
|
|
2348
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1608
2349
|
const receipts = await listReceipts(home);
|
|
1609
2350
|
if (receipts.length === 0) {
|
|
1610
2351
|
output(context, "No managed Yeka skills are installed.");
|
|
@@ -1639,7 +2380,7 @@ var assertUpdatableStatuses = (skillName, statuses) => {
|
|
|
1639
2380
|
};
|
|
1640
2381
|
var updateCommand = async (skillNameInput, options, context = {}) => {
|
|
1641
2382
|
const selectedName = skillNameInput === void 0 ? void 0 : validatedSkillName(skillNameInput);
|
|
1642
|
-
const home =
|
|
2383
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1643
2384
|
const allReceipts = await listReceipts(home);
|
|
1644
2385
|
const groups = receiptGroups(allReceipts);
|
|
1645
2386
|
if (selectedName !== void 0 && !groups.has(selectedName)) {
|
|
@@ -1692,6 +2433,100 @@ var updateCommand = async (skillNameInput, options, context = {}) => {
|
|
|
1692
2433
|
printActions(context, result.name, result.actions, options.dryRun === true);
|
|
1693
2434
|
}
|
|
1694
2435
|
};
|
|
2436
|
+
var removeActionMessage = (skillName, action, dryRun) => {
|
|
2437
|
+
const agent = action.runtime === "codex" ? "Codex" : "Claude";
|
|
2438
|
+
switch (action.action) {
|
|
2439
|
+
case "remove":
|
|
2440
|
+
return dryRun ? `Would remove the ${agent} copy of ${skillName} at ${action.installedPath}.` : `Removed the ${agent} copy of ${skillName} at ${action.installedPath}.`;
|
|
2441
|
+
case "clean-receipt":
|
|
2442
|
+
return dryRun ? `The ${agent} copy of ${skillName} is already gone; would clean its receipt.` : `The ${agent} copy of ${skillName} was already gone; receipt cleaned.`;
|
|
2443
|
+
case "backup-modified":
|
|
2444
|
+
return dryRun ? `Would remove the edited ${agent} copy of ${skillName} and preserve it in a private backup folder.` : `Removed the edited ${agent} copy of ${skillName}; backup: ${action.backupPath}.`;
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
var removeCommand = async (skillNameInput, options, context = {}) => {
|
|
2448
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
2449
|
+
if (skillNameInput === void 0) {
|
|
2450
|
+
const names = await removableSkillNames(home);
|
|
2451
|
+
throw new CliError(
|
|
2452
|
+
"missing_skill_name",
|
|
2453
|
+
names.length === 0 ? "Usage: yeka-skills remove <skill>. No managed Yeka skills are removable." : `Usage: yeka-skills remove <skill>. Removable skills: ${names.join(", ")}.`
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
const skillName = validatedSkillName(skillNameInput);
|
|
2457
|
+
const actions = await removeSkill(skillName, {
|
|
2458
|
+
home,
|
|
2459
|
+
...options.dryRun === void 0 ? {} : { dryRun: options.dryRun },
|
|
2460
|
+
...options.force === void 0 ? {} : { force: options.force }
|
|
2461
|
+
});
|
|
2462
|
+
for (const action of actions) {
|
|
2463
|
+
output(context, removeActionMessage(skillName, action, options.dryRun === true));
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
var readNotesFile = async (notesPath) => {
|
|
2467
|
+
let text;
|
|
2468
|
+
try {
|
|
2469
|
+
text = await readFile4(notesPath, "utf8");
|
|
2470
|
+
} catch {
|
|
2471
|
+
throw new CliError(
|
|
2472
|
+
"notes_file_unreadable",
|
|
2473
|
+
`The notes file could not be read: ${notesPath}. Check the path and try again.`
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
if (Buffer.byteLength(text, "utf8") > 8 * 1024) {
|
|
2477
|
+
throw new CliError(
|
|
2478
|
+
"notes_too_large",
|
|
2479
|
+
"The notes file is over 8 KB. Trim it down and try again."
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
return { text };
|
|
2483
|
+
};
|
|
2484
|
+
var agentLabel2 = (runtime) => runtime === "codex" ? "Codex" : "Claude";
|
|
2485
|
+
var shareCommand = async (skillNameInput, options, context = {}) => {
|
|
2486
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
2487
|
+
if (skillNameInput === void 0) {
|
|
2488
|
+
throw new CliError("missing_skill_name", "Usage: yeka-skills share <skill-name>.");
|
|
2489
|
+
}
|
|
2490
|
+
const skillName = validatedSkillName(skillNameInput);
|
|
2491
|
+
const folder = await resolveSkillFolder(skillName, home);
|
|
2492
|
+
output(
|
|
2493
|
+
context,
|
|
2494
|
+
folder.requestedPath === folder.folderPath ? `Found ${skillName} in your ${agentLabel2(folder.runtime)} skills folder: ${folder.folderPath}` : `Found ${skillName} in your ${agentLabel2(folder.runtime)} skills folder: ${folder.requestedPath} (packaging the folder it points at: ${folder.folderPath})`
|
|
2495
|
+
);
|
|
2496
|
+
const notes = options.notesFile === void 0 ? void 0 : await readNotesFile(options.notesFile);
|
|
2497
|
+
const check = await checkSkillFolder(folder.folderPath, skillName, void 0, notes);
|
|
2498
|
+
if (options.check === true) {
|
|
2499
|
+
if (check.warnings.length === 0) {
|
|
2500
|
+
output(context, "Nothing looked risky.");
|
|
2501
|
+
} else {
|
|
2502
|
+
for (const warning of check.warnings) {
|
|
2503
|
+
output(context, `Heads up: ${warning}`);
|
|
2504
|
+
}
|
|
2505
|
+
output(context, "These are warnings only \u2014 they never block a share. The team's checks decide.");
|
|
2506
|
+
}
|
|
2507
|
+
output(
|
|
2508
|
+
context,
|
|
2509
|
+
`${skillName} is ready to share (${check.fileCount} files, ${(check.archiveBytes / (1024 * 1024)).toFixed(1)} MB packaged). Run \`yeka-skills share ${skillName}\` when you're happy with it.`
|
|
2510
|
+
);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const packaged = await packageSkillFolder(folder.folderPath, notes);
|
|
2514
|
+
const client = context.client ?? new RegistryClient();
|
|
2515
|
+
try {
|
|
2516
|
+
await withRegistrySession(
|
|
2517
|
+
(token) => client.uploadSubmission(skillName, packaged.sha256, packaged.bytes, token)
|
|
2518
|
+
);
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
if (error instanceof CliError && error.code.startsWith("login_")) {
|
|
2521
|
+
throw new CliError(
|
|
2522
|
+
error.code,
|
|
2523
|
+
`${error.message} You can also run \`yeka-skills login\` first, then try the share again.`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
throw error;
|
|
2527
|
+
}
|
|
2528
|
+
output(context, "Shared. You'll hear back in #yeka-skills \u2014 updates usually go live within the hour.");
|
|
2529
|
+
};
|
|
1695
2530
|
var loginCommand = async (context = {}) => {
|
|
1696
2531
|
await deleteSession();
|
|
1697
2532
|
await login();
|
|
@@ -1704,13 +2539,19 @@ var logoutCommand = async (context = {}) => {
|
|
|
1704
2539
|
|
|
1705
2540
|
// src/cli.ts
|
|
1706
2541
|
var program = new Command();
|
|
1707
|
-
program.name("yeka-skills").description("Install and
|
|
2542
|
+
program.name("yeka-skills").description("Install, update, and remove private Gemography agent skills").version("0.1.0").showHelpAfterError();
|
|
1708
2543
|
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
2544
|
program.command("list").description("List managed local skills without network access").action(async () => listCommand());
|
|
1710
2545
|
program.command("status").description("Check local skills and available updates").action(async () => statusCommand());
|
|
1711
2546
|
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
2547
|
async (skill, options) => updateCommand(skill, options)
|
|
1713
2548
|
);
|
|
2549
|
+
program.command("remove").description("Remove one managed skill without registry access").argument("[skill]", "exact skill name").option("--force", "preserve and remove a locally edited copy").option("--dry-run", "verify and show changes without removal").action(
|
|
2550
|
+
async (skill, options) => removeCommand(skill, options)
|
|
2551
|
+
);
|
|
2552
|
+
program.command("share").description("Share one local skill with the team for review").argument("<skill>", "exact skill name").option("--check", "look the skill over without sharing").option("--notes-file <path>", "text file with your interview answers, shared with the reviewers").action(
|
|
2553
|
+
async (skill, options) => shareCommand(skill, options)
|
|
2554
|
+
);
|
|
1714
2555
|
program.command("login").description("Start a new private registry session").action(async () => loginCommand());
|
|
1715
2556
|
program.command("logout").description("Remove the saved private registry session").action(async () => logoutCommand());
|
|
1716
2557
|
var main = async () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yeka-skills",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Install and
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Install, update, and remove private Gemography agent skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"repository": {
|
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=22.15.0"
|
|
15
15
|
},
|
|
16
|
-
"os": [
|
|
17
|
-
"darwin"
|
|
18
|
-
],
|
|
19
16
|
"bin": {
|
|
20
17
|
"yeka-skills": "./dist/cli.js"
|
|
21
18
|
},
|