@helix3/helix-cli 0.1.18-helix3.313 → 0.1.18-helix3.314

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 CHANGED
@@ -1692,9 +1692,21 @@ item
1692
1692
  // backend, which runs the vision critic, records the verdict against that exact version and serves a contact sheet.
1693
1693
  item
1694
1694
  .command('visual-qa')
1695
- .description('Render an item headless in the real HELIX runtime, measure it, and (with --submit) record the visual-QA verdict that Marketplace/Vault listing requires')
1696
- .argument('<item-id>', 'universal item UUID (its CURRENT package version is rendered)')
1695
+ .description('Render an item headless in the real HELIX runtime, measure it, and (with --submit) record the visual-QA verdict that Marketplace/Vault listing requires. --hosted asks the platform to render it on a GPU runner instead')
1696
+ .argument('[item-id]', 'universal item UUID (its CURRENT package version is rendered); omit with --hosted-worker')
1697
1697
  .option('--submit', 'submit frames + measurements to the backend, which judges and records the verdict (needs `helix login`)')
1698
+ .option('--hosted', 'queue a hosted render on the platform GPU runner and wait for the verdict (needs `helix login`; exit 0 pass, 1 fail, 2 error)')
1699
+ .option('--retry', 'with --hosted: re-queue after a failed/errored hosted run of this version')
1700
+ .option('--no-wait', 'with --hosted: queue the render and return without waiting')
1701
+ .option('--timeout <minutes>', 'with --hosted: how long to wait for the verdict', '30')
1702
+ .option('--status', 'read-only: show the hosted visual-QA state and recorded result for the item (no login needed)')
1703
+ .option('--hosted-worker', 'runner mode: claim hosted render jobs, render, submit (HELIX_VQA_BOT_EMAIL / HELIX_VQA_BOT_PASSWORD)')
1704
+ .option('--max-jobs <n>', 'with --hosted-worker: stop after n jobs')
1705
+ .option('--max-minutes <n>', 'with --hosted-worker: stop claiming new jobs after n minutes')
1706
+ .option('--idle-exit-minutes <n>', 'with --hosted-worker: keep polling an empty queue this long before exiting', '0')
1707
+ .option('--poll-seconds <n>', 'with --hosted-worker: queue polling interval', '10')
1708
+ .option('--render-timeout <seconds>', 'with --hosted-worker: hard limit per render', '900')
1709
+ .option('--worker <name>', 'with --hosted-worker: runner identity recorded on the job')
1698
1710
  .option('--local-critic', 'also run the vision critic locally (needs ANTHROPIC_API_KEY or OPENROUTER_API_KEY)')
1699
1711
  .option('--out <dir>', 'bundle directory (frames, bundle.json, contact-sheet.jpg)')
1700
1712
  .option('--storage-state <file>', 'Playwright storageState of a signed-in QA account (Try Now and the customiser need one); default $HELIX_VQA_STORAGE_STATE')
@@ -1705,12 +1717,104 @@ item
1705
1717
  .option('--json', 'print the outcome as JSON')
1706
1718
  .action(async (id, opts) => {
1707
1719
  try {
1708
- const { runVisualQa } = await Promise.resolve().then(() => __importStar(require('./visualQa/command')));
1709
- const creds = opts.submit ? requireAuth() : (0, config_1.loadCredentials)();
1720
+ const creds = opts.submit || opts.hosted ? requireAuth() : (0, config_1.loadCredentials)();
1710
1721
  const apiBase = opts.apiUrl ?? creds?.apiUrl ?? config_1.DEFAULT_API_URL;
1722
+ const webBase = opts.webUrl ?? process.env.HELIX_WEB_URL ?? (0, config_1.webUrlForApi)(apiBase) ?? config_1.DEFAULT_WEB_URL;
1723
+ if (opts.hostedWorker) {
1724
+ const email = process.env.HELIX_VQA_BOT_EMAIL;
1725
+ const password = process.env.HELIX_VQA_BOT_PASSWORD;
1726
+ if (!email || !password)
1727
+ fail('✖ --hosted-worker needs HELIX_VQA_BOT_EMAIL and HELIX_VQA_BOT_PASSWORD in the environment');
1728
+ const { runHostedWorker } = await Promise.resolve().then(() => __importStar(require('./visualQa/hosted')));
1729
+ const workerApi = opts.apiUrl ?? process.env.HELIX_API_URL ?? config_1.DEFAULT_API_URL; // never a stored login: the runner is its own account
1730
+ const summary = await runHostedWorker({
1731
+ apiBase: workerApi,
1732
+ webBase: opts.webUrl ?? process.env.HELIX_WEB_URL ?? (0, config_1.webUrlForApi)(workerApi) ?? config_1.DEFAULT_WEB_URL,
1733
+ email,
1734
+ password,
1735
+ worker: opts.worker ?? process.env.HELIX_VQA_WORKER,
1736
+ maxJobs: opts.maxJobs ? Number(opts.maxJobs) : undefined,
1737
+ maxMinutes: opts.maxMinutes ? Number(opts.maxMinutes) : undefined,
1738
+ idleExitMinutes: Number(opts.idleExitMinutes ?? 0),
1739
+ pollSeconds: Number(opts.pollSeconds ?? 10),
1740
+ renderTimeoutSeconds: Number(opts.renderTimeout ?? 900),
1741
+ out: opts.out,
1742
+ chrome: opts.chrome,
1743
+ cliVersion: (0, lib_1.cliVersion)(),
1744
+ });
1745
+ if (opts.json)
1746
+ console.log(JSON.stringify(summary, null, 2));
1747
+ else
1748
+ console.log(`hosted worker done (${summary.exit}): ${summary.jobs.length} job(s)${summary.jobs.map((j) => `\n ${j.jobId} ${j.outcome}${j.verdict ? ` ${j.verdict}` : ''}${j.error ? ` — ${j.error.slice(0, 120)}` : ''}`).join('')}`);
1749
+ return; // a failing item or a reported harness error is the queue's business; the runner itself succeeded
1750
+ }
1751
+ if (!id)
1752
+ fail('✖ visual-qa needs an <item-id> (omit it only with --hosted-worker)');
1753
+ if (opts.status || opts.hosted) {
1754
+ const hosted = await Promise.resolve().then(() => __importStar(require('./visualQa/hosted')));
1755
+ if (opts.status) {
1756
+ const st = await hosted.getItemVisualQaStatus(apiBase, id);
1757
+ const job = st.hostedJob;
1758
+ if (opts.json)
1759
+ console.log(JSON.stringify(st, null, 2));
1760
+ else {
1761
+ console.log(`hosted job: ${hosted.describeJob(job)}`);
1762
+ if (job?.contactSheetUrl)
1763
+ console.log(` contact sheet: ${job.contactSheetUrl}`);
1764
+ for (const r of job?.reasons ?? [])
1765
+ console.log(` - ${r}`);
1766
+ if (job?.error)
1767
+ console.log(` error: ${job.error}`);
1768
+ console.log(`current version: ${st.currentVersion ? (st.currentVersion.passed ? 'PASSED' : 'no passing run') : 'not pinned to a package version'}`);
1769
+ }
1770
+ process.exitCode = job && hosted.isTerminal(job.status) ? hosted.exitCodeForJob(job) : 0;
1771
+ return;
1772
+ }
1773
+ const token = creds?.token;
1774
+ const req = await hosted.requestHostedRun(apiBase, token, id, !!opts.retry);
1775
+ if (!opts.json) {
1776
+ if (req.alreadyPassed)
1777
+ console.log('✔ this version already has a passing visual-QA run');
1778
+ else if (req.exempt)
1779
+ console.log('✔ exempt: this item has nothing to render');
1780
+ else if (!req.job)
1781
+ console.log(`✖ nothing queued${req.skipped ? ` (${req.skipped})` : ''}${req.message ? `: ${req.message}` : ''}`);
1782
+ else
1783
+ console.log(`hosted render: ${hosted.describeJob(req.job)}`);
1784
+ }
1785
+ let job = req.job;
1786
+ let timedOut = false;
1787
+ if (job && !hosted.isTerminal(job.status) && opts.wait !== false) {
1788
+ const waited = await hosted.waitForHostedJob(apiBase, id, job.id, {
1789
+ timeoutMs: Number(opts.timeout ?? 30) * 60_000,
1790
+ onUpdate: (j) => { if (!opts.json)
1791
+ console.log(` ${hosted.describeJob(j)}`); },
1792
+ });
1793
+ job = waited.job;
1794
+ timedOut = waited.timedOut;
1795
+ }
1796
+ if (opts.json)
1797
+ console.log(JSON.stringify({ ...req, job, timedOut }, null, 2));
1798
+ else if (job && hosted.isTerminal(job.status)) {
1799
+ console.log(`${job.status === 'passed' ? '✔' : '✖'} visual QA ${job.status.toUpperCase()}`);
1800
+ for (const r of job.reasons)
1801
+ console.log(` - ${r}`);
1802
+ if (job.error)
1803
+ console.log(` error: ${job.error}`);
1804
+ if (job.contactSheetUrl)
1805
+ console.log(` contact sheet: ${job.contactSheetUrl}`);
1806
+ }
1807
+ else if (timedOut)
1808
+ console.log(`… still ${job?.status ?? 'queued'} after ${opts.timeout ?? 30} min; check later with: helix item visual-qa ${id} --status`);
1809
+ else if (job)
1810
+ console.log(` check later with: helix item visual-qa ${id} --status`);
1811
+ process.exitCode = req.alreadyPassed || req.exempt ? 0 : job && !hosted.isTerminal(job.status) && opts.wait === false ? 0 : hosted.exitCodeForJob(job, timedOut);
1812
+ return;
1813
+ }
1814
+ const { runVisualQa } = await Promise.resolve().then(() => __importStar(require('./visualQa/command')));
1711
1815
  const outcome = await runVisualQa(id, {
1712
1816
  apiBase,
1713
- webBase: opts.webUrl ?? process.env.HELIX_WEB_URL ?? (0, config_1.webUrlForApi)(apiBase) ?? config_1.DEFAULT_WEB_URL,
1817
+ webBase,
1714
1818
  token: creds?.token ?? null,
1715
1819
  out: opts.out,
1716
1820
  storageState: opts.storageState ?? process.env.HELIX_VQA_STORAGE_STATE,