vibeview 0.12.0 → 0.12.1

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.
@@ -835,7 +835,24 @@ function describeAllExclusions(configured) {
835
835
  return parts.join("; ");
836
836
  }
837
837
  var POLL_INTERVAL_MS = 2e3;
838
+ var QUEUED_POLL_INTERVAL_MS = 1e4;
839
+ function nextPollDelayMs(pendingStatuses, baseMs, queuedMs) {
840
+ if (pendingStatuses.length > 0 && pendingStatuses.every((s) => s === "queued")) {
841
+ return queuedMs;
842
+ }
843
+ return baseMs;
844
+ }
838
845
  var POLL_SAFETY_MARGIN_MS = 10 * 60 * 1e3;
846
+ function runningCapDeadline(startedAt, timeoutS) {
847
+ return startedAt + timeoutS * 1e3 + POLL_SAFETY_MARGIN_MS;
848
+ }
849
+ var DEFAULT_QUEUE_WAIT_MS = 30 * 60 * 1e3;
850
+ function queueWaitMs() {
851
+ const raw = process.env.VIBEVIEW_QUEUE_WAIT_MS;
852
+ if (!raw) return DEFAULT_QUEUE_WAIT_MS;
853
+ const parsed = Number(raw);
854
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_QUEUE_WAIT_MS;
855
+ }
839
856
  function defaultCloudBuildDeps(client) {
840
857
  return {
841
858
  listSourceFiles,
@@ -927,7 +944,7 @@ ${offenders}`);
927
944
  const message = err instanceof Error ? err.message : String(err);
928
945
  fail(opts, message);
929
946
  }
930
- uploadSpinner?.succeed(`uploaded \u2014 ${created.length} build(s) started`);
947
+ uploadSpinner?.succeed(`uploaded \u2014 ${created.length} build(s) submitted`);
931
948
  for (const b of created) {
932
949
  emitCloud(opts.json, { event: "build_created", id: b.id, platform: b.platform });
933
950
  }
@@ -985,75 +1002,179 @@ var MAX_CONSECUTIVE_POLL_FAILURES = 5;
985
1002
  async function waitForCloudBuilds(builds, opts, deps) {
986
1003
  const now = deps.now ?? Date.now;
987
1004
  const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
1005
+ const queuedPollIntervalMs = deps.queuedPollIntervalMs ?? (deps.pollIntervalMs !== void 0 ? pollIntervalMs : QUEUED_POLL_INTERVAL_MS);
988
1006
  const multi = builds.length > 1;
989
1007
  const state = new Map(builds.map((b) => [b.id, b]));
990
1008
  const offsets = new Map(builds.map((b) => [b.id, 0]));
991
1009
  const failures = new Map(builds.map((b) => [b.id, 0]));
1010
+ const finalized = /* @__PURE__ */ new Set();
992
1011
  const abandoned = /* @__PURE__ */ new Set();
1012
+ const pollAbandoned = /* @__PURE__ */ new Set();
1013
+ const transitions = new Map(
1014
+ builds.map((b) => [b.id, newTransitionState()])
1015
+ );
993
1016
  let lastError;
994
- const maxTimeoutS = Math.max(...builds.map((b) => b.timeout_s), 0);
995
- const deadline = now() + maxTimeoutS * 1e3 + POLL_SAFETY_MARGIN_MS;
1017
+ const runningStartedAt = /* @__PURE__ */ new Map();
1018
+ const queueStartedAt = new Map(builds.map((b) => [b.id, now()]));
1019
+ const queueCapMs = queueWaitMs();
1020
+ const waitStartedAt = now();
1021
+ const maxTimeoutMs = Math.max(0, ...builds.map((b) => b.timeout_s * 1e3));
1022
+ const initialBackstopDeadline = waitStartedAt + queueCapMs + maxTimeoutMs + POLL_SAFETY_MARGIN_MS;
1023
+ const backstopDeadlineFor = (id, timeoutS) => {
1024
+ const startedAt = runningStartedAt.get(id);
1025
+ if (startedAt === void 0) return initialBackstopDeadline;
1026
+ return Math.max(initialBackstopDeadline, runningCapDeadline(startedAt, timeoutS));
1027
+ };
996
1028
  const printLine = (id, platform, line) => {
997
1029
  const prefix = multi ? `[${platform}] ` : "";
998
1030
  if (!opts.json) console.error(prefix + line);
999
1031
  };
1000
- const stillWaiting = () => Array.from(state.values()).some(
1001
- (b) => !BUILD_TERMINAL_STATUSES.has(b.status) && !abandoned.has(b.id)
1002
- );
1003
- while (stillWaiting()) {
1004
- if (now() > deadline) {
1005
- throw new Error(
1006
- `cloud build poll timed out after exceeding the ${Math.round((maxTimeoutS * 1e3 + POLL_SAFETY_MARGIN_MS) / 1e3)}s safety limit \u2014 the backend never reported a terminal status`
1007
- );
1032
+ const stillWaiting = () => Array.from(state.values()).some((b) => !finalized.has(b.id) && !abandoned.has(b.id));
1033
+ const buildDeadline = (build) => {
1034
+ const startedAt = runningStartedAt.get(build.id);
1035
+ if (startedAt === void 0) return null;
1036
+ return runningCapDeadline(startedAt, build.timeout_s);
1037
+ };
1038
+ const giveUpLocally = (id, message) => {
1039
+ abandoned.add(id);
1040
+ emitCloud(opts.json, { event: "error", message });
1041
+ if (!opts.json) console.error(chalk.red(message));
1042
+ };
1043
+ const pollAndProcess = async (build) => {
1044
+ const offset = offsets.get(build.id) ?? 0;
1045
+ const logs = await deps.getCloudBuildLogs(build.id, offset);
1046
+ const updated = { ...build, status: logs.status };
1047
+ state.set(build.id, updated);
1048
+ if (logs.status === "running" && !runningStartedAt.has(build.id)) {
1049
+ runningStartedAt.set(build.id, now());
1050
+ }
1051
+ offsets.set(build.id, logs.offset);
1052
+ const transitionState = transitions.get(build.id);
1053
+ printTransition(build.id, build.platform, logs.status, transitionState, opts.json, printLine);
1054
+ if (logs.content) {
1055
+ emitCloud(opts.json, { event: "build_log", id: build.id, content: logs.content });
1056
+ for (const line of logs.content.split("\n")) {
1057
+ if (line.length === 0) continue;
1058
+ printLine(build.id, build.platform, line);
1059
+ }
1060
+ }
1061
+ if (BUILD_TERMINAL_STATUSES.has(logs.status)) {
1062
+ const final = await fetchFinalSummary(build.id, updated, deps);
1063
+ state.set(build.id, final);
1064
+ if (!opts.json) {
1065
+ printOutcomeLine(final, multi);
1066
+ }
1067
+ emitCloud(opts.json, {
1068
+ event: "build_finished",
1069
+ id: final.id,
1070
+ status: final.status,
1071
+ cost_micro: final.cost_micro,
1072
+ error_summary: final.error_summary
1073
+ });
1074
+ finalized.add(build.id);
1008
1075
  }
1076
+ failures.set(build.id, 0);
1077
+ return logs.status;
1078
+ };
1079
+ const finalPollBeforeGivingUp = async (build) => {
1080
+ try {
1081
+ const status = await pollAndProcess(build);
1082
+ return BUILD_TERMINAL_STATUSES.has(status) ? "terminal" : "nonterminal";
1083
+ } catch {
1084
+ return "poll_failed";
1085
+ }
1086
+ };
1087
+ while (stillWaiting()) {
1009
1088
  for (const build of state.values()) {
1010
- if (BUILD_TERMINAL_STATUSES.has(build.status) || abandoned.has(build.id)) continue;
1011
- try {
1012
- const offset = offsets.get(build.id) ?? 0;
1013
- const logs = await deps.getCloudBuildLogs(build.id, offset);
1014
- if (logs.content) {
1015
- emitCloud(opts.json, { event: "build_log", id: build.id, content: logs.content });
1016
- for (const line of logs.content.split("\n")) {
1017
- if (line.length === 0) continue;
1018
- printLine(build.id, build.platform, line);
1089
+ if (abandoned.has(build.id)) continue;
1090
+ if (finalized.has(build.id)) continue;
1091
+ if (BUILD_TERMINAL_STATUSES.has(build.status)) {
1092
+ try {
1093
+ await pollAndProcess(build);
1094
+ } catch (err) {
1095
+ lastError = err;
1096
+ const count = (failures.get(build.id) ?? 0) + 1;
1097
+ failures.set(build.id, count);
1098
+ if (count >= MAX_CONSECUTIVE_POLL_FAILURES) {
1099
+ pollAbandoned.add(build.id);
1100
+ const message = err instanceof Error ? err.message : String(err);
1101
+ giveUpLocally(
1102
+ build.id,
1103
+ `giving up finishing ${build.id} after ${count} consecutive failures: ${message} \u2014 the build likely finished server-side; check its outcome with \`vibeview builds logs ${build.id}\``
1104
+ );
1019
1105
  }
1020
1106
  }
1021
- offsets.set(build.id, logs.offset);
1022
- const updated = { ...build, status: logs.status };
1023
- state.set(build.id, updated);
1024
- failures.set(build.id, 0);
1025
- if (BUILD_TERMINAL_STATUSES.has(logs.status)) {
1026
- const final = await fetchFinalSummary(build.id, updated, deps);
1027
- state.set(build.id, final);
1028
- emitCloud(opts.json, {
1029
- event: "build_finished",
1030
- id: final.id,
1031
- status: final.status,
1032
- cost_micro: final.cost_micro,
1033
- error_summary: final.error_summary
1034
- });
1035
- if (!opts.json) {
1036
- printOutcomeLine(final, multi);
1037
- }
1107
+ continue;
1108
+ }
1109
+ if (build.status === "queued") {
1110
+ const queuedSince = queueStartedAt.get(build.id) ?? now();
1111
+ if (now() - queuedSince > queueCapMs) {
1112
+ const rescue = await finalPollBeforeGivingUp(build);
1113
+ if (state.get(build.id).status !== "queued") continue;
1114
+ giveUpLocally(
1115
+ build.id,
1116
+ rescue === "poll_failed" ? (
1117
+ // PR-445 review F6: the rescue poll failed, so "queued" is the
1118
+ // last status observed BEFORE the gap — not verified server
1119
+ // truth. Say so instead of asserting the build stayed queued.
1120
+ `${build.id} still queued after ${Math.round(queueCapMs / 6e4)}m \u2014 builders busy; giving up waiting on it. Could not re-check its final status (the last poll failed) \u2014 it may have started or finished; check on it with \`vibeview builds\``
1121
+ ) : `${build.id} still queued after ${Math.round(queueCapMs / 6e4)}m \u2014 builders busy; giving up waiting on it. The build stays queued server-side \u2014 check on it later with \`vibeview builds\``
1122
+ );
1123
+ continue;
1124
+ }
1125
+ } else {
1126
+ const deadline = buildDeadline(build);
1127
+ if (deadline !== null && now() > deadline) {
1128
+ if (await finalPollBeforeGivingUp(build) === "terminal") continue;
1129
+ giveUpLocally(
1130
+ build.id,
1131
+ `${build.id} exceeded its ${Math.round((build.timeout_s * 1e3 + POLL_SAFETY_MARGIN_MS) / 1e3)}s time limit client-side \u2014 giving up waiting on it. The build may still be running server-side \u2014 check its outcome with \`vibeview builds logs ${build.id}\``
1132
+ );
1133
+ continue;
1038
1134
  }
1135
+ }
1136
+ try {
1137
+ await pollAndProcess(build);
1039
1138
  } catch (err) {
1040
1139
  lastError = err;
1041
1140
  const count = (failures.get(build.id) ?? 0) + 1;
1042
1141
  failures.set(build.id, count);
1043
1142
  if (count >= MAX_CONSECUTIVE_POLL_FAILURES) {
1044
- abandoned.add(build.id);
1143
+ pollAbandoned.add(build.id);
1045
1144
  const message = err instanceof Error ? err.message : String(err);
1046
- const giveUp = `giving up polling ${build.id} after ${count} consecutive failures: ${message} \u2014 the build may still be running server-side; resume watching it with \`vibeview builds logs ${build.id} --follow\``;
1047
- emitCloud(opts.json, { event: "error", message: giveUp });
1048
- if (!opts.json) console.error(chalk.red(giveUp));
1145
+ giveUpLocally(
1146
+ build.id,
1147
+ `giving up polling ${build.id} after ${count} consecutive failures: ${message} \u2014 the build may still be running server-side; resume watching it with \`vibeview builds logs ${build.id} --follow\``
1148
+ );
1049
1149
  }
1050
1150
  }
1051
1151
  }
1052
1152
  if (stillWaiting()) {
1053
- await new Promise((r) => setTimeout(r, pollIntervalMs));
1153
+ for (const build of state.values()) {
1154
+ if (BUILD_TERMINAL_STATUSES.has(build.status) || abandoned.has(build.id)) continue;
1155
+ const deadline = backstopDeadlineFor(build.id, build.timeout_s);
1156
+ if (now() <= deadline) continue;
1157
+ if (await finalPollBeforeGivingUp(build) === "terminal") continue;
1158
+ const postRescueDeadline = backstopDeadlineFor(build.id, build.timeout_s);
1159
+ if (now() <= postRescueDeadline) continue;
1160
+ const fresh = state.get(build.id);
1161
+ const capMinutes = Math.round((postRescueDeadline - waitStartedAt) / 6e4);
1162
+ giveUpLocally(
1163
+ build.id,
1164
+ `giving up waiting on ${build.id}: still \`${fresh.status}\` with no terminal status after the overall ${capMinutes}m safety limit \u2014 the backend never reported the build finished; check on it later with \`vibeview builds\``
1165
+ );
1166
+ }
1167
+ }
1168
+ if (stillWaiting()) {
1169
+ const pendingStatuses = Array.from(state.values()).filter(
1170
+ (b) => !abandoned.has(b.id) && !finalized.has(b.id) && !BUILD_TERMINAL_STATUSES.has(b.status)
1171
+ ).map((b) => b.status);
1172
+ await new Promise(
1173
+ (r) => setTimeout(r, nextPollDelayMs(pendingStatuses, pollIntervalMs, queuedPollIntervalMs))
1174
+ );
1054
1175
  }
1055
1176
  }
1056
- if (abandoned.size > 0 && !Array.from(state.values()).some((b) => BUILD_TERMINAL_STATUSES.has(b.status))) {
1177
+ if (pollAbandoned.size > 0 && pollAbandoned.size === abandoned.size && !Array.from(state.values()).some((b) => BUILD_TERMINAL_STATUSES.has(b.status))) {
1057
1178
  const message = lastError instanceof Error ? lastError.message : String(lastError);
1058
1179
  throw new Error(
1059
1180
  `cloud build poll failed ${MAX_CONSECUTIVE_POLL_FAILURES} times in a row, giving up: ${message}
@@ -1069,6 +1190,26 @@ async function fetchFinalSummary(id, fallback, deps) {
1069
1190
  return fallback;
1070
1191
  }
1071
1192
  }
1193
+ function newTransitionState() {
1194
+ return { printedQueued: false, printedRunning: false };
1195
+ }
1196
+ function printTransition(id, platform, status, state, json, printLine) {
1197
+ if (status === "queued" && !state.printedQueued) {
1198
+ state.printedQueued = true;
1199
+ if (json) {
1200
+ emitCloud(json, { event: "status", id, status: "queued" });
1201
+ } else {
1202
+ printLine(id, platform, chalk.dim("\u23F3 queued \u2014 waiting for a build slot"));
1203
+ }
1204
+ } else if (status === "running" && !state.printedRunning) {
1205
+ state.printedRunning = true;
1206
+ if (json) {
1207
+ emitCloud(json, { event: "status", id, status: "running" });
1208
+ } else {
1209
+ printLine(id, platform, `${chalk.cyan("\u25B6")} building`);
1210
+ }
1211
+ }
1212
+ }
1072
1213
  function printOutcomeLine(build, multi) {
1073
1214
  const prefix = multi ? `[${build.platform}] ` : "";
1074
1215
  const ok = build.status === "succeeded";
@@ -2958,6 +3099,8 @@ export {
2958
3099
  buildCommand,
2959
3100
  BUILD_TERMINAL_STATUSES,
2960
3101
  fetchFinalSummary,
3102
+ newTransitionState,
3103
+ printTransition,
2961
3104
  printOutcomeLine,
2962
3105
  startDevSession,
2963
3106
  PLATFORM_DEVICE,
package/dist/cli.js CHANGED
@@ -7,11 +7,13 @@ import {
7
7
  dev,
8
8
  fetchFinalSummary,
9
9
  killOwnedMetro,
10
+ newTransitionState,
10
11
  printOutcomeLine,
12
+ printTransition,
11
13
  readDevState,
12
14
  readMetroState,
13
15
  runAgentVerb
14
- } from "./chunk-NJU4AKHN.js";
16
+ } from "./chunk-NFFLJNUI.js";
15
17
  import {
16
18
  ApiClient,
17
19
  EXIT_INFRA_ERROR,
@@ -1247,9 +1249,14 @@ async function followLogs(id, opts, deps) {
1247
1249
  let offset = 0;
1248
1250
  let status = "queued";
1249
1251
  let consecutiveFailures = 0;
1252
+ const transitionState = newTransitionState();
1253
+ const printLine = (_id, _platform, line) => {
1254
+ if (!opts.json) console.error(line);
1255
+ };
1250
1256
  while (!BUILD_TERMINAL_STATUSES.has(status)) {
1251
1257
  try {
1252
1258
  const logs = await deps.getCloudBuildLogs(id, offset);
1259
+ printTransition(id, "", logs.status, transitionState, opts.json, printLine);
1253
1260
  if (logs.content) {
1254
1261
  if (opts.json) {
1255
1262
  console.log(JSON.stringify({ event: "build_log", id, content: logs.content }));
@@ -1710,7 +1717,7 @@ program.command("dev-stop").description("Stop a `vibeview dev --detach` session
1710
1717
  program.command("mcp").description(
1711
1718
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
1712
1719
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
1713
- const { runMcpServer } = await import("./server-LZRNKGVA.js");
1720
+ const { runMcpServer } = await import("./server-A44Z2HUK.js");
1714
1721
  await runMcpServer(opts);
1715
1722
  });
1716
1723
  program.command("agent-setup").description(
@@ -11,7 +11,7 @@ import {
11
11
  readDevState,
12
12
  renderEnvelope,
13
13
  startDevSession
14
- } from "./chunk-NJU4AKHN.js";
14
+ } from "./chunk-NFFLJNUI.js";
15
15
  import {
16
16
  ApiClient,
17
17
  resolveAuth
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "type": "module",
6
6
  "bin": {