ht-skills 0.2.7 → 0.2.9

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 +117 -57
  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
  | || |_ _| / __| |_(_) | |___ | \/ |__ _ _ _| |_____| |_ _ __| |__ _ __
@@ -1519,6 +1521,8 @@ async function cmdPublish(flags, deps = {}) {
1519
1521
  const colorize = ui ? createUiColorizer(ui.pc) : (text) => String(text);
1520
1522
  const outputWidth = getOutputWidth(deps.outputWidth);
1521
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));
1522
1526
  const flowState = {
1523
1527
  auth: "pending",
1524
1528
  pack: "pending",
@@ -1534,6 +1538,10 @@ async function cmdPublish(flags, deps = {}) {
1534
1538
  width: Math.max(44, Math.min(outputWidth - 4, 72)),
1535
1539
  }));
1536
1540
  };
1541
+ const setFlowStatus = (stepId, status) => {
1542
+ flowState[stepId] = status;
1543
+ renderPublishFlow();
1544
+ };
1537
1545
 
1538
1546
  if (ui) {
1539
1547
  // eslint-disable-next-line no-console
@@ -1553,59 +1561,70 @@ async function cmdPublish(flags, deps = {}) {
1553
1561
  printFallbackPublishIntro({ registry, skillDir, archiveName, visibility }, log);
1554
1562
  }
1555
1563
 
1556
- const authSpinner = ui ? ui.spinner() : null;
1557
- flowState.auth = "active";
1558
- renderPublishFlow();
1559
- if (authSpinner) {
1560
- authSpinner.start("Checking login");
1561
- } else {
1564
+ setFlowStatus("auth", "active");
1565
+ if (!ui) {
1562
1566
  log(`Checking login for ${registry}...`);
1563
1567
  }
1564
1568
  const { token } = await ensureValidAuthToken(registry, flags, deps);
1565
- flowState.auth = "done";
1566
- renderPublishFlow();
1567
- if (authSpinner) {
1568
- authSpinner.stop("Login confirmed");
1569
- }
1569
+ setFlowStatus("auth", "done");
1570
1570
 
1571
1571
  const packSpinner = ui ? ui.spinner() : null;
1572
- flowState.pack = "active";
1573
- renderPublishFlow();
1572
+ setFlowStatus("pack", "active");
1574
1573
  if (packSpinner) {
1575
1574
  packSpinner.start(`Packing ${path.basename(skillDir) || skillDir}`);
1576
1575
  } else {
1577
1576
  log(`Packing skill directory: ${skillDir}`);
1578
1577
  }
1579
- const archiveBuffer = await createZipFromDirectory(skillDir);
1580
- flowState.pack = "done";
1581
- renderPublishFlow();
1582
- if (packSpinner) {
1583
- packSpinner.stop(`Created ${archiveName} (${formatBytes(archiveBuffer.length)})`);
1584
- } else {
1585
- log(`Created archive ${archiveName} (${formatBytes(archiveBuffer.length)})`);
1578
+ let archiveBuffer;
1579
+ try {
1580
+ archiveBuffer = await createZipFromDirectory(skillDir);
1581
+ setFlowStatus("pack", "done");
1582
+ if (packSpinner) {
1583
+ packSpinner.stop(`Created ${archiveName} (${formatBytes(archiveBuffer.length)})`);
1584
+ } else {
1585
+ log(`Created archive ${archiveName} (${formatBytes(archiveBuffer.length)})`);
1586
+ }
1587
+ } catch (error) {
1588
+ setFlowStatus("pack", "error");
1589
+ if (packSpinner) {
1590
+ packSpinner.stop("Packing failed");
1591
+ }
1592
+ throw error;
1586
1593
  }
1587
1594
 
1588
1595
  const uploadSpinner = ui ? ui.spinner() : null;
1589
- flowState.inspect = "active";
1590
- renderPublishFlow();
1596
+ setFlowStatus("inspect", "active");
1591
1597
  if (uploadSpinner) {
1592
1598
  uploadSpinner.start("Uploading archive for package inspection");
1593
1599
  } else {
1594
1600
  log("Uploading archive for package inspection...");
1595
1601
  }
1596
- const job = await requestJsonImpl(
1597
- `${registry}/api/skills/inspect-package-jobs/upload?archive_name=${encodeURIComponent(archiveName)}`,
1598
- {
1599
- method: "POST",
1600
- headers: withBearerToken({
1601
- "content-type": "application/zip",
1602
- }, token),
1603
- body: archiveBuffer,
1604
- },
1605
- );
1602
+ let job;
1603
+ try {
1604
+ job = await requestJsonImpl(
1605
+ `${registry}/api/skills/inspect-package-jobs/upload?archive_name=${encodeURIComponent(archiveName)}`,
1606
+ {
1607
+ method: "POST",
1608
+ headers: withBearerToken({
1609
+ "content-type": "application/zip",
1610
+ }, token),
1611
+ body: archiveBuffer,
1612
+ },
1613
+ );
1614
+ } catch (error) {
1615
+ setFlowStatus("inspect", "error");
1616
+ if (uploadSpinner) {
1617
+ uploadSpinner.stop("Inspection upload failed");
1618
+ }
1619
+ throw error;
1620
+ }
1606
1621
 
1607
1622
  const jobId = String(job.job_id || "").trim();
1608
1623
  if (!jobId) {
1624
+ setFlowStatus("inspect", "error");
1625
+ if (uploadSpinner) {
1626
+ uploadSpinner.stop("Inspection upload failed");
1627
+ }
1609
1628
  throw new Error("registry did not return an inspection job id");
1610
1629
  }
1611
1630
  if (uploadSpinner) {
@@ -1616,18 +1635,53 @@ async function cmdPublish(flags, deps = {}) {
1616
1635
 
1617
1636
  let inspection = job;
1618
1637
  let lastProgressKey = "";
1638
+ let consecutivePollErrors = 0;
1639
+ const publishDeadline = Date.now() + publishTimeoutMs;
1619
1640
  const inspectSpinner = ui ? ui.spinner() : null;
1620
1641
  if (inspectSpinner) {
1621
1642
  inspectSpinner.start("Inspecting archive");
1622
1643
  }
1623
1644
  while (inspection.status !== "succeeded" && inspection.status !== "failed") {
1645
+ if (Date.now() > publishDeadline) {
1646
+ setFlowStatus("inspect", "error");
1647
+ if (inspectSpinner) {
1648
+ inspectSpinner.stop("Inspection timed out");
1649
+ }
1650
+ throw new Error(`Inspection timed out after ${Math.round(publishTimeoutMs / 1000)}s`);
1651
+ }
1652
+
1624
1653
  await sleep(pollIntervalMs);
1625
- inspection = await requestJsonImpl(
1626
- `${registry}/api/skills/inspect-package-jobs/${encodeURIComponent(jobId)}`,
1627
- {
1628
- headers: withBearerToken({}, token),
1629
- },
1630
- );
1654
+ try {
1655
+ inspection = await requestJsonImpl(
1656
+ `${registry}/api/skills/inspect-package-jobs/${encodeURIComponent(jobId)}`,
1657
+ {
1658
+ headers: withBearerToken({}, token),
1659
+ },
1660
+ );
1661
+ consecutivePollErrors = 0;
1662
+ } catch (error) {
1663
+ consecutivePollErrors += 1;
1664
+ if (consecutivePollErrors >= pollErrorLimit) {
1665
+ setFlowStatus("inspect", "error");
1666
+ if (inspectSpinner) {
1667
+ inspectSpinner.stop("Inspection polling failed");
1668
+ }
1669
+ throw new Error(`Inspection polling failed ${consecutivePollErrors} times: ${error.message}`);
1670
+ }
1671
+ if (!inspectSpinner) {
1672
+ log(`Inspection polling retry ${consecutivePollErrors}/${pollErrorLimit}: ${error.message}`);
1673
+ }
1674
+ continue;
1675
+ }
1676
+
1677
+ const runtimeError = String(inspection.error?.message || inspection.error || "").trim();
1678
+ if (runtimeError && inspection.status !== "succeeded") {
1679
+ setFlowStatus("inspect", "error");
1680
+ if (inspectSpinner) {
1681
+ inspectSpinner.stop("Inspection failed");
1682
+ }
1683
+ throw new Error(runtimeError);
1684
+ }
1631
1685
 
1632
1686
  const step = String(inspection.progress?.step || inspection.result?.step || "").trim();
1633
1687
  const percent = inspection.progress?.percent;
@@ -1646,15 +1700,13 @@ async function cmdPublish(flags, deps = {}) {
1646
1700
  }
1647
1701
 
1648
1702
  if (inspection.status !== "succeeded") {
1649
- flowState.inspect = "error";
1650
- renderPublishFlow();
1703
+ setFlowStatus("inspect", "error");
1651
1704
  if (inspectSpinner) {
1652
1705
  inspectSpinner.stop("Inspection failed");
1653
1706
  }
1654
1707
  throw new Error(inspection.error || "skill archive inspection failed");
1655
1708
  }
1656
- flowState.inspect = "done";
1657
- renderPublishFlow();
1709
+ setFlowStatus("inspect", "done");
1658
1710
  if (inspectSpinner) {
1659
1711
  inspectSpinner.stop("Inspection passed");
1660
1712
  } else {
@@ -1663,6 +1715,7 @@ async function cmdPublish(flags, deps = {}) {
1663
1715
 
1664
1716
  const preview = inspection.result || {};
1665
1717
  if (!preview.valid || !preview.preview_token) {
1718
+ setFlowStatus("inspect", "error");
1666
1719
  throw new Error(summarizePreviewErrors(preview));
1667
1720
  }
1668
1721
  if (!ui) {
@@ -1684,26 +1737,33 @@ async function cmdPublish(flags, deps = {}) {
1684
1737
  }
1685
1738
 
1686
1739
  const submitSpinner = ui ? ui.spinner() : null;
1687
- flowState.submit = "active";
1688
- renderPublishFlow();
1740
+ setFlowStatus("submit", "active");
1689
1741
  if (submitSpinner) {
1690
1742
  submitSpinner.start(`Submitting review request (${visibility})`);
1691
1743
  } else {
1692
1744
  log(`Submitting review request with access=${visibility}...`);
1693
1745
  }
1694
- const result = await requestJsonImpl(`${registry}/api/skills/submit`, {
1695
- method: "POST",
1696
- headers: withBearerToken({
1697
- "content-type": "application/json",
1698
- }, token),
1699
- body: JSON.stringify(body),
1700
- });
1701
- flowState.submit = "done";
1702
- renderPublishFlow();
1703
- if (submitSpinner) {
1704
- submitSpinner.stop(`Review submission created (${result.submission_id || "pending"})`);
1705
- } else {
1706
- log(`Review submission created: ${result.submission_id || "(no submission id returned)"}`);
1746
+ let result;
1747
+ try {
1748
+ result = await requestJsonImpl(`${registry}/api/skills/submit`, {
1749
+ method: "POST",
1750
+ headers: withBearerToken({
1751
+ "content-type": "application/json",
1752
+ }, token),
1753
+ body: JSON.stringify(body),
1754
+ });
1755
+ setFlowStatus("submit", "done");
1756
+ if (submitSpinner) {
1757
+ submitSpinner.stop(`Review submission created (${result.submission_id || "pending"})`);
1758
+ } else {
1759
+ log(`Review submission created: ${result.submission_id || "(no submission id returned)"}`);
1760
+ }
1761
+ } catch (error) {
1762
+ setFlowStatus("submit", "error");
1763
+ if (submitSpinner) {
1764
+ submitSpinner.stop("Submit failed");
1765
+ }
1766
+ throw error;
1707
1767
  }
1708
1768
 
1709
1769
  const summaryPayload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ht-skills",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
5
  "type": "commonjs",
6
6
  "bin": {