vibeview 0.20.4 → 0.22.0

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.
@@ -6,7 +6,7 @@ import {
6
6
  loadConfig,
7
7
  resolveAuth,
8
8
  uploadAppBuild
9
- } from "./chunk-KON2FOEC.js";
9
+ } from "./chunk-UGCGJ6IN.js";
10
10
 
11
11
  // src/lib/dev-state.ts
12
12
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -528,10 +528,6 @@ function noAppsMessage(platform, hasBuildConfig) {
528
528
  }
529
529
  return `No ${platform} apps found. Run \`vibeview build --platform ${platform}\` to build and upload one (it will ask for your build command), or upload a build with \`vibeview upload-app\`.`;
530
530
  }
531
- function firstRunAppStrategy(apps, buildConfigured) {
532
- if (!buildConfigured) return "picker";
533
- return apps.length === 0 ? "build_now" : "ask";
534
- }
535
531
  function fillLaterHint(skipped, platform) {
536
532
  if (!skipped.build && !skipped.metro) return "";
537
533
  const lines = ["You can enable one-command builds later \u2014 add to vibeview.json:"];
@@ -1586,6 +1582,38 @@ function printOutcomeLine(build, multi) {
1586
1582
  console.error(line);
1587
1583
  }
1588
1584
 
1585
+ // src/lib/roku-push.ts
1586
+ function createRokuPush(opts) {
1587
+ const runBuild = opts.runBuild ?? runBuildForPlatform;
1588
+ return async () => {
1589
+ const started = Date.now();
1590
+ let result;
1591
+ try {
1592
+ result = await runBuild(
1593
+ opts.cwd,
1594
+ "roku",
1595
+ opts.build,
1596
+ { token: opts.token, baseUrl: opts.baseUrl },
1597
+ opts.log
1598
+ );
1599
+ } catch (err) {
1600
+ const message = err.message;
1601
+ if (/artifact not found/i.test(message)) {
1602
+ throw new Error(
1603
+ `${message}
1604
+ your build command must produce a zip \u2014 use the package/zip step, not deploy`
1605
+ );
1606
+ }
1607
+ throw err;
1608
+ }
1609
+ if (!result.build_id) {
1610
+ throw new Error(`upload returned no build id for ${result.name ?? result.public_id}`);
1611
+ }
1612
+ await opts.api.installBuild(opts.sessionId, result.build_id, true);
1613
+ opts.log(`reloaded in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
1614
+ };
1615
+ }
1616
+
1589
1617
  // src/commands/dev.ts
1590
1618
  import { spawn as spawn4 } from "child_process";
1591
1619
  import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
@@ -2015,38 +2043,6 @@ function watchProjectFiles(cwd, artifact, userIgnore = []) {
2015
2043
  };
2016
2044
  }
2017
2045
 
2018
- // src/lib/roku-push.ts
2019
- function createRokuPush(opts) {
2020
- const runBuild = opts.runBuild ?? runBuildForPlatform;
2021
- return async () => {
2022
- const started = Date.now();
2023
- let result;
2024
- try {
2025
- result = await runBuild(
2026
- opts.cwd,
2027
- "roku",
2028
- opts.build,
2029
- { token: opts.token, baseUrl: opts.baseUrl },
2030
- opts.log
2031
- );
2032
- } catch (err) {
2033
- const message = err.message;
2034
- if (/artifact not found/i.test(message)) {
2035
- throw new Error(
2036
- `${message}
2037
- your build command must produce a zip \u2014 use the package/zip step, not deploy`
2038
- );
2039
- }
2040
- throw err;
2041
- }
2042
- if (!result.build_id) {
2043
- throw new Error(`upload returned no build id for ${result.name ?? result.public_id}`);
2044
- }
2045
- await opts.api.installBuild(opts.sessionId, result.build_id, true);
2046
- opts.log(`reloaded in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
2047
- };
2048
- }
2049
-
2050
2046
  // src/commands/dev.ts
2051
2047
  function parseWatchSeconds(flag) {
2052
2048
  if (flag === void 0 || flag === false) return void 0;
@@ -2068,16 +2064,33 @@ async function startDevSession(cfg, onEvent = () => {
2068
2064
  const sessionId = created.session_id;
2069
2065
  let stopping = false;
2070
2066
  let tunnel = null;
2067
+ let queueEntryId = null;
2071
2068
  const stopSession = async (code) => {
2072
2069
  if (stopping) return;
2073
2070
  stopping = true;
2074
2071
  tunnel?.stop();
2072
+ if (queueEntryId !== null) {
2073
+ await api.cancelQueueEntry(queueEntryId).catch(() => {
2074
+ });
2075
+ queueEntryId = null;
2076
+ }
2075
2077
  await api.stopSession(sessionId).catch(() => {
2076
2078
  });
2077
2079
  onEvent({ event: "session_ended", session_id: sessionId, code });
2078
2080
  };
2079
2081
  try {
2080
- await api.startSession(sessionId);
2082
+ const started = await api.startSession(sessionId);
2083
+ if (started.status === "queued") {
2084
+ queueEntryId = started.queue_entry_id ?? null;
2085
+ onEvent({
2086
+ event: "queued",
2087
+ session_id: sessionId,
2088
+ position: started.position ?? null,
2089
+ queue_entry_id: queueEntryId
2090
+ });
2091
+ await pollWhileQueued(api, sessionId, QUEUE_WAIT_MS);
2092
+ queueEntryId = null;
2093
+ }
2081
2094
  const pageUrl = `${cfg.webUrl || cfg.baseUrl}/sandbox/${sessionId}`;
2082
2095
  const reportedWarnings = /* @__PURE__ */ new Set();
2083
2096
  if (usesTunnel) {
@@ -2117,7 +2130,13 @@ async function startDevSession(cfg, onEvent = () => {
2117
2130
  }
2118
2131
  if (cfg.open !== false) await openBrowser(pageUrl).catch(() => {
2119
2132
  });
2120
- const active = await pollUntilActive(api, sessionId, 18e4);
2133
+ const active = await pollUntilActive(
2134
+ api,
2135
+ sessionId,
2136
+ 18e4,
2137
+ 2e3,
2138
+ () => onEvent({ event: "installing", session_id: sessionId })
2139
+ );
2121
2140
  for (const w of active.warnings ?? []) {
2122
2141
  if (reportedWarnings.has(w)) continue;
2123
2142
  reportedWarnings.add(w);
@@ -2142,6 +2161,25 @@ var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App no
2142
2161
  function findBlockingWarning(info) {
2143
2162
  return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
2144
2163
  }
2164
+ var QUEUE_WAIT_MS = 30 * 6e4;
2165
+ async function pollWhileQueued(api, sessionId, timeoutMs, intervalMs = 3e3) {
2166
+ const deadline = Date.now() + timeoutMs;
2167
+ for (; ; ) {
2168
+ const info = await api.getSession(sessionId);
2169
+ const blocking = findBlockingWarning(info);
2170
+ if (blocking) throw new Error(blocking);
2171
+ if (info.status === "stopped" || info.status === "failed") {
2172
+ throw new Error(`session ${info.status} while waiting for a device`);
2173
+ }
2174
+ if (info.status !== "pending" && info.status !== "queued") return info;
2175
+ if (Date.now() > deadline) {
2176
+ throw new Error(
2177
+ `no device became free within ${Math.round(timeoutMs / 6e4)} minutes \u2014 try again later or pick another device`
2178
+ );
2179
+ }
2180
+ await new Promise((r) => setTimeout(r, intervalMs));
2181
+ }
2182
+ }
2145
2183
  async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3) {
2146
2184
  const deadline = Date.now() + timeoutMs;
2147
2185
  for (; ; ) {
@@ -2156,13 +2194,18 @@ async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3)
2156
2194
  await new Promise((r) => setTimeout(r, intervalMs));
2157
2195
  }
2158
2196
  }
2159
- async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3) {
2197
+ async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3, onInstallPending) {
2160
2198
  const deadline = Date.now() + timeoutMs;
2199
+ let pendingReported = false;
2161
2200
  for (; ; ) {
2162
- const info = await api.getSession(sessionId);
2201
+ const info = await api.touchSession(sessionId);
2163
2202
  const blocking = findBlockingWarning(info);
2164
2203
  if (blocking) throw new Error(blocking);
2165
- if (info.status === "active") return info;
2204
+ if (info.status === "active" && info.app_install_pending && !pendingReported) {
2205
+ pendingReported = true;
2206
+ onInstallPending?.();
2207
+ }
2208
+ if (info.status === "active" && !info.app_install_pending) return info;
2166
2209
  if (info.status === "stopped" || info.status === "failed") {
2167
2210
  throw new Error(`session ${info.status} before it became active`);
2168
2211
  }
@@ -2214,6 +2257,12 @@ function validateAppChoice(requested, fromFlag, apps) {
2214
2257
  if (apps.some((a) => a.public_id === requested)) return { appId: requested };
2215
2258
  return fromFlag ? { staleFlag: requested } : { staleCached: requested };
2216
2259
  }
2260
+ function buildAppStrategy(apps, buildFlag, json, isTTY) {
2261
+ if (buildFlag) return "build";
2262
+ if (apps.length === 0) return "build";
2263
+ if (json || !isTTY) return "pick";
2264
+ return "ask";
2265
+ }
2217
2266
  function requireNonInteractive(json) {
2218
2267
  if (!json) return;
2219
2268
  emit(json, {
@@ -2289,9 +2338,10 @@ Run \`vibeview dev\` without --app to pick from the list.`
2289
2338
  chalk3.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
2290
2339
  );
2291
2340
  }
2292
- if (isFirstRun && !appId) {
2293
- const strategy = firstRunAppStrategy(apps, firstRunBuild !== null);
2294
- let action = strategy === "build_now" ? "build" : "pick";
2341
+ const buildConfig = firstRunBuild ?? entry?.build ?? null;
2342
+ if (!appId && buildConfig) {
2343
+ const strategy = buildAppStrategy(apps, Boolean(opts.build), opts.json, process.stdin.isTTY);
2344
+ let action = strategy === "ask" ? "pick" : strategy;
2295
2345
  if (strategy === "ask") {
2296
2346
  action = await select3({
2297
2347
  message: "Run against which build?",
@@ -2307,7 +2357,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
2307
2357
  const result = await runBuildForPlatform(
2308
2358
  process.cwd(),
2309
2359
  platform,
2310
- firstRunBuild,
2360
+ buildConfig,
2311
2361
  opts,
2312
2362
  (l) => console.error(chalk3.dim(l))
2313
2363
  );
@@ -2316,6 +2366,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
2316
2366
  } catch (err) {
2317
2367
  const message = err.message;
2318
2368
  console.error(chalk3.red(`error: ${message}`));
2369
+ emit(opts.json, { event: "error", message });
2319
2370
  process.exit(1);
2320
2371
  }
2321
2372
  }
@@ -2537,9 +2588,20 @@ async function runDevInProcess(opts) {
2537
2588
  } else {
2538
2589
  console.error(chalk3.yellow(`\u26A0 ${evt.message}`));
2539
2590
  }
2591
+ } else if (evt.event === "queued") {
2592
+ const where = evt.position ? ` (position ${evt.position})` : "";
2593
+ console.error(
2594
+ chalk3.yellow(
2595
+ `\u23F3 every device is busy \u2014 waiting in the queue${where}, Ctrl-C to give up`
2596
+ )
2597
+ );
2540
2598
  } else if (evt.event === "tunnel_connected") {
2541
2599
  everConnected = true;
2542
2600
  console.error(chalk3.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
2601
+ } else if (evt.event === "installing") {
2602
+ console.error(
2603
+ chalk3.dim(" Putting the channel on the Roku \u2014 the link appears once it is up.")
2604
+ );
2543
2605
  } else if (evt.event === "session_ended" && !stopping && handle) {
2544
2606
  console.error(chalk3.red("tunnel closed by server \u2014 session ended"));
2545
2607
  stopping = true;
@@ -2649,7 +2711,7 @@ async function runDevInProcess(opts) {
2649
2711
  if (stopping) return;
2650
2712
  let info = null;
2651
2713
  try {
2652
- info = await api.getSession(sessionId);
2714
+ info = await api.touchSession(sessionId);
2653
2715
  } catch {
2654
2716
  }
2655
2717
  const verdict = sessionLivenessVerdict(info);
@@ -3445,6 +3507,7 @@ async function runAgentVerb(cmd, positionals, opts) {
3445
3507
  export {
3446
3508
  readDevState,
3447
3509
  clearDevState,
3510
+ DEV_PLATFORMS,
3448
3511
  PLATFORM_NEEDS_METRO,
3449
3512
  isDevPlatform,
3450
3513
  loadDevConfig,
@@ -3458,7 +3521,9 @@ export {
3458
3521
  newTransitionState,
3459
3522
  printTransition,
3460
3523
  printOutcomeLine,
3524
+ createRokuPush,
3461
3525
  startDevSession,
3526
+ ROKU_LIVENESS_INTERVAL_MS,
3462
3527
  PLATFORM_DEVICE,
3463
3528
  dev,
3464
3529
  AGENT_COMMANDS,
@@ -250,6 +250,13 @@ var ApiClient = class {
250
250
  async getSession(sessionId) {
251
251
  return this.requestRaw("GET", `/sessions/${sessionId}`);
252
252
  }
253
+ /** Report presence on a session and read it back in one call. A Roku dev
254
+ * loop has no tunnel and no browser, so this is what tells the backend
255
+ * someone is still using the session between device commands. Same
256
+ * response shape as `getSession`. */
257
+ async touchSession(sessionId) {
258
+ return this.requestRaw("POST", `/sessions/${sessionId}/presence`);
259
+ }
253
260
  async stopSession(sessionId) {
254
261
  await this.requestRaw("DELETE", `/sessions/${sessionId}`);
255
262
  }