vibeview 0.49.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 #
|
|
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
|
-
|
|
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`
|
|
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` (
|
|
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
|
-
|
|
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,54 @@ 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}.
|
|
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}.
|
|
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}.
|
|
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}.
|
|
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}.
|
|
321
|
+
`vibeview.json: platforms.${platformKey}.${at}.xcode must be a version string like "27.1"`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function normalizeNodeVersion(value) {
|
|
326
|
+
if (typeof value === "number") {
|
|
327
|
+
return Number.isInteger(value) && value > 0 && value < 1e3 ? String(value) : null;
|
|
328
|
+
}
|
|
329
|
+
if (typeof value !== "string") return null;
|
|
330
|
+
const m = /^v?(\d{1,3})(?:\.x)?$/.exec(value.trim());
|
|
331
|
+
return m ? String(Number(m[1])) : null;
|
|
332
|
+
}
|
|
333
|
+
function validateBuildNode(node, platformKey, at = "build") {
|
|
334
|
+
if (node === void 0) return;
|
|
335
|
+
if (normalizeNodeVersion(node) === null) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`vibeview.json: platforms.${platformKey}.${at}.node must be a Node.js major version like "24"`
|
|
205
338
|
);
|
|
206
339
|
}
|
|
207
340
|
}
|
|
@@ -1301,6 +1434,57 @@ function iosArchiveExportCommand(ws, scheme = ws) {
|
|
|
1301
1434
|
function managedExpoIosProductionCommand(cwd, name) {
|
|
1302
1435
|
return `${installCommand(cwd)} && npx expo prebuild --platform ios && ` + iosArchiveExportCommand(name);
|
|
1303
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
|
+
}
|
|
1304
1488
|
function productionDefaultsFor(cwd, platform, evaluatedIosName) {
|
|
1305
1489
|
const flavor = detectFlavor(cwd);
|
|
1306
1490
|
if ((flavor === "flutter" || flavor === "capacitor" || flavor === "native") && platform !== "tvos" && platform !== "roku") {
|
|
@@ -1480,7 +1664,8 @@ function evaluatedExpoName(cwd, readConfig, onEvaluate) {
|
|
|
1480
1664
|
}
|
|
1481
1665
|
async function resolvedDefaultsFor(cwd, platform, opts = {}) {
|
|
1482
1666
|
const { production = false, readConfig = (c) => readExpoConfig(c), onEvaluate } = opts;
|
|
1483
|
-
const
|
|
1667
|
+
const profile = opts.profile ?? (production ? "production" : "development");
|
|
1668
|
+
const base = profile === "production" ? productionDefaultsFor : profile === "preview" ? previewDefaultsFor : defaultsFor;
|
|
1484
1669
|
const dynamic = dynamicExpoConfig(cwd);
|
|
1485
1670
|
if (platform !== "ios" || !dynamic || !isManagedExpo(cwd, "ios")) {
|
|
1486
1671
|
return base(cwd, platform);
|
|
@@ -1525,7 +1710,7 @@ function fillLaterHint(skipped, platform) {
|
|
|
1525
1710
|
const lines = ["You can enable one-command builds later \u2014 add to vibeview.json:"];
|
|
1526
1711
|
if (skipped.build) {
|
|
1527
1712
|
lines.push(
|
|
1528
|
-
` 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>" }`
|
|
1529
1714
|
);
|
|
1530
1715
|
}
|
|
1531
1716
|
if (skipped.metro) {
|
|
@@ -1571,56 +1756,44 @@ async function commandSelect(message, detected) {
|
|
|
1571
1756
|
choices.push({ value: "skip", name: "Skip for now" });
|
|
1572
1757
|
return select({ message, choices });
|
|
1573
1758
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
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 });
|
|
1576
1766
|
evaluatedConfigNote(cwd, defaults);
|
|
1577
|
-
const
|
|
1767
|
+
const label = commandLabel(profile);
|
|
1768
|
+
const choice = await commandSelect(`${label} for ${platform}`, defaults.buildCommand);
|
|
1578
1769
|
if (choice === "skip") return null;
|
|
1579
1770
|
if (choice === "default") {
|
|
1580
1771
|
return { command: defaults.buildCommand, artifact: defaults.artifact };
|
|
1581
1772
|
}
|
|
1582
1773
|
console.log(
|
|
1583
1774
|
chalk2.dim(
|
|
1584
|
-
" 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`")
|
|
1585
1776
|
)
|
|
1586
1777
|
);
|
|
1587
|
-
const command = await input({ message:
|
|
1778
|
+
const command = await input({ message: `${label} for ${platform}`, required: true });
|
|
1588
1779
|
const artifact = await input({
|
|
1589
|
-
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)",
|
|
1590
1781
|
default: defaults.artifact,
|
|
1591
1782
|
required: true
|
|
1592
1783
|
});
|
|
1593
1784
|
return { command, artifact };
|
|
1594
1785
|
}
|
|
1595
|
-
async function
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
)
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
productionArtifact: defaults.artifact
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
console.log(
|
|
1610
|
-
chalk2.dim(
|
|
1611
|
-
" Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && cd android && ./gradlew bundleRelease`"
|
|
1612
|
-
)
|
|
1613
|
-
);
|
|
1614
|
-
const productionCommand = await input({
|
|
1615
|
-
message: `Production build command for ${platform}`,
|
|
1616
|
-
required: true
|
|
1617
|
-
});
|
|
1618
|
-
const productionArtifact = await input({
|
|
1619
|
-
message: "Path to the signed production artifact it produces (.aab/.ipa; * allowed)",
|
|
1620
|
-
default: defaults.artifact,
|
|
1621
|
-
required: true
|
|
1622
|
-
});
|
|
1623
|
-
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 };
|
|
1624
1797
|
}
|
|
1625
1798
|
async function promptMetroCommand(cwd, platform) {
|
|
1626
1799
|
const defaults = defaultsFor(cwd, platform);
|
|
@@ -1800,6 +1973,39 @@ function runTar(args) {
|
|
|
1800
1973
|
});
|
|
1801
1974
|
}
|
|
1802
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
|
+
|
|
1803
2009
|
// src/lib/build-preflight.ts
|
|
1804
2010
|
import { execFile as execFile4 } from "child_process";
|
|
1805
2011
|
import { realpathSync, statSync as statSync4 } from "fs";
|
|
@@ -2072,81 +2278,86 @@ async function resolveBarePlatform(cwd, cfg, opts) {
|
|
|
2072
2278
|
}
|
|
2073
2279
|
return platform;
|
|
2074
2280
|
}
|
|
2075
|
-
function
|
|
2281
|
+
function persistPromptedProfiles(cwd, platform, profiles) {
|
|
2076
2282
|
return updateDevConfig(cwd, platform, (next) => {
|
|
2077
|
-
const entry = next.platforms[platform];
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
}
|
|
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;
|
|
2083
2288
|
});
|
|
2084
2289
|
}
|
|
2085
2290
|
function persistUploadedAppId(cwd, platform, build, appId) {
|
|
2086
2291
|
updateDevConfig(cwd, platform, (next) => {
|
|
2087
2292
|
const entry = next.platforms[platform];
|
|
2088
2293
|
if (!entry) {
|
|
2089
|
-
next.platforms[platform] = { appId, build };
|
|
2294
|
+
next.platforms[platform] = build ? { appId, build } : { appId };
|
|
2090
2295
|
return true;
|
|
2091
2296
|
}
|
|
2092
2297
|
if (entry.appId === appId && entry.build) return false;
|
|
2093
2298
|
entry.appId = appId;
|
|
2094
|
-
if (!entry.build) entry.build = build;
|
|
2299
|
+
if (!entry.build && build) entry.build = build;
|
|
2095
2300
|
return true;
|
|
2096
2301
|
});
|
|
2097
2302
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
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);
|
|
2100
2310
|
if (existing) return existing;
|
|
2101
2311
|
if (opts.json || !process.stdin.isTTY) {
|
|
2102
|
-
fail(
|
|
2103
|
-
opts,
|
|
2104
|
-
`no build config for ${platform}: add platforms.${platform}.build {command, artifact} to vibeview.json`,
|
|
2105
|
-
"config_required"
|
|
2106
|
-
);
|
|
2312
|
+
fail(opts, missingProfileMessage(platform, name), "config_required");
|
|
2107
2313
|
}
|
|
2108
|
-
const answered = await
|
|
2314
|
+
const answered = await promptProfileConfig(cwd, platform, name);
|
|
2109
2315
|
if (!answered) {
|
|
2110
|
-
fail(opts, "build config is required to run vibeview build", "config_required");
|
|
2111
|
-
}
|
|
2112
|
-
cfgRef.current = persistPromptedBuild(cwd, platform, answered);
|
|
2113
|
-
return answered;
|
|
2114
|
-
}
|
|
2115
|
-
function persistPromptedProductionBuild(cwd, platform, production) {
|
|
2116
|
-
return updateDevConfig(cwd, platform, (next) => {
|
|
2117
|
-
mergeProductionPair(next, platform, production);
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
function mergeProductionPair(next, platform, production) {
|
|
2121
|
-
const existingBuild = next.platforms[platform]?.build;
|
|
2122
|
-
if (!existingBuild?.command || !existingBuild?.artifact) {
|
|
2123
|
-
throw new Error(
|
|
2124
|
-
`internal: cannot persist a production build config for ${platform} before its debug command/artifact are resolved`
|
|
2125
|
-
);
|
|
2126
|
-
}
|
|
2127
|
-
next.platforms[platform].build = { ...existingBuild, ...production };
|
|
2128
|
-
}
|
|
2129
|
-
async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
|
|
2130
|
-
const entry = cfgRef.current?.platforms?.[platform]?.build;
|
|
2131
|
-
if (entry?.productionCommand && entry?.productionArtifact) {
|
|
2132
|
-
return { command: entry.productionCommand, artifact: entry.productionArtifact };
|
|
2133
|
-
}
|
|
2134
|
-
if (opts.json || !process.stdin.isTTY) {
|
|
2135
2316
|
fail(
|
|
2136
2317
|
opts,
|
|
2137
|
-
|
|
2318
|
+
name === PRODUCTION ? "a production build config is required for --production" : `a "${name}" build profile is required to run vibeview build`,
|
|
2138
2319
|
"config_required"
|
|
2139
2320
|
);
|
|
2140
2321
|
}
|
|
2141
|
-
const
|
|
2142
|
-
if (!
|
|
2143
|
-
|
|
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
|
+
}
|
|
2144
2334
|
}
|
|
2145
|
-
cfgRef.current =
|
|
2146
|
-
return
|
|
2335
|
+
cfgRef.current = persistPromptedProfiles(cwd, platform, save);
|
|
2336
|
+
return resolveProfile(cfgRef.current.platforms[platform]?.build, name);
|
|
2147
2337
|
}
|
|
2148
2338
|
async function buildCommand(opts, cloudDeps) {
|
|
2149
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 };
|
|
2150
2361
|
const cfgRef = { current: loadDevConfig(cwd) };
|
|
2151
2362
|
try {
|
|
2152
2363
|
if (opts.cloud) {
|
|
@@ -2167,18 +2378,37 @@ async function buildCommand(opts, cloudDeps) {
|
|
|
2167
2378
|
"usage"
|
|
2168
2379
|
);
|
|
2169
2380
|
}
|
|
2381
|
+
if (opts.node !== void 0) {
|
|
2382
|
+
fail(
|
|
2383
|
+
opts,
|
|
2384
|
+
"--node requires --cloud: a local build uses the Node.js on this machine",
|
|
2385
|
+
"usage"
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2170
2388
|
const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
2171
|
-
const
|
|
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
|
+
}
|
|
2172
2397
|
const result = await runBuildForPlatform(
|
|
2173
2398
|
cwd,
|
|
2174
2399
|
platform,
|
|
2175
|
-
|
|
2400
|
+
profile,
|
|
2176
2401
|
opts,
|
|
2177
2402
|
(l) => console.error(chalk3.dim(l))
|
|
2178
2403
|
);
|
|
2179
2404
|
for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
|
|
2180
2405
|
if (result.launch_warning) console.error(chalk3.yellow(`\u26A0 ${result.launch_warning}`));
|
|
2181
|
-
persistUploadedAppId(
|
|
2406
|
+
persistUploadedAppId(
|
|
2407
|
+
cwd,
|
|
2408
|
+
platform,
|
|
2409
|
+
cfgRef.current?.platforms?.[platform]?.build,
|
|
2410
|
+
result.public_id
|
|
2411
|
+
);
|
|
2182
2412
|
if (opts.json) {
|
|
2183
2413
|
console.log(
|
|
2184
2414
|
JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
|
|
@@ -2235,6 +2465,25 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
|
|
|
2235
2465
|
);
|
|
2236
2466
|
}
|
|
2237
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
|
+
}
|
|
2238
2487
|
return chosenPlatforms;
|
|
2239
2488
|
}
|
|
2240
2489
|
if (opts.json || !process.stdin.isTTY) {
|
|
@@ -2250,13 +2499,13 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
|
|
|
2250
2499
|
});
|
|
2251
2500
|
requireCloudCapable(opts, chosen);
|
|
2252
2501
|
requireProductionCapable(opts, chosen);
|
|
2253
|
-
await
|
|
2502
|
+
await resolveBuildProfile(cwd, cfgRef, chosen, opts);
|
|
2254
2503
|
return [chosen];
|
|
2255
2504
|
}
|
|
2256
2505
|
const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
2257
2506
|
requireCloudCapable(opts, platform);
|
|
2258
2507
|
requireProductionCapable(opts, platform);
|
|
2259
|
-
await
|
|
2508
|
+
await resolveBuildProfile(cwd, cfgRef, platform, opts);
|
|
2260
2509
|
return [platform];
|
|
2261
2510
|
}
|
|
2262
2511
|
function validateExplicitPlatform(raw) {
|
|
@@ -2437,6 +2686,10 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
|
2437
2686
|
if (opts.xcode !== void 0 && !XCODE_VERSION_RE.test(opts.xcode)) {
|
|
2438
2687
|
fail(opts, `--xcode must be a version like 27.1, got "${opts.xcode}"`, "usage");
|
|
2439
2688
|
}
|
|
2689
|
+
const nodeFlag = opts.node === void 0 ? void 0 : normalizeNodeVersion(opts.node);
|
|
2690
|
+
if (nodeFlag === null) {
|
|
2691
|
+
fail(opts, `--node must be a Node.js major version like 24, got "${opts.node}"`, "usage");
|
|
2692
|
+
}
|
|
2440
2693
|
const platforms = await resolveCloudPlatforms(cwd, cfgRef, opts);
|
|
2441
2694
|
if (opts.xcode !== void 0 && !platforms.some(platformUsesXcode)) {
|
|
2442
2695
|
fail(
|
|
@@ -2449,18 +2702,19 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
|
2449
2702
|
const client = new ApiClient(baseUrl, token);
|
|
2450
2703
|
const d = deps ?? defaultCloudBuildDeps(client);
|
|
2451
2704
|
const createFromArtifact = await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
productionBuilds.set(p, await resolveProductionBuildConfig(cwd, cfgRef, p, opts));
|
|
2456
|
-
}
|
|
2705
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
2706
|
+
for (const p of platforms) {
|
|
2707
|
+
profiles.set(p, await resolveBuildProfile(cwd, cfgRef, p, opts));
|
|
2457
2708
|
}
|
|
2458
2709
|
const cfg = cfgRef.current;
|
|
2459
2710
|
for (const p of platforms) {
|
|
2711
|
+
const profile = profiles.get(p);
|
|
2460
2712
|
try {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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)) {
|
|
2464
2718
|
throw new UsageError(
|
|
2465
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)`
|
|
2466
2720
|
);
|
|
@@ -2486,7 +2740,7 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
|
2486
2740
|
const uploaded = new Set(files);
|
|
2487
2741
|
const warnings = /* @__PURE__ */ new Map();
|
|
2488
2742
|
for (const p of platforms) {
|
|
2489
|
-
const command =
|
|
2743
|
+
const command = profiles.get(p).command;
|
|
2490
2744
|
const missingPkg = missingPackageJsonError(command, uploaded, cwd);
|
|
2491
2745
|
if (missingPkg) fail(opts, missingPkg, "no_package_json");
|
|
2492
2746
|
for (const w of buildCommandWarnings(command, uploaded)) {
|
|
@@ -2524,34 +2778,19 @@ ${offenders}`);
|
|
|
2524
2778
|
packSpinner?.succeed(`packed source (${files.length} files, ${formatMB(sizeBytes)} MB)`);
|
|
2525
2779
|
const manifestEntries = platforms.map((p) => {
|
|
2526
2780
|
const entry = cfg.platforms[p];
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
const command = production ? production.command : build.command;
|
|
2530
|
-
const artifactPath = production ? production.artifact : build.artifact;
|
|
2531
|
-
const autoIncrement = build.autoIncrement === true;
|
|
2532
|
-
if (autoIncrement && !production && !opts.json) {
|
|
2781
|
+
const profile = profiles.get(p);
|
|
2782
|
+
if (profile.autoIncrement && profile.distribution !== "production" && !opts.json) {
|
|
2533
2783
|
console.error(
|
|
2534
2784
|
chalk3.yellow(
|
|
2535
2785
|
`note: platforms.${p}.build.autoIncrement only applies to production builds (--production) \u2014 ignoring it for this simulator build`
|
|
2536
2786
|
)
|
|
2537
2787
|
);
|
|
2538
2788
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
// safe: resolveCloudAppIds ran for every entry in `platforms` above —
|
|
2545
|
-
// it either persisted a picked appId onto the entry, marked the
|
|
2546
|
-
// platform create-from-artifact, or failed the whole command.
|
|
2547
|
-
...createFromArtifact.has(p) ? {} : { app_public_id: entry.appId },
|
|
2548
|
-
command,
|
|
2549
|
-
artifact_path: artifactPath,
|
|
2550
|
-
distribution: production ? "production" : "simulator",
|
|
2551
|
-
...build.env ? { env: build.env } : {},
|
|
2552
|
-
...autoIncrement && production ? { auto_increment: true } : {},
|
|
2553
|
-
...xcode ? { xcode } : {}
|
|
2554
|
-
};
|
|
2789
|
+
return cloudManifestEntry(p, profile, {
|
|
2790
|
+
xcode: opts.xcode,
|
|
2791
|
+
node: nodeFlag,
|
|
2792
|
+
appPublicId: createFromArtifact.has(p) ? void 0 : entry.appId
|
|
2793
|
+
});
|
|
2555
2794
|
});
|
|
2556
2795
|
const uploadSpinner = !opts.json ? createSpinner("uploading source...") : null;
|
|
2557
2796
|
uploadSpinner?.start();
|
|
@@ -2583,8 +2822,11 @@ ${offenders}`);
|
|
|
2583
2822
|
return;
|
|
2584
2823
|
}
|
|
2585
2824
|
const finished = await waitForCloudBuilds(created, opts, d);
|
|
2586
|
-
|
|
2587
|
-
|
|
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());
|
|
2588
2830
|
}
|
|
2589
2831
|
let downloadFailed = false;
|
|
2590
2832
|
if (opts.download) {
|
|
@@ -3911,7 +4153,20 @@ async function ensureConfig(api, opts) {
|
|
|
3911
4153
|
if (!existing) {
|
|
3912
4154
|
isFirstRun = true;
|
|
3913
4155
|
if (!opts.json && process.stdin.isTTY) {
|
|
3914
|
-
|
|
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
|
+
}
|
|
3915
4170
|
if (PLATFORM_NEEDS_METRO[platform]) {
|
|
3916
4171
|
firstRunMetro = await promptMetroCommand(process.cwd(), platform);
|
|
3917
4172
|
}
|
|
@@ -3944,7 +4199,7 @@ ${hint}`);
|
|
|
3944
4199
|
chalk4.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
|
|
3945
4200
|
);
|
|
3946
4201
|
}
|
|
3947
|
-
const buildConfig = firstRunBuild ?? entry?.build
|
|
4202
|
+
const buildConfig = developmentProfile(firstRunBuild ?? entry?.build);
|
|
3948
4203
|
if (!appId && buildConfig && typeof opts.build !== "string") {
|
|
3949
4204
|
const strategy = buildAppStrategy(apps, opts.build === true, opts.json, process.stdin.isTTY);
|
|
3950
4205
|
let action = strategy === "ask" ? "pick" : strategy;
|
|
@@ -4153,7 +4408,7 @@ async function runDevInProcess(opts) {
|
|
|
4153
4408
|
}
|
|
4154
4409
|
}
|
|
4155
4410
|
const interactive = !opts.json && Boolean(process.stdin.isTTY);
|
|
4156
|
-
const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
|
|
4411
|
+
const plannedBuild = developmentProfile(cfg.savedConfig.platforms?.[cfg.platform]?.build);
|
|
4157
4412
|
if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
|
|
4158
4413
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
4159
4414
|
console.error(chalk4.red(`error: ${message}`));
|
|
@@ -4163,7 +4418,7 @@ async function runDevInProcess(opts) {
|
|
|
4163
4418
|
cfg.commitConfig();
|
|
4164
4419
|
if (opts.build === true && !cfg.builtThisRun) {
|
|
4165
4420
|
const reloaded = loadDevConfig(process.cwd());
|
|
4166
|
-
const build = reloaded?.platforms?.[cfg.platform]?.build;
|
|
4421
|
+
const build = developmentProfile(reloaded?.platforms?.[cfg.platform]?.build);
|
|
4167
4422
|
if (!build) {
|
|
4168
4423
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
4169
4424
|
console.error(chalk4.red(`error: ${message}`));
|
|
@@ -4255,7 +4510,7 @@ async function runDevInProcess(opts) {
|
|
|
4255
4510
|
metroPid = metroResult.pid ?? null;
|
|
4256
4511
|
} else {
|
|
4257
4512
|
const rokuEntry = loadDevConfig(process.cwd())?.platforms?.[cfg.platform];
|
|
4258
|
-
rokuBuild = rokuEntry?.build;
|
|
4513
|
+
rokuBuild = developmentProfile(rokuEntry?.build) ?? void 0;
|
|
4259
4514
|
watchIgnore = rokuEntry?.watchIgnore ?? [];
|
|
4260
4515
|
if (!rokuBuild) {
|
|
4261
4516
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
@@ -5641,6 +5896,7 @@ export {
|
|
|
5641
5896
|
stepsNotCheckedLabel,
|
|
5642
5897
|
notCheckedSuffix,
|
|
5643
5898
|
outcomeOf,
|
|
5899
|
+
developmentProfile,
|
|
5644
5900
|
DEV_PLATFORMS,
|
|
5645
5901
|
PLATFORM_NEEDS_METRO,
|
|
5646
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-
|
|
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
|
|
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,13 +4373,19 @@ 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
|
|
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>",
|
|
4378
4385
|
"--cloud only, ios/tvos: the Xcode version to build with (e.g. 27.1), overriding vibeview.json build.xcode for this run"
|
|
4386
|
+
).option(
|
|
4387
|
+
"--node <major>",
|
|
4388
|
+
"--cloud only: the Node.js major to build with (e.g. 24), overriding vibeview.json build.node and the project .nvmrc for this run"
|
|
4379
4389
|
).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
|
|
4380
4390
|
program.command("submit").description(
|
|
4381
4391
|
"Submit a succeeded production cloud build's artifact to TestFlight (ios/tvos) or a Google Play track (android/androidtv) \u2014 binary upload only; store listing and review stay in the store consoles"
|
|
@@ -4508,7 +4518,7 @@ program.command("dev-reload").description(
|
|
|
4508
4518
|
program.command("mcp").description(
|
|
4509
4519
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
4510
4520
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
4511
|
-
const { runMcpServer } = await import("./server-
|
|
4521
|
+
const { runMcpServer } = await import("./server-IUCBELOJ.js");
|
|
4512
4522
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
4513
4523
|
});
|
|
4514
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-
|
|
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
|
|
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
|
|
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.
|
|
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",
|