@rayul/kordis 0.2.0 → 0.2.2
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/dist/index.js +351 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1172,6 +1172,8 @@ import ora from "ora";
|
|
|
1172
1172
|
var jsonMode = false;
|
|
1173
1173
|
function setJsonMode(on) {
|
|
1174
1174
|
jsonMode = on;
|
|
1175
|
+
if (on) process.env["KORDIS_JSON"] = "1";
|
|
1176
|
+
else delete process.env["KORDIS_JSON"];
|
|
1175
1177
|
}
|
|
1176
1178
|
function isJsonMode() {
|
|
1177
1179
|
return jsonMode;
|
|
@@ -1340,11 +1342,31 @@ function searchCommand() {
|
|
|
1340
1342
|
limit: opts.limit
|
|
1341
1343
|
});
|
|
1342
1344
|
}
|
|
1345
|
+
const queryProvided = query.length > 0;
|
|
1343
1346
|
printDual({
|
|
1344
1347
|
json: res,
|
|
1345
1348
|
human: () => {
|
|
1346
1349
|
if (res.results.length === 0) {
|
|
1347
1350
|
printHuman(c.yellow("(no results)"));
|
|
1351
|
+
if (opts.type) {
|
|
1352
|
+
printHuman(
|
|
1353
|
+
c.gray(
|
|
1354
|
+
` \u2192 no packages of type "${opts.type}" match. Try without --type, or list everything: kordis search --type ${opts.type} --sort recent --limit 25`
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
} else if (queryProvided) {
|
|
1358
|
+
printHuman(
|
|
1359
|
+
c.gray(
|
|
1360
|
+
" \u2192 try `--type skill` (or mcp-server / project-rules / custom-agent / hook) to narrow, or broader keywords"
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
} else {
|
|
1364
|
+
printHuman(
|
|
1365
|
+
c.gray(
|
|
1366
|
+
' \u2192 try keywords (e.g. `kordis search "topic"`) or `kordis search --trending`'
|
|
1367
|
+
)
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1348
1370
|
return;
|
|
1349
1371
|
}
|
|
1350
1372
|
for (const p of res.results) {
|
|
@@ -1366,6 +1388,13 @@ function searchCommand() {
|
|
|
1366
1388
|
if (res.next_cursor) {
|
|
1367
1389
|
printHuman(c.gray(`(more available \u2014 pass --cursor ${res.next_cursor})`));
|
|
1368
1390
|
}
|
|
1391
|
+
if (queryProvided && !opts.type && res.results.length > 3) {
|
|
1392
|
+
printHuman(
|
|
1393
|
+
c.gray(
|
|
1394
|
+
" \u2192 narrow further with `--type skill|mcp-server|project-rules|custom-agent|hook`"
|
|
1395
|
+
)
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1369
1398
|
}
|
|
1370
1399
|
});
|
|
1371
1400
|
});
|
|
@@ -1425,210 +1454,6 @@ var FrontmatterParseError = class extends Error {
|
|
|
1425
1454
|
}
|
|
1426
1455
|
};
|
|
1427
1456
|
|
|
1428
|
-
// src/commands/push.ts
|
|
1429
|
-
function pushCommand() {
|
|
1430
|
-
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(
|
|
1431
|
-
"--slug <slug>",
|
|
1432
|
-
'override slug; accepts "user/slug" to point at a specific package'
|
|
1433
|
-
).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(
|
|
1434
|
-
async (path, opts) => {
|
|
1435
|
-
const bundle = await useClient();
|
|
1436
|
-
const abs = resolve(process.cwd(), path);
|
|
1437
|
-
const s = await stat(abs);
|
|
1438
|
-
let content;
|
|
1439
|
-
let assets = [];
|
|
1440
|
-
if (s.isDirectory()) {
|
|
1441
|
-
({ content, assets } = await bundleDirectory(abs));
|
|
1442
|
-
content = ensureTypeField(content, "skill");
|
|
1443
|
-
} else if (abs.endsWith(".skill")) {
|
|
1444
|
-
({ content, assets } = await bundleSkillZip(abs));
|
|
1445
|
-
content = ensureTypeField(content, "skill");
|
|
1446
|
-
} else {
|
|
1447
|
-
content = await readFile3(abs, "utf8");
|
|
1448
|
-
}
|
|
1449
|
-
let nameFromFm = null;
|
|
1450
|
-
try {
|
|
1451
|
-
const fm = parseFrontmatter(content).frontmatter ?? {};
|
|
1452
|
-
const v = fm["name"];
|
|
1453
|
-
if (typeof v === "string") nameFromFm = v;
|
|
1454
|
-
} catch (err) {
|
|
1455
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
1456
|
-
}
|
|
1457
|
-
let targetUser = null;
|
|
1458
|
-
let targetSlug = null;
|
|
1459
|
-
if (opts.slug && opts.slug.includes("/")) {
|
|
1460
|
-
const [u, s2] = opts.slug.split("/", 2);
|
|
1461
|
-
if (u && s2) {
|
|
1462
|
-
targetUser = u;
|
|
1463
|
-
targetSlug = s2;
|
|
1464
|
-
}
|
|
1465
|
-
} else {
|
|
1466
|
-
if (bundle.username && (opts.slug || nameFromFm)) {
|
|
1467
|
-
targetUser = bundle.username;
|
|
1468
|
-
targetSlug = opts.slug ?? nameFromFm;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
let targetFullSlug;
|
|
1472
|
-
if (targetUser && targetSlug) {
|
|
1473
|
-
try {
|
|
1474
|
-
await bundle.api.getPackage(targetUser, targetSlug);
|
|
1475
|
-
targetFullSlug = `${targetUser}/${targetSlug}`;
|
|
1476
|
-
} catch (err) {
|
|
1477
|
-
if (err instanceof KordisApiError && err.code === "not_found") {
|
|
1478
|
-
targetFullSlug = void 0;
|
|
1479
|
-
} else if (!(err instanceof KordisApiError)) {
|
|
1480
|
-
throw err;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
const spin = spinner(
|
|
1485
|
-
opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
|
|
1486
|
-
);
|
|
1487
|
-
const res = await bundle.authedRequest(
|
|
1488
|
-
(api) => api.publish(
|
|
1489
|
-
{
|
|
1490
|
-
content,
|
|
1491
|
-
assets: assets.length > 0 ? assets : void 0,
|
|
1492
|
-
bump: opts.bump,
|
|
1493
|
-
slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
|
|
1494
|
-
target_full_slug: targetFullSlug,
|
|
1495
|
-
type: opts.type,
|
|
1496
|
-
changelog: opts.changelog,
|
|
1497
|
-
dry_run: !!opts.dryRun
|
|
1498
|
-
},
|
|
1499
|
-
{
|
|
1500
|
-
waitForReviewMs: opts.wait === false ? 0 : 3e3
|
|
1501
|
-
}
|
|
1502
|
-
)
|
|
1503
|
-
);
|
|
1504
|
-
spin.succeed();
|
|
1505
|
-
printDual({
|
|
1506
|
-
json: res,
|
|
1507
|
-
human: () => {
|
|
1508
|
-
printHuman(
|
|
1509
|
-
`${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")}`
|
|
1510
|
-
);
|
|
1511
|
-
if (assets.length > 0) {
|
|
1512
|
-
printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
1513
|
-
}
|
|
1514
|
-
printHuman(` ${c.cyan(res.url)}`);
|
|
1515
|
-
if (res.status === "pending") {
|
|
1516
|
-
printHuman(
|
|
1517
|
-
c.yellow(
|
|
1518
|
-
` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
|
|
1519
|
-
)
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
if (res.review.flags.length > 0) {
|
|
1523
|
-
printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
|
|
1524
|
-
for (const f of res.review.flags) {
|
|
1525
|
-
printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
);
|
|
1532
|
-
}
|
|
1533
|
-
function ensureTypeField(content, defaultType) {
|
|
1534
|
-
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
1535
|
-
if (!fmMatch) return content;
|
|
1536
|
-
const fmBlock = fmMatch[1];
|
|
1537
|
-
if (/^type\s*:/m.test(fmBlock)) return content;
|
|
1538
|
-
const injected = fmMatch[0].replace(/^---\r?\n/, `---
|
|
1539
|
-
type: ${defaultType}
|
|
1540
|
-
`);
|
|
1541
|
-
return content.replace(fmMatch[0], injected);
|
|
1542
|
-
}
|
|
1543
|
-
var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
1544
|
-
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
|
|
1545
|
-
var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
|
|
1546
|
-
function shouldExclude(relPath, isDir) {
|
|
1547
|
-
const parts = relPath.split(posix.sep);
|
|
1548
|
-
const name = parts[parts.length - 1];
|
|
1549
|
-
if (EXCLUDE_NAMES.has(name)) return true;
|
|
1550
|
-
if (isDir && EXCLUDE_DIRS.has(name)) return true;
|
|
1551
|
-
const ext = name.includes(".") ? "." + name.split(".").pop() : "";
|
|
1552
|
-
if (EXCLUDE_EXTS.has(ext)) return true;
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
async function walkDir(base, relDir, collected) {
|
|
1556
|
-
const entries = await readdir(join4(base, relDir), { withFileTypes: true });
|
|
1557
|
-
for (const entry of entries) {
|
|
1558
|
-
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1559
|
-
if (shouldExclude(rel, entry.isDirectory())) continue;
|
|
1560
|
-
if (entry.isDirectory()) {
|
|
1561
|
-
await walkDir(base, rel, collected);
|
|
1562
|
-
} else if (entry.isFile()) {
|
|
1563
|
-
collected.push({ rel, abs: join4(base, rel) });
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
async function readAsset(absPath) {
|
|
1568
|
-
const buf = await readFile3(absPath);
|
|
1569
|
-
const text = buf.toString("utf8");
|
|
1570
|
-
const roundtrip = Buffer.from(text, "utf8");
|
|
1571
|
-
if (roundtrip.equals(buf)) {
|
|
1572
|
-
return { content: text, encoding: "utf8" };
|
|
1573
|
-
}
|
|
1574
|
-
const b64 = buf.toString("base64");
|
|
1575
|
-
printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
|
|
1576
|
-
return { content: b64, encoding: "base64" };
|
|
1577
|
-
}
|
|
1578
|
-
async function bundleDirectory(dir) {
|
|
1579
|
-
const all = [];
|
|
1580
|
-
await walkDir(dir, "", all);
|
|
1581
|
-
const skillEntry = all.find((f) => f.rel === "SKILL.md");
|
|
1582
|
-
if (!skillEntry) {
|
|
1583
|
-
throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
|
|
1584
|
-
}
|
|
1585
|
-
const content = await readFile3(skillEntry.abs, "utf8");
|
|
1586
|
-
const assets = [];
|
|
1587
|
-
for (const f of all) {
|
|
1588
|
-
if (f.rel === "SKILL.md") continue;
|
|
1589
|
-
const { content: fc, encoding } = await readAsset(f.abs);
|
|
1590
|
-
assets.push({ path: f.rel, content: fc, encoding });
|
|
1591
|
-
}
|
|
1592
|
-
return { content, assets };
|
|
1593
|
-
}
|
|
1594
|
-
async function bundleSkillZip(zipPath) {
|
|
1595
|
-
const buf = await readFile3(zipPath);
|
|
1596
|
-
const zip = await JSZip.loadAsync(buf);
|
|
1597
|
-
const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
|
|
1598
|
-
if (entries.length === 0) throw new Error(".skill file is empty");
|
|
1599
|
-
const firstParts = entries[0].split("/");
|
|
1600
|
-
const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
|
|
1601
|
-
const stripped = entries.map((k) => ({
|
|
1602
|
-
zipKey: k,
|
|
1603
|
-
rel: prefix ? k.slice(prefix.length) : k
|
|
1604
|
-
})).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
|
|
1605
|
-
const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
|
|
1606
|
-
if (!skillEntry) {
|
|
1607
|
-
throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
|
|
1608
|
-
}
|
|
1609
|
-
const content = await zip.files[skillEntry.zipKey].async("string");
|
|
1610
|
-
const assets = [];
|
|
1611
|
-
for (const f of stripped) {
|
|
1612
|
-
if (f.rel === "SKILL.md") continue;
|
|
1613
|
-
const rawBuf = await zip.files[f.zipKey].async("uint8array");
|
|
1614
|
-
const buf2 = Buffer.from(rawBuf);
|
|
1615
|
-
const text = buf2.toString("utf8");
|
|
1616
|
-
const roundtrip = Buffer.from(text, "utf8");
|
|
1617
|
-
if (roundtrip.equals(buf2)) {
|
|
1618
|
-
assets.push({ path: f.rel, content: text, encoding: "utf8" });
|
|
1619
|
-
} else {
|
|
1620
|
-
printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
|
|
1621
|
-
assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
return { content, assets };
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
// src/commands/pull.ts
|
|
1628
|
-
import { Command as Command5 } from "commander";
|
|
1629
|
-
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1630
|
-
import { dirname as dirname2, join as join6 } from "path";
|
|
1631
|
-
|
|
1632
1457
|
// ../../packages/harness-schema/src/deps.ts
|
|
1633
1458
|
init_src();
|
|
1634
1459
|
import { z as z10 } from "zod";
|
|
@@ -2103,11 +1928,317 @@ function zodFail(issues) {
|
|
|
2103
1928
|
})
|
|
2104
1929
|
};
|
|
2105
1930
|
}
|
|
1931
|
+
var ASSET_MAX_COUNT = 50;
|
|
1932
|
+
var SAFE_PATH_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/;
|
|
1933
|
+
function isAssetPathSafe(p) {
|
|
1934
|
+
if (!SAFE_PATH_RE.test(p)) return false;
|
|
1935
|
+
return !p.split("/").some((seg) => seg === "..");
|
|
1936
|
+
}
|
|
1937
|
+
function validateAssets(assets) {
|
|
1938
|
+
const flags = [];
|
|
1939
|
+
if (assets.length > ASSET_MAX_COUNT) {
|
|
1940
|
+
flags.push(
|
|
1941
|
+
flag("too_many_assets", `bundle has ${assets.length} files (max ${ASSET_MAX_COUNT})`, {
|
|
1942
|
+
hint: `Bundle has more than ${ASSET_MAX_COUNT} files. Trim demos or move large reference material to an external Git repo.`
|
|
1943
|
+
})
|
|
1944
|
+
);
|
|
1945
|
+
return { ok: false, flags };
|
|
1946
|
+
}
|
|
1947
|
+
for (const asset of assets) {
|
|
1948
|
+
if (!isAssetPathSafe(asset.path)) {
|
|
1949
|
+
flags.push(
|
|
1950
|
+
flag("asset_path_unsafe", `unsafe asset path: "${asset.path}"`, {
|
|
1951
|
+
field: "assets",
|
|
1952
|
+
hint: 'Asset paths must be relative POSIX, no leading "/", no "..". Example: scripts/run.sh'
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return { ok: flags.length === 0, flags };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/commands/push.ts
|
|
1961
|
+
var ASSET_MAX_BYTES = 2 * 1024 * 1024;
|
|
1962
|
+
var BUNDLE_MAX_BYTES = 5 * 1024 * 1024;
|
|
1963
|
+
function pushCommand() {
|
|
1964
|
+
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(
|
|
1965
|
+
"--slug <slug>",
|
|
1966
|
+
'override slug; accepts "user/slug" to point at a specific package'
|
|
1967
|
+
).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(
|
|
1968
|
+
async (path, opts) => {
|
|
1969
|
+
const bundle = await useClient();
|
|
1970
|
+
const abs = resolve(process.cwd(), path);
|
|
1971
|
+
const s = await stat(abs);
|
|
1972
|
+
let content;
|
|
1973
|
+
let assets = [];
|
|
1974
|
+
if (s.isDirectory()) {
|
|
1975
|
+
({ content, assets } = await bundleDirectory(abs));
|
|
1976
|
+
content = ensureTypeField(content, "skill");
|
|
1977
|
+
} else if (abs.endsWith(".skill")) {
|
|
1978
|
+
({ content, assets } = await bundleSkillZip(abs));
|
|
1979
|
+
content = ensureTypeField(content, "skill");
|
|
1980
|
+
} else {
|
|
1981
|
+
content = await readFile3(abs, "utf8");
|
|
1982
|
+
}
|
|
1983
|
+
let nameFromFm = null;
|
|
1984
|
+
try {
|
|
1985
|
+
const fm = parseFrontmatter(content).frontmatter ?? {};
|
|
1986
|
+
const v = fm["name"];
|
|
1987
|
+
if (typeof v === "string") nameFromFm = v;
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
1990
|
+
}
|
|
1991
|
+
if (opts.dryRun) {
|
|
1992
|
+
await runDryRun({ content, assets });
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
let targetUser = null;
|
|
1996
|
+
let targetSlug = null;
|
|
1997
|
+
if (opts.slug && opts.slug.includes("/")) {
|
|
1998
|
+
const [u, s2] = opts.slug.split("/", 2);
|
|
1999
|
+
if (u && s2) {
|
|
2000
|
+
targetUser = u;
|
|
2001
|
+
targetSlug = s2;
|
|
2002
|
+
}
|
|
2003
|
+
} else {
|
|
2004
|
+
if (bundle.username && (opts.slug || nameFromFm)) {
|
|
2005
|
+
targetUser = bundle.username;
|
|
2006
|
+
targetSlug = opts.slug ?? nameFromFm;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
let targetFullSlug;
|
|
2010
|
+
if (targetUser && targetSlug) {
|
|
2011
|
+
try {
|
|
2012
|
+
await bundle.api.getPackage(targetUser, targetSlug);
|
|
2013
|
+
targetFullSlug = `${targetUser}/${targetSlug}`;
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
if (err instanceof KordisApiError && err.code === "not_found") {
|
|
2016
|
+
targetFullSlug = void 0;
|
|
2017
|
+
} else if (!(err instanceof KordisApiError)) {
|
|
2018
|
+
throw err;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
const spin = spinner(
|
|
2023
|
+
opts.dryRun ? "Validating" : targetFullSlug ? `Publishing update to ${targetFullSlug}` : "Publishing"
|
|
2024
|
+
);
|
|
2025
|
+
const res = await bundle.authedRequest(
|
|
2026
|
+
(api) => api.publish(
|
|
2027
|
+
{
|
|
2028
|
+
content,
|
|
2029
|
+
assets: assets.length > 0 ? assets : void 0,
|
|
2030
|
+
bump: opts.bump,
|
|
2031
|
+
slug: opts.slug && !opts.slug.includes("/") ? opts.slug : void 0,
|
|
2032
|
+
target_full_slug: targetFullSlug,
|
|
2033
|
+
type: opts.type,
|
|
2034
|
+
changelog: opts.changelog,
|
|
2035
|
+
dry_run: !!opts.dryRun
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
waitForReviewMs: opts.wait === false ? 0 : 3e3
|
|
2039
|
+
}
|
|
2040
|
+
)
|
|
2041
|
+
);
|
|
2042
|
+
spin.succeed();
|
|
2043
|
+
printDual({
|
|
2044
|
+
json: res,
|
|
2045
|
+
human: () => {
|
|
2046
|
+
printHuman(
|
|
2047
|
+
`${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")}`
|
|
2048
|
+
);
|
|
2049
|
+
if (assets.length > 0) {
|
|
2050
|
+
printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
2051
|
+
}
|
|
2052
|
+
printHuman(` ${c.cyan(res.url)}`);
|
|
2053
|
+
if (res.status === "pending") {
|
|
2054
|
+
printHuman(
|
|
2055
|
+
c.yellow(
|
|
2056
|
+
` ! AI review is still pending \u2014 re-check with \`kordis info ${res.full_slug} --json\` for the verdict.`
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
if (res.review.flags.length > 0) {
|
|
2061
|
+
printHuman(c.yellow(` ${res.review.flags.length} review flag(s):`));
|
|
2062
|
+
for (const f of res.review.flags) {
|
|
2063
|
+
printHuman(c.yellow(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
function checkAssetSizesLocal(assets) {
|
|
2072
|
+
const flags = [];
|
|
2073
|
+
let totalBytes = 0;
|
|
2074
|
+
for (const asset of assets) {
|
|
2075
|
+
const byteLen = asset.encoding === "base64" ? Math.ceil(asset.content.length * 3 / 4) : Buffer.byteLength(asset.content, "utf8");
|
|
2076
|
+
if (byteLen > ASSET_MAX_BYTES) {
|
|
2077
|
+
flags.push({
|
|
2078
|
+
code: "asset_too_large",
|
|
2079
|
+
message: `asset "${asset.path}" is ${byteLen} bytes (max ${ASSET_MAX_BYTES})`,
|
|
2080
|
+
hint: `Single asset exceeds the ${ASSET_MAX_BYTES / 1024 / 1024} MB per-file cap. Trim or move large fixtures to an external Git repo.`
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
totalBytes += byteLen;
|
|
2084
|
+
}
|
|
2085
|
+
if (totalBytes > BUNDLE_MAX_BYTES) {
|
|
2086
|
+
flags.push({
|
|
2087
|
+
code: "bundle_too_large",
|
|
2088
|
+
message: `bundle totals ${totalBytes} bytes (max ${BUNDLE_MAX_BYTES})`,
|
|
2089
|
+
hint: `Total bundle exceeds ${BUNDLE_MAX_BYTES / 1024 / 1024} MB. Remove generated artifacts; check .gitignore parity.`
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
return flags;
|
|
2093
|
+
}
|
|
2094
|
+
async function runDryRun(args) {
|
|
2095
|
+
const { content, assets } = args;
|
|
2096
|
+
const v = validatePackageSource(content);
|
|
2097
|
+
const assetPathResult = assets.length > 0 ? validateAssets(assets) : { ok: true, flags: [] };
|
|
2098
|
+
const sizeFlags = checkAssetSizesLocal(assets);
|
|
2099
|
+
const allFlags = [...v.flags, ...assetPathResult.flags, ...sizeFlags];
|
|
2100
|
+
const ok = v.ok && assetPathResult.ok && sizeFlags.length === 0;
|
|
2101
|
+
printDual({
|
|
2102
|
+
json: {
|
|
2103
|
+
status: ok ? "dry_run_passed" : "dry_run_failed",
|
|
2104
|
+
type: v.type,
|
|
2105
|
+
tier: v.tier,
|
|
2106
|
+
capabilities: v.capabilities,
|
|
2107
|
+
asset_count: assets.length,
|
|
2108
|
+
flags: allFlags,
|
|
2109
|
+
note: "dry-run validates frontmatter + asset paths/sizes locally; AI review (real publish only) is not performed"
|
|
2110
|
+
},
|
|
2111
|
+
human: () => {
|
|
2112
|
+
if (ok) {
|
|
2113
|
+
printHuman(c.green("\u2713 dry-run validation passed"));
|
|
2114
|
+
if (v.type) printHuman(c.gray(` type: ${v.type} (tier ${v.tier})`));
|
|
2115
|
+
if (v.capabilities) {
|
|
2116
|
+
printHuman(
|
|
2117
|
+
c.gray(
|
|
2118
|
+
` capabilities: jit_eligible=${v.capabilities.jit_eligible} executable=${v.capabilities.executable}`
|
|
2119
|
+
)
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
if (assets.length > 0) printHuman(c.gray(` + ${assets.length} bundled file(s)`));
|
|
2123
|
+
printHuman(
|
|
2124
|
+
c.gray(" (AI review only runs on the real push; not performed here)")
|
|
2125
|
+
);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
printHuman(c.red(`\u2717 dry-run validation failed (${allFlags.length} flag(s))`));
|
|
2129
|
+
for (const f of allFlags) {
|
|
2130
|
+
printHuman(c.red(` - ${f.code}${f.message ? `: ${f.message}` : ""}`));
|
|
2131
|
+
if (f.hint) {
|
|
2132
|
+
for (const line of f.hint.split("\n")) printHuman(c.gray(` \u2192 ${line}`));
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
if (!ok) process.exit(30);
|
|
2138
|
+
}
|
|
2139
|
+
function ensureTypeField(content, defaultType) {
|
|
2140
|
+
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
2141
|
+
if (!fmMatch) return content;
|
|
2142
|
+
const fmBlock = fmMatch[1];
|
|
2143
|
+
if (/^type\s*:/m.test(fmBlock)) return content;
|
|
2144
|
+
const injected = fmMatch[0].replace(/^---\r?\n/, `---
|
|
2145
|
+
type: ${defaultType}
|
|
2146
|
+
`);
|
|
2147
|
+
return content.replace(fmMatch[0], injected);
|
|
2148
|
+
}
|
|
2149
|
+
var EXCLUDE_NAMES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
2150
|
+
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["__pycache__", "node_modules", ".git", "evals", "dist", ".turbo"]);
|
|
2151
|
+
var EXCLUDE_EXTS = /* @__PURE__ */ new Set([".pyc", ".pyo", ".rbc"]);
|
|
2152
|
+
function shouldExclude(relPath, isDir) {
|
|
2153
|
+
const parts = relPath.split(posix.sep);
|
|
2154
|
+
const name = parts[parts.length - 1];
|
|
2155
|
+
if (EXCLUDE_NAMES.has(name)) return true;
|
|
2156
|
+
if (isDir && EXCLUDE_DIRS.has(name)) return true;
|
|
2157
|
+
const ext = name.includes(".") ? "." + name.split(".").pop() : "";
|
|
2158
|
+
if (EXCLUDE_EXTS.has(ext)) return true;
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
async function walkDir(base, relDir, collected) {
|
|
2162
|
+
const entries = await readdir(join4(base, relDir), { withFileTypes: true });
|
|
2163
|
+
for (const entry of entries) {
|
|
2164
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
2165
|
+
if (shouldExclude(rel, entry.isDirectory())) continue;
|
|
2166
|
+
if (entry.isDirectory()) {
|
|
2167
|
+
await walkDir(base, rel, collected);
|
|
2168
|
+
} else if (entry.isFile()) {
|
|
2169
|
+
collected.push({ rel, abs: join4(base, rel) });
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async function readAsset(absPath) {
|
|
2174
|
+
const buf = await readFile3(absPath);
|
|
2175
|
+
const text = buf.toString("utf8");
|
|
2176
|
+
const roundtrip = Buffer.from(text, "utf8");
|
|
2177
|
+
if (roundtrip.equals(buf)) {
|
|
2178
|
+
return { content: text, encoding: "utf8" };
|
|
2179
|
+
}
|
|
2180
|
+
const b64 = buf.toString("base64");
|
|
2181
|
+
printHuman(c.gray(` \u2192 encoding ${absPath.split(sep).pop()} as base64`));
|
|
2182
|
+
return { content: b64, encoding: "base64" };
|
|
2183
|
+
}
|
|
2184
|
+
async function bundleDirectory(dir) {
|
|
2185
|
+
const all = [];
|
|
2186
|
+
await walkDir(dir, "", all);
|
|
2187
|
+
const skillEntry = all.find((f) => f.rel === "SKILL.md");
|
|
2188
|
+
if (!skillEntry) {
|
|
2189
|
+
throw new Error(`No SKILL.md found in directory ${dir}. Push a directory that has SKILL.md at its root.`);
|
|
2190
|
+
}
|
|
2191
|
+
const content = await readFile3(skillEntry.abs, "utf8");
|
|
2192
|
+
const assets = [];
|
|
2193
|
+
for (const f of all) {
|
|
2194
|
+
if (f.rel === "SKILL.md") continue;
|
|
2195
|
+
const { content: fc, encoding } = await readAsset(f.abs);
|
|
2196
|
+
assets.push({ path: f.rel, content: fc, encoding });
|
|
2197
|
+
}
|
|
2198
|
+
return { content, assets };
|
|
2199
|
+
}
|
|
2200
|
+
async function bundleSkillZip(zipPath) {
|
|
2201
|
+
const buf = await readFile3(zipPath);
|
|
2202
|
+
const zip = await JSZip.loadAsync(buf);
|
|
2203
|
+
const entries = Object.keys(zip.files).filter((k) => !zip.files[k].dir);
|
|
2204
|
+
if (entries.length === 0) throw new Error(".skill file is empty");
|
|
2205
|
+
const firstParts = entries[0].split("/");
|
|
2206
|
+
const prefix = firstParts.length > 1 ? firstParts[0] + "/" : "";
|
|
2207
|
+
const stripped = entries.map((k) => ({
|
|
2208
|
+
zipKey: k,
|
|
2209
|
+
rel: prefix ? k.slice(prefix.length) : k
|
|
2210
|
+
})).filter((f) => f.rel.length > 0 && !shouldExclude(f.rel, false));
|
|
2211
|
+
const skillEntry = stripped.find((f) => f.rel === "SKILL.md");
|
|
2212
|
+
if (!skillEntry) {
|
|
2213
|
+
throw new Error(`.skill ZIP has no SKILL.md at its root (prefix="${prefix}")`);
|
|
2214
|
+
}
|
|
2215
|
+
const content = await zip.files[skillEntry.zipKey].async("string");
|
|
2216
|
+
const assets = [];
|
|
2217
|
+
for (const f of stripped) {
|
|
2218
|
+
if (f.rel === "SKILL.md") continue;
|
|
2219
|
+
const rawBuf = await zip.files[f.zipKey].async("uint8array");
|
|
2220
|
+
const buf2 = Buffer.from(rawBuf);
|
|
2221
|
+
const text = buf2.toString("utf8");
|
|
2222
|
+
const roundtrip = Buffer.from(text, "utf8");
|
|
2223
|
+
if (roundtrip.equals(buf2)) {
|
|
2224
|
+
assets.push({ path: f.rel, content: text, encoding: "utf8" });
|
|
2225
|
+
} else {
|
|
2226
|
+
printHuman(c.gray(` \u2192 encoding ${f.rel} as base64`));
|
|
2227
|
+
assets.push({ path: f.rel, content: buf2.toString("base64"), encoding: "base64" });
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return { content, assets };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/commands/pull.ts
|
|
2234
|
+
import { Command as Command5 } from "commander";
|
|
2235
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
2236
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
2106
2237
|
|
|
2107
2238
|
// ../../packages/translators/src/skill/identity.ts
|
|
2108
2239
|
function buildSkillFile(input, outputPath) {
|
|
2109
2240
|
const body = input.body.trimStart();
|
|
2110
|
-
const fm = input.frontmatter;
|
|
2241
|
+
const fm = input.frontmatter["version"] === void 0 && input.version ? { ...input.frontmatter, version: input.version } : input.frontmatter;
|
|
2111
2242
|
const name = typeof fm["name"] === "string" ? fm["name"] : input.fullSlug.split("/")[1];
|
|
2112
2243
|
const frontmatter = ["---"];
|
|
2113
2244
|
for (const [k, v] of Object.entries(fm)) {
|
|
@@ -3288,6 +3419,11 @@ function outdatedCommand() {
|
|
|
3288
3419
|
const outdatedCount = entries.filter((e) => e.outdated).length;
|
|
3289
3420
|
if (outdatedCount === 0) {
|
|
3290
3421
|
printHuman(c.green(`\u2713 All ${entries.length} package${entries.length !== 1 ? "s" : ""} are up to date`));
|
|
3422
|
+
printHuman(
|
|
3423
|
+
c.gray(
|
|
3424
|
+
" \u2192 bundled Kordis sub-skills update with the CLI: `npm i -g @rayul/kordis@latest`"
|
|
3425
|
+
)
|
|
3426
|
+
);
|
|
3291
3427
|
return;
|
|
3292
3428
|
}
|
|
3293
3429
|
printHuman(
|
|
@@ -3303,6 +3439,11 @@ function outdatedCommand() {
|
|
|
3303
3439
|
}
|
|
3304
3440
|
printHuman("");
|
|
3305
3441
|
printHuman(c.gray("Run `kordis update` to upgrade all outdated packages."));
|
|
3442
|
+
printHuman(
|
|
3443
|
+
c.gray(
|
|
3444
|
+
" \u2192 only `kordis pull`-installed packages are tracked here. Bundled sub-skills update via `npm i -g @rayul/kordis@latest`."
|
|
3445
|
+
)
|
|
3446
|
+
);
|
|
3306
3447
|
}
|
|
3307
3448
|
});
|
|
3308
3449
|
});
|
|
@@ -3493,6 +3634,11 @@ function diffCommand() {
|
|
|
3493
3634
|
printHuman("");
|
|
3494
3635
|
if (!diff.unified_diff) {
|
|
3495
3636
|
printHuman(c.dim("(no content changes between these versions)"));
|
|
3637
|
+
printHuman(
|
|
3638
|
+
c.dim(
|
|
3639
|
+
" \u2192 likely a metadata-only bump: description, deps, license, or version field"
|
|
3640
|
+
)
|
|
3641
|
+
);
|
|
3496
3642
|
return;
|
|
3497
3643
|
}
|
|
3498
3644
|
for (const line of diff.unified_diff.split("\n")) {
|