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
|
@@ -6,10 +6,14 @@ import {
|
|
|
6
6
|
UsageError,
|
|
7
7
|
createSpinner,
|
|
8
8
|
errorExitCode,
|
|
9
|
+
formatUtcTimestamp,
|
|
9
10
|
loadConfig,
|
|
11
|
+
prepareUploadArtifact,
|
|
12
|
+
queueReasonCopy,
|
|
13
|
+
resolveArtifact,
|
|
10
14
|
resolveAuth,
|
|
11
15
|
uploadAppBuild
|
|
12
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-2JLTC67A.js";
|
|
13
17
|
|
|
14
18
|
// src/lib/dev-config.ts
|
|
15
19
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -164,6 +168,30 @@ function validateBuildEnv(env, platformKey) {
|
|
|
164
168
|
function saveDevConfig(cwd, cfg) {
|
|
165
169
|
writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
|
|
166
170
|
}
|
|
171
|
+
function describeConfigChanges(before, after) {
|
|
172
|
+
const flat = (value, prefix, out) => {
|
|
173
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
174
|
+
for (const [k, v] of Object.entries(value)) {
|
|
175
|
+
flat(v, prefix ? `${prefix}.${k}` : k, out);
|
|
176
|
+
}
|
|
177
|
+
} else if (value !== void 0) {
|
|
178
|
+
out.set(prefix, typeof value === "string" ? value : JSON.stringify(value));
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
};
|
|
182
|
+
const a = flat(before, "", /* @__PURE__ */ new Map());
|
|
183
|
+
const b = flat(after, "", /* @__PURE__ */ new Map());
|
|
184
|
+
const changes = [];
|
|
185
|
+
for (const [key, next] of b) {
|
|
186
|
+
const prev = a.get(key);
|
|
187
|
+
if (prev === void 0) changes.push(`${key} (added) ${next}`);
|
|
188
|
+
else if (prev !== next) changes.push(`${key} ${prev} \u2192 ${next}`);
|
|
189
|
+
}
|
|
190
|
+
for (const [key, prev] of a) {
|
|
191
|
+
if (!b.has(key)) changes.push(`${key} (removed, was ${prev})`);
|
|
192
|
+
}
|
|
193
|
+
return changes;
|
|
194
|
+
}
|
|
167
195
|
function resolveMetro(cfg, platform, portFlag) {
|
|
168
196
|
const entry = cfg?.platforms?.[platform];
|
|
169
197
|
return {
|
|
@@ -221,7 +249,7 @@ function buildKind(isDebug) {
|
|
|
221
249
|
if (isDebug === false) return "release";
|
|
222
250
|
return "unknown";
|
|
223
251
|
}
|
|
224
|
-
function checkDevBuild(app, buildId, platform) {
|
|
252
|
+
function checkDevBuild(app, buildId, platform, opts = {}) {
|
|
225
253
|
if (platform === "roku") {
|
|
226
254
|
throw new Error(
|
|
227
255
|
"a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
|
|
@@ -238,7 +266,7 @@ function checkDevBuild(app, buildId, platform) {
|
|
|
238
266
|
`build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
|
|
239
267
|
);
|
|
240
268
|
}
|
|
241
|
-
if (build.is_debug !== true) {
|
|
269
|
+
if (!opts.standalone && build.is_debug !== true) {
|
|
242
270
|
const why = build.is_debug === false ? "it runs standalone (a release build, or one with an embedded JS bundle)" : "it could not be identified as a Metro debug build";
|
|
243
271
|
throw new Error(
|
|
244
272
|
`build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs ${platformWithArticle(platform)} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
|
|
@@ -249,17 +277,6 @@ function checkDevBuild(app, buildId, platform) {
|
|
|
249
277
|
|
|
250
278
|
// src/commands/list-builds.ts
|
|
251
279
|
import chalk from "chalk";
|
|
252
|
-
|
|
253
|
-
// src/lib/formatters/time.ts
|
|
254
|
-
function formatUtcTimestamp(iso) {
|
|
255
|
-
if (!iso) return "";
|
|
256
|
-
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
|
|
257
|
-
const d = new Date(hasZone ? iso : `${iso}Z`);
|
|
258
|
-
if (Number.isNaN(d.getTime())) return iso;
|
|
259
|
-
return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/commands/list-builds.ts
|
|
263
280
|
function buildRows(app) {
|
|
264
281
|
return app.builds.map((b) => ({
|
|
265
282
|
id: b.id,
|
|
@@ -392,81 +409,24 @@ import { createInterface } from "readline";
|
|
|
392
409
|
import { select as select2 } from "@inquirer/prompts";
|
|
393
410
|
import chalk3 from "chalk";
|
|
394
411
|
|
|
395
|
-
// src/lib/artifact.ts
|
|
396
|
-
import { existsSync as existsSync2, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
|
|
397
|
-
import { spawnSync } from "child_process";
|
|
398
|
-
import { tmpdir } from "os";
|
|
399
|
-
import { basename, dirname as dirname2, isAbsolute, join as join3 } from "path";
|
|
400
|
-
function resolveArtifact(cwd, pattern) {
|
|
401
|
-
const abs = isAbsolute(pattern) ? pattern : join3(cwd, pattern);
|
|
402
|
-
if (!basename(abs).includes("*")) {
|
|
403
|
-
if (!existsSync2(abs)) {
|
|
404
|
-
throw new Error(
|
|
405
|
-
`artifact not found: ${pattern} (looked at ${abs}) \u2014 did the build produce it?`
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
return abs;
|
|
409
|
-
}
|
|
410
|
-
const dir = dirname2(abs);
|
|
411
|
-
const re = new RegExp("^" + basename(abs).split("*").map(escapeRegExp).join(".*") + "$");
|
|
412
|
-
let entries;
|
|
413
|
-
try {
|
|
414
|
-
entries = readdirSync(dir);
|
|
415
|
-
} catch {
|
|
416
|
-
throw new Error(`artifact not found: ${pattern} (directory ${dir} does not exist)`);
|
|
417
|
-
}
|
|
418
|
-
const matches = entries.filter((e) => re.test(e)).map((e) => join3(dir, e)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
419
|
-
if (matches.length === 0) {
|
|
420
|
-
throw new Error(`artifact not found: no match for ${pattern} in ${dir}`);
|
|
421
|
-
}
|
|
422
|
-
return matches[0];
|
|
423
|
-
}
|
|
424
|
-
function escapeRegExp(s) {
|
|
425
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
426
|
-
}
|
|
427
|
-
function prepareUploadArtifact(path) {
|
|
428
|
-
if (!(statSync(path).isDirectory() && path.endsWith(".app"))) {
|
|
429
|
-
return { uploadPath: path, cleanup: () => {
|
|
430
|
-
} };
|
|
431
|
-
}
|
|
432
|
-
const tmp = mkdtempSync(join3(tmpdir(), "vibeview-zip-"));
|
|
433
|
-
const out = join3(tmp, `${basename(path)}.zip`);
|
|
434
|
-
const res = spawnSync("zip", ["-qry", out, basename(path)], { cwd: dirname2(path) });
|
|
435
|
-
if (res.error) {
|
|
436
|
-
rmSync2(tmp, { recursive: true, force: true });
|
|
437
|
-
const errorCode = res.error.code;
|
|
438
|
-
const errorMsg = res.error.message;
|
|
439
|
-
throw new Error(
|
|
440
|
-
`failed to zip ${path}: spawn zip ${errorCode} \u2014 ${errorMsg} \u2014 is the \`zip\` command installed?`
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
if (res.status !== 0) {
|
|
444
|
-
rmSync2(tmp, { recursive: true, force: true });
|
|
445
|
-
throw new Error(
|
|
446
|
-
`failed to zip ${path}: ${res.stderr?.toString().trim() || `zip exited ${res.status}`}`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
return { uploadPath: out, cleanup: () => rmSync2(tmp, { recursive: true, force: true }) };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
412
|
// src/lib/dev-setup.ts
|
|
453
413
|
import chalk2 from "chalk";
|
|
454
414
|
import { input, select } from "@inquirer/prompts";
|
|
455
415
|
|
|
456
416
|
// src/lib/project-detect.ts
|
|
457
|
-
import { existsSync as
|
|
458
|
-
import { join as
|
|
417
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
418
|
+
import { join as join3 } from "path";
|
|
459
419
|
import ignore from "ignore";
|
|
460
420
|
function readJson(cwd, name) {
|
|
461
421
|
try {
|
|
462
|
-
return JSON.parse(readFileSync3(
|
|
422
|
+
return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
|
|
463
423
|
} catch {
|
|
464
424
|
return null;
|
|
465
425
|
}
|
|
466
426
|
}
|
|
467
427
|
function isFile(path) {
|
|
468
428
|
try {
|
|
469
|
-
return
|
|
429
|
+
return statSync(path).isFile();
|
|
470
430
|
} catch {
|
|
471
431
|
return false;
|
|
472
432
|
}
|
|
@@ -474,19 +434,19 @@ function isFile(path) {
|
|
|
474
434
|
function detectFlavor(cwd) {
|
|
475
435
|
let deps = {};
|
|
476
436
|
try {
|
|
477
|
-
const pkg = JSON.parse(readFileSync3(
|
|
437
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
|
|
478
438
|
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
479
439
|
} catch {
|
|
480
440
|
}
|
|
481
441
|
if ("expo" in deps) return "expo";
|
|
482
|
-
if (
|
|
483
|
-
if (isFile(
|
|
442
|
+
if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
|
|
443
|
+
if (isFile(join3(cwd, "manifest"))) return "roku";
|
|
484
444
|
return "unknown";
|
|
485
445
|
}
|
|
486
446
|
function nativeDirShipped(cwd, dir) {
|
|
487
|
-
if (!
|
|
447
|
+
if (!existsSync2(join3(cwd, dir))) return false;
|
|
488
448
|
try {
|
|
489
|
-
const ig = ignore().add(readFileSync3(
|
|
449
|
+
const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
|
|
490
450
|
return !ig.ignores(`${dir}/`);
|
|
491
451
|
} catch {
|
|
492
452
|
return true;
|
|
@@ -502,7 +462,7 @@ function isManagedExpo(cwd, platform) {
|
|
|
502
462
|
}
|
|
503
463
|
function iosWorkspaceName(cwd) {
|
|
504
464
|
try {
|
|
505
|
-
const found =
|
|
465
|
+
const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
|
|
506
466
|
return found ? found.replace(/\.xcworkspace$/, "") : null;
|
|
507
467
|
} catch {
|
|
508
468
|
return null;
|
|
@@ -511,9 +471,9 @@ function iosWorkspaceName(cwd) {
|
|
|
511
471
|
var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
|
|
512
472
|
var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
|
|
513
473
|
function installCommand(cwd) {
|
|
514
|
-
if (
|
|
515
|
-
if (
|
|
516
|
-
if (
|
|
474
|
+
if (existsSync2(join3(cwd, "yarn.lock"))) return "yarn install --immutable";
|
|
475
|
+
if (existsSync2(join3(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
|
|
476
|
+
if (existsSync2(join3(cwd, "package-lock.json"))) return "npm ci";
|
|
517
477
|
return "yarn install --immutable";
|
|
518
478
|
}
|
|
519
479
|
function managedExpoAndroidCommand(cwd) {
|
|
@@ -527,7 +487,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
|
|
|
527
487
|
function rokuDefaults(cwd) {
|
|
528
488
|
const pkg = readJson(cwd, "package.json");
|
|
529
489
|
const scripts = pkg?.scripts ?? {};
|
|
530
|
-
const runner =
|
|
490
|
+
const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
|
|
531
491
|
const packageScript = Object.entries(scripts).find(
|
|
532
492
|
([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
|
|
533
493
|
);
|
|
@@ -585,7 +545,7 @@ function expoSanitizedName(name) {
|
|
|
585
545
|
return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
586
546
|
}
|
|
587
547
|
function managedExpoIosName(cwd) {
|
|
588
|
-
if (
|
|
548
|
+
if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
|
|
589
549
|
return null;
|
|
590
550
|
}
|
|
591
551
|
const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
|
|
@@ -730,9 +690,9 @@ async function promptMetroCommand(cwd, platform) {
|
|
|
730
690
|
|
|
731
691
|
// src/lib/source-pack.ts
|
|
732
692
|
import { execFile, spawn } from "child_process";
|
|
733
|
-
import { lstatSync, mkdtempSync
|
|
734
|
-
import { tmpdir
|
|
735
|
-
import { join as
|
|
693
|
+
import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
694
|
+
import { tmpdir } from "os";
|
|
695
|
+
import { join as join4 } from "path";
|
|
736
696
|
import ignore2 from "ignore";
|
|
737
697
|
var NotAGitRepoError = class extends Error {
|
|
738
698
|
constructor(message) {
|
|
@@ -758,7 +718,7 @@ async function listSourceFiles(projectRoot) {
|
|
|
758
718
|
const stdout = await runGitLsFiles(projectRoot);
|
|
759
719
|
const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
|
|
760
720
|
let filtered = rawPaths;
|
|
761
|
-
const vvignorePath =
|
|
721
|
+
const vvignorePath = join4(projectRoot, ".vvignore");
|
|
762
722
|
if (statOrNull(vvignorePath)?.isFile()) {
|
|
763
723
|
const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
|
|
764
724
|
filtered = filtered.filter((p) => !ig.ignores(p));
|
|
@@ -766,7 +726,7 @@ async function listSourceFiles(projectRoot) {
|
|
|
766
726
|
const result = [];
|
|
767
727
|
for (const relPath of filtered) {
|
|
768
728
|
if (isForceExcluded(relPath)) continue;
|
|
769
|
-
const st = statOrNull(
|
|
729
|
+
const st = statOrNull(join4(projectRoot, relPath));
|
|
770
730
|
if (!st) continue;
|
|
771
731
|
result.push(relPath);
|
|
772
732
|
}
|
|
@@ -816,7 +776,7 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
816
776
|
let totalBytes = 0;
|
|
817
777
|
const sized = [];
|
|
818
778
|
for (const relPath of files) {
|
|
819
|
-
const bytes = lstatSync(
|
|
779
|
+
const bytes = lstatSync(join4(projectRoot, relPath)).size;
|
|
820
780
|
totalBytes += bytes;
|
|
821
781
|
sized.push({ path: relPath, bytes });
|
|
822
782
|
}
|
|
@@ -827,13 +787,13 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
827
787
|
largest
|
|
828
788
|
);
|
|
829
789
|
}
|
|
830
|
-
const tmpDir =
|
|
831
|
-
const cleanup = () =>
|
|
790
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
|
|
791
|
+
const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
|
|
832
792
|
try {
|
|
833
793
|
const supportsNull = await probeTarNullSupport();
|
|
834
|
-
const tarPath =
|
|
794
|
+
const tarPath = join4(tmpDir, "source.tgz");
|
|
835
795
|
if (supportsNull) {
|
|
836
|
-
const listPath =
|
|
796
|
+
const listPath = join4(tmpDir, "files.list");
|
|
837
797
|
writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
|
|
838
798
|
await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
|
|
839
799
|
} else {
|
|
@@ -843,12 +803,12 @@ async function packSource(projectRoot, files, opts = {}) {
|
|
|
843
803
|
`cannot pack source: filename contains a newline (${JSON.stringify(badName)}) and the installed tar doesn't support --null -T; rename the file or install a tar that supports --null`
|
|
844
804
|
);
|
|
845
805
|
}
|
|
846
|
-
const listPath =
|
|
806
|
+
const listPath = join4(tmpDir, "files.list");
|
|
847
807
|
writeFileSync3(listPath, files.map((f) => `${f}
|
|
848
808
|
`).join(""));
|
|
849
809
|
await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
|
|
850
810
|
}
|
|
851
|
-
const sizeBytes =
|
|
811
|
+
const sizeBytes = statSync2(tarPath).size;
|
|
852
812
|
return { tarPath, sizeBytes, cleanup };
|
|
853
813
|
} catch (err) {
|
|
854
814
|
cleanup();
|
|
@@ -864,12 +824,12 @@ var nullSupportCache = null;
|
|
|
864
824
|
function probeTarNullSupport() {
|
|
865
825
|
if (!nullSupportCache) {
|
|
866
826
|
nullSupportCache = new Promise((resolve) => {
|
|
867
|
-
const tmp =
|
|
868
|
-
const listPath =
|
|
827
|
+
const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
|
|
828
|
+
const listPath = join4(tmp, "empty.list");
|
|
869
829
|
writeFileSync3(listPath, "");
|
|
870
|
-
const out =
|
|
830
|
+
const out = join4(tmp, "probe.tgz");
|
|
871
831
|
execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
|
|
872
|
-
|
|
832
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
873
833
|
resolve(!err);
|
|
874
834
|
});
|
|
875
835
|
});
|
|
@@ -1552,7 +1512,9 @@ async function waitForCloudBuilds(builds, opts, deps) {
|
|
|
1552
1512
|
id: final.id,
|
|
1553
1513
|
status: final.status,
|
|
1554
1514
|
cost_micro: final.cost_micro,
|
|
1555
|
-
error_summary: final.error_summary
|
|
1515
|
+
error_summary: final.error_summary,
|
|
1516
|
+
app_build_id: final.app_build_id ?? null,
|
|
1517
|
+
app_public_id: final.app_public_id ?? null
|
|
1556
1518
|
});
|
|
1557
1519
|
finalized.add(build.id);
|
|
1558
1520
|
}
|
|
@@ -1740,7 +1702,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
|
|
|
1740
1702
|
// src/commands/dev.ts
|
|
1741
1703
|
import { spawn as spawn4 } from "child_process";
|
|
1742
1704
|
import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
|
|
1743
|
-
import { dirname as
|
|
1705
|
+
import { dirname as dirname4 } from "path";
|
|
1744
1706
|
import { createInterface as createInterface3 } from "readline";
|
|
1745
1707
|
import { select as select3 } from "@inquirer/prompts";
|
|
1746
1708
|
import chalk4 from "chalk";
|
|
@@ -1935,15 +1897,15 @@ import { spawn as spawn3 } from "child_process";
|
|
|
1935
1897
|
import { createInterface as createInterface2 } from "readline";
|
|
1936
1898
|
|
|
1937
1899
|
// src/lib/metro-state.ts
|
|
1938
|
-
import { existsSync as
|
|
1939
|
-
import { dirname as
|
|
1900
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1901
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1940
1902
|
var STATE_DIR2 = ".vibeview";
|
|
1941
1903
|
var STATE_FILE2 = "metro-state.json";
|
|
1942
1904
|
function statePath2(cwd) {
|
|
1943
|
-
return
|
|
1905
|
+
return join5(cwd, STATE_DIR2, STATE_FILE2);
|
|
1944
1906
|
}
|
|
1945
1907
|
function metroLogPath(cwd) {
|
|
1946
|
-
return
|
|
1908
|
+
return join5(cwd, STATE_DIR2, "metro.log");
|
|
1947
1909
|
}
|
|
1948
1910
|
function isPidAlive2(pid) {
|
|
1949
1911
|
try {
|
|
@@ -1955,7 +1917,7 @@ function isPidAlive2(pid) {
|
|
|
1955
1917
|
}
|
|
1956
1918
|
function writeMetroState(cwd, state) {
|
|
1957
1919
|
const path = statePath2(cwd);
|
|
1958
|
-
mkdirSync2(
|
|
1920
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1959
1921
|
writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
|
|
1960
1922
|
}
|
|
1961
1923
|
function readMetroState(cwd) {
|
|
@@ -1973,7 +1935,7 @@ function readMetroState(cwd) {
|
|
|
1973
1935
|
}
|
|
1974
1936
|
function clearMetroState(cwd) {
|
|
1975
1937
|
const path = statePath2(cwd);
|
|
1976
|
-
if (
|
|
1938
|
+
if (existsSync3(path)) rmSync3(path, { force: true });
|
|
1977
1939
|
}
|
|
1978
1940
|
function killOwnedMetro(cwd) {
|
|
1979
1941
|
const state = readMetroState(cwd);
|
|
@@ -2081,7 +2043,7 @@ async function waitFor(cond, timeoutMs, everyMs) {
|
|
|
2081
2043
|
|
|
2082
2044
|
// src/lib/reload-loop.ts
|
|
2083
2045
|
import { watch } from "fs";
|
|
2084
|
-
import { basename
|
|
2046
|
+
import { basename, dirname as dirname3 } from "path";
|
|
2085
2047
|
function createReloadLoop(deps) {
|
|
2086
2048
|
let inFlight = null;
|
|
2087
2049
|
let pendingFollowUp = false;
|
|
@@ -2162,16 +2124,16 @@ function isIgnoredWatchPath(relPath, artifact, userIgnore = []) {
|
|
|
2162
2124
|
if (IGNORED_FILES.includes(rel)) return true;
|
|
2163
2125
|
if (userIgnore.some((entry) => isUnderPath(rel, entry))) return true;
|
|
2164
2126
|
const artifactPattern = artifact.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2165
|
-
const artifactDir =
|
|
2127
|
+
const artifactDir = dirname3(artifactPattern);
|
|
2166
2128
|
if (artifactDir !== "." && (rel === artifactDir || rel.startsWith(`${artifactDir}/`))) {
|
|
2167
2129
|
return true;
|
|
2168
2130
|
}
|
|
2169
|
-
if (
|
|
2170
|
-
const name =
|
|
2171
|
-
const re = new RegExp(`^${name.split("*").map(
|
|
2172
|
-
return re.test(
|
|
2131
|
+
if (dirname3(rel) !== artifactDir) return false;
|
|
2132
|
+
const name = basename(artifactPattern);
|
|
2133
|
+
const re = new RegExp(`^${name.split("*").map(escapeRegExp).join(".*")}$`);
|
|
2134
|
+
return re.test(basename(rel));
|
|
2173
2135
|
}
|
|
2174
|
-
function
|
|
2136
|
+
function escapeRegExp(s) {
|
|
2175
2137
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2176
2138
|
}
|
|
2177
2139
|
function watchProjectFiles(cwd, artifact, userIgnore = []) {
|
|
@@ -2196,13 +2158,18 @@ function parseWatchSeconds(flag) {
|
|
|
2196
2158
|
}
|
|
2197
2159
|
return seconds;
|
|
2198
2160
|
}
|
|
2161
|
+
function queuedLine(position, reasonMessage) {
|
|
2162
|
+
const where = position ? ` (position ${position})` : "";
|
|
2163
|
+
const why = reasonMessage ?? "Every device is busy.";
|
|
2164
|
+
return `\u23F3 ${why} Waiting in the queue${where}, Ctrl-C to give up.`;
|
|
2165
|
+
}
|
|
2199
2166
|
function emit(json, evt) {
|
|
2200
2167
|
if (json) console.log(JSON.stringify(evt));
|
|
2201
2168
|
}
|
|
2202
2169
|
async function startDevSession(cfg, onEvent = () => {
|
|
2203
2170
|
}, onTunnelState) {
|
|
2204
2171
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2205
|
-
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
|
|
2172
|
+
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
|
|
2206
2173
|
const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
|
|
2207
2174
|
const created = await api.createSession(
|
|
2208
2175
|
devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
|
|
@@ -2232,7 +2199,9 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2232
2199
|
event: "queued",
|
|
2233
2200
|
session_id: sessionId,
|
|
2234
2201
|
position: started.position ?? null,
|
|
2235
|
-
queue_entry_id: queueEntryId
|
|
2202
|
+
queue_entry_id: queueEntryId,
|
|
2203
|
+
queue_reason: started.queue_reason ?? null,
|
|
2204
|
+
message: queueReasonCopy(started.queue_reason)
|
|
2236
2205
|
});
|
|
2237
2206
|
await pollWhileQueued(api, sessionId, QUEUE_WAIT_MS);
|
|
2238
2207
|
queueEntryId = null;
|
|
@@ -2288,7 +2257,7 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2288
2257
|
reportedWarnings.add(w);
|
|
2289
2258
|
onEvent({ event: "warning", message: w });
|
|
2290
2259
|
}
|
|
2291
|
-
return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings };
|
|
2260
|
+
return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings, session: active };
|
|
2292
2261
|
} catch (err) {
|
|
2293
2262
|
await stopSession(1);
|
|
2294
2263
|
throw err;
|
|
@@ -2299,8 +2268,9 @@ function devSessionCreateBody(cfg) {
|
|
|
2299
2268
|
device_type: PLATFORM_DEVICE[cfg.platform].deviceType,
|
|
2300
2269
|
device_category: cfg.deviceCategory,
|
|
2301
2270
|
app_id: cfg.appId,
|
|
2302
|
-
// Roku has no JS bundle to serve, so there is nothing to tunnel
|
|
2303
|
-
|
|
2271
|
+
// Roku has no JS bundle to serve, so there is nothing to tunnel; a
|
|
2272
|
+
// standalone session runs the build as it is, without Metro.
|
|
2273
|
+
tunnel: PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone,
|
|
2304
2274
|
...cfg.model ? { model: cfg.model } : {},
|
|
2305
2275
|
...cfg.buildId ? { build_id: cfg.buildId } : {}
|
|
2306
2276
|
};
|
|
@@ -2314,15 +2284,45 @@ function resolveDeviceModel(pools, platform, model) {
|
|
|
2314
2284
|
const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
|
|
2315
2285
|
if (match) return match;
|
|
2316
2286
|
const names = [...new Set(candidates.map((p) => p.model))].sort();
|
|
2287
|
+
const guess = closestModelName(wanted, names);
|
|
2317
2288
|
throw new Error(
|
|
2318
|
-
names.length ? `no ${platform} device model named "${model}" \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2289
|
+
names.length ? `no ${platform} device model named "${model}"` + (guess ? ` (did you mean "${guess}"?)` : "") + ` \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2319
2290
|
);
|
|
2320
2291
|
}
|
|
2292
|
+
function closestModelName(wanted, names) {
|
|
2293
|
+
const squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2294
|
+
const words = (s) => new Set(s.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
2295
|
+
const w = squash(wanted);
|
|
2296
|
+
if (!w) return null;
|
|
2297
|
+
const unique = (list) => list.length === 1 ? list[0] : null;
|
|
2298
|
+
const same = names.filter((n) => squash(n) === w);
|
|
2299
|
+
if (same.length) return unique(same);
|
|
2300
|
+
const containing = names.filter((n) => squash(n).includes(w) || w.includes(squash(n)));
|
|
2301
|
+
if (containing.length) return unique(containing);
|
|
2302
|
+
const wantedWords = words(wanted);
|
|
2303
|
+
let best = 0;
|
|
2304
|
+
let bestNames = [];
|
|
2305
|
+
for (const n of names) {
|
|
2306
|
+
let shared = 0;
|
|
2307
|
+
for (const word of words(n)) if (wantedWords.has(word)) shared++;
|
|
2308
|
+
if (shared > best) {
|
|
2309
|
+
best = shared;
|
|
2310
|
+
bestNames = [n];
|
|
2311
|
+
} else if (shared === best && shared > 0) {
|
|
2312
|
+
bestNames.push(n);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return best >= 2 ? unique(bestNames) : null;
|
|
2316
|
+
}
|
|
2321
2317
|
var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
|
|
2322
2318
|
function findBlockingWarning(info) {
|
|
2323
2319
|
return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
|
|
2324
2320
|
}
|
|
2325
2321
|
var QUEUE_WAIT_MS = 30 * 6e4;
|
|
2322
|
+
function sessionEndedMessage(info, when) {
|
|
2323
|
+
const reason = info.status === "failed" && info.failure_reason ? `: ${info.failure_reason}` : "";
|
|
2324
|
+
return `session ${info.status} ${when}${reason}`;
|
|
2325
|
+
}
|
|
2326
2326
|
async function pollWhileQueued(api, sessionId, timeoutMs, intervalMs = 3e3) {
|
|
2327
2327
|
const deadline = Date.now() + timeoutMs;
|
|
2328
2328
|
for (; ; ) {
|
|
@@ -2330,7 +2330,7 @@ async function pollWhileQueued(api, sessionId, timeoutMs, intervalMs = 3e3) {
|
|
|
2330
2330
|
const blocking = findBlockingWarning(info);
|
|
2331
2331
|
if (blocking) throw new Error(blocking);
|
|
2332
2332
|
if (info.status === "stopped" || info.status === "failed") {
|
|
2333
|
-
throw new Error(
|
|
2333
|
+
throw new Error(sessionEndedMessage(info, "while waiting for a device"));
|
|
2334
2334
|
}
|
|
2335
2335
|
if (info.status !== "pending" && info.status !== "queued") return info;
|
|
2336
2336
|
if (Date.now() > deadline) {
|
|
@@ -2349,7 +2349,7 @@ async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3)
|
|
|
2349
2349
|
if (blocking) throw new Error(blocking);
|
|
2350
2350
|
if (info.tunnel_url) return info;
|
|
2351
2351
|
if (info.status === "stopped" || info.status === "failed") {
|
|
2352
|
-
throw new Error(
|
|
2352
|
+
throw new Error(sessionEndedMessage(info, "before the tunnel came up"));
|
|
2353
2353
|
}
|
|
2354
2354
|
if (Date.now() > deadline) throw new Error("timed out waiting for the device to start");
|
|
2355
2355
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
@@ -2368,7 +2368,7 @@ async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3, onIn
|
|
|
2368
2368
|
}
|
|
2369
2369
|
if (info.status === "active" && !info.app_install_pending) return info;
|
|
2370
2370
|
if (info.status === "stopped" || info.status === "failed") {
|
|
2371
|
-
throw new Error(
|
|
2371
|
+
throw new Error(sessionEndedMessage(info, "before it became active"));
|
|
2372
2372
|
}
|
|
2373
2373
|
if (Date.now() > deadline) {
|
|
2374
2374
|
throw new Error("timed out waiting for the session to become active");
|
|
@@ -2418,6 +2418,19 @@ function validateAppChoice(requested, fromFlag, apps) {
|
|
|
2418
2418
|
if (apps.some((a) => a.public_id === requested)) return { appId: requested };
|
|
2419
2419
|
return fromFlag ? { staleFlag: requested } : { staleCached: requested };
|
|
2420
2420
|
}
|
|
2421
|
+
function staleAppFlagError(requested, platform, allApps) {
|
|
2422
|
+
const other = allApps.find((a) => a.public_id === requested);
|
|
2423
|
+
if (other) {
|
|
2424
|
+
return {
|
|
2425
|
+
message: `app "${requested}" is ${platformWithArticle(other.platform)} app, not ${platformWithArticle(platform)} app.`,
|
|
2426
|
+
hint: `Run \`vibeview dev --platform ${other.platform} --app ${requested}\`, or \`vibeview dev --platform ${platform}\` without --app to pick ${platformWithArticle(platform)} app.`
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
message: `app "${requested}" not found \u2014 it may have been deleted.`,
|
|
2431
|
+
hint: "Run `vibeview dev` without --app to pick from the list."
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2421
2434
|
function buildAppStrategy(apps, buildFlag, json, isTTY) {
|
|
2422
2435
|
if (buildFlag) return "build";
|
|
2423
2436
|
if (apps.length === 0) return "build";
|
|
@@ -2495,11 +2508,9 @@ async function ensureConfig(api, opts) {
|
|
|
2495
2508
|
const apps = appsForPlatform(allApps, platform);
|
|
2496
2509
|
const choice = validateAppChoice(opts.app || entry?.appId, Boolean(opts.app), apps);
|
|
2497
2510
|
if (choice.staleFlag) {
|
|
2498
|
-
const message =
|
|
2499
|
-
console.error(
|
|
2500
|
-
|
|
2501
|
-
Run \`vibeview dev\` without --app to pick from the list.`
|
|
2502
|
-
);
|
|
2511
|
+
const { message, hint } = staleAppFlagError(choice.staleFlag, platform, allApps);
|
|
2512
|
+
console.error(chalk4.red(`error: ${message}`) + `
|
|
2513
|
+
${hint}`);
|
|
2503
2514
|
emit(opts.json, { event: "error", message });
|
|
2504
2515
|
process.exit(2);
|
|
2505
2516
|
}
|
|
@@ -2594,6 +2605,12 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2594
2605
|
});
|
|
2595
2606
|
const commitConfig = () => {
|
|
2596
2607
|
saveDevConfig(process.cwd(), cfg);
|
|
2608
|
+
if (existing) {
|
|
2609
|
+
const changes = describeConfigChanges(existing, cfg);
|
|
2610
|
+
if (changes.length) {
|
|
2611
|
+
console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2597
2614
|
if (isFirstRun) {
|
|
2598
2615
|
console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
|
|
2599
2616
|
const hint = fillLaterHint(
|
|
@@ -2713,7 +2730,7 @@ async function runDevInProcess(opts) {
|
|
|
2713
2730
|
const captureMetroLine = (line) => {
|
|
2714
2731
|
try {
|
|
2715
2732
|
if (!metroLogStream) {
|
|
2716
|
-
mkdirSync3(
|
|
2733
|
+
mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
|
|
2717
2734
|
metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
|
|
2718
2735
|
}
|
|
2719
2736
|
metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
|
|
@@ -2806,12 +2823,7 @@ async function runDevInProcess(opts) {
|
|
|
2806
2823
|
console.error(chalk4.yellow(`\u26A0 ${evt.message}`));
|
|
2807
2824
|
}
|
|
2808
2825
|
} else if (evt.event === "queued") {
|
|
2809
|
-
|
|
2810
|
-
console.error(
|
|
2811
|
-
chalk4.yellow(
|
|
2812
|
-
`\u23F3 every device is busy \u2014 waiting in the queue${where}, Ctrl-C to give up`
|
|
2813
|
-
)
|
|
2814
|
-
);
|
|
2826
|
+
console.error(chalk4.yellow(queuedLine(evt.position, evt.message)));
|
|
2815
2827
|
} else if (evt.event === "tunnel_connected") {
|
|
2816
2828
|
everConnected = true;
|
|
2817
2829
|
console.error(chalk4.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
|
|
@@ -3056,7 +3068,7 @@ function formatDevicesHuman(devices) {
|
|
|
3056
3068
|
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
3057
3069
|
const lines = [chalk5.bold(fmt(HEADERS2))];
|
|
3058
3070
|
for (const row of rows.sort(rowSort)) {
|
|
3059
|
-
const statusColored = row[5] === "available" ? chalk5.green(row[5]) : row[5] === "busy" ? chalk5.yellow(row[5]) : chalk5.dim(row[5]);
|
|
3071
|
+
const statusColored = row[5] === "available" ? chalk5.green(row[5]) : row[5] === "busy" || row[5] === "updating" ? chalk5.yellow(row[5]) : chalk5.dim(row[5]);
|
|
3060
3072
|
lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
|
|
3061
3073
|
}
|
|
3062
3074
|
return lines.join("\n") + "\n";
|
|
@@ -3070,42 +3082,96 @@ function filterDeviceModels(models, opts) {
|
|
|
3070
3082
|
os_version: m.os_version,
|
|
3071
3083
|
device_type: m.device_type,
|
|
3072
3084
|
category: m.category,
|
|
3073
|
-
form_factor: m.form_factor ?? null
|
|
3085
|
+
form_factor: m.form_factor ?? null,
|
|
3086
|
+
// MCP only: an agent has no other way to see whether a model is free.
|
|
3087
|
+
...opts.availability && typeof m.free === "number" && typeof m.total === "number" ? { free: m.free, total: m.total } : {}
|
|
3074
3088
|
}));
|
|
3075
3089
|
}
|
|
3076
3090
|
var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
|
|
3077
|
-
function formatDeviceModelsHuman(models) {
|
|
3091
|
+
function formatDeviceModelsHuman(models, opts = {}) {
|
|
3078
3092
|
if (models.length === 0) {
|
|
3079
3093
|
return "No device models available right now.\n";
|
|
3080
3094
|
}
|
|
3081
|
-
const
|
|
3095
|
+
const headers = [...MODEL_HEADERS, ...opts.availability ? ["FREE", "BUSY"] : []];
|
|
3096
|
+
const rows = models.map((m) => [
|
|
3097
|
+
m.model,
|
|
3098
|
+
m.os_version,
|
|
3099
|
+
m.device_type,
|
|
3100
|
+
m.category,
|
|
3101
|
+
m.form_factor ?? "-",
|
|
3102
|
+
...opts.availability ? availabilityCells(m) : []
|
|
3103
|
+
]).sort(
|
|
3082
3104
|
(a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
|
|
3083
3105
|
);
|
|
3084
|
-
const widths =
|
|
3106
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3085
3107
|
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
3086
|
-
return [chalk5.bold(fmt(
|
|
3108
|
+
return [chalk5.bold(fmt(headers)), ...rows.map(fmt)].join("\n") + "\n";
|
|
3109
|
+
}
|
|
3110
|
+
function availabilityCells(m) {
|
|
3111
|
+
if (typeof m.free !== "number" || typeof m.total !== "number") return ["?", "?"];
|
|
3112
|
+
return [String(m.free), String(Math.max(0, m.total - m.free))];
|
|
3087
3113
|
}
|
|
3088
3114
|
|
|
3089
3115
|
// src/agent/registry.ts
|
|
3090
|
-
var
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
[
|
|
3094
|
-
"
|
|
3095
|
-
|
|
3116
|
+
var PRESS_BUTTONS = {
|
|
3117
|
+
ios: ["home", "back", "lock", "siri", "enter"],
|
|
3118
|
+
android: ["home", "back", "lock", "enter"],
|
|
3119
|
+
appleTv: [
|
|
3120
|
+
"dpad_up",
|
|
3121
|
+
"dpad_down",
|
|
3122
|
+
"dpad_left",
|
|
3123
|
+
"dpad_right",
|
|
3124
|
+
"dpad_center",
|
|
3125
|
+
"back",
|
|
3126
|
+
"home",
|
|
3127
|
+
"menu"
|
|
3128
|
+
],
|
|
3129
|
+
androidTv: [
|
|
3130
|
+
"dpad_up",
|
|
3131
|
+
"dpad_down",
|
|
3132
|
+
"dpad_left",
|
|
3133
|
+
"dpad_right",
|
|
3134
|
+
"dpad_center",
|
|
3135
|
+
"back",
|
|
3136
|
+
"home",
|
|
3137
|
+
"enter"
|
|
3096
3138
|
],
|
|
3097
|
-
[
|
|
3098
|
-
"
|
|
3099
|
-
|
|
3139
|
+
roku: [
|
|
3140
|
+
"dpad_up",
|
|
3141
|
+
"dpad_down",
|
|
3142
|
+
"dpad_left",
|
|
3143
|
+
"dpad_right",
|
|
3144
|
+
"dpad_center",
|
|
3145
|
+
"back",
|
|
3146
|
+
"enter",
|
|
3147
|
+
"up",
|
|
3148
|
+
"down",
|
|
3149
|
+
"left",
|
|
3150
|
+
"right",
|
|
3151
|
+
"select",
|
|
3152
|
+
"backspace",
|
|
3153
|
+
"play",
|
|
3154
|
+
"rewind",
|
|
3155
|
+
"forward",
|
|
3156
|
+
"replay",
|
|
3157
|
+
"info"
|
|
3100
3158
|
]
|
|
3159
|
+
};
|
|
3160
|
+
var PRESS_PLATFORM_LABELS = [
|
|
3161
|
+
["ios", "iPhone / iPad"],
|
|
3162
|
+
["android", "Android phone / tablet"],
|
|
3163
|
+
["appleTv", "Apple TV"],
|
|
3164
|
+
["androidTv", "Android TV"],
|
|
3165
|
+
["roku", "Roku (beta)"]
|
|
3101
3166
|
];
|
|
3167
|
+
var PRESS_BUTTONS_BY_PLATFORM = PRESS_PLATFORM_LABELS.map(([key, label]) => [label, PRESS_BUTTONS[key]]);
|
|
3168
|
+
function pressButtonLines(indent) {
|
|
3169
|
+
return PRESS_BUTTONS_BY_PLATFORM.map(
|
|
3170
|
+
([platform, buttons]) => `${indent}${platform}: ${buttons.join(", ")}`
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3102
3173
|
function pressButtonsHelp() {
|
|
3103
|
-
return [
|
|
3104
|
-
"Valid buttons:",
|
|
3105
|
-
...PRESS_BUTTONS_BY_PLATFORM.map(
|
|
3106
|
-
([platform, buttons]) => ` ${platform}: ${buttons.join(", ")}`
|
|
3107
|
-
)
|
|
3108
|
-
].join("\n");
|
|
3174
|
+
return ["Valid buttons:", ...pressButtonLines(" ")].join("\n");
|
|
3109
3175
|
}
|
|
3110
3176
|
var AGENT_COMMANDS = [
|
|
3111
3177
|
{
|
|
@@ -3401,7 +3467,7 @@ var AGENT_COMMANDS = [
|
|
|
3401
3467
|
cli: "positional",
|
|
3402
3468
|
type: "string",
|
|
3403
3469
|
required: true,
|
|
3404
|
-
description:
|
|
3470
|
+
description: `Button to press. ${pressButtonLines("").join(". ")}.`
|
|
3405
3471
|
}
|
|
3406
3472
|
]
|
|
3407
3473
|
},
|
|
@@ -3883,7 +3949,6 @@ export {
|
|
|
3883
3949
|
loadDevConfig,
|
|
3884
3950
|
parseBuildId,
|
|
3885
3951
|
checkDevBuild,
|
|
3886
|
-
formatUtcTimestamp,
|
|
3887
3952
|
buildRows,
|
|
3888
3953
|
formatBuildRowsHuman,
|
|
3889
3954
|
listBuilds,
|