vibeview 0.36.1 → 0.37.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/README.md CHANGED
@@ -34,7 +34,7 @@ vibeview dev
34
34
 
35
35
  [Live Development docs →](https://vibeview.io/docs/live-development)
36
36
 
37
- ## Cloud Builds (beta)
37
+ ## Cloud Builds
38
38
 
39
39
  Build your React Native app on VibeView's build fleet — no Mac, no CI
40
40
  pipeline, no local toolchain:
@@ -838,10 +838,24 @@ function iosWorkspaceName(cwd) {
838
838
  var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
839
839
  var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
840
840
  function installCommand(cwd) {
841
- if (existsSync3(join5(cwd, "yarn.lock"))) return "yarn install --immutable";
842
- if (existsSync3(join5(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
843
- if (existsSync3(join5(cwd, "package-lock.json"))) return "npm ci";
844
- return "yarn install --immutable";
841
+ const hit = JS_LOCKFILE_INSTALLS.find(([lockfile]) => existsSync3(join5(cwd, lockfile)));
842
+ return hit ? hit[1] : "npm install";
843
+ }
844
+ var JS_LOCKFILE_INSTALLS = [
845
+ ["yarn.lock", "yarn install --immutable"],
846
+ ["pnpm-lock.yaml", "pnpm install --frozen-lockfile"],
847
+ ["package-lock.json", "npm ci"],
848
+ ["npm-shrinkwrap.json", "npm ci"],
849
+ ["bun.lock", "npm install"],
850
+ ["bun.lockb", "npm install"]
851
+ ];
852
+ function bareInstallPrefix(cwd, platform) {
853
+ const legs = [];
854
+ if (isFile(join5(cwd, "package.json"))) legs.push(installCommand(cwd));
855
+ if (platform === "ios" && isFile(join5(cwd, "ios", "Podfile"))) {
856
+ legs.push("cd ios && pod install && cd ..");
857
+ }
858
+ return legs.map((l) => `${l} && `).join("");
845
859
  }
846
860
  function managedExpoAndroidCommand(cwd) {
847
861
  return `${installCommand(cwd)} && npx expo prebuild --platform android --no-install && cd android && ./gradlew assembleDebug`;
@@ -871,7 +885,7 @@ function rokuDefaults(cwd) {
871
885
  }
872
886
  return { buildCommand: ROKU_ZIP_COMMAND, artifact };
873
887
  }
874
- function defaultsFor(cwd, platform) {
888
+ function defaultsFor(cwd, platform, evaluatedIosName) {
875
889
  const flavor = detectFlavor(cwd);
876
890
  if (platform === "roku") return flavor === "roku" ? rokuDefaults(cwd) : {};
877
891
  if (flavor === "unknown" || flavor === "roku") return {};
@@ -880,13 +894,13 @@ function defaultsFor(cwd, platform) {
880
894
  if (platform === "android") {
881
895
  const managed = isManagedExpo(cwd, "android");
882
896
  return {
883
- buildCommand: managed ? managedExpoAndroidCommand(cwd) : "cd android && ./gradlew assembleDebug",
897
+ buildCommand: managed ? managedExpoAndroidCommand(cwd) : `${bareInstallPrefix(cwd, "android")}cd android && ./gradlew assembleDebug`,
884
898
  artifact: ANDROID_ARTIFACT,
885
899
  metroCommand
886
900
  };
887
901
  }
888
902
  if (isManagedExpo(cwd, "ios")) {
889
- const name = managedExpoIosName(cwd);
903
+ const name = managedExpoIosName(cwd, evaluatedIosName);
890
904
  return {
891
905
  buildCommand: name ? managedExpoIosCommand(cwd, name) : void 0,
892
906
  artifact: IOS_ARTIFACT,
@@ -895,14 +909,14 @@ function defaultsFor(cwd, platform) {
895
909
  }
896
910
  const ws = iosWorkspaceName(cwd);
897
911
  return {
898
- buildCommand: ws ? `xcodebuild -workspace ios/${ws}.xcworkspace -scheme ${ws} -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build` : void 0,
912
+ buildCommand: ws ? `${bareInstallPrefix(cwd, "ios")}xcodebuild -workspace ios/${ws}.xcworkspace -scheme ${ws} -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build CODE_SIGNING_ALLOWED=NO` : void 0,
899
913
  artifact: IOS_ARTIFACT,
900
914
  metroCommand
901
915
  };
902
916
  }
903
917
  var ANDROID_PRODUCTION_ARTIFACT = "android/app/build/outputs/bundle/release/*.aab";
904
918
  function androidProductionCommand(cwd) {
905
- return `${installCommand(cwd)} && cd android && ./gradlew bundleRelease`;
919
+ return `${bareInstallPrefix(cwd, "android")}cd android && ./gradlew bundleRelease`;
906
920
  }
907
921
  var IOS_PRODUCTION_ARTIFACT = "ios/build/*.ipa";
908
922
  function managedExpoAndroidProductionCommand(cwd) {
@@ -911,10 +925,13 @@ function managedExpoAndroidProductionCommand(cwd) {
911
925
  function expoSanitizedName(name) {
912
926
  return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
913
927
  }
914
- function managedExpoIosName(cwd) {
915
- if (existsSync3(join5(cwd, "app.config.js")) || existsSync3(join5(cwd, "app.config.ts"))) {
916
- return null;
917
- }
928
+ var DYNAMIC_EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs", "app.config.cjs"];
929
+ function dynamicExpoConfig(cwd) {
930
+ return DYNAMIC_EXPO_CONFIGS.find((f) => existsSync3(join5(cwd, f)));
931
+ }
932
+ function managedExpoIosName(cwd, evaluatedName) {
933
+ if (evaluatedName !== void 0) return expoSanitizedName(evaluatedName) || null;
934
+ if (dynamicExpoConfig(cwd)) return null;
918
935
  const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
919
936
  if (!raw) return null;
920
937
  const config = "expo" in raw ? raw.expo : raw;
@@ -929,7 +946,10 @@ function iosArchiveExportCommand(ws) {
929
946
  const exportCmd = `xcodebuild -exportArchive -archivePath ${archivePath} -exportPath ios/build -exportOptionsPlist "$VIBEVIEW_EXPORT_OPTIONS_PLIST"`;
930
947
  return `${archiveCmd} && ${exportCmd}`;
931
948
  }
932
- function productionDefaultsFor(cwd, platform) {
949
+ function managedExpoIosProductionCommand(cwd, name) {
950
+ return `${installCommand(cwd)} && npx expo prebuild --platform ios && ` + iosArchiveExportCommand(name);
951
+ }
952
+ function productionDefaultsFor(cwd, platform, evaluatedIosName) {
933
953
  if (platform === "android" || platform === "androidtv") {
934
954
  return {
935
955
  buildCommand: isManagedExpo(cwd, "android") ? managedExpoAndroidProductionCommand(cwd) : androidProductionCommand(cwd),
@@ -941,17 +961,17 @@ function productionDefaultsFor(cwd, platform) {
941
961
  }
942
962
  if (platform === "ios") {
943
963
  if (isManagedExpo(cwd, "ios")) {
944
- const name = managedExpoIosName(cwd);
964
+ const name = managedExpoIosName(cwd, evaluatedIosName);
945
965
  if (!name) return { artifact: IOS_PRODUCTION_ARTIFACT };
946
966
  return {
947
- buildCommand: `${installCommand(cwd)} && npx expo prebuild --platform ios && ` + iosArchiveExportCommand(name),
967
+ buildCommand: managedExpoIosProductionCommand(cwd, name),
948
968
  artifact: IOS_PRODUCTION_ARTIFACT
949
969
  };
950
970
  }
951
971
  const ws = iosWorkspaceName(cwd);
952
972
  if (!ws) return { artifact: IOS_PRODUCTION_ARTIFACT };
953
973
  return {
954
- buildCommand: `${installCommand(cwd)} && ${iosArchiveExportCommand(ws)}`,
974
+ buildCommand: `${bareInstallPrefix(cwd, "ios")}${iosArchiveExportCommand(ws)}`,
955
975
  artifact: IOS_PRODUCTION_ARTIFACT
956
976
  };
957
977
  }
@@ -980,36 +1000,65 @@ function pbxprojText(cwd) {
980
1000
  return null;
981
1001
  }
982
1002
  }
983
- var readExpoConfig = (cwd) => {
1003
+ function parseExpoConfigJson(stdout) {
1004
+ const lines = stdout.trim().split("\n");
1005
+ for (const text of [stdout, lines[lines.length - 1]]) {
1006
+ try {
1007
+ const parsed = JSON.parse(text);
1008
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1009
+ return parsed;
1010
+ }
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ return null;
1015
+ }
1016
+ var EXPO_CONFIG_TIMEOUT_MS = 3e4;
1017
+ var readExpoConfig = (cwd, timeoutMs = EXPO_CONFIG_TIMEOUT_MS) => {
984
1018
  const cli = join5(cwd, "node_modules", "expo", "bin", "cli");
985
1019
  if (!existsSync3(cli)) {
986
- return Promise.reject(new Error("Expo is not installed in this project (node_modules/expo)"));
1020
+ return Promise.reject(
1021
+ new Error("Expo isn't installed in this project yet (run your install command first)")
1022
+ );
987
1023
  }
988
1024
  return new Promise((resolve, reject) => {
989
- execFile(
1025
+ let settled = false;
1026
+ const settle = (fn) => {
1027
+ if (settled) return;
1028
+ settled = true;
1029
+ clearTimeout(timer);
1030
+ fn();
1031
+ };
1032
+ const child = execFile(
990
1033
  process.execPath,
991
1034
  [cli, "config", "--json", "--type", "public"],
992
1035
  {
993
1036
  cwd,
994
- timeout: 3e4,
995
1037
  maxBuffer: 8 * 1024 * 1024,
996
1038
  env: { ...process.env, EXPO_NO_TELEMETRY: "1", CI: "1" }
997
1039
  },
998
1040
  (err, stdout, stderr) => {
999
1041
  if (err) {
1000
1042
  const why = String(stderr || err.message).split("\n").map((l) => l.trim()).find((l) => l.length > 0);
1001
- reject(new Error(`expo config failed${why ? `: ${why}` : ""}`));
1043
+ settle(() => reject(new Error(`expo config failed${why ? `: ${why}` : ""}`)));
1002
1044
  return;
1003
1045
  }
1004
- try {
1005
- const parsed = JSON.parse(stdout);
1006
- if (parsed && typeof parsed === "object") resolve(parsed);
1007
- else reject(new Error("expo config printed no config"));
1008
- } catch {
1009
- reject(new Error("expo config printed something that is not JSON"));
1010
- }
1046
+ const parsed = parseExpoConfigJson(stdout);
1047
+ settle(
1048
+ () => parsed ? resolve(parsed) : reject(new Error("expo config printed something that is not JSON"))
1049
+ );
1011
1050
  }
1012
1051
  );
1052
+ child.stdin?.end();
1053
+ const timer = setTimeout(() => {
1054
+ child.kill("SIGKILL");
1055
+ child.stdout?.destroy();
1056
+ child.stderr?.destroy();
1057
+ settle(
1058
+ () => reject(new Error(`expo config timed out after ${Math.round(timeoutMs / 1e3)} s`))
1059
+ );
1060
+ }, timeoutMs);
1061
+ timer.unref();
1013
1062
  });
1014
1063
  };
1015
1064
  async function localAppIdentity(cwd, platform, readConfig = readExpoConfig) {
@@ -1020,9 +1069,7 @@ async function localAppIdentity(cwd, platform, readConfig = readExpoConfig) {
1020
1069
  let version = null;
1021
1070
  let bundleId = null;
1022
1071
  let versionUnreadable = null;
1023
- const dynamicName = ["app.config.ts", "app.config.js", "app.config.mjs", "app.config.cjs"].find(
1024
- (f) => existsSync3(join5(cwd, f))
1025
- );
1072
+ const dynamicName = dynamicExpoConfig(cwd);
1026
1073
  const staticRaw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
1027
1074
  const isExpo = Boolean(dynamicName) || staticRaw !== null && "expo" in staticRaw || existsSync3(join5(cwd, "node_modules", "expo", "package.json"));
1028
1075
  let expo = null;
@@ -1061,6 +1108,41 @@ async function localAppIdentity(cwd, platform, readConfig = readExpoConfig) {
1061
1108
  }
1062
1109
  return { reactNative, version, bundleId, versionUnreadable: version ? null : versionUnreadable };
1063
1110
  }
1111
+ var evaluatedNameCache = /* @__PURE__ */ new Map();
1112
+ function evaluatedExpoName(cwd, readConfig, onEvaluate) {
1113
+ const cached = evaluatedNameCache.get(cwd);
1114
+ if (cached) return cached;
1115
+ onEvaluate?.();
1116
+ const pending = Promise.resolve().then(() => readConfig(cwd)).then(
1117
+ (config) => typeof config.name === "string" ? { name: config.name } : { error: "the resolved config has no name" },
1118
+ (err) => ({
1119
+ error: err instanceof Error ? err.message : String(err)
1120
+ })
1121
+ );
1122
+ evaluatedNameCache.set(cwd, pending);
1123
+ return pending;
1124
+ }
1125
+ async function resolvedDefaultsFor(cwd, platform, opts = {}) {
1126
+ const { production = false, readConfig = (c) => readExpoConfig(c), onEvaluate } = opts;
1127
+ const base = production ? productionDefaultsFor : defaultsFor;
1128
+ const dynamic = dynamicExpoConfig(cwd);
1129
+ if (platform !== "ios" || !dynamic || !isManagedExpo(cwd, "ios")) {
1130
+ return base(cwd, platform);
1131
+ }
1132
+ const evaluated = await evaluatedExpoName(cwd, readConfig, onEvaluate);
1133
+ if ("error" in evaluated) {
1134
+ return { ...base(cwd, platform), evaluatedConfig: dynamic, evaluationError: evaluated.error };
1135
+ }
1136
+ const named = base(cwd, platform, evaluated.name);
1137
+ if (!named.buildCommand) {
1138
+ return {
1139
+ ...named,
1140
+ evaluatedConfig: dynamic,
1141
+ evaluationError: `the name "${evaluated.name}" has no letters or digits`
1142
+ };
1143
+ }
1144
+ return { ...named, evaluatedConfig: dynamic };
1145
+ }
1064
1146
 
1065
1147
  // src/lib/dev-setup.ts
1066
1148
  function platformChoices(cwd) {
@@ -1095,6 +1177,37 @@ function fillLaterHint(skipped, platform) {
1095
1177
  }
1096
1178
  return lines.join("\n");
1097
1179
  }
1180
+ async function promptDefaults(cwd, platform, opts = {}) {
1181
+ const progress = {};
1182
+ try {
1183
+ return await resolvedDefaultsFor(cwd, platform, {
1184
+ ...opts,
1185
+ onEvaluate: () => {
1186
+ progress.spinner = createSpinner("Reading Expo config\u2026").start();
1187
+ }
1188
+ });
1189
+ } finally {
1190
+ progress.spinner?.stop();
1191
+ }
1192
+ }
1193
+ function evaluatedConfigNote(cwd, defaults) {
1194
+ const file = defaults.evaluatedConfig;
1195
+ if (!file) return;
1196
+ if (defaults.evaluationError) {
1197
+ console.log(chalk2.dim(` Couldn't read the name from ${file}: ${defaults.evaluationError}`));
1198
+ return;
1199
+ }
1200
+ if (notedConfigDirs.has(cwd)) return;
1201
+ notedConfigDirs.add(cwd);
1202
+ console.log(
1203
+ chalk2.dim(
1204
+ ` The iOS name was read from ${file} using your local environment.
1205
+ If it depends on environment variables, set the same ones for the cloud build
1206
+ (build.env, or build variables).`
1207
+ )
1208
+ );
1209
+ }
1210
+ var notedConfigDirs = /* @__PURE__ */ new Set();
1098
1211
  async function commandSelect(message, detected) {
1099
1212
  const choices = [];
1100
1213
  if (detected) choices.push({ value: "default", name: `Use detected default: ${detected}` });
@@ -1103,7 +1216,8 @@ async function commandSelect(message, detected) {
1103
1216
  return select({ message, choices });
1104
1217
  }
1105
1218
  async function promptBuildConfig(cwd, platform) {
1106
- const defaults = defaultsFor(cwd, platform);
1219
+ const defaults = await promptDefaults(cwd, platform);
1220
+ evaluatedConfigNote(cwd, defaults);
1107
1221
  const choice = await commandSelect(`Build command for ${platform}`, defaults.buildCommand);
1108
1222
  if (choice === "skip") return null;
1109
1223
  if (choice === "default") {
@@ -1123,7 +1237,8 @@ async function promptBuildConfig(cwd, platform) {
1123
1237
  return { command, artifact };
1124
1238
  }
1125
1239
  async function promptProductionBuildConfig(cwd, platform) {
1126
- const defaults = productionDefaultsFor(cwd, platform);
1240
+ const defaults = await promptDefaults(cwd, platform, { production: true });
1241
+ evaluatedConfigNote(cwd, defaults);
1127
1242
  const choice = await commandSelect(
1128
1243
  `Production build command for ${platform}`,
1129
1244
  defaults.buildCommand
@@ -1329,6 +1444,203 @@ function runTar(args) {
1329
1444
  });
1330
1445
  }
1331
1446
 
1447
+ // src/lib/build-preflight.ts
1448
+ import { execFile as execFile3 } from "child_process";
1449
+ import { realpathSync, statSync as statSync4 } from "fs";
1450
+ import { dirname as dirname2, isAbsolute, join as join7, relative } from "path";
1451
+ import { parse } from "shell-quote";
1452
+ var DIRECTORY_CHANGERS = /* @__PURE__ */ new Set(["cd", "pushd"]);
1453
+ var DIRECTORY_FLAGS = /^(--cwd|--prefix|--dir|-C)(=|$)/;
1454
+ var VALUE_FLAGS = /^(--cwd|--prefix|--dir|-C|--loglevel|--registry|--workspace|--filter)$/;
1455
+ var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["-g", "--global", "--location=global"]);
1456
+ var SEPARATORS = /* @__PURE__ */ new Set(["&&", "||", ";", ";;", "|", "|&", "&", "(", ")"]);
1457
+ var REDIRECTIONS = /* @__PURE__ */ new Set([">", ">>", "<", ">&", "<&", "&>", "&>>", ">|", "<>"]);
1458
+ var WRAPPERS = /* @__PURE__ */ new Set(["time", "env", "corepack", "command", "exec", "nohup"]);
1459
+ var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "npx", "yarn", "pnpm", "pnpx", "bun", "bunx"]);
1460
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
1461
+ function simpleCommands(command) {
1462
+ const out = [];
1463
+ const flush = (words) => {
1464
+ while (words.length > 0 && ASSIGNMENT.test(words[0])) words.shift();
1465
+ while (words.length > 0 && WRAPPERS.has(words[0])) {
1466
+ words.shift();
1467
+ while (words.length > 0 && (words[0].startsWith("-") || ASSIGNMENT.test(words[0]))) {
1468
+ words.shift();
1469
+ }
1470
+ }
1471
+ if (words.length === 0) return;
1472
+ const base = words[0].replace(/@[^/]*$/, "");
1473
+ const program = JS_PACKAGE_MANAGERS.has(base) ? base : words[0];
1474
+ out.push({ program, args: words.slice(1) });
1475
+ };
1476
+ for (const line of command.replace(/\\\r?\n/g, " ").split(/\r?\n/)) {
1477
+ let tokens;
1478
+ try {
1479
+ tokens = parse(line, (name) => `$${name}`);
1480
+ } catch {
1481
+ continue;
1482
+ }
1483
+ let words = [];
1484
+ let redirectTarget = false;
1485
+ for (const t of tokens) {
1486
+ if (typeof t === "string") {
1487
+ if (redirectTarget) {
1488
+ redirectTarget = false;
1489
+ continue;
1490
+ }
1491
+ if (t === "{" || t === "}") flush(words.splice(0));
1492
+ else if (t.length > 0) words.push(t);
1493
+ } else if ("comment" in t) {
1494
+ break;
1495
+ } else if ("op" in t && t.op === "glob") {
1496
+ words.push(t.pattern);
1497
+ } else if ("op" in t && SEPARATORS.has(t.op)) {
1498
+ flush(words);
1499
+ words = [];
1500
+ } else if ("op" in t && REDIRECTIONS.has(t.op)) {
1501
+ if (words.length > 1 && /^\d+$/.test(words[words.length - 1])) words.pop();
1502
+ redirectTarget = true;
1503
+ }
1504
+ }
1505
+ flush(words);
1506
+ }
1507
+ return out;
1508
+ }
1509
+ function positionals(args) {
1510
+ const out = [];
1511
+ for (let i = 0; i < args.length; i++) {
1512
+ const a = args[i];
1513
+ if (a.startsWith("-")) {
1514
+ if (VALUE_FLAGS.test(a)) i++;
1515
+ continue;
1516
+ }
1517
+ out.push(a);
1518
+ }
1519
+ return out;
1520
+ }
1521
+ var isGlobal = (args) => args.some((a) => GLOBAL_FLAGS.has(a));
1522
+ var VERSION_OR_HELP = /* @__PURE__ */ new Set(["-v", "--version", "-h", "--help"]);
1523
+ var NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
1524
+ "ci",
1525
+ "clean-install",
1526
+ "install-clean",
1527
+ "install",
1528
+ "i",
1529
+ "add",
1530
+ "install-test",
1531
+ "it"
1532
+ ]);
1533
+ var NEEDS_MANIFEST_VERBS = /* @__PURE__ */ new Set([
1534
+ "ci",
1535
+ "clean-install",
1536
+ "install-clean",
1537
+ "install-test",
1538
+ "it",
1539
+ "run",
1540
+ "run-script"
1541
+ ]);
1542
+ function installsInStep({ program, args }) {
1543
+ if (isGlobal(args)) return false;
1544
+ const [verb, ...rest] = positionals(args);
1545
+ if (program === "yarn") {
1546
+ if (verb === void 0) return !args.some((a) => VERSION_OR_HELP.has(a));
1547
+ if (verb === "install" || verb === "add") return true;
1548
+ return verb === "workspaces" && rest[0] === "focus";
1549
+ }
1550
+ if (program === "npm" || program === "pnpm" || program === "bun") {
1551
+ return verb !== void 0 && NPM_INSTALL_VERBS.has(verb);
1552
+ }
1553
+ return false;
1554
+ }
1555
+ function needsManifest({ program, args }) {
1556
+ if (isGlobal(args) || args.some((a) => DIRECTORY_FLAGS.test(a))) return false;
1557
+ const [verb, ...rest] = positionals(args);
1558
+ if (program === "yarn") {
1559
+ if (verb === void 0) return !args.some((a) => VERSION_OR_HELP.has(a));
1560
+ if (verb === "install" || verb === "run") return true;
1561
+ return verb === "workspaces" && rest[0] === "focus";
1562
+ }
1563
+ if (program === "npm" || program === "pnpm" || program === "bun") {
1564
+ if (verb === void 0) return false;
1565
+ if (NEEDS_MANIFEST_VERBS.has(verb)) return true;
1566
+ return (verb === "install" || verb === "i") && rest.length === 0;
1567
+ }
1568
+ return false;
1569
+ }
1570
+ function packageManagerAtRoot(command) {
1571
+ for (const step of simpleCommands(command)) {
1572
+ if (DIRECTORY_CHANGERS.has(step.program)) return null;
1573
+ if (JS_PACKAGE_MANAGERS.has(step.program) && positionals(step.args)[0] === "init") return null;
1574
+ if (needsManifest(step)) return step.program;
1575
+ }
1576
+ return null;
1577
+ }
1578
+ function installsDependencies(command) {
1579
+ return simpleCommands(command).some(installsInStep);
1580
+ }
1581
+ function missingPackageJsonError(command, files, dir) {
1582
+ const pm = packageManagerAtRoot(command);
1583
+ if (!pm || files.has("package.json")) return null;
1584
+ return `No package.json in ${dir}. Run vibeview build from your project root. (Your build command runs \`${pm}\` there; nothing was uploaded or charged.)`;
1585
+ }
1586
+ function missingInstallWarning(command, files) {
1587
+ const hit = JS_LOCKFILE_INSTALLS.find(([lockfile]) => files.has(lockfile));
1588
+ if (!hit || files.has(".pnp.cjs") || installsDependencies(command)) return null;
1589
+ return `Your build command doesn't install dependencies; node_modules is never uploaded, so the build will likely fail. Add \`${hit[1]} &&\` at the start.`;
1590
+ }
1591
+ function missingLockfileWarning(command, files) {
1592
+ if (!files.has("package.json")) return null;
1593
+ if (JS_LOCKFILE_INSTALLS.some(([lockfile]) => files.has(lockfile))) return null;
1594
+ if (!installsDependencies(command)) return null;
1595
+ return "No lockfile in the upload (yarn.lock, package-lock.json, pnpm-lock.yaml): a frozen install (`npm ci`, `--immutable`, `--frozen-lockfile`) will fail, and any other install resolves fresh versions. Commit your lockfile, or run vibeview build from the directory that has it.";
1596
+ }
1597
+ function bunWarning(command) {
1598
+ const step = simpleCommands(command).find((s) => s.program === "bun" || s.program === "bunx");
1599
+ if (!step) return null;
1600
+ return `Your build command runs \`${step.program}\`, but cloud builders don't have Bun, so that step will fail unless your command installs Bun first. Use npm, yarn or pnpm instead.`;
1601
+ }
1602
+ function buildCommandWarnings(command, files) {
1603
+ return [
1604
+ bunWarning(command),
1605
+ missingInstallWarning(command, files),
1606
+ missingLockfileWarning(command, files)
1607
+ ].filter((w) => w !== null);
1608
+ }
1609
+ function projectRootHint(cwd, gitRoot, files, hasPackageJson = (dir) => isFile2(join7(dir, "package.json"))) {
1610
+ if (!gitRoot || files.has("package.json")) return null;
1611
+ cwd = realPathOr(cwd);
1612
+ gitRoot = realPathOr(gitRoot);
1613
+ const rel = relative(gitRoot, cwd);
1614
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
1615
+ for (let dir = dirname2(cwd); ; dir = dirname2(dir)) {
1616
+ if (hasPackageJson(dir)) {
1617
+ return `You're running from ${cwd}; the project root looks like ${dir}. Only ${cwd} will be uploaded.`;
1618
+ }
1619
+ if (dir === gitRoot || dir === dirname2(dir)) return null;
1620
+ }
1621
+ }
1622
+ function realPathOr(path) {
1623
+ try {
1624
+ return realpathSync(path);
1625
+ } catch {
1626
+ return path;
1627
+ }
1628
+ }
1629
+ function isFile2(path) {
1630
+ try {
1631
+ return statSync4(path).isFile();
1632
+ } catch {
1633
+ return false;
1634
+ }
1635
+ }
1636
+ async function gitToplevel(cwd) {
1637
+ return new Promise((resolve) => {
1638
+ execFile3("git", ["rev-parse", "--show-toplevel"], { cwd }, (err, stdout) => {
1639
+ resolve(err ? null : stdout.trim() || null);
1640
+ });
1641
+ });
1642
+ }
1643
+
1332
1644
  // src/commands/build.ts
1333
1645
  var CLOUD_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
1334
1646
  var PRODUCTION_SIGNING_PLATFORMS = ["ios", "android", "androidtv", "tvos"];
@@ -1795,6 +2107,25 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
1795
2107
  }
1796
2108
  throw err;
1797
2109
  }
2110
+ const uploaded = new Set(files);
2111
+ const warnings = /* @__PURE__ */ new Map();
2112
+ for (const p of platforms) {
2113
+ const command = productionBuilds.get(p)?.command ?? cfg.platforms[p].build.command;
2114
+ const missingPkg = missingPackageJsonError(command, uploaded, cwd);
2115
+ if (missingPkg) fail(opts, missingPkg, "no_package_json");
2116
+ for (const w of buildCommandWarnings(command, uploaded)) {
2117
+ warnings.set(w, [...warnings.get(w) ?? [], p]);
2118
+ }
2119
+ }
2120
+ const rootHint = projectRootHint(cwd, await (d.gitToplevel ?? gitToplevel)(cwd), uploaded);
2121
+ if (rootHint) warnings.set(rootHint, [...platforms]);
2122
+ for (const [message, ps] of warnings) {
2123
+ for (const p of ps) emitCloud(opts.json, { event: "warning", platform: p, message });
2124
+ if (!opts.json) {
2125
+ const prefix = platforms.length > 1 && ps.length < platforms.length ? `${ps.join(", ")}: ` : "";
2126
+ console.error(chalk3.yellow(`\u26A0 ${prefix}${message}`));
2127
+ }
2128
+ }
1798
2129
  const packSpinner = !opts.json ? createSpinner(`packing source (${files.length} files)`) : null;
1799
2130
  packSpinner?.start();
1800
2131
  let tarPath;
@@ -2173,7 +2504,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
2173
2504
  // src/commands/dev.ts
2174
2505
  import { spawn as spawn4 } from "child_process";
2175
2506
  import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
2176
- import { dirname as dirname4 } from "path";
2507
+ import { dirname as dirname5 } from "path";
2177
2508
  import { createInterface as createInterface3 } from "readline";
2178
2509
  import { select as select3 } from "@inquirer/prompts";
2179
2510
  import chalk4 from "chalk";
@@ -2376,14 +2707,14 @@ import { createInterface as createInterface2 } from "readline";
2376
2707
 
2377
2708
  // src/lib/metro-state.ts
2378
2709
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "fs";
2379
- import { dirname as dirname2, join as join7 } from "path";
2710
+ import { dirname as dirname3, join as join8 } from "path";
2380
2711
  var STATE_DIR2 = ".vibeview";
2381
2712
  var STATE_FILE2 = "metro-state.json";
2382
2713
  function statePath2(cwd) {
2383
- return join7(cwd, STATE_DIR2, STATE_FILE2);
2714
+ return join8(cwd, STATE_DIR2, STATE_FILE2);
2384
2715
  }
2385
2716
  function metroLogPath(cwd) {
2386
- return join7(cwd, STATE_DIR2, "metro.log");
2717
+ return join8(cwd, STATE_DIR2, "metro.log");
2387
2718
  }
2388
2719
  function isPidAlive2(pid) {
2389
2720
  try {
@@ -2395,7 +2726,7 @@ function isPidAlive2(pid) {
2395
2726
  }
2396
2727
  function writeMetroState(cwd, state) {
2397
2728
  const path = statePath2(cwd);
2398
- mkdirSync4(dirname2(path), { recursive: true });
2729
+ mkdirSync4(dirname3(path), { recursive: true });
2399
2730
  writeFileSync6(path, JSON.stringify(state, null, 2) + "\n");
2400
2731
  }
2401
2732
  function readMetroState(cwd) {
@@ -2534,7 +2865,7 @@ async function waitFor(cond, timeoutMs, everyMs) {
2534
2865
 
2535
2866
  // src/lib/reload-loop.ts
2536
2867
  import { watch } from "fs";
2537
- import { basename, dirname as dirname3 } from "path";
2868
+ import { basename, dirname as dirname4 } from "path";
2538
2869
  function createReloadLoop(deps) {
2539
2870
  let inFlight = null;
2540
2871
  let pendingFollowUp = false;
@@ -2615,11 +2946,11 @@ function isIgnoredWatchPath(relPath, artifact, userIgnore = []) {
2615
2946
  if (IGNORED_FILES.includes(rel)) return true;
2616
2947
  if (userIgnore.some((entry) => isUnderPath(rel, entry))) return true;
2617
2948
  const artifactPattern = artifact.replace(/\\/g, "/").replace(/^\.\//, "");
2618
- const artifactDir = dirname3(artifactPattern);
2949
+ const artifactDir = dirname4(artifactPattern);
2619
2950
  if (artifactDir !== "." && (rel === artifactDir || rel.startsWith(`${artifactDir}/`))) {
2620
2951
  return true;
2621
2952
  }
2622
- if (dirname3(rel) !== artifactDir) return false;
2953
+ if (dirname4(rel) !== artifactDir) return false;
2623
2954
  const name = basename(artifactPattern);
2624
2955
  const re = new RegExp(`^${name.split("*").map(escapeRegExp).join(".*")}$`);
2625
2956
  return re.test(basename(rel));
@@ -3497,7 +3828,7 @@ async function runDevInProcess(opts) {
3497
3828
  const captureMetroLine = (line) => {
3498
3829
  try {
3499
3830
  if (!metroLogStream) {
3500
- mkdirSync5(dirname4(metroLogPath(process.cwd())), { recursive: true });
3831
+ mkdirSync5(dirname5(metroLogPath(process.cwd())), { recursive: true });
3501
3832
  metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
3502
3833
  }
3503
3834
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
@@ -4618,9 +4949,9 @@ function flagKey(cli) {
4618
4949
  const name = cli.replace(/^--/, "");
4619
4950
  return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4620
4951
  }
4621
- function buildToolCall(cmd, positionals, opts) {
4952
+ function buildToolCall(cmd, positionals2, opts) {
4622
4953
  if (cmd.verb === "tap") {
4623
- const target = positionals[0];
4954
+ const target = positionals2[0];
4624
4955
  if (!target) throw new Error('tap requires a target: an element ref (@e5) or "x,y"');
4625
4956
  const coords = parseCoords(target);
4626
4957
  if (coords) {
@@ -4635,7 +4966,7 @@ function buildToolCall(cmd, positionals, opts) {
4635
4966
  return { tool: "tap", args: { ref: stripRef(target) } };
4636
4967
  }
4637
4968
  if (cmd.verb === "long-press") {
4638
- const target = positionals[0];
4969
+ const target = positionals2[0];
4639
4970
  if (!target) throw new Error("long-press requires a ref, e.g. @e5");
4640
4971
  if (parseCoords(target) || target.includes(",") && !REF_RE.test(target)) {
4641
4972
  throw new Error(
@@ -4649,8 +4980,8 @@ function buildToolCall(cmd, positionals, opts) {
4649
4980
  return { tool: cmd.tool, args: args2 };
4650
4981
  }
4651
4982
  if (cmd.verb === "drag") {
4652
- const from = positionals[0];
4653
- const to = positionals[1];
4983
+ const from = positionals2[0];
4984
+ const to = positionals2[1];
4654
4985
  if (!from || !to) {
4655
4986
  throw new Error('drag requires two targets: <from> <to>, each an element ref (@e5) or "x,y"');
4656
4987
  }
@@ -4691,7 +5022,7 @@ function buildToolCall(cmd, positionals, opts) {
4691
5022
  return { tool: cmd.tool, args: args2 };
4692
5023
  }
4693
5024
  if (cmd.verb === "scroll") {
4694
- const direction = positionals[0];
5025
+ const direction = positionals2[0];
4695
5026
  const valid = ["up", "down", "left", "right"];
4696
5027
  if (!direction || !valid.includes(direction)) {
4697
5028
  throw new Error(`scroll: direction must be one of ${valid.join(", ")}, got "${direction}"`);
@@ -4699,7 +5030,7 @@ function buildToolCall(cmd, positionals, opts) {
4699
5030
  return { tool: cmd.tool, args: { name: `scroll_${direction}` } };
4700
5031
  }
4701
5032
  if (cmd.verb === "swipe") {
4702
- const direction = positionals[0];
5033
+ const direction = positionals2[0];
4703
5034
  const valid = ["up", "down", "left", "right"];
4704
5035
  if (!direction || !valid.includes(direction)) {
4705
5036
  throw new Error(`swipe: direction must be one of ${valid.join(", ")}, got "${direction}"`);
@@ -4711,7 +5042,7 @@ function buildToolCall(cmd, positionals, opts) {
4711
5042
  return { tool: cmd.tool, args: args2 };
4712
5043
  }
4713
5044
  if (cmd.verb === "set-posture") {
4714
- const hasPosture = positionals[0] !== void 0 && positionals[0] !== "";
5045
+ const hasPosture = positionals2[0] !== void 0 && positionals2[0] !== "";
4715
5046
  const hasAngle = opts.angle !== void 0 && opts.angle !== "";
4716
5047
  if (hasPosture === hasAngle) {
4717
5048
  throw new FlagError(
@@ -4724,7 +5055,7 @@ function buildToolCall(cmd, positionals, opts) {
4724
5055
  for (const param of cmd.params) {
4725
5056
  let raw;
4726
5057
  if (param.cli === "positional") {
4727
- raw = positionals[positionalIndex];
5058
+ raw = positionals2[positionalIndex];
4728
5059
  positionalIndex += 1;
4729
5060
  } else {
4730
5061
  const key = flagKey(param.cli);
@@ -4774,7 +5105,7 @@ var NON_USE_TOOLS = /* @__PURE__ */ new Set([
4774
5105
  function countsAsUse(tool) {
4775
5106
  return !NON_USE_TOOLS.has(tool);
4776
5107
  }
4777
- async function runAgentVerb(cmd, positionals, opts) {
5108
+ async function runAgentVerb(cmd, positionals2, opts) {
4778
5109
  const { token, baseUrl } = resolveAuth(opts);
4779
5110
  const sessionId = opts.session || readDevState(process.cwd())?.session_id;
4780
5111
  if (!sessionId) {
@@ -4783,7 +5114,7 @@ async function runAgentVerb(cmd, positionals, opts) {
4783
5114
  }
4784
5115
  let call;
4785
5116
  try {
4786
- call = buildToolCall(cmd, positionals, opts);
5117
+ call = buildToolCall(cmd, positionals2, opts);
4787
5118
  } catch (err) {
4788
5119
  process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4789
5120
  `);
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  readMetroState,
26
26
  recordDevUse,
27
27
  runAgentVerb
28
- } from "./chunk-VFCG4U7V.js";
28
+ } from "./chunk-NJ2IXB5H.js";
29
29
  import {
30
30
  ApiClient,
31
31
  ApiStatusError,
@@ -4147,7 +4147,7 @@ program.command("dev-reload").description(
4147
4147
  program.command("mcp").description(
4148
4148
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4149
4149
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4150
- const { runMcpServer } = await import("./server-2CDIRTBV.js");
4150
+ const { runMcpServer } = await import("./server-DF3CDWL4.js");
4151
4151
  await runMcpServer({ ...opts, version: pkg.version });
4152
4152
  });
4153
4153
  program.command("agent-setup").description(
@@ -28,7 +28,7 @@ import {
28
28
  renderEnvelope,
29
29
  resolveMetro,
30
30
  startDevSession
31
- } from "./chunk-VFCG4U7V.js";
31
+ } from "./chunk-NJ2IXB5H.js";
32
32
  import {
33
33
  ApiClient,
34
34
  NotFoundError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.36.1",
3
+ "version": "0.37.1",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -33,11 +33,13 @@
33
33
  "ignore": "^7.0.0",
34
34
  "open": "^11.0.0",
35
35
  "ora": "^9.0.0",
36
+ "shell-quote": "^1.8.3",
36
37
  "ws": "^8.18.0",
37
38
  "zod": "^4.0.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20.0.0",
42
+ "@types/shell-quote": "^1.7.5",
41
43
  "@types/ws": "^8.5.0",
42
44
  "dotenv": "^16.4.5",
43
45
  "tsup": "^8.0.0",