vibeview 0.27.0 → 0.28.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.
- package/dist/{chunk-U7FCGMWG.js → chunk-2JLTC67A.js} +159 -11
- package/dist/{chunk-46X7UN6Y.js → chunk-OKOAWGWV.js} +226 -161
- package/dist/cli.js +409 -95
- package/dist/{server-XJREB7KU.js → server-6SRFOQ6B.js} +298 -53
- package/dist/{upload-app-QUUXKNR2.js → upload-app-WROTRZCA.js} +1 -1
- package/package.json +2 -2
- package/skills/vibeview-agent/SKILL.md +10 -5
|
@@ -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";
|
|
@@ -42,6 +42,32 @@ import { createWriteStream, mkdirSync as mkdirSync2, renameSync, rmSync } from "
|
|
|
42
42
|
import { basename, join as join2, resolve, sep } from "path";
|
|
43
43
|
import { pipeline } from "stream/promises";
|
|
44
44
|
import { Readable } from "stream";
|
|
45
|
+
|
|
46
|
+
// src/lib/formatters/time.ts
|
|
47
|
+
function formatUtcTimestamp(iso) {
|
|
48
|
+
if (!iso) return "";
|
|
49
|
+
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
|
|
50
|
+
const d = new Date(hasZone ? iso : `${iso}Z`);
|
|
51
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
52
|
+
return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
|
|
53
|
+
}
|
|
54
|
+
var NAIVE_ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
|
|
55
|
+
function withUtcTimestamps(value) {
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
return NAIVE_ISO_DATETIME.test(value) ? `${value}Z` : value;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) return value.map((v) => withUtcTimestamps(v));
|
|
60
|
+
if (value !== null && typeof value === "object") {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(value)) {
|
|
63
|
+
out[k] = withUtcTimestamps(v);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/lib/api-client.ts
|
|
45
71
|
var AuthError = class extends Error {
|
|
46
72
|
constructor(message = "Authentication failed. Check your API token.") {
|
|
47
73
|
super(message);
|
|
@@ -53,16 +79,33 @@ function errorExitCode(err, otherwise = 1) {
|
|
|
53
79
|
}
|
|
54
80
|
function reportCommandError(json, err) {
|
|
55
81
|
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
-
if (json)
|
|
82
|
+
if (json) printJsonError(err);
|
|
57
83
|
process.stderr.write(`error: ${message}
|
|
58
84
|
`);
|
|
59
85
|
}
|
|
86
|
+
function printJsonError(err, extra) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
console.log(JSON.stringify({ event: "error", message, ...extra }));
|
|
89
|
+
}
|
|
60
90
|
var NotFoundError = class extends Error {
|
|
61
91
|
constructor(message) {
|
|
62
92
|
super(message);
|
|
63
93
|
this.name = "NotFoundError";
|
|
64
94
|
}
|
|
65
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
|
+
};
|
|
66
109
|
function extractDetail(text) {
|
|
67
110
|
try {
|
|
68
111
|
const parsed = JSON.parse(text);
|
|
@@ -73,9 +116,21 @@ function extractDetail(text) {
|
|
|
73
116
|
}
|
|
74
117
|
return void 0;
|
|
75
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
|
+
}
|
|
76
131
|
async function parseJsonBody(resp) {
|
|
77
132
|
if (resp.status === 204 || resp.status === 205) return void 0;
|
|
78
|
-
return resp.json();
|
|
133
|
+
return withUtcTimestamps(await resp.json());
|
|
79
134
|
}
|
|
80
135
|
var ApiClient = class {
|
|
81
136
|
constructor(baseUrl, token) {
|
|
@@ -95,7 +150,7 @@ var ApiClient = class {
|
|
|
95
150
|
} catch (e) {
|
|
96
151
|
const cause = e.cause;
|
|
97
152
|
const detail = cause?.code ? ` (${cause.code})` : "";
|
|
98
|
-
throw new
|
|
153
|
+
throw new NetworkError(
|
|
99
154
|
`Cannot reach ${this.baseUrl}${detail}. Check the server URL (--base-url flag, VIBEVIEW_BASE_URL, or ~/.vibeview/config.json) and your network.`
|
|
100
155
|
);
|
|
101
156
|
}
|
|
@@ -115,7 +170,10 @@ var ApiClient = class {
|
|
|
115
170
|
const text = await resp.text().catch(() => "");
|
|
116
171
|
if (resp.status === 401) throw new AuthError();
|
|
117
172
|
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
118
|
-
throw new
|
|
173
|
+
throw new ApiStatusError(
|
|
174
|
+
resp.status,
|
|
175
|
+
extractDetail(text) ?? httpErrorMessage(resp.status, text)
|
|
176
|
+
);
|
|
119
177
|
}
|
|
120
178
|
return parseJsonBody(resp);
|
|
121
179
|
}
|
|
@@ -187,7 +245,10 @@ var ApiClient = class {
|
|
|
187
245
|
const text = await resp.text().catch(() => "");
|
|
188
246
|
if (resp.status === 401) throw new AuthError();
|
|
189
247
|
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
190
|
-
throw new
|
|
248
|
+
throw new ApiStatusError(
|
|
249
|
+
resp.status,
|
|
250
|
+
extractDetail(text) ?? httpErrorMessage(resp.status, text)
|
|
251
|
+
);
|
|
191
252
|
}
|
|
192
253
|
return parseJsonBody(resp);
|
|
193
254
|
}
|
|
@@ -207,7 +268,10 @@ var ApiClient = class {
|
|
|
207
268
|
const text = await resp.text().catch(() => "");
|
|
208
269
|
if (resp.status === 401) throw new AuthError();
|
|
209
270
|
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
210
|
-
throw new
|
|
271
|
+
throw new ApiStatusError(
|
|
272
|
+
resp.status,
|
|
273
|
+
extractDetail(text) ?? httpErrorMessage(resp.status, text)
|
|
274
|
+
);
|
|
211
275
|
}
|
|
212
276
|
return parseJsonBody(resp);
|
|
213
277
|
}
|
|
@@ -367,7 +431,10 @@ var ApiClient = class {
|
|
|
367
431
|
if (resp.status === 401) throw new AuthError();
|
|
368
432
|
if (resp.status === 404)
|
|
369
433
|
throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
|
|
370
|
-
throw new
|
|
434
|
+
throw new ApiStatusError(
|
|
435
|
+
resp.status,
|
|
436
|
+
extractDetail(text) ?? httpErrorMessage(resp.status, text)
|
|
437
|
+
);
|
|
371
438
|
}
|
|
372
439
|
const rawFilename = parseContentDispositionFilename(resp.headers.get("content-disposition"));
|
|
373
440
|
const filename = sanitizeDownloadFilename(rawFilename) ?? `${id}.bin`;
|
|
@@ -640,6 +707,63 @@ function sanitizeDownloadFilename(raw) {
|
|
|
640
707
|
return base;
|
|
641
708
|
}
|
|
642
709
|
|
|
710
|
+
// src/lib/artifact.ts
|
|
711
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
|
|
712
|
+
import { spawnSync } from "child_process";
|
|
713
|
+
import { tmpdir } from "os";
|
|
714
|
+
import { basename as basename2, dirname, isAbsolute, join as join3 } from "path";
|
|
715
|
+
function resolveArtifact(cwd, pattern) {
|
|
716
|
+
const abs = isAbsolute(pattern) ? pattern : join3(cwd, pattern);
|
|
717
|
+
if (!basename2(abs).includes("*")) {
|
|
718
|
+
if (!existsSync(abs)) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`artifact not found: ${pattern} (looked at ${abs}) \u2014 did the build produce it?`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return abs;
|
|
724
|
+
}
|
|
725
|
+
const dir = dirname(abs);
|
|
726
|
+
const re = new RegExp("^" + basename2(abs).split("*").map(escapeRegExp).join(".*") + "$");
|
|
727
|
+
let entries;
|
|
728
|
+
try {
|
|
729
|
+
entries = readdirSync(dir);
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error(`artifact not found: ${pattern} (directory ${dir} does not exist)`);
|
|
732
|
+
}
|
|
733
|
+
const matches = entries.filter((e) => re.test(e)).map((e) => join3(dir, e)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
734
|
+
if (matches.length === 0) {
|
|
735
|
+
throw new Error(`artifact not found: no match for ${pattern} in ${dir}`);
|
|
736
|
+
}
|
|
737
|
+
return matches[0];
|
|
738
|
+
}
|
|
739
|
+
function escapeRegExp(s) {
|
|
740
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
741
|
+
}
|
|
742
|
+
function prepareUploadArtifact(path) {
|
|
743
|
+
if (!(statSync(path).isDirectory() && path.endsWith(".app"))) {
|
|
744
|
+
return { uploadPath: path, cleanup: () => {
|
|
745
|
+
} };
|
|
746
|
+
}
|
|
747
|
+
const tmp = mkdtempSync(join3(tmpdir(), "vibeview-zip-"));
|
|
748
|
+
const out = join3(tmp, `${basename2(path)}.zip`);
|
|
749
|
+
const res = spawnSync("zip", ["-qry", out, basename2(path)], { cwd: dirname(path) });
|
|
750
|
+
if (res.error) {
|
|
751
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
752
|
+
const errorCode = res.error.code;
|
|
753
|
+
const errorMsg = res.error.message;
|
|
754
|
+
throw new Error(
|
|
755
|
+
`failed to zip ${path}: spawn zip ${errorCode} \u2014 ${errorMsg} \u2014 is the \`zip\` command installed?`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (res.status !== 0) {
|
|
759
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
760
|
+
throw new Error(
|
|
761
|
+
`failed to zip ${path}: ${res.stderr?.toString().trim() || `zip exited ${res.status}`}`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return { uploadPath: out, cleanup: () => rmSync2(tmp, { recursive: true, force: true }) };
|
|
765
|
+
}
|
|
766
|
+
|
|
643
767
|
// src/lib/spinner.ts
|
|
644
768
|
import ora from "ora";
|
|
645
769
|
function createSpinner(text, opts = {}) {
|
|
@@ -663,16 +787,32 @@ var UsageError = class extends Error {
|
|
|
663
787
|
this.name = "UsageError";
|
|
664
788
|
}
|
|
665
789
|
};
|
|
790
|
+
var DEVICE_STATUS_FILTERS = ["available", "busy", "updating"];
|
|
791
|
+
var QUEUE_REASON_COPY = {
|
|
792
|
+
devices_busy: "All devices are in use right now.",
|
|
793
|
+
devices_updating: "Devices are being updated and will be back shortly.",
|
|
794
|
+
plan_limit: "The most sessions this app can run at once are already running.",
|
|
795
|
+
link_limit: "The most people who can play at once are already playing."
|
|
796
|
+
};
|
|
797
|
+
function queueReasonCopy(reason) {
|
|
798
|
+
return reason ? QUEUE_REASON_COPY[reason] ?? null : null;
|
|
799
|
+
}
|
|
666
800
|
|
|
667
801
|
// src/commands/upload-app.ts
|
|
668
802
|
async function uploadAppBuild(filePath, options) {
|
|
669
|
-
if (!
|
|
803
|
+
if (!existsSync2(filePath)) {
|
|
670
804
|
throw new Error(`File not found: ${filePath}`);
|
|
671
805
|
}
|
|
672
806
|
const { token, baseUrl } = resolveAuth(options);
|
|
673
807
|
const client = new ApiClient(baseUrl, token);
|
|
674
808
|
const note = options.note?.trim() || void 0;
|
|
675
|
-
const
|
|
809
|
+
const { uploadPath, cleanup } = prepareUploadArtifact(filePath);
|
|
810
|
+
let result;
|
|
811
|
+
try {
|
|
812
|
+
result = await client.uploadApp(uploadPath, options.name, note);
|
|
813
|
+
} finally {
|
|
814
|
+
cleanup();
|
|
815
|
+
}
|
|
676
816
|
if (note && result.deduplicated && result.build_id != null) {
|
|
677
817
|
await client.updateBuildNote(result.public_id, result.build_id, note);
|
|
678
818
|
}
|
|
@@ -685,7 +825,7 @@ function describeBuild(result) {
|
|
|
685
825
|
return `Build ${chalk.cyan(String(result.build_id))}${version ? ` ${version}` : ""}${dedup}`;
|
|
686
826
|
}
|
|
687
827
|
async function uploadApp(filePath, options) {
|
|
688
|
-
if (!
|
|
828
|
+
if (!existsSync2(filePath)) {
|
|
689
829
|
console.error(chalk.red(`Error: File not found: ${filePath}`));
|
|
690
830
|
process.exit(EXIT_INFRA_ERROR);
|
|
691
831
|
}
|
|
@@ -746,16 +886,24 @@ export {
|
|
|
746
886
|
loadConfig,
|
|
747
887
|
saveConfig,
|
|
748
888
|
resolveAuth,
|
|
889
|
+
formatUtcTimestamp,
|
|
749
890
|
AuthError,
|
|
750
891
|
errorExitCode,
|
|
751
892
|
reportCommandError,
|
|
893
|
+
printJsonError,
|
|
752
894
|
NotFoundError,
|
|
895
|
+
ApiStatusError,
|
|
896
|
+
NetworkError,
|
|
753
897
|
ApiClient,
|
|
754
898
|
createSpinner,
|
|
755
899
|
EXIT_PASS,
|
|
756
900
|
EXIT_TEST_FAIL,
|
|
757
901
|
EXIT_INFRA_ERROR,
|
|
758
902
|
UsageError,
|
|
903
|
+
DEVICE_STATUS_FILTERS,
|
|
904
|
+
queueReasonCopy,
|
|
905
|
+
resolveArtifact,
|
|
906
|
+
prepareUploadArtifact,
|
|
759
907
|
uploadAppBuild,
|
|
760
908
|
describeBuild,
|
|
761
909
|
uploadApp
|