vibeview 0.12.0 → 0.12.2
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.
|
@@ -128,8 +128,11 @@ function loadDevConfig(cwd) {
|
|
|
128
128
|
`vibeview.json: unknown platform "${key}" (expected one of ${DEV_PLATFORMS.join(", ")})`
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
|
-
if (!entry || typeof entry
|
|
132
|
-
throw new Error(`vibeview.json: platforms.${key}
|
|
131
|
+
if (!entry || typeof entry !== "object") {
|
|
132
|
+
throw new Error(`vibeview.json: platforms.${key} must be an object`);
|
|
133
|
+
}
|
|
134
|
+
if (entry.appId !== void 0 && (typeof entry.appId !== "string" || !entry.appId)) {
|
|
135
|
+
throw new Error(`vibeview.json: platforms.${key}.appId must be a non-empty string`);
|
|
133
136
|
}
|
|
134
137
|
if (entry.build !== void 0) {
|
|
135
138
|
if (typeof entry.build !== "object" || entry.build === null) {
|
|
@@ -411,6 +414,15 @@ function platformChoices(cwd) {
|
|
|
411
414
|
if (detectFlavor(cwd) === "roku") choices.unshift({ value: "roku", name: "Roku (beta)" });
|
|
412
415
|
return choices;
|
|
413
416
|
}
|
|
417
|
+
function appsForPlatform(apps, platform) {
|
|
418
|
+
return apps.filter((a) => a.platform === platform);
|
|
419
|
+
}
|
|
420
|
+
function noAppsMessage(platform, hasBuildConfig) {
|
|
421
|
+
if (hasBuildConfig) {
|
|
422
|
+
return `No ${platform} apps found \u2014 run \`vibeview dev --build\` (or \`vibeview build\`) to build and upload one.`;
|
|
423
|
+
}
|
|
424
|
+
return `No ${platform} apps found. Run \`vibeview build --platform ${platform}\` to build and upload one (it will ask for your build command), or upload a build with \`vibeview upload-app\`.`;
|
|
425
|
+
}
|
|
414
426
|
function firstRunAppStrategy(apps, buildConfigured) {
|
|
415
427
|
if (!buildConfigured) return "picker";
|
|
416
428
|
return apps.length === 0 ? "build_now" : "ask";
|
|
@@ -699,34 +711,59 @@ async function resolveBarePlatform(cwd, cfg, opts) {
|
|
|
699
711
|
}
|
|
700
712
|
return platform;
|
|
701
713
|
}
|
|
714
|
+
function persistPromptedBuild(cwd, cfg, platform, build) {
|
|
715
|
+
const next = cfg ?? { defaultPlatform: platform, platforms: {} };
|
|
716
|
+
const entry = next.platforms[platform];
|
|
717
|
+
if (entry) {
|
|
718
|
+
entry.build = build;
|
|
719
|
+
} else {
|
|
720
|
+
next.platforms[platform] = { build };
|
|
721
|
+
}
|
|
722
|
+
saveDevConfig(cwd, next);
|
|
723
|
+
return next;
|
|
724
|
+
}
|
|
725
|
+
function persistUploadedAppId(cwd, cfg, platform, build, appId) {
|
|
726
|
+
const next = cfg ?? { defaultPlatform: platform, platforms: {} };
|
|
727
|
+
const entry = next.platforms[platform];
|
|
728
|
+
if (!entry) {
|
|
729
|
+
next.platforms[platform] = { appId, build };
|
|
730
|
+
saveDevConfig(cwd, next);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (entry.appId !== appId || !entry.build) {
|
|
734
|
+
entry.appId = appId;
|
|
735
|
+
entry.build = build;
|
|
736
|
+
saveDevConfig(cwd, next);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
|
|
740
|
+
const existing = cfgRef.current?.platforms?.[platform]?.build;
|
|
741
|
+
if (existing) return existing;
|
|
742
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
743
|
+
fail(
|
|
744
|
+
opts,
|
|
745
|
+
`no build config for ${platform}: add platforms.${platform}.build {command, artifact} to vibeview.json`,
|
|
746
|
+
"config_required"
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const answered = await promptBuildConfig(cwd, platform);
|
|
750
|
+
if (!answered) {
|
|
751
|
+
fail(opts, "build config is required to run vibeview build", "config_required");
|
|
752
|
+
}
|
|
753
|
+
cfgRef.current = persistPromptedBuild(cwd, cfgRef.current, platform, answered);
|
|
754
|
+
return answered;
|
|
755
|
+
}
|
|
702
756
|
async function buildCommand(opts, cloudDeps) {
|
|
703
757
|
const cwd = process.cwd();
|
|
704
|
-
const
|
|
758
|
+
const cfgRef = { current: loadDevConfig(cwd) };
|
|
705
759
|
try {
|
|
706
760
|
if (opts.cloud) {
|
|
707
|
-
await runCloudBuildCommand(cwd,
|
|
761
|
+
await runCloudBuildCommand(cwd, cfgRef, opts, cloudDeps);
|
|
708
762
|
return;
|
|
709
763
|
}
|
|
710
|
-
const platform = await resolveBarePlatform(cwd,
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
}
|
|
729
|
-
}
|
|
764
|
+
const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
765
|
+
const build = await resolveBuildConfig(cwd, cfgRef, platform, opts);
|
|
766
|
+
const cfg = cfgRef.current;
|
|
730
767
|
const result = await runBuildForPlatform(
|
|
731
768
|
cwd,
|
|
732
769
|
platform,
|
|
@@ -735,16 +772,7 @@ async function buildCommand(opts, cloudDeps) {
|
|
|
735
772
|
(l) => console.error(chalk.dim(l))
|
|
736
773
|
);
|
|
737
774
|
for (const w of result.warnings ?? []) console.error(chalk.yellow(`\u26A0 ${w}`));
|
|
738
|
-
|
|
739
|
-
const entry = nextCfg.platforms[platform];
|
|
740
|
-
if (!entry) {
|
|
741
|
-
nextCfg.platforms[platform] = { appId: result.public_id, build };
|
|
742
|
-
saveDevConfig(cwd, nextCfg);
|
|
743
|
-
} else if (entry.appId !== result.public_id || !entry.build) {
|
|
744
|
-
entry.appId = result.public_id;
|
|
745
|
-
entry.build = build;
|
|
746
|
-
saveDevConfig(cwd, nextCfg);
|
|
747
|
-
}
|
|
775
|
+
persistUploadedAppId(cwd, cfg, platform, build, result.public_id);
|
|
748
776
|
if (opts.json) {
|
|
749
777
|
console.log(
|
|
750
778
|
JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
|
|
@@ -773,22 +801,29 @@ function fail(opts, message, code) {
|
|
|
773
801
|
function emitCloud(json, evt) {
|
|
774
802
|
if (json) console.log(JSON.stringify(evt));
|
|
775
803
|
}
|
|
776
|
-
async function resolveCloudPlatforms(cwd,
|
|
804
|
+
async function resolveCloudPlatforms(cwd, cfgRef, opts) {
|
|
777
805
|
if (opts.platform === "all") {
|
|
778
|
-
const configured = Object.entries(
|
|
806
|
+
const configured = Object.entries(cfgRef.current?.platforms ?? {});
|
|
779
807
|
const withBuild = configured.filter(([p, entry]) => isCloudPlatform(p) && entry?.build);
|
|
780
|
-
if (withBuild.length
|
|
808
|
+
if (withBuild.length > 0) return withBuild.map(([p]) => p);
|
|
809
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
781
810
|
fail(
|
|
782
811
|
opts,
|
|
783
812
|
`--platform all: ${describeAllExclusions(configured)} \u2014 add platforms.<platform>.build {command, artifact} to vibeview.json`,
|
|
784
813
|
"config_required"
|
|
785
814
|
);
|
|
786
815
|
}
|
|
787
|
-
|
|
816
|
+
const chosen = await resolveBarePlatform(cwd, cfgRef.current, {
|
|
817
|
+
...opts,
|
|
818
|
+
platform: void 0
|
|
819
|
+
});
|
|
820
|
+
requireCloudCapable(opts, chosen);
|
|
821
|
+
await resolveBuildConfig(cwd, cfgRef, chosen, opts);
|
|
822
|
+
return [chosen];
|
|
788
823
|
}
|
|
789
|
-
const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd,
|
|
824
|
+
const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
790
825
|
requireCloudCapable(opts, platform);
|
|
791
|
-
|
|
826
|
+
await resolveBuildConfig(cwd, cfgRef, platform, opts);
|
|
792
827
|
return [platform];
|
|
793
828
|
}
|
|
794
829
|
function validateExplicitPlatform(raw) {
|
|
@@ -808,14 +843,54 @@ function requireCloudCapable(opts, platform) {
|
|
|
808
843
|
);
|
|
809
844
|
}
|
|
810
845
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
846
|
+
function persistPickedAppId(cwd, cfg, platform, appId) {
|
|
847
|
+
const next = cfg ?? { defaultPlatform: platform, platforms: {} };
|
|
848
|
+
const entry = next.platforms[platform];
|
|
849
|
+
if (entry) {
|
|
850
|
+
entry.appId = appId;
|
|
851
|
+
} else {
|
|
852
|
+
next.platforms[platform] = { appId };
|
|
853
|
+
}
|
|
854
|
+
saveDevConfig(cwd, next);
|
|
855
|
+
return next;
|
|
856
|
+
}
|
|
857
|
+
async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
|
|
858
|
+
const missing = platforms.filter((p) => !cfgRef.current?.platforms?.[p]?.appId);
|
|
859
|
+
if (missing.length === 0) return;
|
|
860
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
813
861
|
fail(
|
|
814
862
|
opts,
|
|
815
|
-
`no
|
|
863
|
+
`no app configured for ${missing[0]}: cloud builds attach to an existing app. Run \`vibeview build --platform ${missing[0]}\` once to build and upload locally, or \`vibeview upload-app\`, then retry --cloud`,
|
|
816
864
|
"config_required"
|
|
817
865
|
);
|
|
818
866
|
}
|
|
867
|
+
let allApps;
|
|
868
|
+
try {
|
|
869
|
+
allApps = await listApps();
|
|
870
|
+
} catch (err) {
|
|
871
|
+
fail(
|
|
872
|
+
opts,
|
|
873
|
+
`could not fetch your app list \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
for (const platform of missing) {
|
|
877
|
+
const apps = appsForPlatform(allApps, platform);
|
|
878
|
+
if (apps.length === 0) {
|
|
879
|
+
fail(
|
|
880
|
+
opts,
|
|
881
|
+
`${noAppsMessage(platform, cfgRef.current?.platforms?.[platform]?.build !== void 0)} Then retry \`vibeview build --cloud\`.`,
|
|
882
|
+
"config_required"
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const appId = await select2({
|
|
886
|
+
message: platforms.length > 1 ? `Which app should these ${platform} builds attach to?` : "Which app should these builds attach to?",
|
|
887
|
+
// No platform suffix on the rows: the list is already filtered to this
|
|
888
|
+
// one platform, so `(ios)` on every row would be pure noise (same
|
|
889
|
+
// reasoning as dev.ts's picker).
|
|
890
|
+
choices: apps.map((a) => ({ value: a.public_id, name: a.name }))
|
|
891
|
+
});
|
|
892
|
+
cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, appId);
|
|
893
|
+
}
|
|
819
894
|
}
|
|
820
895
|
function describeAllExclusions(configured) {
|
|
821
896
|
if (configured.length === 0) return "no platforms are configured";
|
|
@@ -835,11 +910,29 @@ function describeAllExclusions(configured) {
|
|
|
835
910
|
return parts.join("; ");
|
|
836
911
|
}
|
|
837
912
|
var POLL_INTERVAL_MS = 2e3;
|
|
913
|
+
var QUEUED_POLL_INTERVAL_MS = 1e4;
|
|
914
|
+
function nextPollDelayMs(pendingStatuses, baseMs, queuedMs) {
|
|
915
|
+
if (pendingStatuses.length > 0 && pendingStatuses.every((s) => s === "queued")) {
|
|
916
|
+
return queuedMs;
|
|
917
|
+
}
|
|
918
|
+
return baseMs;
|
|
919
|
+
}
|
|
838
920
|
var POLL_SAFETY_MARGIN_MS = 10 * 60 * 1e3;
|
|
921
|
+
function runningCapDeadline(startedAt, timeoutS) {
|
|
922
|
+
return startedAt + timeoutS * 1e3 + POLL_SAFETY_MARGIN_MS;
|
|
923
|
+
}
|
|
924
|
+
var DEFAULT_QUEUE_WAIT_MS = 30 * 60 * 1e3;
|
|
925
|
+
function queueWaitMs() {
|
|
926
|
+
const raw = process.env.VIBEVIEW_QUEUE_WAIT_MS;
|
|
927
|
+
if (!raw) return DEFAULT_QUEUE_WAIT_MS;
|
|
928
|
+
const parsed = Number(raw);
|
|
929
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_QUEUE_WAIT_MS;
|
|
930
|
+
}
|
|
839
931
|
function defaultCloudBuildDeps(client) {
|
|
840
932
|
return {
|
|
841
933
|
listSourceFiles,
|
|
842
934
|
packSource,
|
|
935
|
+
listApps: client.listApps.bind(client),
|
|
843
936
|
createCloudBuilds: client.createCloudBuilds.bind(client),
|
|
844
937
|
getCloudBuildLogs: client.getCloudBuildLogs.bind(client),
|
|
845
938
|
getCloudBuild: client.getCloudBuild.bind(client),
|
|
@@ -859,8 +952,13 @@ function formatDuration(startedAt, finishedAt) {
|
|
|
859
952
|
function formatCost(costMicro) {
|
|
860
953
|
return `$${(costMicro / 1e6).toFixed(2)}`;
|
|
861
954
|
}
|
|
862
|
-
async function runCloudBuildCommand(cwd,
|
|
863
|
-
const platforms = await resolveCloudPlatforms(cwd,
|
|
955
|
+
async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
956
|
+
const platforms = await resolveCloudPlatforms(cwd, cfgRef, opts);
|
|
957
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
958
|
+
const client = new ApiClient(baseUrl, token);
|
|
959
|
+
const d = deps ?? defaultCloudBuildDeps(client);
|
|
960
|
+
await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
|
|
961
|
+
const cfg = cfgRef.current;
|
|
864
962
|
for (const p of platforms) {
|
|
865
963
|
try {
|
|
866
964
|
validateBuildEnv(cfg?.platforms?.[p]?.build?.env, p);
|
|
@@ -868,9 +966,6 @@ async function runCloudBuildCommand(cwd, cfg, opts, deps) {
|
|
|
868
966
|
fail(opts, err instanceof Error ? err.message : String(err));
|
|
869
967
|
}
|
|
870
968
|
}
|
|
871
|
-
const { token, baseUrl } = resolveAuth(opts);
|
|
872
|
-
const client = new ApiClient(baseUrl, token);
|
|
873
|
-
const d = deps ?? defaultCloudBuildDeps(client);
|
|
874
969
|
let cleanupTar;
|
|
875
970
|
try {
|
|
876
971
|
let files;
|
|
@@ -910,6 +1005,9 @@ ${offenders}`);
|
|
|
910
1005
|
const build = entry.build;
|
|
911
1006
|
return {
|
|
912
1007
|
platform: p,
|
|
1008
|
+
// Non-null assertion is safe: resolveCloudAppIds ran for every entry
|
|
1009
|
+
// in `platforms` above — it either persisted a picked appId onto the
|
|
1010
|
+
// entry or failed the whole command.
|
|
913
1011
|
app_public_id: entry.appId,
|
|
914
1012
|
command: build.command,
|
|
915
1013
|
artifact_path: build.artifact,
|
|
@@ -927,7 +1025,7 @@ ${offenders}`);
|
|
|
927
1025
|
const message = err instanceof Error ? err.message : String(err);
|
|
928
1026
|
fail(opts, message);
|
|
929
1027
|
}
|
|
930
|
-
uploadSpinner?.succeed(`uploaded \u2014 ${created.length} build(s)
|
|
1028
|
+
uploadSpinner?.succeed(`uploaded \u2014 ${created.length} build(s) submitted`);
|
|
931
1029
|
for (const b of created) {
|
|
932
1030
|
emitCloud(opts.json, { event: "build_created", id: b.id, platform: b.platform });
|
|
933
1031
|
}
|
|
@@ -985,75 +1083,179 @@ var MAX_CONSECUTIVE_POLL_FAILURES = 5;
|
|
|
985
1083
|
async function waitForCloudBuilds(builds, opts, deps) {
|
|
986
1084
|
const now = deps.now ?? Date.now;
|
|
987
1085
|
const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
1086
|
+
const queuedPollIntervalMs = deps.queuedPollIntervalMs ?? (deps.pollIntervalMs !== void 0 ? pollIntervalMs : QUEUED_POLL_INTERVAL_MS);
|
|
988
1087
|
const multi = builds.length > 1;
|
|
989
1088
|
const state = new Map(builds.map((b) => [b.id, b]));
|
|
990
1089
|
const offsets = new Map(builds.map((b) => [b.id, 0]));
|
|
991
1090
|
const failures = new Map(builds.map((b) => [b.id, 0]));
|
|
1091
|
+
const finalized = /* @__PURE__ */ new Set();
|
|
992
1092
|
const abandoned = /* @__PURE__ */ new Set();
|
|
1093
|
+
const pollAbandoned = /* @__PURE__ */ new Set();
|
|
1094
|
+
const transitions = new Map(
|
|
1095
|
+
builds.map((b) => [b.id, newTransitionState()])
|
|
1096
|
+
);
|
|
993
1097
|
let lastError;
|
|
994
|
-
const
|
|
995
|
-
const
|
|
1098
|
+
const runningStartedAt = /* @__PURE__ */ new Map();
|
|
1099
|
+
const queueStartedAt = new Map(builds.map((b) => [b.id, now()]));
|
|
1100
|
+
const queueCapMs = queueWaitMs();
|
|
1101
|
+
const waitStartedAt = now();
|
|
1102
|
+
const maxTimeoutMs = Math.max(0, ...builds.map((b) => b.timeout_s * 1e3));
|
|
1103
|
+
const initialBackstopDeadline = waitStartedAt + queueCapMs + maxTimeoutMs + POLL_SAFETY_MARGIN_MS;
|
|
1104
|
+
const backstopDeadlineFor = (id, timeoutS) => {
|
|
1105
|
+
const startedAt = runningStartedAt.get(id);
|
|
1106
|
+
if (startedAt === void 0) return initialBackstopDeadline;
|
|
1107
|
+
return Math.max(initialBackstopDeadline, runningCapDeadline(startedAt, timeoutS));
|
|
1108
|
+
};
|
|
996
1109
|
const printLine = (id, platform, line) => {
|
|
997
1110
|
const prefix = multi ? `[${platform}] ` : "";
|
|
998
1111
|
if (!opts.json) console.error(prefix + line);
|
|
999
1112
|
};
|
|
1000
|
-
const stillWaiting = () => Array.from(state.values()).some(
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1113
|
+
const stillWaiting = () => Array.from(state.values()).some((b) => !finalized.has(b.id) && !abandoned.has(b.id));
|
|
1114
|
+
const buildDeadline = (build) => {
|
|
1115
|
+
const startedAt = runningStartedAt.get(build.id);
|
|
1116
|
+
if (startedAt === void 0) return null;
|
|
1117
|
+
return runningCapDeadline(startedAt, build.timeout_s);
|
|
1118
|
+
};
|
|
1119
|
+
const giveUpLocally = (id, message) => {
|
|
1120
|
+
abandoned.add(id);
|
|
1121
|
+
emitCloud(opts.json, { event: "error", message });
|
|
1122
|
+
if (!opts.json) console.error(chalk.red(message));
|
|
1123
|
+
};
|
|
1124
|
+
const pollAndProcess = async (build) => {
|
|
1125
|
+
const offset = offsets.get(build.id) ?? 0;
|
|
1126
|
+
const logs = await deps.getCloudBuildLogs(build.id, offset);
|
|
1127
|
+
const updated = { ...build, status: logs.status };
|
|
1128
|
+
state.set(build.id, updated);
|
|
1129
|
+
if (logs.status === "running" && !runningStartedAt.has(build.id)) {
|
|
1130
|
+
runningStartedAt.set(build.id, now());
|
|
1131
|
+
}
|
|
1132
|
+
offsets.set(build.id, logs.offset);
|
|
1133
|
+
const transitionState = transitions.get(build.id);
|
|
1134
|
+
printTransition(build.id, build.platform, logs.status, transitionState, opts.json, printLine);
|
|
1135
|
+
if (logs.content) {
|
|
1136
|
+
emitCloud(opts.json, { event: "build_log", id: build.id, content: logs.content });
|
|
1137
|
+
for (const line of logs.content.split("\n")) {
|
|
1138
|
+
if (line.length === 0) continue;
|
|
1139
|
+
printLine(build.id, build.platform, line);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (BUILD_TERMINAL_STATUSES.has(logs.status)) {
|
|
1143
|
+
const final = await fetchFinalSummary(build.id, updated, deps);
|
|
1144
|
+
state.set(build.id, final);
|
|
1145
|
+
if (!opts.json) {
|
|
1146
|
+
printOutcomeLine(final, multi);
|
|
1147
|
+
}
|
|
1148
|
+
emitCloud(opts.json, {
|
|
1149
|
+
event: "build_finished",
|
|
1150
|
+
id: final.id,
|
|
1151
|
+
status: final.status,
|
|
1152
|
+
cost_micro: final.cost_micro,
|
|
1153
|
+
error_summary: final.error_summary
|
|
1154
|
+
});
|
|
1155
|
+
finalized.add(build.id);
|
|
1156
|
+
}
|
|
1157
|
+
failures.set(build.id, 0);
|
|
1158
|
+
return logs.status;
|
|
1159
|
+
};
|
|
1160
|
+
const finalPollBeforeGivingUp = async (build) => {
|
|
1161
|
+
try {
|
|
1162
|
+
const status = await pollAndProcess(build);
|
|
1163
|
+
return BUILD_TERMINAL_STATUSES.has(status) ? "terminal" : "nonterminal";
|
|
1164
|
+
} catch {
|
|
1165
|
+
return "poll_failed";
|
|
1008
1166
|
}
|
|
1167
|
+
};
|
|
1168
|
+
while (stillWaiting()) {
|
|
1009
1169
|
for (const build of state.values()) {
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1170
|
+
if (abandoned.has(build.id)) continue;
|
|
1171
|
+
if (finalized.has(build.id)) continue;
|
|
1172
|
+
if (BUILD_TERMINAL_STATUSES.has(build.status)) {
|
|
1173
|
+
try {
|
|
1174
|
+
await pollAndProcess(build);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
lastError = err;
|
|
1177
|
+
const count = (failures.get(build.id) ?? 0) + 1;
|
|
1178
|
+
failures.set(build.id, count);
|
|
1179
|
+
if (count >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1180
|
+
pollAbandoned.add(build.id);
|
|
1181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1182
|
+
giveUpLocally(
|
|
1183
|
+
build.id,
|
|
1184
|
+
`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}\``
|
|
1185
|
+
);
|
|
1019
1186
|
}
|
|
1020
1187
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
const
|
|
1027
|
-
state.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (build.status === "queued") {
|
|
1191
|
+
const queuedSince = queueStartedAt.get(build.id) ?? now();
|
|
1192
|
+
if (now() - queuedSince > queueCapMs) {
|
|
1193
|
+
const rescue = await finalPollBeforeGivingUp(build);
|
|
1194
|
+
if (state.get(build.id).status !== "queued") continue;
|
|
1195
|
+
giveUpLocally(
|
|
1196
|
+
build.id,
|
|
1197
|
+
rescue === "poll_failed" ? (
|
|
1198
|
+
// PR-445 review F6: the rescue poll failed, so "queued" is the
|
|
1199
|
+
// last status observed BEFORE the gap — not verified server
|
|
1200
|
+
// truth. Say so instead of asserting the build stayed queued.
|
|
1201
|
+
`${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\``
|
|
1202
|
+
) : `${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\``
|
|
1203
|
+
);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
const deadline = buildDeadline(build);
|
|
1208
|
+
if (deadline !== null && now() > deadline) {
|
|
1209
|
+
if (await finalPollBeforeGivingUp(build) === "terminal") continue;
|
|
1210
|
+
giveUpLocally(
|
|
1211
|
+
build.id,
|
|
1212
|
+
`${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}\``
|
|
1213
|
+
);
|
|
1214
|
+
continue;
|
|
1038
1215
|
}
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
await pollAndProcess(build);
|
|
1039
1219
|
} catch (err) {
|
|
1040
1220
|
lastError = err;
|
|
1041
1221
|
const count = (failures.get(build.id) ?? 0) + 1;
|
|
1042
1222
|
failures.set(build.id, count);
|
|
1043
1223
|
if (count >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1044
|
-
|
|
1224
|
+
pollAbandoned.add(build.id);
|
|
1045
1225
|
const message = err instanceof Error ? err.message : String(err);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1226
|
+
giveUpLocally(
|
|
1227
|
+
build.id,
|
|
1228
|
+
`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\``
|
|
1229
|
+
);
|
|
1049
1230
|
}
|
|
1050
1231
|
}
|
|
1051
1232
|
}
|
|
1052
1233
|
if (stillWaiting()) {
|
|
1053
|
-
|
|
1234
|
+
for (const build of state.values()) {
|
|
1235
|
+
if (BUILD_TERMINAL_STATUSES.has(build.status) || abandoned.has(build.id)) continue;
|
|
1236
|
+
const deadline = backstopDeadlineFor(build.id, build.timeout_s);
|
|
1237
|
+
if (now() <= deadline) continue;
|
|
1238
|
+
if (await finalPollBeforeGivingUp(build) === "terminal") continue;
|
|
1239
|
+
const postRescueDeadline = backstopDeadlineFor(build.id, build.timeout_s);
|
|
1240
|
+
if (now() <= postRescueDeadline) continue;
|
|
1241
|
+
const fresh = state.get(build.id);
|
|
1242
|
+
const capMinutes = Math.round((postRescueDeadline - waitStartedAt) / 6e4);
|
|
1243
|
+
giveUpLocally(
|
|
1244
|
+
build.id,
|
|
1245
|
+
`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\``
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (stillWaiting()) {
|
|
1250
|
+
const pendingStatuses = Array.from(state.values()).filter(
|
|
1251
|
+
(b) => !abandoned.has(b.id) && !finalized.has(b.id) && !BUILD_TERMINAL_STATUSES.has(b.status)
|
|
1252
|
+
).map((b) => b.status);
|
|
1253
|
+
await new Promise(
|
|
1254
|
+
(r) => setTimeout(r, nextPollDelayMs(pendingStatuses, pollIntervalMs, queuedPollIntervalMs))
|
|
1255
|
+
);
|
|
1054
1256
|
}
|
|
1055
1257
|
}
|
|
1056
|
-
if (
|
|
1258
|
+
if (pollAbandoned.size > 0 && pollAbandoned.size === abandoned.size && !Array.from(state.values()).some((b) => BUILD_TERMINAL_STATUSES.has(b.status))) {
|
|
1057
1259
|
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1058
1260
|
throw new Error(
|
|
1059
1261
|
`cloud build poll failed ${MAX_CONSECUTIVE_POLL_FAILURES} times in a row, giving up: ${message}
|
|
@@ -1069,6 +1271,26 @@ async function fetchFinalSummary(id, fallback, deps) {
|
|
|
1069
1271
|
return fallback;
|
|
1070
1272
|
}
|
|
1071
1273
|
}
|
|
1274
|
+
function newTransitionState() {
|
|
1275
|
+
return { printedQueued: false, printedRunning: false };
|
|
1276
|
+
}
|
|
1277
|
+
function printTransition(id, platform, status, state, json, printLine) {
|
|
1278
|
+
if (status === "queued" && !state.printedQueued) {
|
|
1279
|
+
state.printedQueued = true;
|
|
1280
|
+
if (json) {
|
|
1281
|
+
emitCloud(json, { event: "status", id, status: "queued" });
|
|
1282
|
+
} else {
|
|
1283
|
+
printLine(id, platform, chalk.dim("\u23F3 queued \u2014 waiting for a build slot"));
|
|
1284
|
+
}
|
|
1285
|
+
} else if (status === "running" && !state.printedRunning) {
|
|
1286
|
+
state.printedRunning = true;
|
|
1287
|
+
if (json) {
|
|
1288
|
+
emitCloud(json, { event: "status", id, status: "running" });
|
|
1289
|
+
} else {
|
|
1290
|
+
printLine(id, platform, `${chalk.cyan("\u25B6")} building`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1072
1294
|
function printOutcomeLine(build, multi) {
|
|
1073
1295
|
const prefix = multi ? `[${build.platform}] ` : "";
|
|
1074
1296
|
const ok = build.status === "succeeded";
|
|
@@ -1704,20 +1926,11 @@ var PLATFORM_DEVICE = {
|
|
|
1704
1926
|
// handheld type plus category tv) — the worker registers it as such.
|
|
1705
1927
|
roku: { deviceType: "roku", tv: true }
|
|
1706
1928
|
};
|
|
1707
|
-
function appsForPlatform(apps, platform) {
|
|
1708
|
-
return apps.filter((a) => a.platform === platform);
|
|
1709
|
-
}
|
|
1710
1929
|
function validateAppChoice(requested, fromFlag, apps) {
|
|
1711
1930
|
if (!requested) return {};
|
|
1712
1931
|
if (apps.some((a) => a.public_id === requested)) return { appId: requested };
|
|
1713
1932
|
return fromFlag ? { staleFlag: requested } : { staleCached: requested };
|
|
1714
1933
|
}
|
|
1715
|
-
function noAppsMessage(platform, hasBuildConfig) {
|
|
1716
|
-
if (hasBuildConfig) {
|
|
1717
|
-
return `No ${platform} apps found \u2014 run \`vibeview dev --build\` (or \`vibeview build\`) to build and upload one.`;
|
|
1718
|
-
}
|
|
1719
|
-
return `No ${platform} apps found. Run \`vibeview build --platform ${platform}\` to build and upload one (it will ask for your build command), or upload a build with \`vibeview upload-app\`.`;
|
|
1720
|
-
}
|
|
1721
1934
|
function requireNonInteractive(json) {
|
|
1722
1935
|
if (!json) return;
|
|
1723
1936
|
emit(json, {
|
|
@@ -2958,6 +3171,8 @@ export {
|
|
|
2958
3171
|
buildCommand,
|
|
2959
3172
|
BUILD_TERMINAL_STATUSES,
|
|
2960
3173
|
fetchFinalSummary,
|
|
3174
|
+
newTransitionState,
|
|
3175
|
+
printTransition,
|
|
2961
3176
|
printOutcomeLine,
|
|
2962
3177
|
startDevSession,
|
|
2963
3178
|
PLATFORM_DEVICE,
|
package/dist/cli.js
CHANGED
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
dev,
|
|
8
8
|
fetchFinalSummary,
|
|
9
9
|
killOwnedMetro,
|
|
10
|
+
newTransitionState,
|
|
10
11
|
printOutcomeLine,
|
|
12
|
+
printTransition,
|
|
11
13
|
readDevState,
|
|
12
14
|
readMetroState,
|
|
13
15
|
runAgentVerb
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OYLVR6XA.js";
|
|
15
17
|
import {
|
|
16
18
|
ApiClient,
|
|
17
19
|
EXIT_INFRA_ERROR,
|
|
@@ -1247,9 +1249,14 @@ async function followLogs(id, opts, deps) {
|
|
|
1247
1249
|
let offset = 0;
|
|
1248
1250
|
let status = "queued";
|
|
1249
1251
|
let consecutiveFailures = 0;
|
|
1252
|
+
const transitionState = newTransitionState();
|
|
1253
|
+
const printLine = (_id, _platform, line) => {
|
|
1254
|
+
if (!opts.json) console.error(line);
|
|
1255
|
+
};
|
|
1250
1256
|
while (!BUILD_TERMINAL_STATUSES.has(status)) {
|
|
1251
1257
|
try {
|
|
1252
1258
|
const logs = await deps.getCloudBuildLogs(id, offset);
|
|
1259
|
+
printTransition(id, "", logs.status, transitionState, opts.json, printLine);
|
|
1253
1260
|
if (logs.content) {
|
|
1254
1261
|
if (opts.json) {
|
|
1255
1262
|
console.log(JSON.stringify({ event: "build_log", id, content: logs.content }));
|
|
@@ -1710,7 +1717,7 @@ program.command("dev-stop").description("Stop a `vibeview dev --detach` session
|
|
|
1710
1717
|
program.command("mcp").description(
|
|
1711
1718
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
|
|
1712
1719
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
1713
|
-
const { runMcpServer } = await import("./server-
|
|
1720
|
+
const { runMcpServer } = await import("./server-DRMLNXEZ.js");
|
|
1714
1721
|
await runMcpServer(opts);
|
|
1715
1722
|
});
|
|
1716
1723
|
program.command("agent-setup").description(
|