ht-skills 0.2.6 → 0.2.8
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/lib/cli.js +241 -22
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -36,6 +36,8 @@ const CONFIG_FILE_NAME = "config.json";
|
|
|
36
36
|
const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
37
|
const DEFAULT_LOGIN_POLL_MS = 1500;
|
|
38
38
|
const DEFAULT_PUBLISH_POLL_MS = 1500;
|
|
39
|
+
const DEFAULT_PUBLISH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
40
|
+
const DEFAULT_PUBLISH_POLL_ERROR_LIMIT = 3;
|
|
39
41
|
|
|
40
42
|
const BANNER_TEXT = String.raw` _ _ _____ ___ _ _ _ _ __ __ _ _ _
|
|
41
43
|
| || |_ _| / __| |_(_) | |___ | \/ |__ _ _ _| |_____| |_ _ __| |__ _ __
|
|
@@ -819,6 +821,64 @@ function formatSearchCard(item, { index = 0, colorize = (text) => text, width =
|
|
|
819
821
|
return [title, blank, firstLine, ...detailRendered, blank, bottom].join("\n");
|
|
820
822
|
}
|
|
821
823
|
|
|
824
|
+
const PUBLISH_FLOW_STEPS = [
|
|
825
|
+
{ id: "auth", label: "Login" },
|
|
826
|
+
{ id: "pack", label: "Pack" },
|
|
827
|
+
{ id: "inspect", label: "Inspect" },
|
|
828
|
+
{ id: "submit", label: "Submit" },
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
function getPublishStepStatusLabel(status) {
|
|
832
|
+
switch (status) {
|
|
833
|
+
case "done":
|
|
834
|
+
return "Done";
|
|
835
|
+
case "active":
|
|
836
|
+
return "Active";
|
|
837
|
+
case "error":
|
|
838
|
+
return "Error";
|
|
839
|
+
default:
|
|
840
|
+
return "Pending";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderPublishFlowCard(flowState, { colorize = (text) => text, width = 80 } = {}) {
|
|
845
|
+
const maxInnerWidth = Math.max(28, width - 6);
|
|
846
|
+
const lines = PUBLISH_FLOW_STEPS.map((step, index) => {
|
|
847
|
+
const status = flowState?.[step.id] || "pending";
|
|
848
|
+
const symbol = status === "done"
|
|
849
|
+
? colorize("●", "success")
|
|
850
|
+
: status === "active"
|
|
851
|
+
? colorize("◉", "accent")
|
|
852
|
+
: status === "error"
|
|
853
|
+
? colorize("▲", "warn")
|
|
854
|
+
: colorize("○", "muted");
|
|
855
|
+
const label = status === "done"
|
|
856
|
+
? colorize(step.label, "success")
|
|
857
|
+
: status === "active"
|
|
858
|
+
? colorize(step.label, "accent")
|
|
859
|
+
: status === "error"
|
|
860
|
+
? colorize(step.label, "warn")
|
|
861
|
+
: step.label;
|
|
862
|
+
const statusLabel = colorize(getPublishStepStatusLabel(status), status === "pending" ? "muted" : status === "error" ? "warn" : status === "active" ? "accent" : "success");
|
|
863
|
+
const plainLine = `${index + 1}. ${step.label} ${getPublishStepStatusLabel(status)}`;
|
|
864
|
+
const paddedWidth = Math.max(0, maxInnerWidth - getDisplayWidth(plainLine));
|
|
865
|
+
return `│ ${symbol} ${label}${" ".repeat(paddedWidth)} ${statusLabel} │`;
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const contentWidth = Math.max(
|
|
869
|
+
28,
|
|
870
|
+
...PUBLISH_FLOW_STEPS.map((step, index) => getDisplayWidth(`${index + 1}. ${step.label} Pending`)),
|
|
871
|
+
);
|
|
872
|
+
const title = `◇ ${colorize("Publish Flow", "accent")} ${colorize("─".repeat(Math.max(0, contentWidth - getDisplayWidth("Publish Flow") - 1)), "muted")}╮`;
|
|
873
|
+
const normalizedLines = lines.map((line) => {
|
|
874
|
+
const plainWidth = getDisplayWidth(line);
|
|
875
|
+
const targetWidth = contentWidth + 4;
|
|
876
|
+
return plainWidth < targetWidth ? `${line.slice(0, -2)}${" ".repeat(targetWidth - plainWidth)} │` : line;
|
|
877
|
+
});
|
|
878
|
+
const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
|
|
879
|
+
return [title, ...normalizedLines, bottom].join("\n");
|
|
880
|
+
}
|
|
881
|
+
|
|
822
882
|
function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
|
|
823
883
|
log("");
|
|
824
884
|
log("ht-skills");
|
|
@@ -848,6 +908,19 @@ function printFallbackSearchIntro({ registry, query, limit }, log = console.log)
|
|
|
848
908
|
log("");
|
|
849
909
|
}
|
|
850
910
|
|
|
911
|
+
function printFallbackPublishIntro({ registry, skillDir, archiveName, visibility }, log = console.log) {
|
|
912
|
+
log("");
|
|
913
|
+
log(renderGradientBanner());
|
|
914
|
+
log("");
|
|
915
|
+
log("Publish");
|
|
916
|
+
log("");
|
|
917
|
+
log(`Source: ${registry}`);
|
|
918
|
+
log(`Directory: ${skillDir}`);
|
|
919
|
+
log(`Archive: ${archiveName}`);
|
|
920
|
+
log(`Access: ${visibility}`);
|
|
921
|
+
log("");
|
|
922
|
+
}
|
|
923
|
+
|
|
851
924
|
async function loadTerminalUi() {
|
|
852
925
|
const [{ intro, outro, note, multiselect, select, spinner, isCancel, cancel }, pcModule] = await Promise.all([
|
|
853
926
|
import("@clack/prompts"),
|
|
@@ -1440,17 +1513,79 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1440
1513
|
const log = deps.log || ((message) => console.log(message));
|
|
1441
1514
|
const registry = getRegistryUrl(flags);
|
|
1442
1515
|
const skillDir = path.resolve(flags._[0] || ".");
|
|
1443
|
-
|
|
1516
|
+
const visibility = String(flags.access || flags.visibility || "public").trim().toLowerCase() || "public";
|
|
1517
|
+
const archiveName = `${path.basename(skillDir) || "skill"}.zip`;
|
|
1518
|
+
const isInteractive = isInteractiveSession(deps);
|
|
1519
|
+
const canUseFancyUi = Boolean(isInteractive && !deps.disableUi);
|
|
1520
|
+
const ui = deps.ui || (canUseFancyUi ? await loadTerminalUi().catch(() => null) : null);
|
|
1521
|
+
const colorize = ui ? createUiColorizer(ui.pc) : (text) => String(text);
|
|
1522
|
+
const outputWidth = getOutputWidth(deps.outputWidth);
|
|
1523
|
+
const pollIntervalMs = Math.max(500, Number(flags["poll-interval"] || deps.pollIntervalMs || DEFAULT_PUBLISH_POLL_MS));
|
|
1524
|
+
const publishTimeoutMs = Math.max(10_000, Number(flags.timeout || deps.timeoutMs || DEFAULT_PUBLISH_TIMEOUT_MS));
|
|
1525
|
+
const pollErrorLimit = Math.max(1, Number(flags["poll-error-limit"] || deps.pollErrorLimit || DEFAULT_PUBLISH_POLL_ERROR_LIMIT));
|
|
1526
|
+
const flowState = {
|
|
1527
|
+
auth: "pending",
|
|
1528
|
+
pack: "pending",
|
|
1529
|
+
inspect: "pending",
|
|
1530
|
+
submit: "pending",
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
const renderPublishFlow = () => {
|
|
1534
|
+
if (!ui) return;
|
|
1535
|
+
// eslint-disable-next-line no-console
|
|
1536
|
+
console.log(renderPublishFlowCard(flowState, {
|
|
1537
|
+
colorize,
|
|
1538
|
+
width: Math.max(44, Math.min(outputWidth - 4, 72)),
|
|
1539
|
+
}));
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
if (ui) {
|
|
1543
|
+
// eslint-disable-next-line no-console
|
|
1544
|
+
console.log(`\n${renderGradientBanner()}\n`);
|
|
1545
|
+
ui.intro(ui.pc.bgCyan(ui.pc.black(" ht-skills ")));
|
|
1546
|
+
ui.note(
|
|
1547
|
+
[
|
|
1548
|
+
`${colorize("Source", "muted")}: ${registry}`,
|
|
1549
|
+
`${colorize("Directory", "muted")}: ${skillDir}`,
|
|
1550
|
+
`${colorize("Archive", "muted")}: ${archiveName}`,
|
|
1551
|
+
`${colorize("Access", "muted")}: ${visibility}`,
|
|
1552
|
+
].join("\n"),
|
|
1553
|
+
"Publish",
|
|
1554
|
+
);
|
|
1555
|
+
renderPublishFlow();
|
|
1556
|
+
} else {
|
|
1557
|
+
printFallbackPublishIntro({ registry, skillDir, archiveName, visibility }, log);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
flowState.auth = "active";
|
|
1561
|
+
if (!ui) {
|
|
1562
|
+
log(`Checking login for ${registry}...`);
|
|
1563
|
+
}
|
|
1444
1564
|
const { token } = await ensureValidAuthToken(registry, flags, deps);
|
|
1565
|
+
flowState.auth = "done";
|
|
1445
1566
|
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1567
|
+
const packSpinner = ui ? ui.spinner() : null;
|
|
1568
|
+
flowState.pack = "active";
|
|
1569
|
+
if (packSpinner) {
|
|
1570
|
+
packSpinner.start(`Packing ${path.basename(skillDir) || skillDir}`);
|
|
1571
|
+
} else {
|
|
1572
|
+
log(`Packing skill directory: ${skillDir}`);
|
|
1573
|
+
}
|
|
1448
1574
|
const archiveBuffer = await createZipFromDirectory(skillDir);
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1575
|
+
flowState.pack = "done";
|
|
1576
|
+
if (packSpinner) {
|
|
1577
|
+
packSpinner.stop(`Created ${archiveName} (${formatBytes(archiveBuffer.length)})`);
|
|
1578
|
+
} else {
|
|
1579
|
+
log(`Created archive ${archiveName} (${formatBytes(archiveBuffer.length)})`);
|
|
1580
|
+
}
|
|
1452
1581
|
|
|
1453
|
-
|
|
1582
|
+
const uploadSpinner = ui ? ui.spinner() : null;
|
|
1583
|
+
flowState.inspect = "active";
|
|
1584
|
+
if (uploadSpinner) {
|
|
1585
|
+
uploadSpinner.start("Uploading archive for package inspection");
|
|
1586
|
+
} else {
|
|
1587
|
+
log("Uploading archive for package inspection...");
|
|
1588
|
+
}
|
|
1454
1589
|
const job = await requestJsonImpl(
|
|
1455
1590
|
`${registry}/api/skills/inspect-package-jobs/upload?archive_name=${encodeURIComponent(archiveName)}`,
|
|
1456
1591
|
{
|
|
@@ -1466,18 +1601,61 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1466
1601
|
if (!jobId) {
|
|
1467
1602
|
throw new Error("registry did not return an inspection job id");
|
|
1468
1603
|
}
|
|
1469
|
-
|
|
1604
|
+
if (uploadSpinner) {
|
|
1605
|
+
uploadSpinner.stop(`Inspection job created (${jobId})`);
|
|
1606
|
+
} else {
|
|
1607
|
+
log(`Inspection job created: ${jobId}`);
|
|
1608
|
+
}
|
|
1470
1609
|
|
|
1471
1610
|
let inspection = job;
|
|
1472
1611
|
let lastProgressKey = "";
|
|
1612
|
+
let consecutivePollErrors = 0;
|
|
1613
|
+
const publishDeadline = Date.now() + publishTimeoutMs;
|
|
1614
|
+
const inspectSpinner = ui ? ui.spinner() : null;
|
|
1615
|
+
if (inspectSpinner) {
|
|
1616
|
+
inspectSpinner.start("Inspecting archive");
|
|
1617
|
+
}
|
|
1473
1618
|
while (inspection.status !== "succeeded" && inspection.status !== "failed") {
|
|
1619
|
+
if (Date.now() > publishDeadline) {
|
|
1620
|
+
flowState.inspect = "error";
|
|
1621
|
+
if (inspectSpinner) {
|
|
1622
|
+
inspectSpinner.stop("Inspection timed out");
|
|
1623
|
+
}
|
|
1624
|
+
throw new Error(`Inspection timed out after ${Math.round(publishTimeoutMs / 1000)}s`);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1474
1627
|
await sleep(pollIntervalMs);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1628
|
+
try {
|
|
1629
|
+
inspection = await requestJsonImpl(
|
|
1630
|
+
`${registry}/api/skills/inspect-package-jobs/${encodeURIComponent(jobId)}`,
|
|
1631
|
+
{
|
|
1632
|
+
headers: withBearerToken({}, token),
|
|
1633
|
+
},
|
|
1634
|
+
);
|
|
1635
|
+
consecutivePollErrors = 0;
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
consecutivePollErrors += 1;
|
|
1638
|
+
if (consecutivePollErrors >= pollErrorLimit) {
|
|
1639
|
+
flowState.inspect = "error";
|
|
1640
|
+
if (inspectSpinner) {
|
|
1641
|
+
inspectSpinner.stop("Inspection polling failed");
|
|
1642
|
+
}
|
|
1643
|
+
throw new Error(`Inspection polling failed ${consecutivePollErrors} times: ${error.message}`);
|
|
1644
|
+
}
|
|
1645
|
+
if (!inspectSpinner) {
|
|
1646
|
+
log(`Inspection polling retry ${consecutivePollErrors}/${pollErrorLimit}: ${error.message}`);
|
|
1647
|
+
}
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const runtimeError = String(inspection.error?.message || inspection.error || "").trim();
|
|
1652
|
+
if (runtimeError && inspection.status !== "succeeded") {
|
|
1653
|
+
flowState.inspect = "error";
|
|
1654
|
+
if (inspectSpinner) {
|
|
1655
|
+
inspectSpinner.stop("Inspection failed");
|
|
1656
|
+
}
|
|
1657
|
+
throw new Error(runtimeError);
|
|
1658
|
+
}
|
|
1481
1659
|
|
|
1482
1660
|
const step = String(inspection.progress?.step || inspection.result?.step || "").trim();
|
|
1483
1661
|
const percent = inspection.progress?.percent;
|
|
@@ -1487,22 +1665,36 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1487
1665
|
const progressLabel = step
|
|
1488
1666
|
? `${step}${typeof percent === "number" ? ` ${percent}%` : ""}`
|
|
1489
1667
|
: inspection.status;
|
|
1490
|
-
|
|
1668
|
+
if (inspectSpinner) {
|
|
1669
|
+
inspectSpinner.message(`Inspecting archive (${progressLabel})`);
|
|
1670
|
+
} else {
|
|
1671
|
+
log(`Inspection in progress: ${progressLabel}`);
|
|
1672
|
+
}
|
|
1491
1673
|
}
|
|
1492
1674
|
}
|
|
1493
1675
|
|
|
1494
1676
|
if (inspection.status !== "succeeded") {
|
|
1677
|
+
flowState.inspect = "error";
|
|
1678
|
+
if (inspectSpinner) {
|
|
1679
|
+
inspectSpinner.stop("Inspection failed");
|
|
1680
|
+
}
|
|
1495
1681
|
throw new Error(inspection.error || "skill archive inspection failed");
|
|
1496
1682
|
}
|
|
1497
|
-
|
|
1683
|
+
flowState.inspect = "done";
|
|
1684
|
+
if (inspectSpinner) {
|
|
1685
|
+
inspectSpinner.stop("Inspection passed");
|
|
1686
|
+
} else {
|
|
1687
|
+
log("Inspection passed.");
|
|
1688
|
+
}
|
|
1498
1689
|
|
|
1499
1690
|
const preview = inspection.result || {};
|
|
1500
1691
|
if (!preview.valid || !preview.preview_token) {
|
|
1501
1692
|
throw new Error(summarizePreviewErrors(preview));
|
|
1502
1693
|
}
|
|
1503
|
-
|
|
1694
|
+
if (!ui) {
|
|
1695
|
+
log(`Preview token created: ${preview.preview_token}`);
|
|
1696
|
+
}
|
|
1504
1697
|
|
|
1505
|
-
const visibility = String(flags.access || flags.visibility || "public").trim().toLowerCase() || "public";
|
|
1506
1698
|
const body = {
|
|
1507
1699
|
preview_token: preview.preview_token,
|
|
1508
1700
|
visibility,
|
|
@@ -1517,7 +1709,13 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1517
1709
|
body.publish_now = true;
|
|
1518
1710
|
}
|
|
1519
1711
|
|
|
1520
|
-
|
|
1712
|
+
const submitSpinner = ui ? ui.spinner() : null;
|
|
1713
|
+
flowState.submit = "active";
|
|
1714
|
+
if (submitSpinner) {
|
|
1715
|
+
submitSpinner.start(`Submitting review request (${visibility})`);
|
|
1716
|
+
} else {
|
|
1717
|
+
log(`Submitting review request with access=${visibility}...`);
|
|
1718
|
+
}
|
|
1521
1719
|
const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
|
|
1522
1720
|
method: "POST",
|
|
1523
1721
|
headers: withBearerToken({
|
|
@@ -1525,9 +1723,14 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1525
1723
|
}, token),
|
|
1526
1724
|
body: JSON.stringify(body),
|
|
1527
1725
|
});
|
|
1528
|
-
|
|
1726
|
+
flowState.submit = "done";
|
|
1727
|
+
if (submitSpinner) {
|
|
1728
|
+
submitSpinner.stop(`Review submission created (${result.submission_id || "pending"})`);
|
|
1729
|
+
} else {
|
|
1730
|
+
log(`Review submission created: ${result.submission_id || "(no submission id returned)"}`);
|
|
1731
|
+
}
|
|
1529
1732
|
|
|
1530
|
-
|
|
1733
|
+
const summaryPayload = {
|
|
1531
1734
|
status: result.status,
|
|
1532
1735
|
submission_id: result.submission_id || null,
|
|
1533
1736
|
created_at: result.created_at || null,
|
|
@@ -1535,7 +1738,23 @@ async function cmdPublish(flags, deps = {}) {
|
|
|
1535
1738
|
publication: result.publication || null,
|
|
1536
1739
|
preview_token: preview.preview_token,
|
|
1537
1740
|
archive_name: archiveName,
|
|
1538
|
-
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
if (ui) {
|
|
1744
|
+
ui.note(
|
|
1745
|
+
[
|
|
1746
|
+
`${colorize("Status", "muted")}: ${result.status || "pending"}`,
|
|
1747
|
+
`${colorize("Submission", "muted")}: ${result.submission_id || "-"}`,
|
|
1748
|
+
`${colorize("Preview", "muted")}: ${preview.preview_token}`,
|
|
1749
|
+
`${colorize("Access", "muted")}: ${summaryPayload.visibility || visibility}`,
|
|
1750
|
+
].join("\n"),
|
|
1751
|
+
"Published",
|
|
1752
|
+
);
|
|
1753
|
+
renderPublishFlow();
|
|
1754
|
+
ui.outro(`Submitted ${ui.pc.cyan(archiveName)} for review.`);
|
|
1755
|
+
} else {
|
|
1756
|
+
log(JSON.stringify(summaryPayload, null, 2));
|
|
1757
|
+
}
|
|
1539
1758
|
|
|
1540
1759
|
return result;
|
|
1541
1760
|
}
|