yeka-skills 0.1.1 → 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 +27 -2
- package/dist/cli.js +888 -61
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
@@ -20,13 +20,38 @@ Commands:
|
|
|
20
20
|
```text
|
|
21
21
|
yeka-skills add <skill>
|
|
22
22
|
yeka-skills list
|
|
23
|
+
yeka-skills share <skill>
|
|
23
24
|
yeka-skills status
|
|
24
25
|
yeka-skills update [skill]
|
|
26
|
+
yeka-skills remove <skill>
|
|
25
27
|
yeka-skills login
|
|
26
28
|
yeka-skills logout
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
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.
|
|
30
55
|
|
|
31
56
|
Managed installation receipts and backups stay in `~/Library/Application Support/Yeka Skills/`.
|
|
32
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";
|
|
@@ -116,7 +116,7 @@ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
|
116
116
|
import {
|
|
117
117
|
chmod,
|
|
118
118
|
cp,
|
|
119
|
-
lstat as
|
|
119
|
+
lstat as lstat4,
|
|
120
120
|
mkdir as mkdir3,
|
|
121
121
|
mkdtemp,
|
|
122
122
|
rename as rename2,
|
|
@@ -412,11 +412,45 @@ var extractVerifiedArchive = async (archivePath, destination, expectedContentHas
|
|
|
412
412
|
|
|
413
413
|
// src/receipts.ts
|
|
414
414
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
415
|
-
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";
|
|
416
416
|
import os2 from "os";
|
|
417
417
|
import path4 from "path";
|
|
418
418
|
var rootIdentifier = (targetRoot) => createHash2("sha256").update(targetRoot).digest("hex").slice(0, 16);
|
|
419
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
|
+
};
|
|
420
454
|
var writeReceipt = async (receipt, home = os2.homedir()) => {
|
|
421
455
|
const validated = InstallReceiptSchema.parse(receipt);
|
|
422
456
|
const destination = receiptPath(receipt.targetRoot, receipt.skillName, home);
|
|
@@ -457,50 +491,76 @@ var readReceipt = async (targetRoot, skillName, home = os2.homedir()) => {
|
|
|
457
491
|
});
|
|
458
492
|
}
|
|
459
493
|
};
|
|
460
|
-
var
|
|
494
|
+
var scanReceiptRecords = async (home = os2.homedir()) => {
|
|
461
495
|
const root = localPaths(home).receipts;
|
|
496
|
+
if (!await receiptStoreExists(home, root)) {
|
|
497
|
+
return { records: [], invalidFiles: [] };
|
|
498
|
+
}
|
|
462
499
|
let rootEntries;
|
|
463
500
|
try {
|
|
464
501
|
rootEntries = await readdir2(root, { withFileTypes: true });
|
|
465
502
|
} catch (error) {
|
|
466
503
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
467
|
-
return [];
|
|
504
|
+
return { records: [], invalidFiles: [] };
|
|
468
505
|
}
|
|
469
506
|
throw error;
|
|
470
507
|
}
|
|
471
|
-
const
|
|
508
|
+
const records = [];
|
|
509
|
+
const invalidFiles = [];
|
|
472
510
|
for (const rootEntry of rootEntries) {
|
|
473
511
|
if (!rootEntry.isDirectory()) {
|
|
512
|
+
if (/^[0-9a-f]{16}$/.test(rootEntry.name)) {
|
|
513
|
+
invalidFiles.push(path4.join(root, rootEntry.name));
|
|
514
|
+
}
|
|
474
515
|
continue;
|
|
475
516
|
}
|
|
476
517
|
const files = await readdir2(path4.join(root, rootEntry.name), { withFileTypes: true });
|
|
477
518
|
for (const file of files) {
|
|
478
|
-
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);
|
|
479
525
|
continue;
|
|
480
526
|
}
|
|
481
527
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
} catch (error) {
|
|
488
|
-
throw new CliError("invalid_receipt", `The local receipt ${file.name} is invalid.`, {
|
|
489
|
-
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))
|
|
490
533
|
});
|
|
534
|
+
} catch {
|
|
535
|
+
invalidFiles.push(filePath);
|
|
491
536
|
}
|
|
492
537
|
}
|
|
493
538
|
}
|
|
494
|
-
|
|
495
|
-
(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
|
+
)
|
|
496
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);
|
|
497
557
|
};
|
|
498
558
|
|
|
499
559
|
// src/installer.ts
|
|
500
560
|
var isNotFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
501
561
|
var fingerprint = async (entryPath) => {
|
|
502
562
|
try {
|
|
503
|
-
const entry = await
|
|
563
|
+
const entry = await lstat4(entryPath, { bigint: true });
|
|
504
564
|
return {
|
|
505
565
|
dev: entry.dev,
|
|
506
566
|
ino: entry.ino,
|
|
@@ -525,7 +585,7 @@ var assertSafeTarget = async (target, skillName, home) => {
|
|
|
525
585
|
const parent = path5.dirname(resolvedRoot);
|
|
526
586
|
let parentStat;
|
|
527
587
|
try {
|
|
528
|
-
parentStat = await
|
|
588
|
+
parentStat = await lstat4(parent);
|
|
529
589
|
} catch (error) {
|
|
530
590
|
if (isNotFound(error)) {
|
|
531
591
|
throw new CliError("runtime_not_found", `The ${target.runtime} configuration directory is missing.`);
|
|
@@ -537,7 +597,7 @@ var assertSafeTarget = async (target, skillName, home) => {
|
|
|
537
597
|
}
|
|
538
598
|
const rootFingerprint = await fingerprint(resolvedRoot);
|
|
539
599
|
if (rootFingerprint !== null) {
|
|
540
|
-
const rootStat = await
|
|
600
|
+
const rootStat = await lstat4(resolvedRoot);
|
|
541
601
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
542
602
|
throw new CliError("unsafe_target", `The ${target.runtime} skills directory is not a real directory.`);
|
|
543
603
|
}
|
|
@@ -580,8 +640,8 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
580
640
|
if (oldReceipt !== null && (oldReceipt.runtime !== target.runtime || path5.resolve(oldReceipt.targetRoot) !== targetRoot || path5.resolve(oldReceipt.installedPath) !== installedPath)) {
|
|
581
641
|
throw new CliError("invalid_receipt_target", `The local receipt for ${skill.name} has an invalid target.`);
|
|
582
642
|
}
|
|
583
|
-
const
|
|
584
|
-
if (
|
|
643
|
+
const entryFingerprint2 = await fingerprint(installedPath);
|
|
644
|
+
if (entryFingerprint2 === null) {
|
|
585
645
|
return {
|
|
586
646
|
action: "install",
|
|
587
647
|
runtime: target.runtime,
|
|
@@ -598,11 +658,11 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
598
658
|
targetRoot,
|
|
599
659
|
installedPath,
|
|
600
660
|
existed: true,
|
|
601
|
-
fingerprint:
|
|
661
|
+
fingerprint: entryFingerprint2,
|
|
602
662
|
oldReceipt
|
|
603
663
|
};
|
|
604
664
|
}
|
|
605
|
-
const installedStat = await
|
|
665
|
+
const installedStat = await lstat4(installedPath);
|
|
606
666
|
if (!installedStat.isDirectory() || installedStat.isSymbolicLink()) {
|
|
607
667
|
throw new CliError(
|
|
608
668
|
"local_changes",
|
|
@@ -632,7 +692,7 @@ var preflightTarget = async (target, skill, home) => {
|
|
|
632
692
|
targetRoot,
|
|
633
693
|
installedPath,
|
|
634
694
|
existed: true,
|
|
635
|
-
fingerprint:
|
|
695
|
+
fingerprint: entryFingerprint2,
|
|
636
696
|
oldReceipt
|
|
637
697
|
};
|
|
638
698
|
};
|
|
@@ -644,7 +704,7 @@ var ensureTargetRoot = async (targetRoot) => {
|
|
|
644
704
|
throw error;
|
|
645
705
|
}
|
|
646
706
|
}
|
|
647
|
-
const rootStat = await
|
|
707
|
+
const rootStat = await lstat4(targetRoot);
|
|
648
708
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
649
709
|
throw new CliError("unsafe_target", `The skills target is not a real directory: ${targetRoot}`);
|
|
650
710
|
}
|
|
@@ -737,7 +797,7 @@ var installSkillFromArchive = async (options) => {
|
|
|
737
797
|
}
|
|
738
798
|
const operationCache = localPaths(home).cache;
|
|
739
799
|
await mkdir3(operationCache, { recursive: true, mode: 448 });
|
|
740
|
-
const cacheStat = await
|
|
800
|
+
const cacheStat = await lstat4(operationCache);
|
|
741
801
|
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
742
802
|
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
743
803
|
}
|
|
@@ -1038,14 +1098,14 @@ var deleteSession = async () => {
|
|
|
1038
1098
|
};
|
|
1039
1099
|
|
|
1040
1100
|
// src/local-status.ts
|
|
1041
|
-
import { lstat as
|
|
1101
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
1042
1102
|
import os4 from "os";
|
|
1043
1103
|
var isNotFound2 = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1044
1104
|
var inspectLocalStatus = async (receipt, remote, home = os4.homedir()) => {
|
|
1045
1105
|
targetsFromReceipts([receipt], receipt.skillName, home);
|
|
1046
1106
|
let installedStat;
|
|
1047
1107
|
try {
|
|
1048
|
-
installedStat = await
|
|
1108
|
+
installedStat = await lstat5(receipt.installedPath);
|
|
1049
1109
|
} catch (error) {
|
|
1050
1110
|
if (isNotFound2(error)) {
|
|
1051
1111
|
return { kind: "missing", receipt, ...remote === void 0 ? {} : { remote } };
|
|
@@ -1249,8 +1309,346 @@ var login = async () => {
|
|
|
1249
1309
|
}
|
|
1250
1310
|
};
|
|
1251
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
|
+
|
|
1252
1650
|
// src/registry-client.ts
|
|
1253
|
-
import { createHash as
|
|
1651
|
+
import { createHash as createHash7 } from "crypto";
|
|
1254
1652
|
import { open, unlink as unlink2 } from "fs/promises";
|
|
1255
1653
|
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
1256
1654
|
var parseContentLength = (value) => {
|
|
@@ -1355,7 +1753,7 @@ var RegistryClient = class {
|
|
|
1355
1753
|
throw new CliError("artifact_size_mismatch", "The skill artifact size does not match its manifest.");
|
|
1356
1754
|
}
|
|
1357
1755
|
const file = await open(destination, "wx", 384);
|
|
1358
|
-
const hash =
|
|
1756
|
+
const hash = createHash7("sha256");
|
|
1359
1757
|
const reader = response.body.getReader();
|
|
1360
1758
|
let bytesWritten = 0;
|
|
1361
1759
|
try {
|
|
@@ -1403,6 +1801,60 @@ var RegistryClient = class {
|
|
|
1403
1801
|
);
|
|
1404
1802
|
}
|
|
1405
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
|
+
}
|
|
1406
1858
|
};
|
|
1407
1859
|
|
|
1408
1860
|
// src/session.ts
|
|
@@ -1424,13 +1876,253 @@ var withRegistrySession = async (operation, dependencies = defaultDependencies)
|
|
|
1424
1876
|
}
|
|
1425
1877
|
};
|
|
1426
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
|
+
|
|
1427
2119
|
// src/targets.ts
|
|
1428
|
-
import { lstat as
|
|
1429
|
-
import
|
|
1430
|
-
import
|
|
2120
|
+
import { lstat as lstat8 } from "fs/promises";
|
|
2121
|
+
import os6 from "os";
|
|
2122
|
+
import path8 from "path";
|
|
1431
2123
|
var isRealDirectory = async (candidate) => {
|
|
1432
2124
|
try {
|
|
1433
|
-
const entry = await
|
|
2125
|
+
const entry = await lstat8(candidate);
|
|
1434
2126
|
return entry.isDirectory() && !entry.isSymbolicLink();
|
|
1435
2127
|
} catch (error) {
|
|
1436
2128
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1441,7 +2133,7 @@ var isRealDirectory = async (candidate) => {
|
|
|
1441
2133
|
};
|
|
1442
2134
|
var pathExists = async (candidate) => {
|
|
1443
2135
|
try {
|
|
1444
|
-
await
|
|
2136
|
+
await lstat8(candidate);
|
|
1445
2137
|
return true;
|
|
1446
2138
|
} catch (error) {
|
|
1447
2139
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1451,17 +2143,17 @@ var pathExists = async (candidate) => {
|
|
|
1451
2143
|
}
|
|
1452
2144
|
};
|
|
1453
2145
|
var resolveCodexTarget = async (home, skillName) => {
|
|
1454
|
-
const currentRoot =
|
|
1455
|
-
const currentParent =
|
|
1456
|
-
const codexConfig =
|
|
1457
|
-
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");
|
|
1458
2150
|
const legacyActive = await isRealDirectory(legacyRoot);
|
|
1459
2151
|
const currentActive = await isRealDirectory(currentRoot) || await isRealDirectory(currentParent) || await isRealDirectory(codexConfig) && !legacyActive;
|
|
1460
2152
|
if (currentActive) {
|
|
1461
|
-
if (legacyActive && await pathExists(
|
|
2153
|
+
if (legacyActive && await pathExists(path8.join(legacyRoot, skillName))) {
|
|
1462
2154
|
throw new CliError(
|
|
1463
2155
|
"legacy_codex_conflict",
|
|
1464
|
-
`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.`
|
|
1465
2157
|
);
|
|
1466
2158
|
}
|
|
1467
2159
|
return { runtime: "codex", targetRoot: currentRoot, legacy: false };
|
|
@@ -1472,17 +2164,17 @@ var resolveCodexTarget = async (home, skillName) => {
|
|
|
1472
2164
|
return null;
|
|
1473
2165
|
};
|
|
1474
2166
|
var resolveClaudeTarget = async (home) => {
|
|
1475
|
-
const claudeConfig =
|
|
2167
|
+
const claudeConfig = path8.join(home, ".claude");
|
|
1476
2168
|
if (!await isRealDirectory(claudeConfig)) {
|
|
1477
2169
|
return null;
|
|
1478
2170
|
}
|
|
1479
2171
|
return {
|
|
1480
2172
|
runtime: "claude",
|
|
1481
|
-
targetRoot:
|
|
2173
|
+
targetRoot: path8.join(claudeConfig, "skills"),
|
|
1482
2174
|
legacy: false
|
|
1483
2175
|
};
|
|
1484
2176
|
};
|
|
1485
|
-
var resolveInstallTargets = async (skill, home =
|
|
2177
|
+
var resolveInstallTargets = async (skill, home = os6.homedir()) => {
|
|
1486
2178
|
const targets = [];
|
|
1487
2179
|
if (skill.runtimes.includes("codex")) {
|
|
1488
2180
|
const codex = await resolveCodexTarget(home, skill.name);
|
|
@@ -1526,8 +2218,8 @@ var findSkill = (manifest, name) => {
|
|
|
1526
2218
|
};
|
|
1527
2219
|
var ensurePrivateCache = async (home) => {
|
|
1528
2220
|
const cache = localPaths(home).cache;
|
|
1529
|
-
await
|
|
1530
|
-
const cacheStat = await
|
|
2221
|
+
await mkdir6(cache, { recursive: true, mode: 448 });
|
|
2222
|
+
const cacheStat = await lstat9(cache);
|
|
1531
2223
|
if (!cacheStat.isDirectory() || cacheStat.isSymbolicLink()) {
|
|
1532
2224
|
throw new CliError("unsafe_cache", "The Yeka Skills cache is not a real directory.");
|
|
1533
2225
|
}
|
|
@@ -1535,9 +2227,9 @@ var ensurePrivateCache = async (home) => {
|
|
|
1535
2227
|
};
|
|
1536
2228
|
var downloadAndInstall = async (client, token, skill, targets, options, home) => {
|
|
1537
2229
|
const cache = await ensurePrivateCache(home);
|
|
1538
|
-
const downloadRoot = await
|
|
1539
|
-
await
|
|
1540
|
-
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`);
|
|
1541
2233
|
try {
|
|
1542
2234
|
await client.downloadArtifact(skill, token, archivePath);
|
|
1543
2235
|
return await installSkillFromArchive({
|
|
@@ -1548,7 +2240,7 @@ var downloadAndInstall = async (client, token, skill, targets, options, home) =>
|
|
|
1548
2240
|
home
|
|
1549
2241
|
});
|
|
1550
2242
|
} finally {
|
|
1551
|
-
await
|
|
2243
|
+
await rm5(downloadRoot, { recursive: true, force: true });
|
|
1552
2244
|
}
|
|
1553
2245
|
};
|
|
1554
2246
|
var actionMessage = (skillName, action, dryRun) => {
|
|
@@ -1571,17 +2263,52 @@ var printActions = (context, skillName, actions, dryRun) => {
|
|
|
1571
2263
|
}
|
|
1572
2264
|
}
|
|
1573
2265
|
};
|
|
2266
|
+
var COMPANION_SKILL_NAME = "yeka-skills";
|
|
1574
2267
|
var addCommand = async (skillNameInput, options, context = {}) => {
|
|
1575
2268
|
const skillName = validatedSkillName(skillNameInput);
|
|
1576
|
-
const home =
|
|
2269
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1577
2270
|
const client = context.client ?? new RegistryClient();
|
|
1578
|
-
const actions = await withRegistrySession(async (token) => {
|
|
2271
|
+
const { actions, companionInstalled } = await withRegistrySession(async (token) => {
|
|
1579
2272
|
const manifest = await client.getManifest(token);
|
|
1580
2273
|
const skill = findSkill(manifest, skillName);
|
|
1581
2274
|
const targets = await resolveInstallTargets(skill, home);
|
|
1582
|
-
|
|
1583
|
-
|
|
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);
|
|
1584
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
|
+
}
|
|
1585
2312
|
};
|
|
1586
2313
|
var receiptGroups = (receipts) => {
|
|
1587
2314
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -1607,7 +2334,7 @@ var statusLabel = (status) => {
|
|
|
1607
2334
|
}
|
|
1608
2335
|
};
|
|
1609
2336
|
var listCommand = async (context = {}) => {
|
|
1610
|
-
const home =
|
|
2337
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1611
2338
|
const receipts = await listReceipts(home);
|
|
1612
2339
|
if (receipts.length === 0) {
|
|
1613
2340
|
output(context, "No managed Yeka skills are installed.");
|
|
@@ -1618,7 +2345,7 @@ var listCommand = async (context = {}) => {
|
|
|
1618
2345
|
}
|
|
1619
2346
|
};
|
|
1620
2347
|
var statusCommand = async (context = {}) => {
|
|
1621
|
-
const home =
|
|
2348
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1622
2349
|
const receipts = await listReceipts(home);
|
|
1623
2350
|
if (receipts.length === 0) {
|
|
1624
2351
|
output(context, "No managed Yeka skills are installed.");
|
|
@@ -1653,7 +2380,7 @@ var assertUpdatableStatuses = (skillName, statuses) => {
|
|
|
1653
2380
|
};
|
|
1654
2381
|
var updateCommand = async (skillNameInput, options, context = {}) => {
|
|
1655
2382
|
const selectedName = skillNameInput === void 0 ? void 0 : validatedSkillName(skillNameInput);
|
|
1656
|
-
const home =
|
|
2383
|
+
const home = path9.resolve(context.home ?? os7.homedir());
|
|
1657
2384
|
const allReceipts = await listReceipts(home);
|
|
1658
2385
|
const groups = receiptGroups(allReceipts);
|
|
1659
2386
|
if (selectedName !== void 0 && !groups.has(selectedName)) {
|
|
@@ -1706,6 +2433,100 @@ var updateCommand = async (skillNameInput, options, context = {}) => {
|
|
|
1706
2433
|
printActions(context, result.name, result.actions, options.dryRun === true);
|
|
1707
2434
|
}
|
|
1708
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
|
+
};
|
|
1709
2530
|
var loginCommand = async (context = {}) => {
|
|
1710
2531
|
await deleteSession();
|
|
1711
2532
|
await login();
|
|
@@ -1718,13 +2539,19 @@ var logoutCommand = async (context = {}) => {
|
|
|
1718
2539
|
|
|
1719
2540
|
// src/cli.ts
|
|
1720
2541
|
var program = new Command();
|
|
1721
|
-
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();
|
|
1722
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));
|
|
1723
2544
|
program.command("list").description("List managed local skills without network access").action(async () => listCommand());
|
|
1724
2545
|
program.command("status").description("Check local skills and available updates").action(async () => statusCommand());
|
|
1725
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(
|
|
1726
2547
|
async (skill, options) => updateCommand(skill, options)
|
|
1727
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
|
+
);
|
|
1728
2555
|
program.command("login").description("Start a new private registry session").action(async () => loginCommand());
|
|
1729
2556
|
program.command("logout").description("Remove the saved private registry session").action(async () => logoutCommand());
|
|
1730
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": {
|