@rayul/kordis 0.2.1 → 0.3.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/assets/kordis-publishing/SKILL.md +24 -8
- package/dist/index.js +394 -211
- package/package.json +1 -1
|
@@ -31,15 +31,22 @@ Publish a valid, AI-review-passing version. ALWAYS dry-run first.
|
|
|
31
31
|
|
|
32
32
|
## Deleting a package
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
- Web UI: `https://kordis.store/p/<full_slug>` (if a delete button is available)
|
|
36
|
-
- Registry owner: soft-delete via the prod Postgres
|
|
37
|
-
(`UPDATE packages SET deleted_at = NOW() WHERE full_slug = '...'`)
|
|
38
|
-
- Workaround: push a new version with `visibility: unlisted` or a
|
|
39
|
-
deprecation notice in the body
|
|
34
|
+
### `kordis delete <full_slug> [--force]`
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
Soft-deletes a package you own. Removes from search / info / read /
|
|
37
|
+
pull. The slug is **permanently reserved** after deletion (supply-chain
|
|
38
|
+
safety) — it cannot be reused even by you. There is **no CLI undo**;
|
|
39
|
+
the registry operator can restore from the soft-delete row if asked.
|
|
40
|
+
|
|
41
|
+
Without `--force` the CLI shows the package metadata and prompts for
|
|
42
|
+
confirmation. Use `--force` ONLY when the user has explicitly confirmed
|
|
43
|
+
the destructive action (e.g. they said "yes, delete it"). Never pass
|
|
44
|
+
`--force` on the user's behalf.
|
|
45
|
+
|
|
46
|
+
If the body contains third-party content, MUST preserve the original
|
|
47
|
+
copyright and license text inside the body and link to source via
|
|
48
|
+
`homepage` — even on the version you intend to delete, in case the
|
|
49
|
+
operator restores it.
|
|
43
50
|
|
|
44
51
|
## Commands
|
|
45
52
|
|
|
@@ -122,6 +129,15 @@ User: "release with a note that breaking-changes the API"
|
|
|
122
129
|
2. clean → drop --dry-run for the real push
|
|
123
130
|
</example>
|
|
124
131
|
|
|
132
|
+
<example>
|
|
133
|
+
User: "delete <full_slug>"
|
|
134
|
+
1. confirm with the user that they understand the slug is permanently
|
|
135
|
+
reserved (cannot be reused) and there is no CLI undo
|
|
136
|
+
2. kordis delete <full_slug> # interactive confirmation prompt
|
|
137
|
+
- if user already confirmed in prior turn, kordis delete <full_slug> --force
|
|
138
|
+
3. show the {status, full_slug} reply to the user
|
|
139
|
+
</example>
|
|
140
|
+
|
|
125
141
|
## Forking another Kordis package
|
|
126
142
|
|
|
127
143
|
When the user asks to fork / adapt / republish someone else's package
|
package/dist/index.js
CHANGED
|
@@ -631,7 +631,17 @@ var init_api = __esm({
|
|
|
631
631
|
* KORDIS.md recommends but does not enforce.
|
|
632
632
|
*/
|
|
633
633
|
attribution: z9.string(),
|
|
634
|
-
read_at: z9.string().datetime({ offset: true })
|
|
634
|
+
read_at: z9.string().datetime({ offset: true }),
|
|
635
|
+
/**
|
|
636
|
+
* Paths (NOT contents) of bundled assets that ship with the package —
|
|
637
|
+
* `references/*.md`, `assets/*.html`, `scripts/*.py`, etc. Agents reading a
|
|
638
|
+
* skill that references its own bundled files (e.g. "use
|
|
639
|
+
* `assets/template.html`") need a structured signal to switch to
|
|
640
|
+
* `kordis pull`; without it, the only hint is in body prose. Optional in
|
|
641
|
+
* the schema for backwards compatibility with pre-0.2.3 servers; the
|
|
642
|
+
* current server always populates it (empty array when no assets).
|
|
643
|
+
*/
|
|
644
|
+
bundled_assets: z9.array(z9.string()).optional()
|
|
635
645
|
});
|
|
636
646
|
PullPackageResponseSchema = z9.object({
|
|
637
647
|
status: z9.enum(["installed", "already_installed", "skipped"]),
|
|
@@ -1041,7 +1051,7 @@ async function saveCliConfig(cfg) {
|
|
|
1041
1051
|
}
|
|
1042
1052
|
|
|
1043
1053
|
// src/keychain.ts
|
|
1044
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, unlink } from "fs/promises";
|
|
1054
|
+
import { access, readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, unlink } from "fs/promises";
|
|
1045
1055
|
import { homedir as homedir2 } from "os";
|
|
1046
1056
|
import { join as join2 } from "path";
|
|
1047
1057
|
var SERVICE = "kordis";
|
|
@@ -1054,12 +1064,28 @@ async function loadKeytar() {
|
|
|
1054
1064
|
return null;
|
|
1055
1065
|
}
|
|
1056
1066
|
}
|
|
1067
|
+
function kordisHome() {
|
|
1068
|
+
return process.env["KORDIS_HOME"] ?? join2(homedir2(), ".kordis");
|
|
1069
|
+
}
|
|
1057
1070
|
function fallbackPath() {
|
|
1058
|
-
return join2(
|
|
1071
|
+
return join2(kordisHome(), ".tokens.json");
|
|
1072
|
+
}
|
|
1073
|
+
function warningSentinelPath() {
|
|
1074
|
+
return join2(kordisHome(), ".warned-no-keychain");
|
|
1075
|
+
}
|
|
1076
|
+
async function pathExists(p) {
|
|
1077
|
+
try {
|
|
1078
|
+
await access(p);
|
|
1079
|
+
return true;
|
|
1080
|
+
} catch {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1059
1083
|
}
|
|
1060
1084
|
async function createKeychain() {
|
|
1061
1085
|
const keytar = await loadKeytar();
|
|
1062
1086
|
if (keytar) {
|
|
1087
|
+
await unlink(warningSentinelPath()).catch(() => {
|
|
1088
|
+
});
|
|
1063
1089
|
return {
|
|
1064
1090
|
async save(username, tokens) {
|
|
1065
1091
|
await keytar.setPassword(SERVICE, username, JSON.stringify(tokens));
|
|
@@ -1073,15 +1099,22 @@ async function createKeychain() {
|
|
|
1073
1099
|
}
|
|
1074
1100
|
};
|
|
1075
1101
|
}
|
|
1076
|
-
if (!plaintextWarningEmitted && process.env["KORDIS_JSON"] !== "1") {
|
|
1102
|
+
if (!plaintextWarningEmitted && process.env["KORDIS_JSON"] !== "1" && !await pathExists(warningSentinelPath())) {
|
|
1077
1103
|
plaintextWarningEmitted = true;
|
|
1078
1104
|
const yellow = "\x1B[33m";
|
|
1079
1105
|
const reset = "\x1B[0m";
|
|
1080
1106
|
process.stderr.write(
|
|
1081
1107
|
`${yellow}[kordis] WARNING: OS keychain unavailable; falling back to plaintext token storage at ~/.kordis/.tokens.json (mode 0600).
|
|
1082
|
-
[kordis] On macOS install Xcode Command Line Tools, on Linux install libsecret-1-dev to enable keytar
|
|
1108
|
+
[kordis] On macOS install Xcode Command Line Tools, on Linux install libsecret-1-dev to enable keytar.
|
|
1109
|
+
[kordis] (suppressing this warning on subsequent runs \u2014 delete ~/.kordis/.warned-no-keychain to re-enable)${reset}
|
|
1083
1110
|
`
|
|
1084
1111
|
);
|
|
1112
|
+
await mkdir2(kordisHome(), { recursive: true }).catch(() => {
|
|
1113
|
+
});
|
|
1114
|
+
await writeFile2(warningSentinelPath(), "", { mode: 384 }).catch(() => {
|
|
1115
|
+
});
|
|
1116
|
+
} else if (!plaintextWarningEmitted) {
|
|
1117
|
+
plaintextWarningEmitted = true;
|
|
1085
1118
|
}
|
|
1086
1119
|
const store = await readAll();
|
|
1087
1120
|
return {
|
|
@@ -1342,11 +1375,31 @@ function searchCommand() {
|
|
|
1342
1375
|
limit: opts.limit
|
|
1343
1376
|
});
|
|
1344
1377
|
}
|
|
1378
|
+
const queryProvided = query.length > 0;
|
|
1345
1379
|
printDual({
|
|
1346
1380
|
json: res,
|
|
1347
1381
|
human: () => {
|
|
1348
1382
|
if (res.results.length === 0) {
|
|
1349
1383
|
printHuman(c.yellow("(no results)"));
|
|
1384
|
+
if (opts.type) {
|
|
1385
|
+
printHuman(
|
|
1386
|
+
c.gray(
|
|
1387
|
+
` \u2192 no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25`
|
|
1388
|
+
)
|
|
1389
|
+
);
|
|
1390
|
+
} else if (queryProvided) {
|
|
1391
|
+
printHuman(
|
|
1392
|
+
c.gray(
|
|
1393
|
+
" \u2192 try `--type skill` (or mcp-server / project-rules / custom-agent / hook) to narrow, or broader keywords"
|
|
1394
|
+
)
|
|
1395
|
+
);
|
|
1396
|
+
} else {
|
|
1397
|
+
printHuman(
|
|
1398
|
+
c.gray(
|
|
1399
|
+
' \u2192 try keywords (e.g. `kordis search "topic"`) or `kordis search --trending`'
|
|
1400
|
+
)
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1350
1403
|
return;
|
|
1351
1404
|
}
|
|
1352
1405
|
for (const p of res.results) {
|
|
@@ -1368,6 +1421,13 @@ function searchCommand() {
|
|
|
1368
1421
|
if (res.next_cursor) {
|
|
1369
1422
|
printHuman(c.gray(`(more available \u2014 pass --cursor ${res.next_cursor})`));
|
|
1370
1423
|
}
|
|
1424
|
+
if (queryProvided && !opts.type && res.results.length > 3) {
|
|
1425
|
+
printHuman(
|
|
1426
|
+
c.gray(
|
|
1427
|
+
" \u2192 narrow further with `--type skill|mcp-server|project-rules|custom-agent|hook`"
|
|
1428
|
+
)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1371
1431
|
}
|
|
1372
1432
|
});
|
|
1373
1433
|
});
|
|
@@ -1427,210 +1487,6 @@ var FrontmatterParseError = class extends Error {
|
|
|
1427
1487
|
}
|
|
1428
1488
|
};
|
|
1429
1489
|
|
|
1430
|
-
// src/commands/push.ts
|
|
1431
|
-
function pushCommand() {
|
|
1432
|
-
return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option(
|
|
1433
|
-
"--slug <slug>",
|
|
1434
|
-
'override slug; accepts "user/slug" to point at a specific package'
|
|
1435
|
-
).option("--type <type>", "override type inference").option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").action(
|
|
1436
|
-
async (path, opts) => {
|
|
1437
|
-
const bundle = await useClient();
|
|
1438
|
-
const abs = resolve(process.cwd(), path);
|
|
1439
|
-
const s = await stat(abs);
|
|
1440
|
-
let content;
|
|
1441
|
-
let assets = [];
|
|
1442
|
-
if (s.isDirectory()) {
|
|
1443
|
-
({ content, assets } = await bundleDirectory(abs));
|
|
1444
|
-
content = ensureTypeField(content, "skill");
|
|
1445
|
-
} else if (abs.endsWith(".skill")) {
|
|
1446
|
-
({ content, assets } = await bundleSkillZip(abs));
|
|
1447
|
-
content = ensureTypeField(content, "skill");
|
|
1448
|
-
} else {
|
|
1449
|
-
content = await readFile3(abs, "utf8");
|
|
1450
|
-
}
|
|
1451
|
-
let nameFromFm = null;
|
|
1452
|
-
try {
|
|
1453
|
-
const fm = parseFrontmatter(content).frontmatter ?? {};
|
|
1454
|
-
const v = fm["name"];
|
|
1455
|
-
if (typeof v === "string") nameFromFm = v;
|
|
1456
|
-
} catch (err) {
|
|
1457
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
1458
|
-
}
|
|
1459
|
-
let targetUser = null;
|
|
1460
|
-
let targetSlug = null;
|
|
1461
|
-
if (opts.slug && opts.slug.includes("/")) {
|
|
1462
|
-
const [u, s2] = opts.slug.split("/", 2);
|
|
1463
|
-
if (u && s2) {
|
|
1464
|
-
targetUser = u;
|
|
1465
|
-
targetSlug = s2;
|
|
1466
|
-
}
|
|
1467
|
-
} else {
|
|
1468
|
-
if (bundle.username && (opts.slug || nameFromFm)) {
|
|
1469
|
-
targetUser = bundle.username;
|
|
1470
|
-
targetSlug = opts.slug ?? nameFromFm;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
let targetFullSlug;
|
|
1474
|
-
if (targetUser && targetSlug) {
|
|
1475
|
-
try {
|
|
1476
|
-
await bundle.api.getPackage(targetUser, targetSlug);
|
|
1477
|
-
targetFullSlug = `${targetUser}/${targetSlug}`;
|
|
1478
|
-
} catch (err) {
|
|
1479
|
-
if (err instanceof KordisApiError && err.code === "not_found") {
|
|
1480
|
-
targetFullSlug = void 0;
|
|
1481
|
-
} else if (!(err instanceof KordisApiError)) {
|
|
1482
|
-
throw err;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
const spin = spinner(
|
|
1487
|
-
opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
|
|
1488
|
-
);
|
|
1489
|
-
const res = await bundle.authedRequest(
|
|
1490
|
-
(api) => api.publish(
|
|
1491
|
-
{
|
|
1492
|
-
content,
|
|
1493
|
-
assets: assets.length > 0 ? assets : void 0,
|
|
1494
|
-
bump: opts.bump,
|
|
1495
|
-
slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
|
|
1496
|
-
target_full_slug: targetFullSlug,
|
|
1497
|
-
type: opts.type,
|
|
1498
|
-
changelog: opts.changelog,
|
|
1499
|
-
dry_run: !!opts.dryRun
|
|
1500
|
-
},
|
|
1501
|
-
{
|
|
1502
|
-
waitForReviewMs: opts.wait === false ? 0 : 3e3
|
|
1503
|
-
}
|
|
1504
|
-
)
|
|
1505
|
-
);
|
|
1506
|
-
spin.succeed();
|
|
1507
|
-
printDual({
|
|
1508
|
-
json: res,
|
|
1509
|
-
human: () => {
|
|
1510
|
-
printHuman(
|
|
1511
|
-
`${c.bold(res.full_slug)} ${c.gray(`v${res.version}`)} \u2192 ${res.status === "published" ? c.green("published") : res.status === "pending" ? c.yellow("pending review") : c.red("rejected")}`
|
|
1512
|
-
);
|
|
1513
|
-
if (assets.length > 0) {
|
|
1514
|
-
printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
1515
|
-
}
|
|
1516
|
-
printHuman(` ${c.cyan(res.url)}`);
|
|
1517
|
-
if (res.status === "pending") {
|
|
1518
|
-
printHuman(
|
|
1519
|
-
c.yellow(
|
|
1520
|
-
` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
|
|
1521
|
-
)
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
if (res.review.flags.length > 0) {
|
|
1525
|
-
printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
|
|
1526
|
-
for (const f of res.review.flags) {
|
|
1527
|
-
printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
function ensureTypeField(content, defaultType) {
|
|
1536
|
-
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
1537
|
-
if (!fmMatch) return content;
|
|
1538
|
-
const fmBlock = fmMatch[1];
|
|
1539
|
-
if (/^type\s*:/m.test(fmBlock)) return content;
|
|
1540
|
-
const injected = fmMatch[0].replace(/^---\r?\n/, `---
|
|
1541
|
-
type: ${defaultType}
|
|
1542
|
-
`);
|
|
1543
|
-
return content.replace(fmMatch[0], injected);
|
|
1544
|
-
}
|
|
1545
|
-
var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
1546
|
-
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
|
|
1547
|
-
var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
|
|
1548
|
-
function shouldExclude(relPath, isDir) {
|
|
1549
|
-
const parts = relPath.split(posix.sep);
|
|
1550
|
-
const name = parts[parts.length - 1];
|
|
1551
|
-
if (EXCLUDE_NAMES.has(name)) return true;
|
|
1552
|
-
if (isDir && EXCLUDE_DIRS.has(name)) return true;
|
|
1553
|
-
const ext = name.includes(".") ? "." + name.split(".").pop() : "";
|
|
1554
|
-
if (EXCLUDE_EXTS.has(ext)) return true;
|
|
1555
|
-
return false;
|
|
1556
|
-
}
|
|
1557
|
-
async function walkDir(base, relDir, collected) {
|
|
1558
|
-
const entries = await readdir(join4(base, relDir), { withFileTypes: true });
|
|
1559
|
-
for (const entry of entries) {
|
|
1560
|
-
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1561
|
-
if (shouldExclude(rel, entry.isDirectory())) continue;
|
|
1562
|
-
if (entry.isDirectory()) {
|
|
1563
|
-
await walkDir(base, rel, collected);
|
|
1564
|
-
} else if (entry.isFile()) {
|
|
1565
|
-
collected.push({ rel, abs: join4(base, rel) });
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
async function readAsset(absPath) {
|
|
1570
|
-
const buf = await readFile3(absPath);
|
|
1571
|
-
const text = buf.toString("utf8");
|
|
1572
|
-
const roundtrip = Buffer.from(text, "utf8");
|
|
1573
|
-
if (roundtrip.equals(buf)) {
|
|
1574
|
-
return { content: text, encoding: "utf8" };
|
|
1575
|
-
}
|
|
1576
|
-
const b64 = buf.toString("base64");
|
|
1577
|
-
printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
|
|
1578
|
-
return { content: b64, encoding: "base64" };
|
|
1579
|
-
}
|
|
1580
|
-
async function bundleDirectory(dir) {
|
|
1581
|
-
const all = [];
|
|
1582
|
-
await walkDir(dir, "", all);
|
|
1583
|
-
const skillEntry = all.find((f) => f.rel === "SKILL.md");
|
|
1584
|
-
if (!skillEntry) {
|
|
1585
|
-
throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
|
|
1586
|
-
}
|
|
1587
|
-
const content = await readFile3(skillEntry.abs, "utf8");
|
|
1588
|
-
const assets = [];
|
|
1589
|
-
for (const f of all) {
|
|
1590
|
-
if (f.rel === "SKILL.md") continue;
|
|
1591
|
-
const { content: fc, encoding } = await readAsset(f.abs);
|
|
1592
|
-
assets.push({ path: f.rel, content: fc, encoding });
|
|
1593
|
-
}
|
|
1594
|
-
return { content, assets };
|
|
1595
|
-
}
|
|
1596
|
-
async function bundleSkillZip(zipPath) {
|
|
1597
|
-
const buf = await readFile3(zipPath);
|
|
1598
|
-
const zip = await JSZip.loadAsync(buf);
|
|
1599
|
-
const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
|
|
1600
|
-
if (entries.length === 0) throw new Error(".skill file is empty");
|
|
1601
|
-
const firstParts = entries[0].split("/");
|
|
1602
|
-
const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
|
|
1603
|
-
const stripped = entries.map((k) => ({
|
|
1604
|
-
zipKey: k,
|
|
1605
|
-
rel: prefix ? k.slice(prefix.length) : k
|
|
1606
|
-
})).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
|
|
1607
|
-
const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
|
|
1608
|
-
if (!skillEntry) {
|
|
1609
|
-
throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
|
|
1610
|
-
}
|
|
1611
|
-
const content = await zip.files[skillEntry.zipKey].async("string");
|
|
1612
|
-
const assets = [];
|
|
1613
|
-
for (const f of stripped) {
|
|
1614
|
-
if (f.rel === "SKILL.md") continue;
|
|
1615
|
-
const rawBuf = await zip.files[f.zipKey].async("uint8array");
|
|
1616
|
-
const buf2 = Buffer.from(rawBuf);
|
|
1617
|
-
const text = buf2.toString("utf8");
|
|
1618
|
-
const roundtrip = Buffer.from(text, "utf8");
|
|
1619
|
-
if (roundtrip.equals(buf2)) {
|
|
1620
|
-
assets.push({ path: f.rel, content: text, encoding: "utf8" });
|
|
1621
|
-
} else {
|
|
1622
|
-
printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
|
|
1623
|
-
assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
return { content, assets };
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// src/commands/pull.ts
|
|
1630
|
-
import { Command as Command5 } from "commander";
|
|
1631
|
-
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1632
|
-
import { dirname as dirname2, join as join6 } from "path";
|
|
1633
|
-
|
|
1634
1490
|
// ../../packages/harness-schema/src/deps.ts
|
|
1635
1491
|
init_src();
|
|
1636
1492
|
import { z as z10 } from "zod";
|
|
@@ -2105,6 +1961,312 @@ function zodFail(issues) {
|
|
|
2105
1961
|
})
|
|
2106
1962
|
};
|
|
2107
1963
|
}
|
|
1964
|
+
var ASSET_MAX_COUNT = 50;
|
|
1965
|
+
var SAFE_PATH_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/;
|
|
1966
|
+
function isAssetPathSafe(p) {
|
|
1967
|
+
if (!SAFE_PATH_RE.test(p)) return false;
|
|
1968
|
+
return !p.split("/").some((seg) => seg === "..");
|
|
1969
|
+
}
|
|
1970
|
+
function validateAssets(assets) {
|
|
1971
|
+
const flags = [];
|
|
1972
|
+
if (assets.length > ASSET_MAX_COUNT) {
|
|
1973
|
+
flags.push(
|
|
1974
|
+
flag("too_many_assets", `bundle has ${assets.length} files (max ${ASSET_MAX_COUNT})`, {
|
|
1975
|
+
hint: `Bundle has more than ${ASSET_MAX_COUNT} files. Trim demos or move large reference material to an external Git repo.`
|
|
1976
|
+
})
|
|
1977
|
+
);
|
|
1978
|
+
return { ok: false, flags };
|
|
1979
|
+
}
|
|
1980
|
+
for (const asset of assets) {
|
|
1981
|
+
if (!isAssetPathSafe(asset.path)) {
|
|
1982
|
+
flags.push(
|
|
1983
|
+
flag("asset_path_unsafe", `unsafe asset path: "${asset.path}"`, {
|
|
1984
|
+
field: "assets",
|
|
1985
|
+
hint: 'Asset paths must be relative POSIX, no leading "/", no "..". Example: scripts/run.sh'
|
|
1986
|
+
})
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return { ok: flags.length === 0, flags };
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/commands/push.ts
|
|
1994
|
+
var ASSET_MAX_BYTES = 2 * 1024 * 1024;
|
|
1995
|
+
var BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
1996
|
+
function pushCommand() {
|
|
1997
|
+
return new Command4("push").description("publish a new package or a new version").argument("<path>", "SKILL.md file, .skill ZIP, or skill directory").option("--bump <strategy>", "patch|minor|major (update only)", "patch").option(
|
|
1998
|
+
"--slug <slug>",
|
|
1999
|
+
'override slug; accepts "user/slug" to point at a specific package'
|
|
2000
|
+
).option("--type <type>", "override type inference").option("--changelog <text>", "optional changelog entry").option("--dry-run", "validate only; do not publish").option("--no-wait", "return immediately without waiting for review").action(
|
|
2001
|
+
async (path, opts) => {
|
|
2002
|
+
const bundle = await useClient();
|
|
2003
|
+
const abs = resolve(process.cwd(), path);
|
|
2004
|
+
const s = await stat(abs);
|
|
2005
|
+
let content;
|
|
2006
|
+
let assets = [];
|
|
2007
|
+
if (s.isDirectory()) {
|
|
2008
|
+
({ content, assets } = await bundleDirectory(abs));
|
|
2009
|
+
content = ensureTypeField(content, "skill");
|
|
2010
|
+
} else if (abs.endsWith(".skill")) {
|
|
2011
|
+
({ content, assets } = await bundleSkillZip(abs));
|
|
2012
|
+
content = ensureTypeField(content, "skill");
|
|
2013
|
+
} else {
|
|
2014
|
+
content = await readFile3(abs, "utf8");
|
|
2015
|
+
}
|
|
2016
|
+
let nameFromFm = null;
|
|
2017
|
+
try {
|
|
2018
|
+
const fm = parseFrontmatter(content).frontmatter ?? {};
|
|
2019
|
+
const v = fm["name"];
|
|
2020
|
+
if (typeof v === "string") nameFromFm = v;
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
2023
|
+
}
|
|
2024
|
+
if (opts.dryRun) {
|
|
2025
|
+
await runDryRun({ content, assets });
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
let targetUser = null;
|
|
2029
|
+
let targetSlug = null;
|
|
2030
|
+
if (opts.slug && opts.slug.includes("/")) {
|
|
2031
|
+
const [u, s2] = opts.slug.split("/", 2);
|
|
2032
|
+
if (u && s2) {
|
|
2033
|
+
targetUser = u;
|
|
2034
|
+
targetSlug = s2;
|
|
2035
|
+
}
|
|
2036
|
+
} else {
|
|
2037
|
+
if (bundle.username && (opts.slug || nameFromFm)) {
|
|
2038
|
+
targetUser = bundle.username;
|
|
2039
|
+
targetSlug = opts.slug ?? nameFromFm;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
let targetFullSlug;
|
|
2043
|
+
if (targetUser && targetSlug) {
|
|
2044
|
+
try {
|
|
2045
|
+
await bundle.api.getPackage(targetUser, targetSlug);
|
|
2046
|
+
targetFullSlug = `${targetUser}/${targetSlug}`;
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
if (err instanceof KordisApiError && err.code === "not_found") {
|
|
2049
|
+
targetFullSlug = void 0;
|
|
2050
|
+
} else if (!(err instanceof KordisApiError)) {
|
|
2051
|
+
throw err;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
const spin = spinner(
|
|
2056
|
+
opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
|
|
2057
|
+
);
|
|
2058
|
+
const res = await bundle.authedRequest(
|
|
2059
|
+
(api) => api.publish(
|
|
2060
|
+
{
|
|
2061
|
+
content,
|
|
2062
|
+
assets: assets.length > 0 ? assets : void 0,
|
|
2063
|
+
bump: opts.bump,
|
|
2064
|
+
slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
|
|
2065
|
+
target_full_slug: targetFullSlug,
|
|
2066
|
+
type: opts.type,
|
|
2067
|
+
changelog: opts.changelog,
|
|
2068
|
+
dry_run: !!opts.dryRun
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
waitForReviewMs: opts.wait === false ? 0 : 3e3
|
|
2072
|
+
}
|
|
2073
|
+
)
|
|
2074
|
+
);
|
|
2075
|
+
spin.succeed();
|
|
2076
|
+
printDual({
|
|
2077
|
+
json: res,
|
|
2078
|
+
human: () => {
|
|
2079
|
+
printHuman(
|
|
2080
|
+
`${c.bold(res.full_slug)} ${c.gray(`v${res.version}`)} \u2192 ${res.status === "published" ? c.green("published") : res.status === "pending" ? c.yellow("pending review") : c.red("rejected")}`
|
|
2081
|
+
);
|
|
2082
|
+
if (assets.length > 0) {
|
|
2083
|
+
printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
2084
|
+
}
|
|
2085
|
+
printHuman(` ${c.cyan(res.url)}`);
|
|
2086
|
+
if (res.status === "pending") {
|
|
2087
|
+
printHuman(
|
|
2088
|
+
c.yellow(
|
|
2089
|
+
` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
|
|
2090
|
+
)
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
if (res.review.flags.length > 0) {
|
|
2094
|
+
printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
|
|
2095
|
+
for (const f of res.review.flags) {
|
|
2096
|
+
printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
function checkAssetSizesLocal(assets) {
|
|
2105
|
+
const flags = [];
|
|
2106
|
+
let totalBytes = 0;
|
|
2107
|
+
for (const asset of assets) {
|
|
2108
|
+
const byteLen = asset.encoding === "base64" ? Math.ceil(asset.content.length * 3 / 4) : Buffer.byteLength(asset.content, "utf8");
|
|
2109
|
+
if (byteLen > ASSET_MAX_BYTES) {
|
|
2110
|
+
flags.push({
|
|
2111
|
+
code: "asset_too_large",
|
|
2112
|
+
message: `asset "${asset.path}" is ${byteLen} bytes (max ${ASSET_MAX_BYTES})`,
|
|
2113
|
+
hint: `Single asset exceeds the ${ASSET_MAX_BYTES / 1024 / 1024} MB per-file cap. Trim or move large fixtures to an external Git repo.`
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
totalBytes += byteLen;
|
|
2117
|
+
}
|
|
2118
|
+
if (totalBytes > BUNDLE_MAX_BYTES) {
|
|
2119
|
+
flags.push({
|
|
2120
|
+
code: "bundle_too_large",
|
|
2121
|
+
message: `bundle totals ${totalBytes} bytes (max ${BUNDLE_MAX_BYTES})`,
|
|
2122
|
+
hint: `Total bundle exceeds ${BUNDLE_MAX_BYTES / 1024 / 1024} MB. Remove generated artifacts; check .gitignore parity.`
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
return flags;
|
|
2126
|
+
}
|
|
2127
|
+
async function runDryRun(args) {
|
|
2128
|
+
const { content, assets } = args;
|
|
2129
|
+
const v = validatePackageSource(content);
|
|
2130
|
+
const assetPathResult = assets.length > 0 ? validateAssets(assets) : { ok: true, flags: [] };
|
|
2131
|
+
const sizeFlags = checkAssetSizesLocal(assets);
|
|
2132
|
+
const allFlags = [...v.flags, ...assetPathResult.flags, ...sizeFlags];
|
|
2133
|
+
const ok = v.ok && assetPathResult.ok && sizeFlags.length === 0;
|
|
2134
|
+
printDual({
|
|
2135
|
+
json: {
|
|
2136
|
+
status: ok ? "dry_run_passed" : "dry_run_failed",
|
|
2137
|
+
type: v.type,
|
|
2138
|
+
tier: v.tier,
|
|
2139
|
+
capabilities: v.capabilities,
|
|
2140
|
+
asset_count: assets.length,
|
|
2141
|
+
flags: allFlags,
|
|
2142
|
+
note: "dry-run validates frontmatter + asset paths/sizes locally; AI review (real publish only) is not performed"
|
|
2143
|
+
},
|
|
2144
|
+
human: () => {
|
|
2145
|
+
if (ok) {
|
|
2146
|
+
printHuman(c.green("\u2713 dry-run validation passed"));
|
|
2147
|
+
if (v.type) printHuman(c.gray(` type: ${v.type} (tier ${v.tier})`));
|
|
2148
|
+
if (v.capabilities) {
|
|
2149
|
+
printHuman(
|
|
2150
|
+
c.gray(
|
|
2151
|
+
` capabilities: jit_eligible=${v.capabilities.jit_eligible} executable=${v.capabilities.executable}`
|
|
2152
|
+
)
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
if (assets.length > 0) printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
2156
|
+
printHuman(
|
|
2157
|
+
c.gray(" (AI review only runs on the real push; not performed here)")
|
|
2158
|
+
);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
printHuman(c.red(`\u2717 dry-run validation failed (${allFlags.length} flag(s))`));
|
|
2162
|
+
for (const f of allFlags) {
|
|
2163
|
+
printHuman(c.red(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
2164
|
+
if (f.hint) {
|
|
2165
|
+
for (const line of f.hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
if (!ok) process.exit(30);
|
|
2171
|
+
}
|
|
2172
|
+
function ensureTypeField(content, defaultType) {
|
|
2173
|
+
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
2174
|
+
if (!fmMatch) return content;
|
|
2175
|
+
const fmBlock = fmMatch[1];
|
|
2176
|
+
if (/^type\s*:/m.test(fmBlock)) return content;
|
|
2177
|
+
const injected = fmMatch[0].replace(/^---\r?\n/, `---
|
|
2178
|
+
type: ${defaultType}
|
|
2179
|
+
`);
|
|
2180
|
+
return content.replace(fmMatch[0], injected);
|
|
2181
|
+
}
|
|
2182
|
+
var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
2183
|
+
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
|
|
2184
|
+
var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
|
|
2185
|
+
function shouldExclude(relPath, isDir) {
|
|
2186
|
+
const parts = relPath.split(posix.sep);
|
|
2187
|
+
const name = parts[parts.length - 1];
|
|
2188
|
+
if (EXCLUDE_NAMES.has(name)) return true;
|
|
2189
|
+
if (isDir && EXCLUDE_DIRS.has(name)) return true;
|
|
2190
|
+
const ext = name.includes(".") ? "." + name.split(".").pop() : "";
|
|
2191
|
+
if (EXCLUDE_EXTS.has(ext)) return true;
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
async function walkDir(base, relDir, collected) {
|
|
2195
|
+
const entries = await readdir(join4(base, relDir), { withFileTypes: true });
|
|
2196
|
+
for (const entry of entries) {
|
|
2197
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
2198
|
+
if (shouldExclude(rel, entry.isDirectory())) continue;
|
|
2199
|
+
if (entry.isDirectory()) {
|
|
2200
|
+
await walkDir(base, rel, collected);
|
|
2201
|
+
} else if (entry.isFile()) {
|
|
2202
|
+
collected.push({ rel, abs: join4(base, rel) });
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async function readAsset(absPath) {
|
|
2207
|
+
const buf = await readFile3(absPath);
|
|
2208
|
+
const text = buf.toString("utf8");
|
|
2209
|
+
const roundtrip = Buffer.from(text, "utf8");
|
|
2210
|
+
if (roundtrip.equals(buf)) {
|
|
2211
|
+
return { content: text, encoding: "utf8" };
|
|
2212
|
+
}
|
|
2213
|
+
const b64 = buf.toString("base64");
|
|
2214
|
+
printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
|
|
2215
|
+
return { content: b64, encoding: "base64" };
|
|
2216
|
+
}
|
|
2217
|
+
async function bundleDirectory(dir) {
|
|
2218
|
+
const all = [];
|
|
2219
|
+
await walkDir(dir, "", all);
|
|
2220
|
+
const skillEntry = all.find((f) => f.rel === "SKILL.md");
|
|
2221
|
+
if (!skillEntry) {
|
|
2222
|
+
throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
|
|
2223
|
+
}
|
|
2224
|
+
const content = await readFile3(skillEntry.abs, "utf8");
|
|
2225
|
+
const assets = [];
|
|
2226
|
+
for (const f of all) {
|
|
2227
|
+
if (f.rel === "SKILL.md") continue;
|
|
2228
|
+
const { content: fc, encoding } = await readAsset(f.abs);
|
|
2229
|
+
assets.push({ path: f.rel, content: fc, encoding });
|
|
2230
|
+
}
|
|
2231
|
+
return { content, assets };
|
|
2232
|
+
}
|
|
2233
|
+
async function bundleSkillZip(zipPath) {
|
|
2234
|
+
const buf = await readFile3(zipPath);
|
|
2235
|
+
const zip = await JSZip.loadAsync(buf);
|
|
2236
|
+
const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
|
|
2237
|
+
if (entries.length === 0) throw new Error(".skill file is empty");
|
|
2238
|
+
const firstParts = entries[0].split("/");
|
|
2239
|
+
const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
|
|
2240
|
+
const stripped = entries.map((k) => ({
|
|
2241
|
+
zipKey: k,
|
|
2242
|
+
rel: prefix ? k.slice(prefix.length) : k
|
|
2243
|
+
})).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
|
|
2244
|
+
const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
|
|
2245
|
+
if (!skillEntry) {
|
|
2246
|
+
throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
|
|
2247
|
+
}
|
|
2248
|
+
const content = await zip.files[skillEntry.zipKey].async("string");
|
|
2249
|
+
const assets = [];
|
|
2250
|
+
for (const f of stripped) {
|
|
2251
|
+
if (f.rel === "SKILL.md") continue;
|
|
2252
|
+
const rawBuf = await zip.files[f.zipKey].async("uint8array");
|
|
2253
|
+
const buf2 = Buffer.from(rawBuf);
|
|
2254
|
+
const text = buf2.toString("utf8");
|
|
2255
|
+
const roundtrip = Buffer.from(text, "utf8");
|
|
2256
|
+
if (roundtrip.equals(buf2)) {
|
|
2257
|
+
assets.push({ path: f.rel, content: text, encoding: "utf8" });
|
|
2258
|
+
} else {
|
|
2259
|
+
printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
|
|
2260
|
+
assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return { content, assets };
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/commands/pull.ts
|
|
2267
|
+
import { Command as Command5 } from "commander";
|
|
2268
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
2269
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
2108
2270
|
|
|
2109
2271
|
// ../../packages/translators/src/skill/identity.ts
|
|
2110
2272
|
function buildSkillFile(input, outputPath) {
|
|
@@ -2733,6 +2895,17 @@ function readCommand() {
|
|
|
2733
2895
|
}
|
|
2734
2896
|
process.stdout.write(res.body);
|
|
2735
2897
|
if (!res.body.endsWith("\n")) process.stdout.write("\n");
|
|
2898
|
+
const bundled = res.bundled_assets ?? [];
|
|
2899
|
+
if (bundled.length > 0) {
|
|
2900
|
+
process.stderr.write(
|
|
2901
|
+
`${c.gray("\u2014")} ${c.bold(`\u24D8 ${bundled.length} bundled asset(s)`)} \u2014 fetch with ${c.cyan(`kordis pull ${res.full_slug}`)}:
|
|
2902
|
+
`
|
|
2903
|
+
);
|
|
2904
|
+
for (const path of bundled) {
|
|
2905
|
+
process.stderr.write(` ${c.gray("-")} ${path}
|
|
2906
|
+
`);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2736
2909
|
process.stderr.write(
|
|
2737
2910
|
`${c.gray("\u2014")} ${c.bold(res.full_slug)}${c.gray("@")}${res.version} ${c.gray("\xB7")} ${res.type} (tier ${res.tier}) ${c.gray("\xB7")} ${c.cyan(res.attribution)}
|
|
2738
2911
|
`
|
|
@@ -2815,7 +2988,7 @@ function logoutCommand() {
|
|
|
2815
2988
|
}
|
|
2816
2989
|
|
|
2817
2990
|
// src/commands/setup.ts
|
|
2818
|
-
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, access } from "fs/promises";
|
|
2991
|
+
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, access as access2 } from "fs/promises";
|
|
2819
2992
|
import { homedir as homedir4 } from "os";
|
|
2820
2993
|
import { join as join7, dirname as dirname3 } from "path";
|
|
2821
2994
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -2840,7 +3013,7 @@ var SKILL_TARGETS = [
|
|
|
2840
3013
|
];
|
|
2841
3014
|
async function exists(path) {
|
|
2842
3015
|
try {
|
|
2843
|
-
await
|
|
3016
|
+
await access2(path);
|
|
2844
3017
|
return true;
|
|
2845
3018
|
} catch {
|
|
2846
3019
|
return false;
|
|
@@ -3290,6 +3463,11 @@ function outdatedCommand() {
|
|
|
3290
3463
|
const outdatedCount = entries.filter((e) => e.outdated).length;
|
|
3291
3464
|
if (outdatedCount === 0) {
|
|
3292
3465
|
printHuman(c.green(`\u2713 All ${entries.length} package${entries.length !== 1 ? "s" : ""} are up to date`));
|
|
3466
|
+
printHuman(
|
|
3467
|
+
c.gray(
|
|
3468
|
+
" \u2192 bundled Kordis sub-skills update with the CLI: `npm i -g @rayul/kordis@latest`"
|
|
3469
|
+
)
|
|
3470
|
+
);
|
|
3293
3471
|
return;
|
|
3294
3472
|
}
|
|
3295
3473
|
printHuman(
|
|
@@ -3305,6 +3483,11 @@ function outdatedCommand() {
|
|
|
3305
3483
|
}
|
|
3306
3484
|
printHuman("");
|
|
3307
3485
|
printHuman(c.gray("Run `kordis update` to upgrade all outdated packages."));
|
|
3486
|
+
printHuman(
|
|
3487
|
+
c.gray(
|
|
3488
|
+
" \u2192 only `kordis pull`-installed packages are tracked here. Bundled sub-skills update via `npm i -g @rayul/kordis@latest`."
|
|
3489
|
+
)
|
|
3490
|
+
);
|
|
3308
3491
|
}
|
|
3309
3492
|
});
|
|
3310
3493
|
});
|