vibeview 0.11.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.
- package/dist/{chunk-4UGLSIZM.js → chunk-NFFLJNUI.js} +752 -73
- package/dist/{chunk-AG3RZCPW.js → chunk-OHRTLUBD.js} +123 -8
- package/dist/cli.js +253 -21
- package/dist/{server-S6OCJERW.js → server-A44Z2HUK.js} +3 -3
- package/dist/{upload-app-JIOFLMS3.js → upload-app-G5SEYBTU.js} +1 -1
- package/package.json +2 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ApiClient,
|
|
4
|
+
createSpinner,
|
|
4
5
|
loadConfig,
|
|
5
6
|
resolveAuth,
|
|
6
7
|
uploadAppBuild
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OHRTLUBD.js";
|
|
8
9
|
|
|
9
10
|
// src/lib/dev-state.ts
|
|
10
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -72,6 +73,7 @@ function clearDevState(cwd) {
|
|
|
72
73
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
73
74
|
import { join as join2 } from "path";
|
|
74
75
|
var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv", "roku"];
|
|
76
|
+
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
75
77
|
var PLATFORM_NEEDS_METRO = {
|
|
76
78
|
ios: true,
|
|
77
79
|
android: true,
|
|
@@ -165,6 +167,26 @@ function loadDevConfig(cwd) {
|
|
|
165
167
|
}
|
|
166
168
|
return cfg;
|
|
167
169
|
}
|
|
170
|
+
function validateBuildEnv(env, platformKey) {
|
|
171
|
+
if (env === void 0) return;
|
|
172
|
+
if (typeof env !== "object" || env === null || Array.isArray(env)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`vibeview.json: platforms.${platformKey}.build.env must be an object of string values`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
for (const [envKey, envValue] of Object.entries(env)) {
|
|
178
|
+
if (typeof envValue !== "string") {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`vibeview.json: platforms.${platformKey}.build.env must be an object of string values`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (!ENV_KEY_RE.test(envKey)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`vibeview.json: platforms.${platformKey}.build.env key "${envKey}" is not a valid environment variable name (must match ${ENV_KEY_RE})`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
168
190
|
function saveDevConfig(cwd, cfg) {
|
|
169
191
|
writeFileSync2(join2(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
|
|
170
192
|
}
|
|
@@ -198,7 +220,7 @@ function buildSavedConfig(input2) {
|
|
|
198
220
|
[platform]: {
|
|
199
221
|
appId,
|
|
200
222
|
deviceCategory: isTv ? void 0 : deviceCategory,
|
|
201
|
-
...existing?.platforms?.[platform]?.build ? { build: existing.platforms[platform].build } : {},
|
|
223
|
+
...existing?.platforms?.[platform]?.build ? { build: { ...existing.platforms[platform].build } } : {},
|
|
202
224
|
...isFirstRun && firstRunBuild ? { build: firstRunBuild } : {},
|
|
203
225
|
// Never invented — absent means "no extra ignores", and first-run
|
|
204
226
|
// setup does not ask for it. Only ever carried forward.
|
|
@@ -224,7 +246,7 @@ async function checkMetro(port, timeoutMs = 1500) {
|
|
|
224
246
|
}
|
|
225
247
|
|
|
226
248
|
// src/commands/build.ts
|
|
227
|
-
import { spawn } from "child_process";
|
|
249
|
+
import { spawn as spawn2 } from "child_process";
|
|
228
250
|
import { createInterface } from "readline";
|
|
229
251
|
import { select as select2 } from "@inquirer/prompts";
|
|
230
252
|
import chalk from "chalk";
|
|
@@ -436,7 +458,187 @@ async function promptMetroCommand(cwd, platform) {
|
|
|
436
458
|
return input({ message: "Metro command", required: true });
|
|
437
459
|
}
|
|
438
460
|
|
|
461
|
+
// src/lib/source-pack.ts
|
|
462
|
+
import { execFile, spawn } from "child_process";
|
|
463
|
+
import { lstatSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
464
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
465
|
+
import { join as join5 } from "path";
|
|
466
|
+
import ignore from "ignore";
|
|
467
|
+
var NotAGitRepoError = class extends Error {
|
|
468
|
+
constructor(message) {
|
|
469
|
+
super(message);
|
|
470
|
+
this.name = "NotAGitRepoError";
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var SourceTooLargeError = class extends Error {
|
|
474
|
+
constructor(message, largest) {
|
|
475
|
+
super(message);
|
|
476
|
+
this.largest = largest;
|
|
477
|
+
this.name = "SourceTooLargeError";
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var FORCE_EXCLUDE_PREFIXES = ["node_modules/", "ios/Pods/", ".git/"];
|
|
481
|
+
function isForceExcluded(relPath) {
|
|
482
|
+
for (const prefix of FORCE_EXCLUDE_PREFIXES) {
|
|
483
|
+
if (relPath.startsWith(prefix) || relPath.includes(`/${prefix}`)) return true;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
async function listSourceFiles(projectRoot) {
|
|
488
|
+
const stdout = await runGitLsFiles(projectRoot);
|
|
489
|
+
const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
|
|
490
|
+
let filtered = rawPaths;
|
|
491
|
+
const vvignorePath = join5(projectRoot, ".vvignore");
|
|
492
|
+
if (statOrNull(vvignorePath)?.isFile()) {
|
|
493
|
+
const ig = ignore().add(readFileSync4(vvignorePath, "utf-8"));
|
|
494
|
+
filtered = filtered.filter((p) => !ig.ignores(p));
|
|
495
|
+
}
|
|
496
|
+
const result = [];
|
|
497
|
+
for (const relPath of filtered) {
|
|
498
|
+
if (isForceExcluded(relPath)) continue;
|
|
499
|
+
const st = statOrNull(join5(projectRoot, relPath));
|
|
500
|
+
if (!st) continue;
|
|
501
|
+
result.push(relPath);
|
|
502
|
+
}
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
function statOrNull(path) {
|
|
506
|
+
try {
|
|
507
|
+
return lstatSync(path);
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function runGitLsFiles(projectRoot) {
|
|
513
|
+
return new Promise((resolve, reject) => {
|
|
514
|
+
const child = spawn("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
|
515
|
+
cwd: projectRoot,
|
|
516
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
517
|
+
});
|
|
518
|
+
const stdoutChunks = [];
|
|
519
|
+
const stderrChunks = [];
|
|
520
|
+
child.stdout.on("data", (c) => stdoutChunks.push(c));
|
|
521
|
+
child.stderr.on("data", (c) => stderrChunks.push(c));
|
|
522
|
+
child.on("error", (err) => {
|
|
523
|
+
reject(
|
|
524
|
+
new NotAGitRepoError(
|
|
525
|
+
`failed to run git in ${projectRoot}: ${err.message} \u2014 is git installed and on PATH?`
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
child.on("close", (code) => {
|
|
530
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
531
|
+
if (code !== 0) {
|
|
532
|
+
if (/not a git repository/i.test(stderr)) {
|
|
533
|
+
reject(new NotAGitRepoError(`${projectRoot} is not a git repository`));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
reject(new Error(`git ls-files failed (exit ${code}) in ${projectRoot}: ${stderr.trim()}`));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
resolve(Buffer.concat(stdoutChunks).toString("utf-8"));
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
var DEFAULT_CAP_BYTES = 2 * 1024 * 1024 * 1024;
|
|
544
|
+
async function packSource(projectRoot, files, opts = {}) {
|
|
545
|
+
const capBytes = opts.capBytes ?? DEFAULT_CAP_BYTES;
|
|
546
|
+
let totalBytes = 0;
|
|
547
|
+
const sized = [];
|
|
548
|
+
for (const relPath of files) {
|
|
549
|
+
const bytes = lstatSync(join5(projectRoot, relPath)).size;
|
|
550
|
+
totalBytes += bytes;
|
|
551
|
+
sized.push({ path: relPath, bytes });
|
|
552
|
+
}
|
|
553
|
+
if (totalBytes > capBytes) {
|
|
554
|
+
const largest = sized.slice().sort((a, b) => b.bytes - a.bytes).slice(0, 5);
|
|
555
|
+
throw new SourceTooLargeError(
|
|
556
|
+
`source exceeds the ${formatBytes(capBytes)} pack limit (${formatBytes(totalBytes)} across ${files.length} files)`,
|
|
557
|
+
largest
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const tmpDir = mkdtempSync2(join5(tmpdir2(), "vibeview-source-pack-"));
|
|
561
|
+
const cleanup = () => rmSync3(tmpDir, { recursive: true, force: true });
|
|
562
|
+
try {
|
|
563
|
+
const supportsNull = await probeTarNullSupport();
|
|
564
|
+
const tarPath = join5(tmpDir, "source.tgz");
|
|
565
|
+
if (supportsNull) {
|
|
566
|
+
const listPath = join5(tmpDir, "files.list");
|
|
567
|
+
writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
|
|
568
|
+
await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
|
|
569
|
+
} else {
|
|
570
|
+
const badName = files.find((f) => f.includes("\n"));
|
|
571
|
+
if (badName) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`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`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const listPath = join5(tmpDir, "files.list");
|
|
577
|
+
writeFileSync3(listPath, files.map((f) => `${f}
|
|
578
|
+
`).join(""));
|
|
579
|
+
await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
|
|
580
|
+
}
|
|
581
|
+
const sizeBytes = statSync3(tarPath).size;
|
|
582
|
+
return { tarPath, sizeBytes, cleanup };
|
|
583
|
+
} catch (err) {
|
|
584
|
+
cleanup();
|
|
585
|
+
throw err;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function formatBytes(bytes) {
|
|
589
|
+
const mb = bytes / (1024 * 1024);
|
|
590
|
+
if (mb >= 1024) return `${(mb / 1024).toFixed(2)} GiB`;
|
|
591
|
+
return `${mb.toFixed(1)} MiB`;
|
|
592
|
+
}
|
|
593
|
+
var nullSupportCache = null;
|
|
594
|
+
function probeTarNullSupport() {
|
|
595
|
+
if (!nullSupportCache) {
|
|
596
|
+
nullSupportCache = new Promise((resolve) => {
|
|
597
|
+
const tmp = mkdtempSync2(join5(tmpdir2(), "vibeview-tar-probe-"));
|
|
598
|
+
const listPath = join5(tmp, "empty.list");
|
|
599
|
+
writeFileSync3(listPath, "");
|
|
600
|
+
const out = join5(tmp, "probe.tgz");
|
|
601
|
+
execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
|
|
602
|
+
rmSync3(tmp, { recursive: true, force: true });
|
|
603
|
+
resolve(!err);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return nullSupportCache;
|
|
608
|
+
}
|
|
609
|
+
function runTar(args) {
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const child = spawn("tar", args, {
|
|
612
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
613
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
614
|
+
});
|
|
615
|
+
const stderrChunks = [];
|
|
616
|
+
child.stderr.on("data", (c) => stderrChunks.push(c));
|
|
617
|
+
child.on("error", (err) => {
|
|
618
|
+
reject(new Error(`failed to run tar: ${err.message} \u2014 is tar installed and on PATH?`));
|
|
619
|
+
});
|
|
620
|
+
child.on("close", (code) => {
|
|
621
|
+
if (code !== 0) {
|
|
622
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
623
|
+
reject(new Error(`tar failed (exit ${code}): ${stderr}`));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
resolve();
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
439
631
|
// src/commands/build.ts
|
|
632
|
+
var CLOUD_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
|
|
633
|
+
function isCloudPlatform(p) {
|
|
634
|
+
return CLOUD_PLATFORMS.includes(p);
|
|
635
|
+
}
|
|
636
|
+
var BuildFailError = class extends Error {
|
|
637
|
+
constructor(message) {
|
|
638
|
+
super(message);
|
|
639
|
+
this.name = "BuildFailError";
|
|
640
|
+
}
|
|
641
|
+
};
|
|
440
642
|
var TAIL_LINES = 30;
|
|
441
643
|
async function runBuildForPlatform(cwd, platform, build, opts, log, deps = { upload: uploadAppBuild }) {
|
|
442
644
|
const tail = await runShell(build.command, cwd, log);
|
|
@@ -463,7 +665,7 @@ async function runBuildForPlatform(cwd, platform, build, opts, log, deps = { upl
|
|
|
463
665
|
}
|
|
464
666
|
function runShell(command, cwd, log) {
|
|
465
667
|
return new Promise((resolve) => {
|
|
466
|
-
const child =
|
|
668
|
+
const child = spawn2(command, { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
467
669
|
const lines = [];
|
|
468
670
|
const onLine = (line) => {
|
|
469
671
|
lines.push(line);
|
|
@@ -476,9 +678,7 @@ function runShell(command, cwd, log) {
|
|
|
476
678
|
child.on("error", () => resolve({ code: 127, lines }));
|
|
477
679
|
});
|
|
478
680
|
}
|
|
479
|
-
async function
|
|
480
|
-
const cwd = process.cwd();
|
|
481
|
-
const cfg = loadDevConfig(cwd);
|
|
681
|
+
async function resolveBarePlatform(cwd, cfg, opts) {
|
|
482
682
|
if (opts.platform && !isDevPlatform(opts.platform)) {
|
|
483
683
|
console.error(chalk.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
|
|
484
684
|
process.exit(2);
|
|
@@ -491,35 +691,42 @@ async function buildCommand(opts) {
|
|
|
491
691
|
"no platform: pass --platform or run `vibeview dev` once to set up vibeview.json",
|
|
492
692
|
"config_required"
|
|
493
693
|
);
|
|
494
|
-
return;
|
|
495
694
|
}
|
|
496
695
|
platform = await select2({
|
|
497
696
|
message: "Platform to run on",
|
|
498
697
|
choices: platformChoices(cwd)
|
|
499
698
|
});
|
|
500
699
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const answered = await promptBuildConfig(cwd, platform);
|
|
512
|
-
if (!answered) {
|
|
513
|
-
fail(opts, "build config is required to run vibeview build", "config_required");
|
|
700
|
+
return platform;
|
|
701
|
+
}
|
|
702
|
+
async function buildCommand(opts, cloudDeps) {
|
|
703
|
+
const cwd = process.cwd();
|
|
704
|
+
const cfg = loadDevConfig(cwd);
|
|
705
|
+
try {
|
|
706
|
+
if (opts.cloud) {
|
|
707
|
+
await runCloudBuildCommand(cwd, cfg, opts, cloudDeps);
|
|
514
708
|
return;
|
|
515
709
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
710
|
+
const platform = await resolveBarePlatform(cwd, cfg, opts);
|
|
711
|
+
let build = cfg?.platforms?.[platform]?.build;
|
|
712
|
+
if (!build) {
|
|
713
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
714
|
+
fail(
|
|
715
|
+
opts,
|
|
716
|
+
`no build config for ${platform}: add platforms.${platform}.build {command, artifact} to vibeview.json`,
|
|
717
|
+
"config_required"
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const answered = await promptBuildConfig(cwd, platform);
|
|
721
|
+
if (!answered) {
|
|
722
|
+
fail(opts, "build config is required to run vibeview build", "config_required");
|
|
723
|
+
}
|
|
724
|
+
build = answered;
|
|
725
|
+
if (cfg?.platforms?.[platform]) {
|
|
726
|
+
cfg.platforms[platform].build = build;
|
|
727
|
+
saveDevConfig(cwd, cfg);
|
|
728
|
+
}
|
|
520
729
|
}
|
|
521
|
-
}
|
|
522
|
-
try {
|
|
523
730
|
const result = await runBuildForPlatform(
|
|
524
731
|
cwd,
|
|
525
732
|
platform,
|
|
@@ -549,17 +756,474 @@ async function buildCommand(opts) {
|
|
|
549
756
|
);
|
|
550
757
|
}
|
|
551
758
|
} catch (err) {
|
|
552
|
-
|
|
759
|
+
if (!(err instanceof BuildFailError)) {
|
|
760
|
+
printFailure(opts, err instanceof Error ? err.message : String(err));
|
|
761
|
+
}
|
|
762
|
+
process.exitCode = 1;
|
|
553
763
|
}
|
|
554
764
|
}
|
|
555
|
-
function
|
|
765
|
+
function printFailure(opts, message, code) {
|
|
556
766
|
if (opts.json) console.log(JSON.stringify({ event: "error", code, message }));
|
|
557
767
|
console.error(chalk.red(`error: ${message}`));
|
|
558
|
-
|
|
768
|
+
}
|
|
769
|
+
function fail(opts, message, code) {
|
|
770
|
+
printFailure(opts, message, code);
|
|
771
|
+
throw new BuildFailError(message);
|
|
772
|
+
}
|
|
773
|
+
function emitCloud(json, evt) {
|
|
774
|
+
if (json) console.log(JSON.stringify(evt));
|
|
775
|
+
}
|
|
776
|
+
async function resolveCloudPlatforms(cwd, cfg, opts) {
|
|
777
|
+
if (opts.platform === "all") {
|
|
778
|
+
const configured = Object.entries(cfg?.platforms ?? {});
|
|
779
|
+
const withBuild = configured.filter(([p, entry]) => isCloudPlatform(p) && entry?.build);
|
|
780
|
+
if (withBuild.length === 0) {
|
|
781
|
+
fail(
|
|
782
|
+
opts,
|
|
783
|
+
`--platform all: ${describeAllExclusions(configured)} \u2014 add platforms.<platform>.build {command, artifact} to vibeview.json`,
|
|
784
|
+
"config_required"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return withBuild.map(([p]) => p);
|
|
788
|
+
}
|
|
789
|
+
const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd, cfg, opts);
|
|
790
|
+
requireCloudCapable(opts, platform);
|
|
791
|
+
requireBuildBlock(opts, cfg, platform);
|
|
792
|
+
return [platform];
|
|
793
|
+
}
|
|
794
|
+
function validateExplicitPlatform(raw) {
|
|
795
|
+
if (!isDevPlatform(raw)) {
|
|
796
|
+
console.error(
|
|
797
|
+
chalk.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}, or "all"`)
|
|
798
|
+
);
|
|
799
|
+
process.exit(2);
|
|
800
|
+
}
|
|
801
|
+
return raw;
|
|
802
|
+
}
|
|
803
|
+
function requireCloudCapable(opts, platform) {
|
|
804
|
+
if (!isCloudPlatform(platform)) {
|
|
805
|
+
fail(
|
|
806
|
+
opts,
|
|
807
|
+
`cloud builds don't support ${platform} \u2014 pass one of ${CLOUD_PLATFORMS.join(", ")}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function requireBuildBlock(opts, cfg, platform) {
|
|
812
|
+
if (!cfg?.platforms?.[platform]?.build) {
|
|
813
|
+
fail(
|
|
814
|
+
opts,
|
|
815
|
+
`no build config for ${platform}: add platforms.${platform}.build {command, artifact} to vibeview.json`,
|
|
816
|
+
"config_required"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function describeAllExclusions(configured) {
|
|
821
|
+
if (configured.length === 0) return "no platforms are configured";
|
|
822
|
+
const unsupported = configured.filter(([p]) => !isCloudPlatform(p)).map(([p]) => p);
|
|
823
|
+
const missingBuild = configured.filter(([p, entry]) => isCloudPlatform(p) && !entry?.build).map(([p]) => p);
|
|
824
|
+
const parts = [];
|
|
825
|
+
if (unsupported.length > 0) {
|
|
826
|
+
parts.push(
|
|
827
|
+
`${unsupported.join(", ")} ${unsupported.length === 1 ? "isn't" : "aren't"} supported for cloud builds`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (missingBuild.length > 0) {
|
|
831
|
+
parts.push(
|
|
832
|
+
`${missingBuild.join(", ")} ${missingBuild.length === 1 ? "has" : "have"} no build block`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return parts.join("; ");
|
|
836
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
856
|
+
function defaultCloudBuildDeps(client) {
|
|
857
|
+
return {
|
|
858
|
+
listSourceFiles,
|
|
859
|
+
packSource,
|
|
860
|
+
createCloudBuilds: client.createCloudBuilds.bind(client),
|
|
861
|
+
getCloudBuildLogs: client.getCloudBuildLogs.bind(client),
|
|
862
|
+
getCloudBuild: client.getCloudBuild.bind(client),
|
|
863
|
+
downloadCloudBuildArtifact: client.downloadCloudBuildArtifact.bind(client)
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function formatMB(bytes) {
|
|
867
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
868
|
+
}
|
|
869
|
+
function formatDuration(startedAt, finishedAt) {
|
|
870
|
+
if (!startedAt || !finishedAt) return null;
|
|
871
|
+
const ms = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
872
|
+
if (!Number.isFinite(ms) || ms < 0) return null;
|
|
873
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
874
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
875
|
+
}
|
|
876
|
+
function formatCost(costMicro) {
|
|
877
|
+
return `$${(costMicro / 1e6).toFixed(2)}`;
|
|
878
|
+
}
|
|
879
|
+
async function runCloudBuildCommand(cwd, cfg, opts, deps) {
|
|
880
|
+
const platforms = await resolveCloudPlatforms(cwd, cfg, opts);
|
|
881
|
+
for (const p of platforms) {
|
|
882
|
+
try {
|
|
883
|
+
validateBuildEnv(cfg?.platforms?.[p]?.build?.env, p);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
fail(opts, err instanceof Error ? err.message : String(err));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
889
|
+
const client = new ApiClient(baseUrl, token);
|
|
890
|
+
const d = deps ?? defaultCloudBuildDeps(client);
|
|
891
|
+
let cleanupTar;
|
|
892
|
+
try {
|
|
893
|
+
let files;
|
|
894
|
+
try {
|
|
895
|
+
files = await d.listSourceFiles(cwd);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
if (err instanceof NotAGitRepoError) {
|
|
898
|
+
fail(
|
|
899
|
+
opts,
|
|
900
|
+
`${err.message} \u2014 cloud builds pack your project source via git; run this from inside a git repository`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
throw err;
|
|
904
|
+
}
|
|
905
|
+
const packSpinner = !opts.json ? createSpinner(`packing source (${files.length} files)`) : null;
|
|
906
|
+
packSpinner?.start();
|
|
907
|
+
let tarPath;
|
|
908
|
+
let sizeBytes;
|
|
909
|
+
try {
|
|
910
|
+
const packed = await d.packSource(cwd, files);
|
|
911
|
+
tarPath = packed.tarPath;
|
|
912
|
+
sizeBytes = packed.sizeBytes;
|
|
913
|
+
cleanupTar = packed.cleanup;
|
|
914
|
+
} catch (err) {
|
|
915
|
+
packSpinner?.stop();
|
|
916
|
+
if (err instanceof SourceTooLargeError) {
|
|
917
|
+
const offenders = err.largest.map((f) => ` ${f.path} (${formatMB(f.bytes)} MB)`).join("\n");
|
|
918
|
+
fail(opts, `${err.message}
|
|
919
|
+
Largest files:
|
|
920
|
+
${offenders}`);
|
|
921
|
+
}
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
packSpinner?.succeed(`packed source (${files.length} files, ${formatMB(sizeBytes)} MB)`);
|
|
925
|
+
const manifestEntries = platforms.map((p) => {
|
|
926
|
+
const entry = cfg.platforms[p];
|
|
927
|
+
const build = entry.build;
|
|
928
|
+
return {
|
|
929
|
+
platform: p,
|
|
930
|
+
app_public_id: entry.appId,
|
|
931
|
+
command: build.command,
|
|
932
|
+
artifact_path: build.artifact,
|
|
933
|
+
...build.env ? { env: build.env } : {}
|
|
934
|
+
};
|
|
935
|
+
});
|
|
936
|
+
const uploadSpinner = !opts.json ? createSpinner("uploading source...") : null;
|
|
937
|
+
uploadSpinner?.start();
|
|
938
|
+
let created;
|
|
939
|
+
try {
|
|
940
|
+
const resp = await d.createCloudBuilds({ builds: manifestEntries }, tarPath);
|
|
941
|
+
created = resp.builds;
|
|
942
|
+
} catch (err) {
|
|
943
|
+
uploadSpinner?.stop();
|
|
944
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
945
|
+
fail(opts, message);
|
|
946
|
+
}
|
|
947
|
+
uploadSpinner?.succeed(`uploaded \u2014 ${created.length} build(s) submitted`);
|
|
948
|
+
for (const b of created) {
|
|
949
|
+
emitCloud(opts.json, { event: "build_created", id: b.id, platform: b.platform });
|
|
950
|
+
}
|
|
951
|
+
if (!opts.json) {
|
|
952
|
+
for (const b of created) {
|
|
953
|
+
console.error(` ${chalk.cyan(b.id)} ${chalk.dim(`[${b.platform}]`)}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const shouldWait = opts.wait !== false || opts.download;
|
|
957
|
+
if (!shouldWait) {
|
|
958
|
+
process.exitCode = 0;
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const finished = await waitForCloudBuilds(created, opts, d);
|
|
962
|
+
let downloadFailed = false;
|
|
963
|
+
if (opts.download) {
|
|
964
|
+
for (const b of finished.values()) {
|
|
965
|
+
if (b.status !== "succeeded") continue;
|
|
966
|
+
try {
|
|
967
|
+
const result = await d.downloadCloudBuildArtifact(b.id, cwd);
|
|
968
|
+
emitCloud(opts.json, {
|
|
969
|
+
event: "artifact_saved",
|
|
970
|
+
id: b.id,
|
|
971
|
+
filename: result.filename,
|
|
972
|
+
path: result.path
|
|
973
|
+
});
|
|
974
|
+
if (!opts.json) {
|
|
975
|
+
console.error(chalk.green(`\u2713 saved ${result.filename}`));
|
|
976
|
+
}
|
|
977
|
+
} catch (err) {
|
|
978
|
+
downloadFailed = true;
|
|
979
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
980
|
+
emitCloud(opts.json, {
|
|
981
|
+
event: "error",
|
|
982
|
+
message: `error downloading ${b.id}: ${message}`
|
|
983
|
+
});
|
|
984
|
+
if (!opts.json) console.error(chalk.red(`error downloading ${b.id}: ${message}`));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const allSucceeded = Array.from(finished.values()).every((b) => b.status === "succeeded");
|
|
989
|
+
process.exitCode = allSucceeded && !downloadFailed ? 0 : 1;
|
|
990
|
+
} finally {
|
|
991
|
+
cleanupTar?.();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
var BUILD_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
995
|
+
"succeeded",
|
|
996
|
+
"failed",
|
|
997
|
+
"canceled",
|
|
998
|
+
"timed_out",
|
|
999
|
+
"infra_failed"
|
|
1000
|
+
]);
|
|
1001
|
+
var MAX_CONSECUTIVE_POLL_FAILURES = 5;
|
|
1002
|
+
async function waitForCloudBuilds(builds, opts, deps) {
|
|
1003
|
+
const now = deps.now ?? Date.now;
|
|
1004
|
+
const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
1005
|
+
const queuedPollIntervalMs = deps.queuedPollIntervalMs ?? (deps.pollIntervalMs !== void 0 ? pollIntervalMs : QUEUED_POLL_INTERVAL_MS);
|
|
1006
|
+
const multi = builds.length > 1;
|
|
1007
|
+
const state = new Map(builds.map((b) => [b.id, b]));
|
|
1008
|
+
const offsets = new Map(builds.map((b) => [b.id, 0]));
|
|
1009
|
+
const failures = new Map(builds.map((b) => [b.id, 0]));
|
|
1010
|
+
const finalized = /* @__PURE__ */ new Set();
|
|
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
|
+
);
|
|
1016
|
+
let lastError;
|
|
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
|
+
};
|
|
1028
|
+
const printLine = (id, platform, line) => {
|
|
1029
|
+
const prefix = multi ? `[${platform}] ` : "";
|
|
1030
|
+
if (!opts.json) console.error(prefix + line);
|
|
1031
|
+
};
|
|
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);
|
|
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()) {
|
|
1088
|
+
for (const build of state.values()) {
|
|
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
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
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;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
await pollAndProcess(build);
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
lastError = err;
|
|
1140
|
+
const count = (failures.get(build.id) ?? 0) + 1;
|
|
1141
|
+
failures.set(build.id, count);
|
|
1142
|
+
if (count >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1143
|
+
pollAbandoned.add(build.id);
|
|
1144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
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
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (stillWaiting()) {
|
|
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
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (pollAbandoned.size > 0 && pollAbandoned.size === abandoned.size && !Array.from(state.values()).some((b) => BUILD_TERMINAL_STATUSES.has(b.status))) {
|
|
1178
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`cloud build poll failed ${MAX_CONSECUTIVE_POLL_FAILURES} times in a row, giving up: ${message}
|
|
1181
|
+
The build may still be running server-side \u2014 resume watching it with \`vibeview builds logs <id> --follow\`.`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return state;
|
|
1185
|
+
}
|
|
1186
|
+
async function fetchFinalSummary(id, fallback, deps) {
|
|
1187
|
+
try {
|
|
1188
|
+
return await deps.getCloudBuild(id);
|
|
1189
|
+
} catch {
|
|
1190
|
+
return fallback;
|
|
1191
|
+
}
|
|
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
|
+
}
|
|
1213
|
+
function printOutcomeLine(build, multi) {
|
|
1214
|
+
const prefix = multi ? `[${build.platform}] ` : "";
|
|
1215
|
+
const ok = build.status === "succeeded";
|
|
1216
|
+
const mark = ok ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
1217
|
+
const duration = formatDuration(build.started_at, build.finished_at);
|
|
1218
|
+
let line = `${prefix}${mark} ${build.status}`;
|
|
1219
|
+
if (duration) line += ` (${duration})`;
|
|
1220
|
+
if (build.error_summary) line += ` \u2014 ${build.error_summary}`;
|
|
1221
|
+
if (build.cost_micro > 0) line += ` \u2014 cost: ${formatCost(build.cost_micro)} usage credit`;
|
|
1222
|
+
console.error(line);
|
|
559
1223
|
}
|
|
560
1224
|
|
|
561
1225
|
// src/commands/dev.ts
|
|
562
|
-
import { spawn as
|
|
1226
|
+
import { spawn as spawn4 } from "child_process";
|
|
563
1227
|
import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
|
|
564
1228
|
import { dirname as dirname5 } from "path";
|
|
565
1229
|
import { createInterface as createInterface3 } from "readline";
|
|
@@ -732,19 +1396,19 @@ var TunnelClient = class {
|
|
|
732
1396
|
};
|
|
733
1397
|
|
|
734
1398
|
// src/lib/metro-manager.ts
|
|
735
|
-
import { spawn as
|
|
1399
|
+
import { spawn as spawn3 } from "child_process";
|
|
736
1400
|
import { createInterface as createInterface2 } from "readline";
|
|
737
1401
|
|
|
738
1402
|
// src/lib/metro-state.ts
|
|
739
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
740
|
-
import { dirname as dirname3, join as
|
|
1403
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1404
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
741
1405
|
var STATE_DIR2 = ".vibeview";
|
|
742
1406
|
var STATE_FILE2 = "metro-state.json";
|
|
743
1407
|
function statePath2(cwd) {
|
|
744
|
-
return
|
|
1408
|
+
return join6(cwd, STATE_DIR2, STATE_FILE2);
|
|
745
1409
|
}
|
|
746
1410
|
function metroLogPath(cwd) {
|
|
747
|
-
return
|
|
1411
|
+
return join6(cwd, STATE_DIR2, "metro.log");
|
|
748
1412
|
}
|
|
749
1413
|
function isPidAlive2(pid) {
|
|
750
1414
|
try {
|
|
@@ -757,12 +1421,12 @@ function isPidAlive2(pid) {
|
|
|
757
1421
|
function writeMetroState(cwd, state) {
|
|
758
1422
|
const path = statePath2(cwd);
|
|
759
1423
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
760
|
-
|
|
1424
|
+
writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
|
|
761
1425
|
}
|
|
762
1426
|
function readMetroState(cwd) {
|
|
763
1427
|
let parsed;
|
|
764
1428
|
try {
|
|
765
|
-
parsed = JSON.parse(
|
|
1429
|
+
parsed = JSON.parse(readFileSync5(statePath2(cwd), "utf-8"));
|
|
766
1430
|
} catch {
|
|
767
1431
|
return null;
|
|
768
1432
|
}
|
|
@@ -774,7 +1438,7 @@ function readMetroState(cwd) {
|
|
|
774
1438
|
}
|
|
775
1439
|
function clearMetroState(cwd) {
|
|
776
1440
|
const path = statePath2(cwd);
|
|
777
|
-
if (existsSync4(path))
|
|
1441
|
+
if (existsSync4(path)) rmSync4(path, { force: true });
|
|
778
1442
|
}
|
|
779
1443
|
function killOwnedMetro(cwd) {
|
|
780
1444
|
const state = readMetroState(cwd);
|
|
@@ -832,7 +1496,7 @@ If Metro uses another port, pass --metro-port <port>.`
|
|
|
832
1496
|
return { status: "spawned", warnings };
|
|
833
1497
|
}
|
|
834
1498
|
async function spawnMetro(cwd, command, port, log, timeoutMs) {
|
|
835
|
-
const child =
|
|
1499
|
+
const child = spawn3(command, {
|
|
836
1500
|
cwd,
|
|
837
1501
|
shell: true,
|
|
838
1502
|
detached: true,
|
|
@@ -1157,6 +1821,10 @@ function lateWarningVerdict(info, reportedWarnings) {
|
|
|
1157
1821
|
return { toReport, blocking };
|
|
1158
1822
|
}
|
|
1159
1823
|
var ROKU_LIVENESS_INTERVAL_MS = 1e4;
|
|
1824
|
+
var ROKU_INSTALL_PENDING_INTERVAL_MS = 2e3;
|
|
1825
|
+
function rokuLivenessDelayMs(info) {
|
|
1826
|
+
return info?.app_install_pending ? ROKU_INSTALL_PENDING_INTERVAL_MS : ROKU_LIVENESS_INTERVAL_MS;
|
|
1827
|
+
}
|
|
1160
1828
|
function rokuKeyAction(char) {
|
|
1161
1829
|
if (char === "") return "interrupt";
|
|
1162
1830
|
switch (char.toLowerCase()) {
|
|
@@ -1619,36 +2287,42 @@ async function runDevInProcess(opts) {
|
|
|
1619
2287
|
}
|
|
1620
2288
|
const sessionId = handle.sessionId;
|
|
1621
2289
|
const reportedWarnings = handle.reportedWarnings;
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
info =
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
reportedWarnings
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
2290
|
+
let keepAlive;
|
|
2291
|
+
const scheduleLiveness = (delayMs) => {
|
|
2292
|
+
keepAlive = setTimeout(() => {
|
|
2293
|
+
void (async () => {
|
|
2294
|
+
if (stopping) return;
|
|
2295
|
+
let info = null;
|
|
2296
|
+
try {
|
|
2297
|
+
info = await api.getSession(sessionId);
|
|
2298
|
+
} catch {
|
|
2299
|
+
}
|
|
2300
|
+
const verdict = sessionLivenessVerdict(info);
|
|
2301
|
+
if (verdict.end && !stopping) {
|
|
2302
|
+
console.error(chalk2.red(verdict.message));
|
|
2303
|
+
emit(opts.json, { event: "session_ended", session_id: sessionId, code: 1 });
|
|
2304
|
+
endedEmitted = true;
|
|
2305
|
+
await stopSession(1);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const warningVerdict = lateWarningVerdict(info, reportedWarnings);
|
|
2309
|
+
for (const w of warningVerdict.toReport) {
|
|
2310
|
+
reportedWarnings.add(w);
|
|
2311
|
+
console.error(chalk2.yellow(`\u26A0 ${w}`));
|
|
2312
|
+
emit(opts.json, { event: "warning", message: w });
|
|
2313
|
+
}
|
|
2314
|
+
if (warningVerdict.blocking && !stopping) {
|
|
2315
|
+
emit(opts.json, { event: "session_ended", session_id: sessionId, code: 1 });
|
|
2316
|
+
endedEmitted = true;
|
|
2317
|
+
await stopSession(1);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (!stopping) scheduleLiveness(rokuLivenessDelayMs(info));
|
|
2321
|
+
})();
|
|
2322
|
+
}, delayMs);
|
|
2323
|
+
};
|
|
2324
|
+
scheduleLiveness(ROKU_INSTALL_PENDING_INTERVAL_MS);
|
|
2325
|
+
process.on("exit", () => clearTimeout(keepAlive));
|
|
1652
2326
|
}
|
|
1653
2327
|
} catch (err) {
|
|
1654
2328
|
const message = err.message;
|
|
@@ -1662,7 +2336,7 @@ async function dev(opts) {
|
|
|
1662
2336
|
return runDevInProcess(opts);
|
|
1663
2337
|
}
|
|
1664
2338
|
const rawArgs = process.argv.slice(2).filter((a) => a !== "--detach");
|
|
1665
|
-
const child =
|
|
2339
|
+
const child = spawn4(process.execPath, [process.argv[1], ...rawArgs, "--json", "--no-open"], {
|
|
1666
2340
|
detached: true,
|
|
1667
2341
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1668
2342
|
});
|
|
@@ -2194,7 +2868,7 @@ function agentExitCode(env) {
|
|
|
2194
2868
|
}
|
|
2195
2869
|
|
|
2196
2870
|
// src/commands/agent.ts
|
|
2197
|
-
import { writeFileSync as
|
|
2871
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2198
2872
|
var REF_RE = /^@?(e\d+)$/;
|
|
2199
2873
|
var COORDS_RE = /^(-?\d+),\s*(-?\d+)$/;
|
|
2200
2874
|
var FlagError = class extends Error {
|
|
@@ -2395,7 +3069,7 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
2395
3069
|
`);
|
|
2396
3070
|
process.exit(1);
|
|
2397
3071
|
}
|
|
2398
|
-
|
|
3072
|
+
writeFileSync5(outPath, imageBytes);
|
|
2399
3073
|
if (opts.json) {
|
|
2400
3074
|
console.log(JSON.stringify(envelope));
|
|
2401
3075
|
} else {
|
|
@@ -2423,6 +3097,11 @@ export {
|
|
|
2423
3097
|
readMetroState,
|
|
2424
3098
|
killOwnedMetro,
|
|
2425
3099
|
buildCommand,
|
|
3100
|
+
BUILD_TERMINAL_STATUSES,
|
|
3101
|
+
fetchFinalSummary,
|
|
3102
|
+
newTransitionState,
|
|
3103
|
+
printTransition,
|
|
3104
|
+
printOutcomeLine,
|
|
2426
3105
|
startDevSession,
|
|
2427
3106
|
PLATFORM_DEVICE,
|
|
2428
3107
|
dev,
|