vibeview 0.27.1 → 0.28.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/commands/upload-app.ts
4
4
  import chalk from "chalk";
5
- import { existsSync } from "fs";
5
+ import { existsSync as existsSync2 } from "fs";
6
6
 
7
7
  // src/lib/config.ts
8
8
  import { readFileSync, writeFileSync, mkdirSync } from "fs";
@@ -83,9 +83,9 @@ function reportCommandError(json, err) {
83
83
  process.stderr.write(`error: ${message}
84
84
  `);
85
85
  }
86
- function printJsonError(err) {
86
+ function printJsonError(err, extra) {
87
87
  const message = err instanceof Error ? err.message : String(err);
88
- console.log(JSON.stringify({ event: "error", message }));
88
+ console.log(JSON.stringify({ event: "error", message, ...extra }));
89
89
  }
90
90
  var NotFoundError = class extends Error {
91
91
  constructor(message) {
@@ -93,6 +93,19 @@ var NotFoundError = class extends Error {
93
93
  this.name = "NotFoundError";
94
94
  }
95
95
  };
96
+ var ApiStatusError = class extends Error {
97
+ constructor(status, message) {
98
+ super(message);
99
+ this.status = status;
100
+ this.name = "ApiStatusError";
101
+ }
102
+ };
103
+ var NetworkError = class extends Error {
104
+ constructor(message) {
105
+ super(message);
106
+ this.name = "NetworkError";
107
+ }
108
+ };
96
109
  function extractDetail(text) {
97
110
  try {
98
111
  const parsed = JSON.parse(text);
@@ -103,6 +116,18 @@ function extractDetail(text) {
103
116
  }
104
117
  return void 0;
105
118
  }
119
+ function httpErrorMessage(status, text) {
120
+ const body = text.trim();
121
+ const looksLikeHtml = /^<(!doctype|html|head|body)\b/i.test(body) || /<\/html>\s*$/i.test(body);
122
+ const meaning = {
123
+ 502: "the server could not be reached (bad gateway) \u2014 try again in a moment",
124
+ 503: "the server is unavailable right now \u2014 try again in a moment",
125
+ 504: "the server took too long to answer (gateway timeout) \u2014 the action may still have run; check before retrying"
126
+ };
127
+ if (meaning[status]) return `API error ${status}: ${meaning[status]}`;
128
+ if (!body || looksLikeHtml) return `API error ${status}`;
129
+ return `API error ${status}: ${body.length > 300 ? `${body.slice(0, 300)}\u2026` : body}`;
130
+ }
106
131
  async function parseJsonBody(resp) {
107
132
  if (resp.status === 204 || resp.status === 205) return void 0;
108
133
  return withUtcTimestamps(await resp.json());
@@ -125,7 +150,7 @@ var ApiClient = class {
125
150
  } catch (e) {
126
151
  const cause = e.cause;
127
152
  const detail = cause?.code ? ` (${cause.code})` : "";
128
- throw new Error(
153
+ throw new NetworkError(
129
154
  `Cannot reach ${this.baseUrl}${detail}. Check the server URL (--base-url flag, VIBEVIEW_BASE_URL, or ~/.vibeview/config.json) and your network.`
130
155
  );
131
156
  }
@@ -145,7 +170,10 @@ var ApiClient = class {
145
170
  const text = await resp.text().catch(() => "");
146
171
  if (resp.status === 401) throw new AuthError();
147
172
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
148
- throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
173
+ throw new ApiStatusError(
174
+ resp.status,
175
+ extractDetail(text) ?? httpErrorMessage(resp.status, text)
176
+ );
149
177
  }
150
178
  return parseJsonBody(resp);
151
179
  }
@@ -163,9 +191,10 @@ var ApiClient = class {
163
191
  model: options?.model,
164
192
  provider: options?.provider,
165
193
  mode: options?.mode,
166
- // When pinning a specific device, omit device_category — the backend
167
- // rejects the combo.
168
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
194
+ // Omitted unless asked for: the backend picks tv for a TV suite and
195
+ // phone otherwise. Never sent with a pinned device (the backend
196
+ // rejects the combo).
197
+ device_category: options?.device ? void 0 : options?.deviceCategory,
169
198
  device_id: options?.device,
170
199
  build_id: options?.build,
171
200
  metadata: options?.metadata
@@ -178,7 +207,7 @@ var ApiClient = class {
178
207
  model: options?.model,
179
208
  provider: options?.provider,
180
209
  mode: options?.mode,
181
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
210
+ device_category: options?.device ? void 0 : options?.deviceCategory,
182
211
  device_id: options?.device,
183
212
  build_id: options?.build,
184
213
  metadata: options?.metadata
@@ -217,7 +246,10 @@ var ApiClient = class {
217
246
  const text = await resp.text().catch(() => "");
218
247
  if (resp.status === 401) throw new AuthError();
219
248
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
220
- throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
249
+ throw new ApiStatusError(
250
+ resp.status,
251
+ extractDetail(text) ?? httpErrorMessage(resp.status, text)
252
+ );
221
253
  }
222
254
  return parseJsonBody(resp);
223
255
  }
@@ -237,7 +269,10 @@ var ApiClient = class {
237
269
  const text = await resp.text().catch(() => "");
238
270
  if (resp.status === 401) throw new AuthError();
239
271
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
240
- throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
272
+ throw new ApiStatusError(
273
+ resp.status,
274
+ extractDetail(text) ?? httpErrorMessage(resp.status, text)
275
+ );
241
276
  }
242
277
  return parseJsonBody(resp);
243
278
  }
@@ -397,7 +432,10 @@ var ApiClient = class {
397
432
  if (resp.status === 401) throw new AuthError();
398
433
  if (resp.status === 404)
399
434
  throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
400
- throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
435
+ throw new ApiStatusError(
436
+ resp.status,
437
+ extractDetail(text) ?? httpErrorMessage(resp.status, text)
438
+ );
401
439
  }
402
440
  const rawFilename = parseContentDispositionFilename(resp.headers.get("content-disposition"));
403
441
  const filename = sanitizeDownloadFilename(rawFilename) ?? `${id}.bin`;
@@ -670,6 +708,63 @@ function sanitizeDownloadFilename(raw) {
670
708
  return base;
671
709
  }
672
710
 
711
+ // src/lib/artifact.ts
712
+ import { existsSync, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
713
+ import { spawnSync } from "child_process";
714
+ import { tmpdir } from "os";
715
+ import { basename as basename2, dirname, isAbsolute, join as join3 } from "path";
716
+ function resolveArtifact(cwd, pattern) {
717
+ const abs = isAbsolute(pattern) ? pattern : join3(cwd, pattern);
718
+ if (!basename2(abs).includes("*")) {
719
+ if (!existsSync(abs)) {
720
+ throw new Error(
721
+ `artifact not found: ${pattern} (looked at ${abs}) \u2014 did the build produce it?`
722
+ );
723
+ }
724
+ return abs;
725
+ }
726
+ const dir = dirname(abs);
727
+ const re = new RegExp("^" + basename2(abs).split("*").map(escapeRegExp).join(".*") + "$");
728
+ let entries;
729
+ try {
730
+ entries = readdirSync(dir);
731
+ } catch {
732
+ throw new Error(`artifact not found: ${pattern} (directory ${dir} does not exist)`);
733
+ }
734
+ const matches = entries.filter((e) => re.test(e)).map((e) => join3(dir, e)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
735
+ if (matches.length === 0) {
736
+ throw new Error(`artifact not found: no match for ${pattern} in ${dir}`);
737
+ }
738
+ return matches[0];
739
+ }
740
+ function escapeRegExp(s) {
741
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
742
+ }
743
+ function prepareUploadArtifact(path) {
744
+ if (!(statSync(path).isDirectory() && path.endsWith(".app"))) {
745
+ return { uploadPath: path, cleanup: () => {
746
+ } };
747
+ }
748
+ const tmp = mkdtempSync(join3(tmpdir(), "vibeview-zip-"));
749
+ const out = join3(tmp, `${basename2(path)}.zip`);
750
+ const res = spawnSync("zip", ["-qry", out, basename2(path)], { cwd: dirname(path) });
751
+ if (res.error) {
752
+ rmSync2(tmp, { recursive: true, force: true });
753
+ const errorCode = res.error.code;
754
+ const errorMsg = res.error.message;
755
+ throw new Error(
756
+ `failed to zip ${path}: spawn zip ${errorCode} \u2014 ${errorMsg} \u2014 is the \`zip\` command installed?`
757
+ );
758
+ }
759
+ if (res.status !== 0) {
760
+ rmSync2(tmp, { recursive: true, force: true });
761
+ throw new Error(
762
+ `failed to zip ${path}: ${res.stderr?.toString().trim() || `zip exited ${res.status}`}`
763
+ );
764
+ }
765
+ return { uploadPath: out, cleanup: () => rmSync2(tmp, { recursive: true, force: true }) };
766
+ }
767
+
673
768
  // src/lib/spinner.ts
674
769
  import ora from "ora";
675
770
  function createSpinner(text, opts = {}) {
@@ -706,13 +801,19 @@ function queueReasonCopy(reason) {
706
801
 
707
802
  // src/commands/upload-app.ts
708
803
  async function uploadAppBuild(filePath, options) {
709
- if (!existsSync(filePath)) {
804
+ if (!existsSync2(filePath)) {
710
805
  throw new Error(`File not found: ${filePath}`);
711
806
  }
712
807
  const { token, baseUrl } = resolveAuth(options);
713
808
  const client = new ApiClient(baseUrl, token);
714
809
  const note = options.note?.trim() || void 0;
715
- const result = await client.uploadApp(filePath, options.name, note);
810
+ const { uploadPath, cleanup } = prepareUploadArtifact(filePath);
811
+ let result;
812
+ try {
813
+ result = await client.uploadApp(uploadPath, options.name, note);
814
+ } finally {
815
+ cleanup();
816
+ }
716
817
  if (note && result.deduplicated && result.build_id != null) {
717
818
  await client.updateBuildNote(result.public_id, result.build_id, note);
718
819
  }
@@ -725,7 +826,7 @@ function describeBuild(result) {
725
826
  return `Build ${chalk.cyan(String(result.build_id))}${version ? ` ${version}` : ""}${dedup}`;
726
827
  }
727
828
  async function uploadApp(filePath, options) {
728
- if (!existsSync(filePath)) {
829
+ if (!existsSync2(filePath)) {
729
830
  console.error(chalk.red(`Error: File not found: ${filePath}`));
730
831
  process.exit(EXIT_INFRA_ERROR);
731
832
  }
@@ -792,6 +893,8 @@ export {
792
893
  reportCommandError,
793
894
  printJsonError,
794
895
  NotFoundError,
896
+ ApiStatusError,
897
+ NetworkError,
795
898
  ApiClient,
796
899
  createSpinner,
797
900
  EXIT_PASS,
@@ -800,6 +903,8 @@ export {
800
903
  UsageError,
801
904
  DEVICE_STATUS_FILTERS,
802
905
  queueReasonCopy,
906
+ resolveArtifact,
907
+ prepareUploadArtifact,
803
908
  uploadAppBuild,
804
909
  describeBuild,
805
910
  uploadApp