vibeview 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,11 +40,34 @@ Build your React Native app on VibeView's build fleet — no Mac, no CI
40
40
  pipeline, no local toolchain:
41
41
 
42
42
  ```bash
43
- vibeview build --cloud # simulator/emulator build
43
+ vibeview build --cloud # development build (loads JS from Metro)
44
+ vibeview build --cloud --profile preview # Release build that runs without Metro
44
45
  vibeview build --cloud --production # signed release build (.ipa / .aab)
45
46
  ```
46
47
 
47
- A regular cloud build lands in your app's Build History, ready to run on a
48
+ Each command builds a named profile from `vibeview.json` (`development`,
49
+ `preview`, `production`, or one of your own; the `profiles` shape needs CLI
50
+ 0.51.0 or later):
51
+
52
+ ```json
53
+ {
54
+ "platforms": {
55
+ "ios": {
56
+ "build": {
57
+ "profiles": {
58
+ "development": { "command": "...", "artifact": "..." },
59
+ "preview": { "command": "...", "artifact": "..." },
60
+ "production": { "command": "...", "artifact": "...", "autoIncrement": true }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ A missing profile is asked for once and saved; a React Native or Expo setup
69
+ that accepts the suggested command also saves `preview`. A simulator or
70
+ emulator cloud build lands in your app's Build History, ready to run on a
48
71
  cloud device. Add `--production` for a **signed release build** on iOS,
49
72
  tvOS, Android, or Android TV, using signing credentials you store once:
50
73
 
@@ -74,7 +97,7 @@ vibeview webhooks add --url https://ci.example.com/hooks/vibeview
74
97
  Webhooks POST a signed payload to your endpoint whenever a build finishes.
75
98
  Manage running builds with `vibeview builds list | logs | download | cancel`.
76
99
 
77
- Set `"autoIncrement": true` in a platform's `build` block and every
100
+ Set `"autoIncrement": true` on a platform's `production` profile and every
78
101
  production build gets a server-assigned build number, injected into the
79
102
  build as `VIBEVIEW_BUILD_NUMBER` (inspect or seed the counters with
80
103
  `vibeview versions list/set`).
@@ -159,7 +182,7 @@ Output formats: `human`, `json`, and `junit` for any CI system. The exit code re
159
182
  | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
160
183
  | `vibeview login` / `logout` / `whoami` | Authenticate via browser, sign out, show who you are |
161
184
  | `vibeview dev` | Live development on a cloud device |
162
- | `vibeview build` | Build your app — locally, or on the build fleet with `--cloud` (add `--production` for a signed release) |
185
+ | `vibeview build` | Build your app — locally, or on the build fleet with `--cloud` (`--profile <name>` picks the build; `--production` for a signed release) |
163
186
  | `vibeview builds list/logs/download/cancel/delete-artifact` | Manage cloud builds and their artifacts |
164
187
  | `vibeview versions list/set` | Server-side build-number counters for `autoIncrement` production builds |
165
188
  | `vibeview credentials add/generate/list/rm/download` | App-signing credentials for production builds (`generate` covers iOS, tvOS, and Android) |
@@ -40,6 +40,149 @@ function isNotRunMessage(message) {
40
40
  return (message ?? "").startsWith("Not run:");
41
41
  }
42
42
 
43
+ // src/lib/build-profiles.ts
44
+ var DEVELOPMENT = "development";
45
+ var PREVIEW = "preview";
46
+ var PRODUCTION = "production";
47
+ var PROFILE_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
48
+ var DISTRIBUTIONS = ["simulator", "production"];
49
+ function defaultDistribution(name) {
50
+ return name === PRODUCTION ? "production" : "simulator";
51
+ }
52
+ function rawProfile(build, name) {
53
+ const own = build.profiles && Object.hasOwn(build.profiles, name) ? build.profiles[name] : void 0;
54
+ if (own) return own;
55
+ if (name === DEVELOPMENT && build.command && build.artifact) {
56
+ return { command: build.command, artifact: build.artifact };
57
+ }
58
+ if (name === PRODUCTION && build.productionCommand && build.productionArtifact) {
59
+ return { command: build.productionCommand, artifact: build.productionArtifact };
60
+ }
61
+ return null;
62
+ }
63
+ function hasProfile(build, name) {
64
+ return !!build && rawProfile(build, name) !== null;
65
+ }
66
+ function resolveProfile(build, name) {
67
+ if (!build) return null;
68
+ const p = rawProfile(build, name);
69
+ if (!p) return null;
70
+ const distribution = p.distribution ?? defaultDistribution(name);
71
+ const env = build.env === void 0 && p.env === void 0 ? void 0 : { ...build.env, ...p.env };
72
+ const xcode = p.xcode ?? build.xcode;
73
+ const node = p.node ?? build.node;
74
+ const autoIncrement = (p.autoIncrement ?? (name === PRODUCTION ? build.autoIncrement : void 0)) === true;
75
+ return {
76
+ name,
77
+ command: p.command,
78
+ artifact: p.artifact,
79
+ distribution,
80
+ ...env !== void 0 ? { env } : {},
81
+ ...xcode !== void 0 ? { xcode } : {},
82
+ ...node !== void 0 ? { node } : {},
83
+ autoIncrement
84
+ };
85
+ }
86
+ function missingProfileMessage(platformKey, name) {
87
+ return `no "${name}" profile for ${platformKey}: add platforms.${platformKey}.build.profiles.${name} {command, artifact} to vibeview.json`;
88
+ }
89
+ function developmentProfile(build) {
90
+ return resolveProfile(build, DEVELOPMENT);
91
+ }
92
+ function withProfile(build, name, profile) {
93
+ const {
94
+ command,
95
+ artifact,
96
+ productionCommand,
97
+ productionArtifact,
98
+ autoIncrement,
99
+ profiles,
100
+ ...shared
101
+ } = build ?? {};
102
+ const next = {};
103
+ const out = { ...shared };
104
+ if (command && artifact) next[DEVELOPMENT] = { command, artifact };
105
+ if (productionCommand && productionArtifact) {
106
+ next[PRODUCTION] = { command: productionCommand, artifact: productionArtifact };
107
+ } else if (name !== PRODUCTION) {
108
+ if (productionCommand) out.productionCommand = productionCommand;
109
+ if (productionArtifact) out.productionArtifact = productionArtifact;
110
+ }
111
+ Object.assign(next, profiles);
112
+ next[name] = profile;
113
+ out.profiles = next;
114
+ if (autoIncrement !== void 0) {
115
+ const production = next[PRODUCTION];
116
+ if (!production) out.autoIncrement = autoIncrement;
117
+ else if (production.autoIncrement === void 0) {
118
+ next[PRODUCTION] = { ...production, autoIncrement };
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ function isObject(v) {
124
+ return typeof v === "object" && v !== null && !Array.isArray(v);
125
+ }
126
+ function nonEmpty(v) {
127
+ return typeof v === "string" && v.length > 0;
128
+ }
129
+ function validateBuildBlock(build, key) {
130
+ if (!isObject(build)) {
131
+ throw new Error(`vibeview.json: platforms.${key}.build must be an object with profiles`);
132
+ }
133
+ const at = `vibeview.json: platforms.${key}.build`;
134
+ if ("command" in build || "artifact" in build) {
135
+ if (!nonEmpty(build.command)) throw new Error(`${at}.command must be a non-empty string`);
136
+ if (!nonEmpty(build.artifact)) throw new Error(`${at}.artifact must be a non-empty string`);
137
+ }
138
+ for (const field of ["productionCommand", "productionArtifact"]) {
139
+ if (field in build && !nonEmpty(build[field])) {
140
+ throw new Error(`${at}.${field} must be a non-empty string`);
141
+ }
142
+ }
143
+ const profiles = build.profiles;
144
+ if (profiles !== void 0) {
145
+ if (!isObject(profiles)) {
146
+ throw new Error(`${at}.profiles must be an object of named profiles`);
147
+ }
148
+ for (const [name, p] of Object.entries(profiles)) {
149
+ if (!PROFILE_NAME_RE.test(name)) {
150
+ throw new Error(
151
+ `${at}.profiles: "${name}" is not a valid profile name (lowercase letters, digits and dashes, starting with a letter)`
152
+ );
153
+ }
154
+ if (!isObject(p)) throw new Error(`${at}.profiles.${name} must be an object`);
155
+ if (!nonEmpty(p.command)) {
156
+ throw new Error(`${at}.profiles.${name}.command must be a non-empty string`);
157
+ }
158
+ if (!nonEmpty(p.artifact)) {
159
+ throw new Error(`${at}.profiles.${name}.artifact must be a non-empty string`);
160
+ }
161
+ if (p.distribution !== void 0 && !DISTRIBUTIONS.includes(p.distribution)) {
162
+ throw new Error(`${at}.profiles.${name}.distribution must be "simulator" or "production"`);
163
+ }
164
+ }
165
+ if ("command" in build && DEVELOPMENT in profiles) {
166
+ throw new Error(
167
+ `${at}: the development profile is defined twice \u2014 keep profiles.development and remove build.command and build.artifact`
168
+ );
169
+ }
170
+ if ("productionCommand" in build && PRODUCTION in profiles) {
171
+ throw new Error(
172
+ `${at}: the production profile is defined twice \u2014 keep profiles.production and remove build.productionCommand and build.productionArtifact`
173
+ );
174
+ }
175
+ }
176
+ const legacyDevelopment = nonEmpty(build.command);
177
+ const legacyProduction = nonEmpty(build.productionCommand) && nonEmpty(build.productionArtifact);
178
+ const named = isObject(profiles) && Object.keys(profiles).length > 0;
179
+ if (!legacyDevelopment && !legacyProduction && !named) {
180
+ throw new Error(
181
+ `${at} needs at least one profile: add build.profiles.development {command, artifact}`
182
+ );
183
+ }
184
+ }
185
+
43
186
  // src/lib/dev-config.ts
44
187
  import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
45
188
  import { join } from "path";
@@ -121,33 +264,7 @@ function loadDevConfig(cwd) {
121
264
  throw new Error(`vibeview.json: platforms.${key}.appId must be a non-empty string`);
122
265
  }
123
266
  if (entry.build !== void 0) {
124
- if (typeof entry.build !== "object" || entry.build === null) {
125
- throw new Error(
126
- `vibeview.json: platforms.${key}.build must be an object with command and artifact`
127
- );
128
- }
129
- if (typeof entry.build.command !== "string" || !entry.build.command) {
130
- throw new Error(`vibeview.json: platforms.${key}.build.command must be a non-empty string`);
131
- }
132
- if (typeof entry.build.artifact !== "string" || !entry.build.artifact) {
133
- throw new Error(
134
- `vibeview.json: platforms.${key}.build.artifact must be a non-empty string`
135
- );
136
- }
137
- if (entry.build.productionCommand !== void 0) {
138
- if (typeof entry.build.productionCommand !== "string" || !entry.build.productionCommand) {
139
- throw new Error(
140
- `vibeview.json: platforms.${key}.build.productionCommand must be a non-empty string`
141
- );
142
- }
143
- }
144
- if (entry.build.productionArtifact !== void 0) {
145
- if (typeof entry.build.productionArtifact !== "string" || !entry.build.productionArtifact) {
146
- throw new Error(
147
- `vibeview.json: platforms.${key}.build.productionArtifact must be a non-empty string`
148
- );
149
- }
150
- }
267
+ validateBuildBlock(entry.build, key);
151
268
  }
152
269
  if (entry.watchIgnore !== void 0) {
153
270
  if (!Array.isArray(entry.watchIgnore)) {
@@ -170,38 +287,38 @@ function loadDevConfig(cwd) {
170
287
  }
171
288
  return cfg;
172
289
  }
173
- function validateBuildEnv(env, platformKey) {
290
+ function validateBuildEnv(env, platformKey, at = "build") {
174
291
  if (env === void 0) return;
175
292
  if (typeof env !== "object" || env === null || Array.isArray(env)) {
176
293
  throw new Error(
177
- `vibeview.json: platforms.${platformKey}.build.env must be an object of string values`
294
+ `vibeview.json: platforms.${platformKey}.${at}.env must be an object of string values`
178
295
  );
179
296
  }
180
297
  for (const [envKey, envValue] of Object.entries(env)) {
181
298
  if (typeof envValue !== "string") {
182
299
  throw new Error(
183
- `vibeview.json: platforms.${platformKey}.build.env must be an object of string values`
300
+ `vibeview.json: platforms.${platformKey}.${at}.env must be an object of string values`
184
301
  );
185
302
  }
186
303
  if (!ENV_KEY_RE.test(envKey)) {
187
304
  throw new Error(
188
- `vibeview.json: platforms.${platformKey}.build.env key "${envKey}" is not a valid environment variable name (must match ${ENV_KEY_RE})`
305
+ `vibeview.json: platforms.${platformKey}.${at}.env key "${envKey}" is not a valid environment variable name (must match ${ENV_KEY_RE})`
189
306
  );
190
307
  }
191
308
  }
192
309
  }
193
310
  var XCODE_PLATFORMS = ["ios", "tvos"];
194
311
  var XCODE_VERSION_RE = /^\d+\.\d+$/;
195
- function validateBuildXcode(xcode, platformKey) {
312
+ function validateBuildXcode(xcode, platformKey, at = "build") {
196
313
  if (xcode === void 0) return;
197
314
  if (!XCODE_PLATFORMS.includes(platformKey)) {
198
315
  throw new Error(
199
- `vibeview.json: platforms.${platformKey}.build.xcode only applies to ios and tvos builds \u2014 remove it from the ${platformKey} block`
316
+ `vibeview.json: platforms.${platformKey}.${at}.xcode only applies to ios and tvos builds \u2014 remove it from the ${platformKey} block`
200
317
  );
201
318
  }
202
319
  if (typeof xcode !== "string" || !XCODE_VERSION_RE.test(xcode)) {
203
320
  throw new Error(
204
- `vibeview.json: platforms.${platformKey}.build.xcode must be a version string like "27.1"`
321
+ `vibeview.json: platforms.${platformKey}.${at}.xcode must be a version string like "27.1"`
205
322
  );
206
323
  }
207
324
  }
@@ -213,11 +330,11 @@ function normalizeNodeVersion(value) {
213
330
  const m = /^v?(\d{1,3})(?:\.x)?$/.exec(value.trim());
214
331
  return m ? String(Number(m[1])) : null;
215
332
  }
216
- function validateBuildNode(node, platformKey) {
333
+ function validateBuildNode(node, platformKey, at = "build") {
217
334
  if (node === void 0) return;
218
335
  if (normalizeNodeVersion(node) === null) {
219
336
  throw new Error(
220
- `vibeview.json: platforms.${platformKey}.build.node must be a Node.js major version like "24"`
337
+ `vibeview.json: platforms.${platformKey}.${at}.node must be a Node.js major version like "24"`
221
338
  );
222
339
  }
223
340
  }
@@ -1317,6 +1434,57 @@ function iosArchiveExportCommand(ws, scheme = ws) {
1317
1434
  function managedExpoIosProductionCommand(cwd, name) {
1318
1435
  return `${installCommand(cwd)} && npx expo prebuild --platform ios && ` + iosArchiveExportCommand(name);
1319
1436
  }
1437
+ function hasSeparatePreview(cwd, platform) {
1438
+ const flavor = detectFlavor(cwd);
1439
+ if (flavor !== "expo" && flavor !== "bare-rn") return false;
1440
+ if (platform === "ios") return true;
1441
+ if (platform !== "android") return false;
1442
+ return isManagedExpo(cwd, "android") || releaseSignedWithDebugKey(cwd);
1443
+ }
1444
+ function releaseSignedWithDebugKey(cwd) {
1445
+ for (const file of ["android/app/build.gradle", "android/app/build.gradle.kts"]) {
1446
+ let text;
1447
+ try {
1448
+ text = readFileSync6(join5(cwd, file), "utf-8");
1449
+ } catch {
1450
+ continue;
1451
+ }
1452
+ const release = gradleBlock(gradleBlock(text, "buildTypes") ?? "", "release");
1453
+ if (release === null) return false;
1454
+ return /signingConfig\s*=?\s*signingConfigs\s*\.\s*(?:debug\b|getByName\(\s*"debug"\s*\))/.test(
1455
+ release
1456
+ );
1457
+ }
1458
+ return false;
1459
+ }
1460
+ function gradleBlock(text, name) {
1461
+ const m = new RegExp(`(?:(?:^|[^\\w.])${name}|getByName\\(\\s*"${name}"\\s*\\))\\s*\\{`).exec(
1462
+ text
1463
+ );
1464
+ if (!m) return null;
1465
+ let depth = 1;
1466
+ const start = m.index + m[0].length;
1467
+ for (let i = start; i < text.length; i++) {
1468
+ if (text[i] === "{") depth++;
1469
+ else if (text[i] === "}" && --depth === 0) return text.slice(start, i);
1470
+ }
1471
+ return null;
1472
+ }
1473
+ function toReleaseBuild(dev2) {
1474
+ const swap = (v) => v?.replace(/-configuration Debug\b/g, "-configuration Release").replace(/Debug-iphonesimulator/g, "Release-iphonesimulator").replace(/assembleDebug\b/g, "assembleRelease").replace(/apk\/debug\//g, "apk/release/");
1475
+ const out = {};
1476
+ if (dev2.buildCommand !== void 0) out.buildCommand = swap(dev2.buildCommand);
1477
+ if (dev2.artifact !== void 0) out.artifact = swap(dev2.artifact);
1478
+ if (dev2.metroCommand !== void 0) out.metroCommand = dev2.metroCommand;
1479
+ return out;
1480
+ }
1481
+ function previewDefaultsFor(cwd, platform, evaluatedIosName) {
1482
+ const dev2 = defaultsFor(cwd, platform, evaluatedIosName);
1483
+ if (hasSeparatePreview(cwd, platform)) return toReleaseBuild(dev2);
1484
+ const flavor = detectFlavor(cwd);
1485
+ if (platform === "android" && (flavor === "expo" || flavor === "bare-rn")) return {};
1486
+ return dev2;
1487
+ }
1320
1488
  function productionDefaultsFor(cwd, platform, evaluatedIosName) {
1321
1489
  const flavor = detectFlavor(cwd);
1322
1490
  if ((flavor === "flutter" || flavor === "capacitor" || flavor === "native") && platform !== "tvos" && platform !== "roku") {
@@ -1496,7 +1664,8 @@ function evaluatedExpoName(cwd, readConfig, onEvaluate) {
1496
1664
  }
1497
1665
  async function resolvedDefaultsFor(cwd, platform, opts = {}) {
1498
1666
  const { production = false, readConfig = (c) => readExpoConfig(c), onEvaluate } = opts;
1499
- const base = production ? productionDefaultsFor : defaultsFor;
1667
+ const profile = opts.profile ?? (production ? "production" : "development");
1668
+ const base = profile === "production" ? productionDefaultsFor : profile === "preview" ? previewDefaultsFor : defaultsFor;
1500
1669
  const dynamic = dynamicExpoConfig(cwd);
1501
1670
  if (platform !== "ios" || !dynamic || !isManagedExpo(cwd, "ios")) {
1502
1671
  return base(cwd, platform);
@@ -1541,7 +1710,7 @@ function fillLaterHint(skipped, platform) {
1541
1710
  const lines = ["You can enable one-command builds later \u2014 add to vibeview.json:"];
1542
1711
  if (skipped.build) {
1543
1712
  lines.push(
1544
- ` platforms.${platform}.build = { "command": "<your build command>", "artifact": "<path to .app/.apk>" }`
1713
+ ` platforms.${platform}.build.profiles.development = { "command": "<your build command>", "artifact": "<path to .app/.apk>" }`
1545
1714
  );
1546
1715
  }
1547
1716
  if (skipped.metro) {
@@ -1587,56 +1756,44 @@ async function commandSelect(message, detected) {
1587
1756
  choices.push({ value: "skip", name: "Skip for now" });
1588
1757
  return select({ message, choices });
1589
1758
  }
1590
- async function promptBuildConfig(cwd, platform) {
1591
- const defaults = await promptDefaults(cwd, platform);
1759
+ function commandLabel(profile) {
1760
+ if (profile === DEVELOPMENT) return "Build command";
1761
+ return `${profile.charAt(0).toUpperCase()}${profile.slice(1)} build command`;
1762
+ }
1763
+ async function promptProfileConfig(cwd, platform, profile) {
1764
+ const production = profile === PRODUCTION;
1765
+ const defaults = await promptDefaults(cwd, platform, { profile });
1592
1766
  evaluatedConfigNote(cwd, defaults);
1593
- const choice = await commandSelect(`Build command for ${platform}`, defaults.buildCommand);
1767
+ const label = commandLabel(profile);
1768
+ const choice = await commandSelect(`${label} for ${platform}`, defaults.buildCommand);
1594
1769
  if (choice === "skip") return null;
1595
1770
  if (choice === "default") {
1596
1771
  return { command: defaults.buildCommand, artifact: defaults.artifact };
1597
1772
  }
1598
1773
  console.log(
1599
1774
  chalk2.dim(
1600
- " Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && yarn build:android`"
1775
+ " Tip: include your install step \u2014 dependencies are not uploaded.\n" + (production ? " e.g. `yarn install --immutable && cd android && ./gradlew bundleRelease`" : " e.g. `yarn install --immutable && yarn build:android`")
1601
1776
  )
1602
1777
  );
1603
- const command = await input({ message: `Build command for ${platform}`, required: true });
1778
+ const command = await input({ message: `${label} for ${platform}`, required: true });
1604
1779
  const artifact = await input({
1605
- message: platform === "roku" ? "Path to the channel zip it produces (* allowed)" : "Path to the built app it produces (.apk, or .app dir; * allowed)",
1780
+ message: production ? "Path to the signed production artifact it produces (.aab/.ipa; * allowed)" : platform === "roku" ? "Path to the channel zip it produces (* allowed)" : "Path to the built app it produces (.apk, or .app dir; * allowed)",
1606
1781
  default: defaults.artifact,
1607
1782
  required: true
1608
1783
  });
1609
1784
  return { command, artifact };
1610
1785
  }
1611
- async function promptProductionBuildConfig(cwd, platform) {
1612
- const defaults = await promptDefaults(cwd, platform, { production: true });
1613
- evaluatedConfigNote(cwd, defaults);
1614
- const choice = await commandSelect(
1615
- `Production build command for ${platform}`,
1616
- defaults.buildCommand
1617
- );
1618
- if (choice === "skip") return null;
1619
- if (choice === "default") {
1620
- return {
1621
- productionCommand: defaults.buildCommand,
1622
- productionArtifact: defaults.artifact
1623
- };
1624
- }
1625
- console.log(
1626
- chalk2.dim(
1627
- " Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && cd android && ./gradlew bundleRelease`"
1628
- )
1629
- );
1630
- const productionCommand = await input({
1631
- message: `Production build command for ${platform}`,
1632
- required: true
1633
- });
1634
- const productionArtifact = await input({
1635
- message: "Path to the signed production artifact it produces (.aab/.ipa; * allowed)",
1636
- default: defaults.artifact,
1637
- required: true
1638
- });
1639
- return { productionCommand, productionArtifact };
1786
+ async function promptBuildConfig(cwd, platform) {
1787
+ return promptProfileConfig(cwd, platform, DEVELOPMENT);
1788
+ }
1789
+ async function companionPreview(cwd, platform, development) {
1790
+ if (!hasSeparatePreview(cwd, platform)) return null;
1791
+ const dev2 = await promptDefaults(cwd, platform);
1792
+ if (dev2.buildCommand !== development.command || dev2.artifact !== development.artifact)
1793
+ return null;
1794
+ const preview = toReleaseBuild(dev2);
1795
+ if (!preview.buildCommand || !preview.artifact) return null;
1796
+ return { command: preview.buildCommand, artifact: preview.artifact };
1640
1797
  }
1641
1798
  async function promptMetroCommand(cwd, platform) {
1642
1799
  const defaults = defaultsFor(cwd, platform);
@@ -1816,6 +1973,39 @@ function runTar(args) {
1816
1973
  });
1817
1974
  }
1818
1975
 
1976
+ // src/lib/cloud-manifest-entry.ts
1977
+ function validateCloudProfile(build, platformKey, name) {
1978
+ validateBuildEnv(build.env, platformKey);
1979
+ validateBuildXcode(build.xcode, platformKey);
1980
+ validateBuildNode(build.node, platformKey);
1981
+ const own = build.profiles?.[name];
1982
+ if (own) {
1983
+ const at = `build.profiles.${name}`;
1984
+ validateBuildEnv(own.env, platformKey, at);
1985
+ validateBuildXcode(own.xcode, platformKey, at);
1986
+ validateBuildNode(own.node, platformKey, at);
1987
+ }
1988
+ }
1989
+ function cloudManifestEntry(platform, profile, opts = {}) {
1990
+ const production = profile.distribution === "production";
1991
+ const xcode = platformUsesXcode(platform) ? opts.xcode ?? profile.xcode : void 0;
1992
+ const node = opts.node ?? (profile.node === void 0 ? void 0 : normalizeNodeVersion(profile.node));
1993
+ return {
1994
+ platform,
1995
+ ...opts.appPublicId ? { app_public_id: opts.appPublicId } : {},
1996
+ profile: profile.name,
1997
+ command: profile.command,
1998
+ artifact_path: profile.artifact,
1999
+ distribution: profile.distribution,
2000
+ ...profile.env !== void 0 ? { env: profile.env } : {},
2001
+ // autoIncrement is a PRODUCTION-only concept (the server-side build
2002
+ // number is a store number).
2003
+ ...profile.autoIncrement && production ? { auto_increment: true } : {},
2004
+ ...xcode ? { xcode } : {},
2005
+ ...node ? { node } : {}
2006
+ };
2007
+ }
2008
+
1819
2009
  // src/lib/build-preflight.ts
1820
2010
  import { execFile as execFile4 } from "child_process";
1821
2011
  import { realpathSync, statSync as statSync4 } from "fs";
@@ -2088,81 +2278,86 @@ async function resolveBarePlatform(cwd, cfg, opts) {
2088
2278
  }
2089
2279
  return platform;
2090
2280
  }
2091
- function persistPromptedBuild(cwd, platform, build) {
2281
+ function persistPromptedProfiles(cwd, platform, profiles) {
2092
2282
  return updateDevConfig(cwd, platform, (next) => {
2093
- const entry = next.platforms[platform];
2094
- if (entry) {
2095
- entry.build = build;
2096
- } else {
2097
- next.platforms[platform] = { build };
2098
- }
2283
+ const entry = next.platforms[platform] ?? (next.platforms[platform] = {});
2284
+ let build = entry.build;
2285
+ for (const [name, profile] of Object.entries(profiles))
2286
+ build = withProfile(build, name, profile);
2287
+ entry.build = build;
2099
2288
  });
2100
2289
  }
2101
2290
  function persistUploadedAppId(cwd, platform, build, appId) {
2102
2291
  updateDevConfig(cwd, platform, (next) => {
2103
2292
  const entry = next.platforms[platform];
2104
2293
  if (!entry) {
2105
- next.platforms[platform] = { appId, build };
2294
+ next.platforms[platform] = build ? { appId, build } : { appId };
2106
2295
  return true;
2107
2296
  }
2108
2297
  if (entry.appId === appId && entry.build) return false;
2109
2298
  entry.appId = appId;
2110
- if (!entry.build) entry.build = build;
2299
+ if (!entry.build && build) entry.build = build;
2111
2300
  return true;
2112
2301
  });
2113
2302
  }
2114
- async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
2115
- const existing = cfgRef.current?.platforms?.[platform]?.build;
2303
+ function requestedProfile(opts) {
2304
+ return opts.profile ?? (opts.production ? PRODUCTION : DEVELOPMENT);
2305
+ }
2306
+ async function resolveBuildProfile(cwd, cfgRef, platform, opts) {
2307
+ const name = requestedProfile(opts);
2308
+ const build = cfgRef.current?.platforms?.[platform]?.build;
2309
+ const existing = resolveProfile(build, name);
2116
2310
  if (existing) return existing;
2117
2311
  if (opts.json || !process.stdin.isTTY) {
2118
- fail(
2119
- opts,
2120
- `no build config for ${platform}: add platforms.${platform}.build {command, artifact} to vibeview.json`,
2121
- "config_required"
2122
- );
2312
+ fail(opts, missingProfileMessage(platform, name), "config_required");
2123
2313
  }
2124
- const answered = await promptBuildConfig(cwd, platform);
2314
+ const answered = await promptProfileConfig(cwd, platform, name);
2125
2315
  if (!answered) {
2126
- fail(opts, "build config is required to run vibeview build", "config_required");
2127
- }
2128
- cfgRef.current = persistPromptedBuild(cwd, platform, answered);
2129
- return answered;
2130
- }
2131
- function persistPromptedProductionBuild(cwd, platform, production) {
2132
- return updateDevConfig(cwd, platform, (next) => {
2133
- mergeProductionPair(next, platform, production);
2134
- });
2135
- }
2136
- function mergeProductionPair(next, platform, production) {
2137
- const existingBuild = next.platforms[platform]?.build;
2138
- if (!existingBuild?.command || !existingBuild?.artifact) {
2139
- throw new Error(
2140
- `internal: cannot persist a production build config for ${platform} before its debug command/artifact are resolved`
2141
- );
2142
- }
2143
- next.platforms[platform].build = { ...existingBuild, ...production };
2144
- }
2145
- async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
2146
- const entry = cfgRef.current?.platforms?.[platform]?.build;
2147
- if (entry?.productionCommand && entry?.productionArtifact) {
2148
- return { command: entry.productionCommand, artifact: entry.productionArtifact };
2149
- }
2150
- if (opts.json || !process.stdin.isTTY) {
2151
2316
  fail(
2152
2317
  opts,
2153
- `no production build config for ${platform}: add platforms.${platform}.build.productionCommand/productionArtifact to vibeview.json`,
2318
+ name === PRODUCTION ? "a production build config is required for --production" : `a "${name}" build profile is required to run vibeview build`,
2154
2319
  "config_required"
2155
2320
  );
2156
2321
  }
2157
- const answered = await promptProductionBuildConfig(cwd, platform);
2158
- if (!answered) {
2159
- fail(opts, "a production build config is required for --production", "config_required");
2322
+ const save = { [name]: answered };
2323
+ if (name === DEVELOPMENT && !hasProfile(build, PREVIEW)) {
2324
+ const preview = await companionPreview(cwd, platform, answered);
2325
+ if (preview) {
2326
+ save[PREVIEW] = preview;
2327
+ console.error(
2328
+ chalk3.dim(
2329
+ ` Also saved a preview profile for ${platform}: the same build in Release, which runs without Metro
2330
+ (share links, tests, GitHub pull requests). Build it with \`vibeview build --cloud --profile preview\`.`
2331
+ )
2332
+ );
2333
+ }
2160
2334
  }
2161
- cfgRef.current = persistPromptedProductionBuild(cwd, platform, answered);
2162
- return { command: answered.productionCommand, artifact: answered.productionArtifact };
2335
+ cfgRef.current = persistPromptedProfiles(cwd, platform, save);
2336
+ return resolveProfile(cfgRef.current.platforms[platform]?.build, name);
2163
2337
  }
2164
2338
  async function buildCommand(opts, cloudDeps) {
2165
2339
  const cwd = process.cwd();
2340
+ if (opts.profile !== void 0) {
2341
+ if (!PROFILE_NAME_RE.test(opts.profile)) {
2342
+ printFailure(
2343
+ opts,
2344
+ `--profile must be a profile name (lowercase letters, digits and dashes, starting with a letter), got "${opts.profile}"`,
2345
+ "usage"
2346
+ );
2347
+ process.exitCode = 2;
2348
+ return;
2349
+ }
2350
+ if (opts.production && opts.profile !== PRODUCTION) {
2351
+ printFailure(
2352
+ opts,
2353
+ `--production builds the production profile \u2014 pass --production or --profile ${opts.profile}, not both`,
2354
+ "usage"
2355
+ );
2356
+ process.exitCode = 2;
2357
+ return;
2358
+ }
2359
+ }
2360
+ opts = { ...opts, production: requestedProfile(opts) === PRODUCTION };
2166
2361
  const cfgRef = { current: loadDevConfig(cwd) };
2167
2362
  try {
2168
2363
  if (opts.cloud) {
@@ -2191,17 +2386,29 @@ async function buildCommand(opts, cloudDeps) {
2191
2386
  );
2192
2387
  }
2193
2388
  const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
2194
- const build = await resolveBuildConfig(cwd, cfgRef, platform, opts);
2389
+ const profile = await resolveBuildProfile(cwd, cfgRef, platform, opts);
2390
+ if (profile.distribution === "production") {
2391
+ fail(
2392
+ opts,
2393
+ `the "${profile.name}" profile is a signed production build, which runs in the cloud (vibeview build --cloud --profile ${profile.name})`,
2394
+ "usage"
2395
+ );
2396
+ }
2195
2397
  const result = await runBuildForPlatform(
2196
2398
  cwd,
2197
2399
  platform,
2198
- build,
2400
+ profile,
2199
2401
  opts,
2200
2402
  (l) => console.error(chalk3.dim(l))
2201
2403
  );
2202
2404
  for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
2203
2405
  if (result.launch_warning) console.error(chalk3.yellow(`\u26A0 ${result.launch_warning}`));
2204
- persistUploadedAppId(cwd, platform, build, result.public_id);
2406
+ persistUploadedAppId(
2407
+ cwd,
2408
+ platform,
2409
+ cfgRef.current?.platforms?.[platform]?.build,
2410
+ result.public_id
2411
+ );
2205
2412
  if (opts.json) {
2206
2413
  console.log(
2207
2414
  JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
@@ -2258,6 +2465,25 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
2258
2465
  );
2259
2466
  }
2260
2467
  }
2468
+ if (opts.json || !process.stdin.isTTY) {
2469
+ const name = requestedProfile(opts);
2470
+ const missing = chosenPlatforms.filter(
2471
+ (p) => !hasProfile(cfgRef.current?.platforms?.[p]?.build, name)
2472
+ );
2473
+ for (const p of missing) {
2474
+ const reason = `no "${name}" profile in vibeview.json`;
2475
+ emitCloud(opts.json, { event: "skipped", platform: p, reason });
2476
+ if (!opts.json) console.error(chalk3.yellow(`\u26A0 skipping ${p}: ${reason}`));
2477
+ }
2478
+ chosenPlatforms = chosenPlatforms.filter((p) => !missing.includes(p));
2479
+ if (chosenPlatforms.length === 0) {
2480
+ fail(
2481
+ opts,
2482
+ `--platform all: no configured platform has a "${name}" profile \u2014 add platforms.<platform>.build.profiles.${name} {command, artifact} to vibeview.json`,
2483
+ "config_required"
2484
+ );
2485
+ }
2486
+ }
2261
2487
  return chosenPlatforms;
2262
2488
  }
2263
2489
  if (opts.json || !process.stdin.isTTY) {
@@ -2273,13 +2499,13 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
2273
2499
  });
2274
2500
  requireCloudCapable(opts, chosen);
2275
2501
  requireProductionCapable(opts, chosen);
2276
- await resolveBuildConfig(cwd, cfgRef, chosen, opts);
2502
+ await resolveBuildProfile(cwd, cfgRef, chosen, opts);
2277
2503
  return [chosen];
2278
2504
  }
2279
2505
  const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd, cfgRef.current, opts);
2280
2506
  requireCloudCapable(opts, platform);
2281
2507
  requireProductionCapable(opts, platform);
2282
- await resolveBuildConfig(cwd, cfgRef, platform, opts);
2508
+ await resolveBuildProfile(cwd, cfgRef, platform, opts);
2283
2509
  return [platform];
2284
2510
  }
2285
2511
  function validateExplicitPlatform(raw) {
@@ -2476,19 +2702,19 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
2476
2702
  const client = new ApiClient(baseUrl, token);
2477
2703
  const d = deps ?? defaultCloudBuildDeps(client);
2478
2704
  const createFromArtifact = await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
2479
- const productionBuilds = /* @__PURE__ */ new Map();
2480
- if (opts.production) {
2481
- for (const p of platforms) {
2482
- productionBuilds.set(p, await resolveProductionBuildConfig(cwd, cfgRef, p, opts));
2483
- }
2705
+ const profiles = /* @__PURE__ */ new Map();
2706
+ for (const p of platforms) {
2707
+ profiles.set(p, await resolveBuildProfile(cwd, cfgRef, p, opts));
2484
2708
  }
2485
2709
  const cfg = cfgRef.current;
2486
2710
  for (const p of platforms) {
2711
+ const profile = profiles.get(p);
2487
2712
  try {
2488
- validateBuildEnv(cfg?.platforms?.[p]?.build?.env, p);
2489
- validateBuildXcode(cfg?.platforms?.[p]?.build?.xcode, p);
2490
- validateBuildNode(cfg?.platforms?.[p]?.build?.node, p);
2491
- if (cfg?.platforms?.[p]?.build?.autoIncrement === true && opts.production && createFromArtifact.has(p)) {
2713
+ validateCloudProfile(cfg.platforms[p].build, p, profile.name);
2714
+ if (profile.distribution === "production" && !PRODUCTION_SIGNING_PLATFORMS.includes(p)) {
2715
+ throw new UsageError(productionUnsupportedReason(p));
2716
+ }
2717
+ if (profile.autoIncrement && profile.distribution === "production" && createFromArtifact.has(p)) {
2492
2718
  throw new UsageError(
2493
2719
  `platforms.${p}.build.autoIncrement needs the build linked to an app \u2014 set appId in vibeview.json (a brand-new app's first build can't auto-increment)`
2494
2720
  );
@@ -2514,7 +2740,7 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
2514
2740
  const uploaded = new Set(files);
2515
2741
  const warnings = /* @__PURE__ */ new Map();
2516
2742
  for (const p of platforms) {
2517
- const command = productionBuilds.get(p)?.command ?? cfg.platforms[p].build.command;
2743
+ const command = profiles.get(p).command;
2518
2744
  const missingPkg = missingPackageJsonError(command, uploaded, cwd);
2519
2745
  if (missingPkg) fail(opts, missingPkg, "no_package_json");
2520
2746
  for (const w of buildCommandWarnings(command, uploaded)) {
@@ -2552,36 +2778,19 @@ ${offenders}`);
2552
2778
  packSpinner?.succeed(`packed source (${files.length} files, ${formatMB(sizeBytes)} MB)`);
2553
2779
  const manifestEntries = platforms.map((p) => {
2554
2780
  const entry = cfg.platforms[p];
2555
- const build = entry.build;
2556
- const production = productionBuilds.get(p);
2557
- const command = production ? production.command : build.command;
2558
- const artifactPath = production ? production.artifact : build.artifact;
2559
- const autoIncrement = build.autoIncrement === true;
2560
- if (autoIncrement && !production && !opts.json) {
2781
+ const profile = profiles.get(p);
2782
+ if (profile.autoIncrement && profile.distribution !== "production" && !opts.json) {
2561
2783
  console.error(
2562
2784
  chalk3.yellow(
2563
2785
  `note: platforms.${p}.build.autoIncrement only applies to production builds (--production) \u2014 ignoring it for this simulator build`
2564
2786
  )
2565
2787
  );
2566
2788
  }
2567
- const xcode = platformUsesXcode(p) ? opts.xcode ?? build.xcode : void 0;
2568
- const node = nodeFlag ?? (build.node === void 0 ? void 0 : normalizeNodeVersion(build.node));
2569
- return {
2570
- platform: p,
2571
- // OMITTED, never invented, for a platform whose app the artifact will
2572
- // create (resolveCloudAppIds). Otherwise the non-null assertion is
2573
- // safe: resolveCloudAppIds ran for every entry in `platforms` above —
2574
- // it either persisted a picked appId onto the entry, marked the
2575
- // platform create-from-artifact, or failed the whole command.
2576
- ...createFromArtifact.has(p) ? {} : { app_public_id: entry.appId },
2577
- command,
2578
- artifact_path: artifactPath,
2579
- distribution: production ? "production" : "simulator",
2580
- ...build.env ? { env: build.env } : {},
2581
- ...autoIncrement && production ? { auto_increment: true } : {},
2582
- ...xcode ? { xcode } : {},
2583
- ...node ? { node } : {}
2584
- };
2789
+ return cloudManifestEntry(p, profile, {
2790
+ xcode: opts.xcode,
2791
+ node: nodeFlag,
2792
+ appPublicId: createFromArtifact.has(p) ? void 0 : entry.appId
2793
+ });
2585
2794
  });
2586
2795
  const uploadSpinner = !opts.json ? createSpinner("uploading source...") : null;
2587
2796
  uploadSpinner?.start();
@@ -2613,8 +2822,11 @@ ${offenders}`);
2613
2822
  return;
2614
2823
  }
2615
2824
  const finished = await waitForCloudBuilds(created, opts, d);
2616
- if (!opts.production) {
2617
- persistResolvedAppIds(cwd, cfgRef, createFromArtifact, finished.values());
2825
+ const unlinkedSimulator = new Set(
2826
+ [...createFromArtifact].filter((p) => profiles.get(p)?.distribution !== "production")
2827
+ );
2828
+ if (unlinkedSimulator.size > 0) {
2829
+ persistResolvedAppIds(cwd, cfgRef, unlinkedSimulator, finished.values());
2618
2830
  }
2619
2831
  let downloadFailed = false;
2620
2832
  if (opts.download) {
@@ -3941,7 +4153,20 @@ async function ensureConfig(api, opts) {
3941
4153
  if (!existing) {
3942
4154
  isFirstRun = true;
3943
4155
  if (!opts.json && process.stdin.isTTY) {
3944
- firstRunBuild = await promptBuildConfig(process.cwd(), platform);
4156
+ const answered = await promptBuildConfig(process.cwd(), platform);
4157
+ if (answered) {
4158
+ firstRunBuild = withProfile(void 0, DEVELOPMENT, answered);
4159
+ const preview = await companionPreview(process.cwd(), platform, answered);
4160
+ if (preview) {
4161
+ firstRunBuild = withProfile(firstRunBuild, PREVIEW, preview);
4162
+ console.error(
4163
+ chalk4.dim(
4164
+ ` Also saved a preview profile for ${platform}: the same build in Release, which runs without Metro
4165
+ (share links, tests, GitHub pull requests). Build it with \`vibeview build --cloud --profile preview\`.`
4166
+ )
4167
+ );
4168
+ }
4169
+ }
3945
4170
  if (PLATFORM_NEEDS_METRO[platform]) {
3946
4171
  firstRunMetro = await promptMetroCommand(process.cwd(), platform);
3947
4172
  }
@@ -3974,7 +4199,7 @@ ${hint}`);
3974
4199
  chalk4.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
3975
4200
  );
3976
4201
  }
3977
- const buildConfig = firstRunBuild ?? entry?.build ?? null;
4202
+ const buildConfig = developmentProfile(firstRunBuild ?? entry?.build);
3978
4203
  if (!appId && buildConfig && typeof opts.build !== "string") {
3979
4204
  const strategy = buildAppStrategy(apps, opts.build === true, opts.json, process.stdin.isTTY);
3980
4205
  let action = strategy === "ask" ? "pick" : strategy;
@@ -4183,7 +4408,7 @@ async function runDevInProcess(opts) {
4183
4408
  }
4184
4409
  }
4185
4410
  const interactive = !opts.json && Boolean(process.stdin.isTTY);
4186
- const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
4411
+ const plannedBuild = developmentProfile(cfg.savedConfig.platforms?.[cfg.platform]?.build);
4187
4412
  if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
4188
4413
  const message = noBuildConfigMessage(cfg.platform, interactive);
4189
4414
  console.error(chalk4.red(`error: ${message}`));
@@ -4193,7 +4418,7 @@ async function runDevInProcess(opts) {
4193
4418
  cfg.commitConfig();
4194
4419
  if (opts.build === true && !cfg.builtThisRun) {
4195
4420
  const reloaded = loadDevConfig(process.cwd());
4196
- const build = reloaded?.platforms?.[cfg.platform]?.build;
4421
+ const build = developmentProfile(reloaded?.platforms?.[cfg.platform]?.build);
4197
4422
  if (!build) {
4198
4423
  const message = noBuildConfigMessage(cfg.platform, interactive);
4199
4424
  console.error(chalk4.red(`error: ${message}`));
@@ -4285,7 +4510,7 @@ async function runDevInProcess(opts) {
4285
4510
  metroPid = metroResult.pid ?? null;
4286
4511
  } else {
4287
4512
  const rokuEntry = loadDevConfig(process.cwd())?.platforms?.[cfg.platform];
4288
- rokuBuild = rokuEntry?.build;
4513
+ rokuBuild = developmentProfile(rokuEntry?.build) ?? void 0;
4289
4514
  watchIgnore = rokuEntry?.watchIgnore ?? [];
4290
4515
  if (!rokuBuild) {
4291
4516
  const message = noBuildConfigMessage(cfg.platform, interactive);
@@ -5671,6 +5896,7 @@ export {
5671
5896
  stepsNotCheckedLabel,
5672
5897
  notCheckedSuffix,
5673
5898
  outcomeOf,
5899
+ developmentProfile,
5674
5900
  DEV_PLATFORMS,
5675
5901
  PLATFORM_NEEDS_METRO,
5676
5902
  isDevPlatform,
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  clearDevState,
9
9
  createRokuPush,
10
10
  dev,
11
+ developmentProfile,
11
12
  feedbackSentLine,
12
13
  fetchFinalSummary,
13
14
  filterDeviceModels,
@@ -34,7 +35,7 @@ import {
34
35
  runAgentVerb,
35
36
  runStepsNotChecked,
36
37
  stepsNotCheckedLabel
37
- } from "./chunk-BPHIX73G.js";
38
+ } from "./chunk-ECWDMUNS.js";
38
39
  import {
39
40
  ApiClient,
40
41
  ApiStatusError,
@@ -2065,7 +2066,7 @@ async function devReload(opts) {
2065
2066
  }
2066
2067
  let build;
2067
2068
  try {
2068
- build = loadDevConfig(cwd)?.platforms?.roku?.build;
2069
+ build = developmentProfile(loadDevConfig(cwd)?.platforms?.roku?.build);
2069
2070
  } catch (err) {
2070
2071
  fail(
2071
2072
  opts,
@@ -2078,7 +2079,7 @@ async function devReload(opts) {
2078
2079
  fail(
2079
2080
  opts,
2080
2081
  "config_required",
2081
- "no build config for roku \u2014 run `vibeview build --platform roku` once to set it up",
2082
+ "no development build profile for roku \u2014 run `vibeview build --platform roku` once to set it up",
2082
2083
  1
2083
2084
  );
2084
2085
  }
@@ -2119,7 +2120,7 @@ import chalk16 from "chalk";
2119
2120
 
2120
2121
  // src/lib/formatters/builds.ts
2121
2122
  import chalk15 from "chalk";
2122
- var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
2123
+ var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP", "PROFILE"];
2123
2124
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
2124
2125
  function formatCostCell(costMicro, status) {
2125
2126
  if (costMicro <= 0 && UNBILLED_STATUSES.has(status)) return "-";
@@ -2144,7 +2145,10 @@ function formatBuildsHuman(builds2) {
2144
2145
  b.status,
2145
2146
  formatUtcTimestamp(b.created_at),
2146
2147
  formatCostCell(b.cost_micro, b.status),
2147
- b.app_public_id ?? ""
2148
+ b.app_public_id ?? "",
2149
+ // Last, so the columns scripts already read keep their place. A build
2150
+ // from a CLI that predates profiles shows its distribution.
2151
+ b.profile ?? b.distribution ?? ""
2148
2152
  ]);
2149
2153
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2150
2154
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
@@ -4369,9 +4373,12 @@ program.command("build").description("Run your configured build command and uplo
4369
4373
  "--download",
4370
4374
  "--cloud only: download the artifact of each succeeded build (implies wait)",
4371
4375
  false
4376
+ ).option(
4377
+ "--profile <name>",
4378
+ "The vibeview.json build profile to build: development (default), preview, production, or one of your own"
4372
4379
  ).option(
4373
4380
  "--production",
4374
- "--cloud only: build a signed, installable production artifact (.aab/.ipa) using uploaded signing credentials (see `vibeview credentials add`), instead of the default unsigned simulator build",
4381
+ "--cloud only: build the production profile, a signed, installable artifact (.aab/.ipa) using uploaded signing credentials (see `vibeview credentials add`); same as --profile production",
4375
4382
  false
4376
4383
  ).option(
4377
4384
  "--xcode <version>",
@@ -4511,7 +4518,7 @@ program.command("dev-reload").description(
4511
4518
  program.command("mcp").description(
4512
4519
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4513
4520
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4514
- const { runMcpServer } = await import("./server-Q2PYPAKB.js");
4521
+ const { runMcpServer } = await import("./server-IUCBELOJ.js");
4515
4522
  await runMcpServer({ ...opts, version: pkg.version });
4516
4523
  });
4517
4524
  program.command("agent-setup").description(
@@ -15,6 +15,7 @@ import {
15
15
  createRokuPush,
16
16
  decodeScreenshotResult,
17
17
  describeDevBuild,
18
+ developmentProfile,
18
19
  effectiveDevConfig,
19
20
  feedbackSentLine,
20
21
  filterDeviceModels,
@@ -34,7 +35,7 @@ import {
34
35
  renderEnvelope,
35
36
  resolveMetro,
36
37
  startDevSession
37
- } from "./chunk-BPHIX73G.js";
38
+ } from "./chunk-ECWDMUNS.js";
38
39
  import {
39
40
  ApiClient,
40
41
  NotFoundError,
@@ -1099,7 +1100,7 @@ function createDevSessionController(auth, webUrl) {
1099
1100
  let idle = null;
1100
1101
  const readRokuBuild = () => {
1101
1102
  try {
1102
- return loadDevConfig(process.cwd())?.platforms?.roku?.build ?? null;
1103
+ return developmentProfile(loadDevConfig(process.cwd())?.platforms?.roku?.build);
1103
1104
  } catch (err) {
1104
1105
  throw new Error(
1105
1106
  `vibeview.json could not be read: ${err instanceof Error ? err.message : String(err)}`
@@ -1361,7 +1362,7 @@ function createDevSessionController(auth, webUrl) {
1361
1362
  }
1362
1363
  if (!target.build) {
1363
1364
  throw new Error(
1364
- "no build config for roku \u2014 add platforms.roku.build ({ command, artifact }) to vibeview.json, or run `vibeview build --platform roku` once to set it up"
1365
+ "no development build profile for roku \u2014 add platforms.roku.build.profiles.development ({ command, artifact }) to vibeview.json, or run `vibeview build --platform roku` once to set it up"
1365
1366
  );
1366
1367
  }
1367
1368
  const started = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
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",