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.
Files changed (2) hide show
  1. package/lib/cli.js +241 -22
  2. 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
- log(`Checking login for ${registry}...`);
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 archiveName = `${path.basename(skillDir) || "skill"}.zip`;
1447
- log(`Packing skill directory: ${skillDir}`);
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
- log(`Created archive ${archiveName} (${formatBytes(archiveBuffer.length)})`);
1450
-
1451
- const pollIntervalMs = Math.max(500, Number(flags["poll-interval"] || deps.pollIntervalMs || DEFAULT_PUBLISH_POLL_MS));
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
- log("Uploading archive for package inspection...");
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
- log(`Inspection job created: ${jobId}`);
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
- inspection = await requestJsonImpl(
1476
- `${registry}/api/skills/inspect-package-jobs/${encodeURIComponent(jobId)}`,
1477
- {
1478
- headers: withBearerToken({}, token),
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
- log(`Inspection in progress: ${progressLabel}`);
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
- log("Inspection passed.");
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
- log(`Preview token created: ${preview.preview_token}`);
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
- log(`Submitting review request with access=${visibility}...`);
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
- log(`Review submission created: ${result.submission_id || "(no submission id returned)"}`);
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
- log(JSON.stringify({
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
- }, null, 2));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ht-skills",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
5
  "type": "commonjs",
6
6
  "bin": {