vibeview 0.50.0 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -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,157 @@ 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 profileNames(build) {
|
|
64
|
+
if (!build) return [];
|
|
65
|
+
const names = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const n of [DEVELOPMENT, PREVIEW, PRODUCTION, ...Object.keys(build.profiles ?? {})]) {
|
|
67
|
+
if (rawProfile(build, n)) names.add(n);
|
|
68
|
+
}
|
|
69
|
+
return [...names];
|
|
70
|
+
}
|
|
71
|
+
function hasProfile(build, name) {
|
|
72
|
+
return !!build && rawProfile(build, name) !== null;
|
|
73
|
+
}
|
|
74
|
+
function resolveProfile(build, name) {
|
|
75
|
+
if (!build) return null;
|
|
76
|
+
const p = rawProfile(build, name);
|
|
77
|
+
if (!p) return null;
|
|
78
|
+
const distribution = p.distribution ?? defaultDistribution(name);
|
|
79
|
+
const env = build.env === void 0 && p.env === void 0 ? void 0 : { ...build.env, ...p.env };
|
|
80
|
+
const xcode = p.xcode ?? build.xcode;
|
|
81
|
+
const node = p.node ?? build.node;
|
|
82
|
+
const autoIncrement = (p.autoIncrement ?? (name === PRODUCTION ? build.autoIncrement : void 0)) === true;
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
command: p.command,
|
|
86
|
+
artifact: p.artifact,
|
|
87
|
+
distribution,
|
|
88
|
+
...env !== void 0 ? { env } : {},
|
|
89
|
+
...xcode !== void 0 ? { xcode } : {},
|
|
90
|
+
...node !== void 0 ? { node } : {},
|
|
91
|
+
autoIncrement
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function missingProfileMessage(platformKey, name) {
|
|
95
|
+
return `no "${name}" profile for ${platformKey}: add platforms.${platformKey}.build.profiles.${name} {command, artifact} to vibeview.json`;
|
|
96
|
+
}
|
|
97
|
+
function developmentProfile(build) {
|
|
98
|
+
return resolveProfile(build, DEVELOPMENT);
|
|
99
|
+
}
|
|
100
|
+
function withProfile(build, name, profile) {
|
|
101
|
+
const {
|
|
102
|
+
command,
|
|
103
|
+
artifact,
|
|
104
|
+
productionCommand,
|
|
105
|
+
productionArtifact,
|
|
106
|
+
autoIncrement,
|
|
107
|
+
profiles,
|
|
108
|
+
...shared
|
|
109
|
+
} = build ?? {};
|
|
110
|
+
const next = {};
|
|
111
|
+
const out = { ...shared };
|
|
112
|
+
if (command && artifact) next[DEVELOPMENT] = { command, artifact };
|
|
113
|
+
if (productionCommand && productionArtifact) {
|
|
114
|
+
next[PRODUCTION] = { command: productionCommand, artifact: productionArtifact };
|
|
115
|
+
} else if (name !== PRODUCTION) {
|
|
116
|
+
if (productionCommand) out.productionCommand = productionCommand;
|
|
117
|
+
if (productionArtifact) out.productionArtifact = productionArtifact;
|
|
118
|
+
}
|
|
119
|
+
Object.assign(next, profiles);
|
|
120
|
+
next[name] = profile;
|
|
121
|
+
out.profiles = next;
|
|
122
|
+
if (autoIncrement !== void 0) {
|
|
123
|
+
const production = next[PRODUCTION];
|
|
124
|
+
if (!production) out.autoIncrement = autoIncrement;
|
|
125
|
+
else if (production.autoIncrement === void 0) {
|
|
126
|
+
next[PRODUCTION] = { ...production, autoIncrement };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function isObject(v) {
|
|
132
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
133
|
+
}
|
|
134
|
+
function nonEmpty(v) {
|
|
135
|
+
return typeof v === "string" && v.length > 0;
|
|
136
|
+
}
|
|
137
|
+
function validateBuildBlock(build, key) {
|
|
138
|
+
if (!isObject(build)) {
|
|
139
|
+
throw new Error(`vibeview.json: platforms.${key}.build must be an object with profiles`);
|
|
140
|
+
}
|
|
141
|
+
const at = `vibeview.json: platforms.${key}.build`;
|
|
142
|
+
if ("command" in build || "artifact" in build) {
|
|
143
|
+
if (!nonEmpty(build.command)) throw new Error(`${at}.command must be a non-empty string`);
|
|
144
|
+
if (!nonEmpty(build.artifact)) throw new Error(`${at}.artifact must be a non-empty string`);
|
|
145
|
+
}
|
|
146
|
+
for (const field of ["productionCommand", "productionArtifact"]) {
|
|
147
|
+
if (field in build && !nonEmpty(build[field])) {
|
|
148
|
+
throw new Error(`${at}.${field} must be a non-empty string`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const profiles = build.profiles;
|
|
152
|
+
if (profiles !== void 0) {
|
|
153
|
+
if (!isObject(profiles)) {
|
|
154
|
+
throw new Error(`${at}.profiles must be an object of named profiles`);
|
|
155
|
+
}
|
|
156
|
+
for (const [name, p] of Object.entries(profiles)) {
|
|
157
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`${at}.profiles: "${name}" is not a valid profile name (lowercase letters, digits and dashes, starting with a letter)`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!isObject(p)) throw new Error(`${at}.profiles.${name} must be an object`);
|
|
163
|
+
if (!nonEmpty(p.command)) {
|
|
164
|
+
throw new Error(`${at}.profiles.${name}.command must be a non-empty string`);
|
|
165
|
+
}
|
|
166
|
+
if (!nonEmpty(p.artifact)) {
|
|
167
|
+
throw new Error(`${at}.profiles.${name}.artifact must be a non-empty string`);
|
|
168
|
+
}
|
|
169
|
+
if (p.distribution !== void 0 && !DISTRIBUTIONS.includes(p.distribution)) {
|
|
170
|
+
throw new Error(`${at}.profiles.${name}.distribution must be "simulator" or "production"`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if ("command" in build && DEVELOPMENT in profiles) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`${at}: the development profile is defined twice \u2014 keep profiles.development and remove build.command and build.artifact`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if ("productionCommand" in build && PRODUCTION in profiles) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`${at}: the production profile is defined twice \u2014 keep profiles.production and remove build.productionCommand and build.productionArtifact`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const legacyDevelopment = nonEmpty(build.command);
|
|
185
|
+
const legacyProduction = nonEmpty(build.productionCommand) && nonEmpty(build.productionArtifact);
|
|
186
|
+
const named = isObject(profiles) && Object.keys(profiles).length > 0;
|
|
187
|
+
if (!legacyDevelopment && !legacyProduction && !named) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`${at} needs at least one profile: add build.profiles.development {command, artifact}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
43
194
|
// src/lib/dev-config.ts
|
|
44
195
|
import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
45
196
|
import { join } from "path";
|
|
@@ -121,33 +272,7 @@ function loadDevConfig(cwd) {
|
|
|
121
272
|
throw new Error(`vibeview.json: platforms.${key}.appId must be a non-empty string`);
|
|
122
273
|
}
|
|
123
274
|
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
|
-
}
|
|
275
|
+
validateBuildBlock(entry.build, key);
|
|
151
276
|
}
|
|
152
277
|
if (entry.watchIgnore !== void 0) {
|
|
153
278
|
if (!Array.isArray(entry.watchIgnore)) {
|
|
@@ -170,38 +295,38 @@ function loadDevConfig(cwd) {
|
|
|
170
295
|
}
|
|
171
296
|
return cfg;
|
|
172
297
|
}
|
|
173
|
-
function validateBuildEnv(env, platformKey) {
|
|
298
|
+
function validateBuildEnv(env, platformKey, at = "build") {
|
|
174
299
|
if (env === void 0) return;
|
|
175
300
|
if (typeof env !== "object" || env === null || Array.isArray(env)) {
|
|
176
301
|
throw new Error(
|
|
177
|
-
`vibeview.json: platforms.${platformKey}.
|
|
302
|
+
`vibeview.json: platforms.${platformKey}.${at}.env must be an object of string values`
|
|
178
303
|
);
|
|
179
304
|
}
|
|
180
305
|
for (const [envKey, envValue] of Object.entries(env)) {
|
|
181
306
|
if (typeof envValue !== "string") {
|
|
182
307
|
throw new Error(
|
|
183
|
-
`vibeview.json: platforms.${platformKey}.
|
|
308
|
+
`vibeview.json: platforms.${platformKey}.${at}.env must be an object of string values`
|
|
184
309
|
);
|
|
185
310
|
}
|
|
186
311
|
if (!ENV_KEY_RE.test(envKey)) {
|
|
187
312
|
throw new Error(
|
|
188
|
-
`vibeview.json: platforms.${platformKey}.
|
|
313
|
+
`vibeview.json: platforms.${platformKey}.${at}.env key "${envKey}" is not a valid environment variable name (must match ${ENV_KEY_RE})`
|
|
189
314
|
);
|
|
190
315
|
}
|
|
191
316
|
}
|
|
192
317
|
}
|
|
193
318
|
var XCODE_PLATFORMS = ["ios", "tvos"];
|
|
194
319
|
var XCODE_VERSION_RE = /^\d+\.\d+$/;
|
|
195
|
-
function validateBuildXcode(xcode, platformKey) {
|
|
320
|
+
function validateBuildXcode(xcode, platformKey, at = "build") {
|
|
196
321
|
if (xcode === void 0) return;
|
|
197
322
|
if (!XCODE_PLATFORMS.includes(platformKey)) {
|
|
198
323
|
throw new Error(
|
|
199
|
-
`vibeview.json: platforms.${platformKey}.
|
|
324
|
+
`vibeview.json: platforms.${platformKey}.${at}.xcode only applies to ios and tvos builds \u2014 remove it from the ${platformKey} block`
|
|
200
325
|
);
|
|
201
326
|
}
|
|
202
327
|
if (typeof xcode !== "string" || !XCODE_VERSION_RE.test(xcode)) {
|
|
203
328
|
throw new Error(
|
|
204
|
-
`vibeview.json: platforms.${platformKey}.
|
|
329
|
+
`vibeview.json: platforms.${platformKey}.${at}.xcode must be a version string like "27.1"`
|
|
205
330
|
);
|
|
206
331
|
}
|
|
207
332
|
}
|
|
@@ -213,11 +338,11 @@ function normalizeNodeVersion(value) {
|
|
|
213
338
|
const m = /^v?(\d{1,3})(?:\.x)?$/.exec(value.trim());
|
|
214
339
|
return m ? String(Number(m[1])) : null;
|
|
215
340
|
}
|
|
216
|
-
function validateBuildNode(node, platformKey) {
|
|
341
|
+
function validateBuildNode(node, platformKey, at = "build") {
|
|
217
342
|
if (node === void 0) return;
|
|
218
343
|
if (normalizeNodeVersion(node) === null) {
|
|
219
344
|
throw new Error(
|
|
220
|
-
`vibeview.json: platforms.${platformKey}.
|
|
345
|
+
`vibeview.json: platforms.${platformKey}.${at}.node must be a Node.js major version like "24"`
|
|
221
346
|
);
|
|
222
347
|
}
|
|
223
348
|
}
|
|
@@ -1317,6 +1442,57 @@ function iosArchiveExportCommand(ws, scheme = ws) {
|
|
|
1317
1442
|
function managedExpoIosProductionCommand(cwd, name) {
|
|
1318
1443
|
return `${installCommand(cwd)} && npx expo prebuild --platform ios && ` + iosArchiveExportCommand(name);
|
|
1319
1444
|
}
|
|
1445
|
+
function hasSeparatePreview(cwd, platform) {
|
|
1446
|
+
const flavor = detectFlavor(cwd);
|
|
1447
|
+
if (flavor !== "expo" && flavor !== "bare-rn") return false;
|
|
1448
|
+
if (platform === "ios") return true;
|
|
1449
|
+
if (platform !== "android") return false;
|
|
1450
|
+
return isManagedExpo(cwd, "android") || releaseSignedWithDebugKey(cwd);
|
|
1451
|
+
}
|
|
1452
|
+
function releaseSignedWithDebugKey(cwd) {
|
|
1453
|
+
for (const file of ["android/app/build.gradle", "android/app/build.gradle.kts"]) {
|
|
1454
|
+
let text;
|
|
1455
|
+
try {
|
|
1456
|
+
text = readFileSync6(join5(cwd, file), "utf-8");
|
|
1457
|
+
} catch {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const release = gradleBlock(gradleBlock(text, "buildTypes") ?? "", "release");
|
|
1461
|
+
if (release === null) return false;
|
|
1462
|
+
return /signingConfig\s*=?\s*signingConfigs\s*\.\s*(?:debug\b|getByName\(\s*"debug"\s*\))/.test(
|
|
1463
|
+
release
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
function gradleBlock(text, name) {
|
|
1469
|
+
const m = new RegExp(`(?:(?:^|[^\\w.])${name}|getByName\\(\\s*"${name}"\\s*\\))\\s*\\{`).exec(
|
|
1470
|
+
text
|
|
1471
|
+
);
|
|
1472
|
+
if (!m) return null;
|
|
1473
|
+
let depth = 1;
|
|
1474
|
+
const start = m.index + m[0].length;
|
|
1475
|
+
for (let i = start; i < text.length; i++) {
|
|
1476
|
+
if (text[i] === "{") depth++;
|
|
1477
|
+
else if (text[i] === "}" && --depth === 0) return text.slice(start, i);
|
|
1478
|
+
}
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
function toReleaseBuild(dev2) {
|
|
1482
|
+
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/");
|
|
1483
|
+
const out = {};
|
|
1484
|
+
if (dev2.buildCommand !== void 0) out.buildCommand = swap(dev2.buildCommand);
|
|
1485
|
+
if (dev2.artifact !== void 0) out.artifact = swap(dev2.artifact);
|
|
1486
|
+
if (dev2.metroCommand !== void 0) out.metroCommand = dev2.metroCommand;
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
function previewDefaultsFor(cwd, platform, evaluatedIosName) {
|
|
1490
|
+
const dev2 = defaultsFor(cwd, platform, evaluatedIosName);
|
|
1491
|
+
if (hasSeparatePreview(cwd, platform)) return toReleaseBuild(dev2);
|
|
1492
|
+
const flavor = detectFlavor(cwd);
|
|
1493
|
+
if (platform === "android" && (flavor === "expo" || flavor === "bare-rn")) return {};
|
|
1494
|
+
return dev2;
|
|
1495
|
+
}
|
|
1320
1496
|
function productionDefaultsFor(cwd, platform, evaluatedIosName) {
|
|
1321
1497
|
const flavor = detectFlavor(cwd);
|
|
1322
1498
|
if ((flavor === "flutter" || flavor === "capacitor" || flavor === "native") && platform !== "tvos" && platform !== "roku") {
|
|
@@ -1496,7 +1672,8 @@ function evaluatedExpoName(cwd, readConfig, onEvaluate) {
|
|
|
1496
1672
|
}
|
|
1497
1673
|
async function resolvedDefaultsFor(cwd, platform, opts = {}) {
|
|
1498
1674
|
const { production = false, readConfig = (c) => readExpoConfig(c), onEvaluate } = opts;
|
|
1499
|
-
const
|
|
1675
|
+
const profile = opts.profile ?? (production ? "production" : "development");
|
|
1676
|
+
const base = profile === "production" ? productionDefaultsFor : profile === "preview" ? previewDefaultsFor : defaultsFor;
|
|
1500
1677
|
const dynamic = dynamicExpoConfig(cwd);
|
|
1501
1678
|
if (platform !== "ios" || !dynamic || !isManagedExpo(cwd, "ios")) {
|
|
1502
1679
|
return base(cwd, platform);
|
|
@@ -1541,7 +1718,7 @@ function fillLaterHint(skipped, platform) {
|
|
|
1541
1718
|
const lines = ["You can enable one-command builds later \u2014 add to vibeview.json:"];
|
|
1542
1719
|
if (skipped.build) {
|
|
1543
1720
|
lines.push(
|
|
1544
|
-
` platforms.${platform}.build = { "command": "<your build command>", "artifact": "<path to .app/.apk>" }`
|
|
1721
|
+
` platforms.${platform}.build.profiles.development = { "command": "<your build command>", "artifact": "<path to .app/.apk>" }`
|
|
1545
1722
|
);
|
|
1546
1723
|
}
|
|
1547
1724
|
if (skipped.metro) {
|
|
@@ -1587,56 +1764,106 @@ async function commandSelect(message, detected) {
|
|
|
1587
1764
|
choices.push({ value: "skip", name: "Skip for now" });
|
|
1588
1765
|
return select({ message, choices });
|
|
1589
1766
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1767
|
+
var PROFILES_DOCS_URL = "https://vibeview.io/docs/cloud-builds#build-profiles";
|
|
1768
|
+
var SIGNING_DOCS_URL = "https://vibeview.io/docs/app-signing";
|
|
1769
|
+
function usesMetroBuild(cwd, platform) {
|
|
1770
|
+
const flavor = detectFlavor(cwd);
|
|
1771
|
+
return (flavor === "expo" || flavor === "bare-rn") && (platform === "ios" || platform === "android");
|
|
1772
|
+
}
|
|
1773
|
+
function profileIntro(cwd, platform, profile, defaults) {
|
|
1774
|
+
const label = platformLabel(platform);
|
|
1775
|
+
const metro = usesMetroBuild(cwd, platform);
|
|
1776
|
+
if (profile === DEVELOPMENT) {
|
|
1777
|
+
return [
|
|
1778
|
+
`No development build profile for ${label} in vibeview.json yet.`,
|
|
1779
|
+
metro ? " development is the debug build VibeView devices run while you code: it loads your JavaScript from Metro (vibeview dev)." : " development is the build VibeView devices run while you work: vibeview build and vibeview dev use it.",
|
|
1780
|
+
" The answer is saved to vibeview.json. Commit it so your team and CI build the same way.",
|
|
1781
|
+
` Build profiles: ${PROFILES_DOCS_URL}`
|
|
1782
|
+
];
|
|
1783
|
+
}
|
|
1784
|
+
if (profile === PREVIEW) {
|
|
1785
|
+
const lines = [
|
|
1786
|
+
`No preview build profile for ${label} in vibeview.json yet.`,
|
|
1787
|
+
" preview is a standalone build that runs without a dev server: for share links, tests, teammates and GitHub pull requests."
|
|
1788
|
+
];
|
|
1789
|
+
if (!defaults.buildCommand && metro && platform === "android") {
|
|
1790
|
+
lines.push(
|
|
1791
|
+
" No suggestion: your release build doesn't use the debug key, and cloud builds don't have your own keystore.",
|
|
1792
|
+
" Enter a command that makes an APK a device can install (signed with the debug key), or skip."
|
|
1793
|
+
);
|
|
1794
|
+
} else if (defaults.buildCommand && !metro) {
|
|
1795
|
+
lines.push(
|
|
1796
|
+
" Your development build already runs by itself, so the suggestion is the same command."
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
lines.push(` Build profiles: ${PROFILES_DOCS_URL}`);
|
|
1800
|
+
return lines;
|
|
1801
|
+
}
|
|
1802
|
+
if (profile === PRODUCTION) {
|
|
1803
|
+
return [
|
|
1804
|
+
`No production build profile for ${label} in vibeview.json yet.`,
|
|
1805
|
+
" production is the signed build for the stores (.ipa for TestFlight, .aab for Google Play).",
|
|
1806
|
+
` It builds in the cloud with the signing you set up in VibeView: ${SIGNING_DOCS_URL}`
|
|
1807
|
+
];
|
|
1808
|
+
}
|
|
1809
|
+
return [
|
|
1810
|
+
`No "${profile}" build profile for ${label} in vibeview.json yet.`,
|
|
1811
|
+
" A profile of your own makes a build that runs on VibeView devices, like development.",
|
|
1812
|
+
` To make it a signed store build, add "distribution": "production" to it in vibeview.json: ${PROFILES_DOCS_URL}`
|
|
1813
|
+
];
|
|
1814
|
+
}
|
|
1815
|
+
function previewSkippedNote(cwd, platform) {
|
|
1816
|
+
if (!usesMetroBuild(cwd, platform)) return [];
|
|
1817
|
+
const why = platform === "android" && !hasSeparatePreview(cwd, platform) ? "your release build doesn't use the debug key" : "you entered your own build command, so there's no Release version of it to suggest";
|
|
1818
|
+
return [
|
|
1819
|
+
` No preview profile saved (${why}). For a build that runs without Metro (share links, tests,`,
|
|
1820
|
+
" GitHub pull requests), add one with `vibeview build --cloud --profile preview`."
|
|
1821
|
+
];
|
|
1822
|
+
}
|
|
1823
|
+
function previewSavedNote() {
|
|
1824
|
+
return [
|
|
1825
|
+
" preview is the same build in Release, which runs without Metro: for share links, tests and",
|
|
1826
|
+
" GitHub pull requests. Build it with `vibeview build --cloud --profile preview`."
|
|
1827
|
+
];
|
|
1828
|
+
}
|
|
1829
|
+
function commandLabel(profile, platform) {
|
|
1830
|
+
return `Build command for ${platformLabel(platform)} (${profile} profile)`;
|
|
1831
|
+
}
|
|
1832
|
+
async function promptProfileConfig(cwd, platform, profile) {
|
|
1833
|
+
const production = profile === PRODUCTION;
|
|
1834
|
+
const defaults = await promptDefaults(cwd, platform, { profile });
|
|
1835
|
+
console.log(chalk2.dim(profileIntro(cwd, platform, profile, defaults).join("\n")));
|
|
1592
1836
|
evaluatedConfigNote(cwd, defaults);
|
|
1593
|
-
const
|
|
1837
|
+
const label = commandLabel(profile, platform);
|
|
1838
|
+
const choice = await commandSelect(label, defaults.buildCommand);
|
|
1594
1839
|
if (choice === "skip") return null;
|
|
1595
1840
|
if (choice === "default") {
|
|
1596
1841
|
return { command: defaults.buildCommand, artifact: defaults.artifact };
|
|
1597
1842
|
}
|
|
1598
1843
|
console.log(
|
|
1599
1844
|
chalk2.dim(
|
|
1600
|
-
" Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && yarn build:android`"
|
|
1845
|
+
" 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
1846
|
)
|
|
1602
1847
|
);
|
|
1603
|
-
const command = await input({ message:
|
|
1848
|
+
const command = await input({ message: label, required: true });
|
|
1604
1849
|
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)",
|
|
1850
|
+
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
1851
|
default: defaults.artifact,
|
|
1607
1852
|
required: true
|
|
1608
1853
|
});
|
|
1609
1854
|
return { command, artifact };
|
|
1610
1855
|
}
|
|
1611
|
-
async function
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
)
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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 };
|
|
1856
|
+
async function promptBuildConfig(cwd, platform) {
|
|
1857
|
+
return promptProfileConfig(cwd, platform, DEVELOPMENT);
|
|
1858
|
+
}
|
|
1859
|
+
async function companionPreview(cwd, platform, development) {
|
|
1860
|
+
if (!hasSeparatePreview(cwd, platform)) return null;
|
|
1861
|
+
const dev2 = await promptDefaults(cwd, platform);
|
|
1862
|
+
if (dev2.buildCommand !== development.command || dev2.artifact !== development.artifact)
|
|
1863
|
+
return null;
|
|
1864
|
+
const preview = toReleaseBuild(dev2);
|
|
1865
|
+
if (!preview.buildCommand || !preview.artifact) return null;
|
|
1866
|
+
return { command: preview.buildCommand, artifact: preview.artifact };
|
|
1640
1867
|
}
|
|
1641
1868
|
async function promptMetroCommand(cwd, platform) {
|
|
1642
1869
|
const defaults = defaultsFor(cwd, platform);
|
|
@@ -1816,6 +2043,39 @@ function runTar(args) {
|
|
|
1816
2043
|
});
|
|
1817
2044
|
}
|
|
1818
2045
|
|
|
2046
|
+
// src/lib/cloud-manifest-entry.ts
|
|
2047
|
+
function validateCloudProfile(build, platformKey, name) {
|
|
2048
|
+
validateBuildEnv(build.env, platformKey);
|
|
2049
|
+
validateBuildXcode(build.xcode, platformKey);
|
|
2050
|
+
validateBuildNode(build.node, platformKey);
|
|
2051
|
+
const own = build.profiles?.[name];
|
|
2052
|
+
if (own) {
|
|
2053
|
+
const at = `build.profiles.${name}`;
|
|
2054
|
+
validateBuildEnv(own.env, platformKey, at);
|
|
2055
|
+
validateBuildXcode(own.xcode, platformKey, at);
|
|
2056
|
+
validateBuildNode(own.node, platformKey, at);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function cloudManifestEntry(platform, profile, opts = {}) {
|
|
2060
|
+
const production = profile.distribution === "production";
|
|
2061
|
+
const xcode = platformUsesXcode(platform) ? opts.xcode ?? profile.xcode : void 0;
|
|
2062
|
+
const node = opts.node ?? (profile.node === void 0 ? void 0 : normalizeNodeVersion(profile.node));
|
|
2063
|
+
return {
|
|
2064
|
+
platform,
|
|
2065
|
+
...opts.appPublicId ? { app_public_id: opts.appPublicId } : {},
|
|
2066
|
+
profile: profile.name,
|
|
2067
|
+
command: profile.command,
|
|
2068
|
+
artifact_path: profile.artifact,
|
|
2069
|
+
distribution: profile.distribution,
|
|
2070
|
+
...profile.env !== void 0 ? { env: profile.env } : {},
|
|
2071
|
+
// autoIncrement is a PRODUCTION-only concept (the server-side build
|
|
2072
|
+
// number is a store number).
|
|
2073
|
+
...profile.autoIncrement && production ? { auto_increment: true } : {},
|
|
2074
|
+
...xcode ? { xcode } : {},
|
|
2075
|
+
...node ? { node } : {}
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1819
2079
|
// src/lib/build-preflight.ts
|
|
1820
2080
|
import { execFile as execFile4 } from "child_process";
|
|
1821
2081
|
import { realpathSync, statSync as statSync4 } from "fs";
|
|
@@ -2088,81 +2348,84 @@ async function resolveBarePlatform(cwd, cfg, opts) {
|
|
|
2088
2348
|
}
|
|
2089
2349
|
return platform;
|
|
2090
2350
|
}
|
|
2091
|
-
function
|
|
2351
|
+
function persistPromptedProfiles(cwd, platform, profiles) {
|
|
2092
2352
|
return updateDevConfig(cwd, platform, (next) => {
|
|
2093
|
-
const entry = next.platforms[platform];
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
}
|
|
2353
|
+
const entry = next.platforms[platform] ?? (next.platforms[platform] = {});
|
|
2354
|
+
let build = entry.build;
|
|
2355
|
+
for (const [name, profile] of Object.entries(profiles))
|
|
2356
|
+
build = withProfile(build, name, profile);
|
|
2357
|
+
entry.build = build;
|
|
2099
2358
|
});
|
|
2100
2359
|
}
|
|
2101
2360
|
function persistUploadedAppId(cwd, platform, build, appId) {
|
|
2102
2361
|
updateDevConfig(cwd, platform, (next) => {
|
|
2103
2362
|
const entry = next.platforms[platform];
|
|
2104
2363
|
if (!entry) {
|
|
2105
|
-
next.platforms[platform] = { appId, build };
|
|
2364
|
+
next.platforms[platform] = build ? { appId, build } : { appId };
|
|
2106
2365
|
return true;
|
|
2107
2366
|
}
|
|
2108
2367
|
if (entry.appId === appId && entry.build) return false;
|
|
2109
2368
|
entry.appId = appId;
|
|
2110
|
-
if (!entry.build) entry.build = build;
|
|
2369
|
+
if (!entry.build && build) entry.build = build;
|
|
2111
2370
|
return true;
|
|
2112
2371
|
});
|
|
2113
2372
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2373
|
+
function requestedProfile(opts) {
|
|
2374
|
+
return opts.profile ?? (opts.production ? PRODUCTION : DEVELOPMENT);
|
|
2375
|
+
}
|
|
2376
|
+
async function resolveBuildProfile(cwd, cfgRef, platform, opts) {
|
|
2377
|
+
const name = requestedProfile(opts);
|
|
2378
|
+
const build = cfgRef.current?.platforms?.[platform]?.build;
|
|
2379
|
+
const existing = resolveProfile(build, name);
|
|
2116
2380
|
if (existing) return existing;
|
|
2117
2381
|
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
|
-
);
|
|
2382
|
+
fail(opts, missingProfileMessage(platform, name), "config_required");
|
|
2123
2383
|
}
|
|
2124
|
-
const answered = await
|
|
2384
|
+
const answered = await promptProfileConfig(cwd, platform, name);
|
|
2125
2385
|
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
2386
|
fail(
|
|
2152
2387
|
opts,
|
|
2153
|
-
|
|
2388
|
+
name === PRODUCTION ? "a production build config is required for --production" : `a "${name}" build profile is required to run vibeview build`,
|
|
2154
2389
|
"config_required"
|
|
2155
2390
|
);
|
|
2156
2391
|
}
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2392
|
+
const save = { [name]: answered };
|
|
2393
|
+
let note = [];
|
|
2394
|
+
if (name === DEVELOPMENT && !hasProfile(build, PREVIEW)) {
|
|
2395
|
+
const preview = await companionPreview(cwd, platform, answered);
|
|
2396
|
+
if (preview) save[PREVIEW] = preview;
|
|
2397
|
+
note = preview ? previewSavedNote() : previewSkippedNote(cwd, platform);
|
|
2160
2398
|
}
|
|
2161
|
-
cfgRef.current =
|
|
2162
|
-
|
|
2399
|
+
cfgRef.current = persistPromptedProfiles(cwd, platform, save);
|
|
2400
|
+
console.error(
|
|
2401
|
+
chalk3.green("\u2713") + ` Saved the ${Object.keys(save).join(" and ")} build profile${Object.keys(save).length > 1 ? "s" : ""} for ${platformLabel(platform)} to vibeview.json`
|
|
2402
|
+
);
|
|
2403
|
+
if (note.length) console.error(chalk3.dim(note.join("\n")));
|
|
2404
|
+
return resolveProfile(cfgRef.current.platforms[platform]?.build, name);
|
|
2163
2405
|
}
|
|
2164
2406
|
async function buildCommand(opts, cloudDeps) {
|
|
2165
2407
|
const cwd = process.cwd();
|
|
2408
|
+
if (opts.profile !== void 0) {
|
|
2409
|
+
if (!PROFILE_NAME_RE.test(opts.profile)) {
|
|
2410
|
+
printFailure(
|
|
2411
|
+
opts,
|
|
2412
|
+
`--profile must be a profile name (lowercase letters, digits and dashes, starting with a letter), got "${opts.profile}"`,
|
|
2413
|
+
"usage"
|
|
2414
|
+
);
|
|
2415
|
+
process.exitCode = 2;
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (opts.production && opts.profile !== PRODUCTION) {
|
|
2419
|
+
printFailure(
|
|
2420
|
+
opts,
|
|
2421
|
+
`--production builds the production profile \u2014 pass --production or --profile ${opts.profile}, not both`,
|
|
2422
|
+
"usage"
|
|
2423
|
+
);
|
|
2424
|
+
process.exitCode = 2;
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
opts = { ...opts, production: requestedProfile(opts) === PRODUCTION };
|
|
2166
2429
|
const cfgRef = { current: loadDevConfig(cwd) };
|
|
2167
2430
|
try {
|
|
2168
2431
|
if (opts.cloud) {
|
|
@@ -2191,17 +2454,29 @@ async function buildCommand(opts, cloudDeps) {
|
|
|
2191
2454
|
);
|
|
2192
2455
|
}
|
|
2193
2456
|
const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
2194
|
-
const
|
|
2457
|
+
const profile = await resolveBuildProfile(cwd, cfgRef, platform, opts);
|
|
2458
|
+
if (profile.distribution === "production") {
|
|
2459
|
+
fail(
|
|
2460
|
+
opts,
|
|
2461
|
+
`the "${profile.name}" profile is a signed production build, which runs in the cloud (vibeview build --cloud --profile ${profile.name})`,
|
|
2462
|
+
"usage"
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2195
2465
|
const result = await runBuildForPlatform(
|
|
2196
2466
|
cwd,
|
|
2197
2467
|
platform,
|
|
2198
|
-
|
|
2468
|
+
profile,
|
|
2199
2469
|
opts,
|
|
2200
2470
|
(l) => console.error(chalk3.dim(l))
|
|
2201
2471
|
);
|
|
2202
2472
|
for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
|
|
2203
2473
|
if (result.launch_warning) console.error(chalk3.yellow(`\u26A0 ${result.launch_warning}`));
|
|
2204
|
-
persistUploadedAppId(
|
|
2474
|
+
persistUploadedAppId(
|
|
2475
|
+
cwd,
|
|
2476
|
+
platform,
|
|
2477
|
+
cfgRef.current?.platforms?.[platform]?.build,
|
|
2478
|
+
result.public_id
|
|
2479
|
+
);
|
|
2205
2480
|
if (opts.json) {
|
|
2206
2481
|
console.log(
|
|
2207
2482
|
JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
|
|
@@ -2213,6 +2488,10 @@ async function buildCommand(opts, cloudDeps) {
|
|
|
2213
2488
|
);
|
|
2214
2489
|
}
|
|
2215
2490
|
} catch (err) {
|
|
2491
|
+
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
2492
|
+
process.exitCode = 130;
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2216
2495
|
if (!(err instanceof BuildFailError)) {
|
|
2217
2496
|
printFailure(opts, err instanceof Error ? err.message : String(err));
|
|
2218
2497
|
}
|
|
@@ -2258,6 +2537,25 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
|
|
|
2258
2537
|
);
|
|
2259
2538
|
}
|
|
2260
2539
|
}
|
|
2540
|
+
if (opts.json || !process.stdin.isTTY) {
|
|
2541
|
+
const name = requestedProfile(opts);
|
|
2542
|
+
const missing = chosenPlatforms.filter(
|
|
2543
|
+
(p) => !hasProfile(cfgRef.current?.platforms?.[p]?.build, name)
|
|
2544
|
+
);
|
|
2545
|
+
for (const p of missing) {
|
|
2546
|
+
const reason = `no "${name}" profile in vibeview.json`;
|
|
2547
|
+
emitCloud(opts.json, { event: "skipped", platform: p, reason });
|
|
2548
|
+
if (!opts.json) console.error(chalk3.yellow(`\u26A0 skipping ${p}: ${reason}`));
|
|
2549
|
+
}
|
|
2550
|
+
chosenPlatforms = chosenPlatforms.filter((p) => !missing.includes(p));
|
|
2551
|
+
if (chosenPlatforms.length === 0) {
|
|
2552
|
+
fail(
|
|
2553
|
+
opts,
|
|
2554
|
+
`--platform all: no configured platform has a "${name}" profile \u2014 add platforms.<platform>.build.profiles.${name} {command, artifact} to vibeview.json`,
|
|
2555
|
+
"config_required"
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2261
2559
|
return chosenPlatforms;
|
|
2262
2560
|
}
|
|
2263
2561
|
if (opts.json || !process.stdin.isTTY) {
|
|
@@ -2273,13 +2571,13 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
|
|
|
2273
2571
|
});
|
|
2274
2572
|
requireCloudCapable(opts, chosen);
|
|
2275
2573
|
requireProductionCapable(opts, chosen);
|
|
2276
|
-
await
|
|
2574
|
+
await resolveBuildProfile(cwd, cfgRef, chosen, opts);
|
|
2277
2575
|
return [chosen];
|
|
2278
2576
|
}
|
|
2279
2577
|
const platform = opts.platform ? validateExplicitPlatform(opts.platform) : await resolveBarePlatform(cwd, cfgRef.current, opts);
|
|
2280
2578
|
requireCloudCapable(opts, platform);
|
|
2281
2579
|
requireProductionCapable(opts, platform);
|
|
2282
|
-
await
|
|
2580
|
+
await resolveBuildProfile(cwd, cfgRef, platform, opts);
|
|
2283
2581
|
return [platform];
|
|
2284
2582
|
}
|
|
2285
2583
|
function validateExplicitPlatform(raw) {
|
|
@@ -2476,19 +2774,19 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
|
2476
2774
|
const client = new ApiClient(baseUrl, token);
|
|
2477
2775
|
const d = deps ?? defaultCloudBuildDeps(client);
|
|
2478
2776
|
const createFromArtifact = await resolveCloudAppIds(cwd, cfgRef, platforms, opts, d.listApps);
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
productionBuilds.set(p, await resolveProductionBuildConfig(cwd, cfgRef, p, opts));
|
|
2483
|
-
}
|
|
2777
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
2778
|
+
for (const p of platforms) {
|
|
2779
|
+
profiles.set(p, await resolveBuildProfile(cwd, cfgRef, p, opts));
|
|
2484
2780
|
}
|
|
2485
2781
|
const cfg = cfgRef.current;
|
|
2486
2782
|
for (const p of platforms) {
|
|
2783
|
+
const profile = profiles.get(p);
|
|
2487
2784
|
try {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2785
|
+
validateCloudProfile(cfg.platforms[p].build, p, profile.name);
|
|
2786
|
+
if (profile.distribution === "production" && !PRODUCTION_SIGNING_PLATFORMS.includes(p)) {
|
|
2787
|
+
throw new UsageError(productionUnsupportedReason(p));
|
|
2788
|
+
}
|
|
2789
|
+
if (profile.autoIncrement && profile.distribution === "production" && createFromArtifact.has(p)) {
|
|
2492
2790
|
throw new UsageError(
|
|
2493
2791
|
`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
2792
|
);
|
|
@@ -2514,7 +2812,7 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
|
|
|
2514
2812
|
const uploaded = new Set(files);
|
|
2515
2813
|
const warnings = /* @__PURE__ */ new Map();
|
|
2516
2814
|
for (const p of platforms) {
|
|
2517
|
-
const command =
|
|
2815
|
+
const command = profiles.get(p).command;
|
|
2518
2816
|
const missingPkg = missingPackageJsonError(command, uploaded, cwd);
|
|
2519
2817
|
if (missingPkg) fail(opts, missingPkg, "no_package_json");
|
|
2520
2818
|
for (const w of buildCommandWarnings(command, uploaded)) {
|
|
@@ -2552,36 +2850,19 @@ ${offenders}`);
|
|
|
2552
2850
|
packSpinner?.succeed(`packed source (${files.length} files, ${formatMB(sizeBytes)} MB)`);
|
|
2553
2851
|
const manifestEntries = platforms.map((p) => {
|
|
2554
2852
|
const entry = cfg.platforms[p];
|
|
2555
|
-
const
|
|
2556
|
-
|
|
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) {
|
|
2853
|
+
const profile = profiles.get(p);
|
|
2854
|
+
if (profile.autoIncrement && profile.distribution !== "production" && !opts.json) {
|
|
2561
2855
|
console.error(
|
|
2562
2856
|
chalk3.yellow(
|
|
2563
2857
|
`note: platforms.${p}.build.autoIncrement only applies to production builds (--production) \u2014 ignoring it for this simulator build`
|
|
2564
2858
|
)
|
|
2565
2859
|
);
|
|
2566
2860
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
-
};
|
|
2861
|
+
return cloudManifestEntry(p, profile, {
|
|
2862
|
+
xcode: opts.xcode,
|
|
2863
|
+
node: nodeFlag,
|
|
2864
|
+
appPublicId: createFromArtifact.has(p) ? void 0 : entry.appId
|
|
2865
|
+
});
|
|
2585
2866
|
});
|
|
2586
2867
|
const uploadSpinner = !opts.json ? createSpinner("uploading source...") : null;
|
|
2587
2868
|
uploadSpinner?.start();
|
|
@@ -2613,8 +2894,11 @@ ${offenders}`);
|
|
|
2613
2894
|
return;
|
|
2614
2895
|
}
|
|
2615
2896
|
const finished = await waitForCloudBuilds(created, opts, d);
|
|
2616
|
-
|
|
2617
|
-
|
|
2897
|
+
const unlinkedSimulator = new Set(
|
|
2898
|
+
[...createFromArtifact].filter((p) => profiles.get(p)?.distribution !== "production")
|
|
2899
|
+
);
|
|
2900
|
+
if (unlinkedSimulator.size > 0) {
|
|
2901
|
+
persistResolvedAppIds(cwd, cfgRef, unlinkedSimulator, finished.values());
|
|
2618
2902
|
}
|
|
2619
2903
|
let downloadFailed = false;
|
|
2620
2904
|
if (opts.download) {
|
|
@@ -3941,7 +4225,14 @@ async function ensureConfig(api, opts) {
|
|
|
3941
4225
|
if (!existing) {
|
|
3942
4226
|
isFirstRun = true;
|
|
3943
4227
|
if (!opts.json && process.stdin.isTTY) {
|
|
3944
|
-
|
|
4228
|
+
const answered = await promptBuildConfig(process.cwd(), platform);
|
|
4229
|
+
if (answered) {
|
|
4230
|
+
firstRunBuild = withProfile(void 0, DEVELOPMENT, answered);
|
|
4231
|
+
const preview = await companionPreview(process.cwd(), platform, answered);
|
|
4232
|
+
if (preview) firstRunBuild = withProfile(firstRunBuild, PREVIEW, preview);
|
|
4233
|
+
const note = preview ? previewSavedNote() : previewSkippedNote(process.cwd(), platform);
|
|
4234
|
+
if (note.length) console.error(chalk4.dim(note.join("\n")));
|
|
4235
|
+
}
|
|
3945
4236
|
if (PLATFORM_NEEDS_METRO[platform]) {
|
|
3946
4237
|
firstRunMetro = await promptMetroCommand(process.cwd(), platform);
|
|
3947
4238
|
}
|
|
@@ -3974,7 +4265,7 @@ ${hint}`);
|
|
|
3974
4265
|
chalk4.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
|
|
3975
4266
|
);
|
|
3976
4267
|
}
|
|
3977
|
-
const buildConfig = firstRunBuild ?? entry?.build
|
|
4268
|
+
const buildConfig = developmentProfile(firstRunBuild ?? entry?.build);
|
|
3978
4269
|
if (!appId && buildConfig && typeof opts.build !== "string") {
|
|
3979
4270
|
const strategy = buildAppStrategy(apps, opts.build === true, opts.json, process.stdin.isTTY);
|
|
3980
4271
|
let action = strategy === "ask" ? "pick" : strategy;
|
|
@@ -4106,7 +4397,13 @@ ${hint}`);
|
|
|
4106
4397
|
);
|
|
4107
4398
|
}
|
|
4108
4399
|
if (isFirstRun) {
|
|
4109
|
-
|
|
4400
|
+
const saved = profileNames(firstRunBuild ?? void 0);
|
|
4401
|
+
console.error(
|
|
4402
|
+
chalk4.dim(
|
|
4403
|
+
`Created vibeview.json (platform: ${platform}, app: ${appId}` + (saved.length ? `, build profiles: ${saved.join(", ")}` : "") + ")." + // The development prompt's intro already said to commit it.
|
|
4404
|
+
(firstRunBuild ? "" : " Commit it so your team and CI use the same setup.")
|
|
4405
|
+
)
|
|
4406
|
+
);
|
|
4110
4407
|
const hint = fillLaterHint(
|
|
4111
4408
|
{ build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
|
|
4112
4409
|
platform
|
|
@@ -4183,7 +4480,7 @@ async function runDevInProcess(opts) {
|
|
|
4183
4480
|
}
|
|
4184
4481
|
}
|
|
4185
4482
|
const interactive = !opts.json && Boolean(process.stdin.isTTY);
|
|
4186
|
-
const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
|
|
4483
|
+
const plannedBuild = developmentProfile(cfg.savedConfig.platforms?.[cfg.platform]?.build);
|
|
4187
4484
|
if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
|
|
4188
4485
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
4189
4486
|
console.error(chalk4.red(`error: ${message}`));
|
|
@@ -4193,7 +4490,7 @@ async function runDevInProcess(opts) {
|
|
|
4193
4490
|
cfg.commitConfig();
|
|
4194
4491
|
if (opts.build === true && !cfg.builtThisRun) {
|
|
4195
4492
|
const reloaded = loadDevConfig(process.cwd());
|
|
4196
|
-
const build = reloaded?.platforms?.[cfg.platform]?.build;
|
|
4493
|
+
const build = developmentProfile(reloaded?.platforms?.[cfg.platform]?.build);
|
|
4197
4494
|
if (!build) {
|
|
4198
4495
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
4199
4496
|
console.error(chalk4.red(`error: ${message}`));
|
|
@@ -4285,7 +4582,7 @@ async function runDevInProcess(opts) {
|
|
|
4285
4582
|
metroPid = metroResult.pid ?? null;
|
|
4286
4583
|
} else {
|
|
4287
4584
|
const rokuEntry = loadDevConfig(process.cwd())?.platforms?.[cfg.platform];
|
|
4288
|
-
rokuBuild = rokuEntry?.build;
|
|
4585
|
+
rokuBuild = developmentProfile(rokuEntry?.build) ?? void 0;
|
|
4289
4586
|
watchIgnore = rokuEntry?.watchIgnore ?? [];
|
|
4290
4587
|
if (!rokuBuild) {
|
|
4291
4588
|
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
@@ -5671,6 +5968,7 @@ export {
|
|
|
5671
5968
|
stepsNotCheckedLabel,
|
|
5672
5969
|
notCheckedSuffix,
|
|
5673
5970
|
outcomeOf,
|
|
5971
|
+
developmentProfile,
|
|
5674
5972
|
DEV_PLATFORMS,
|
|
5675
5973
|
PLATFORM_NEEDS_METRO,
|
|
5676
5974
|
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-IVVLPLPM.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,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
|
|
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-
|
|
4521
|
+
const { runMcpServer } = await import("./server-322WCIKD.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-
|
|
38
|
+
} from "./chunk-IVVLPLPM.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.1",
|
|
4
4
|
"description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
|
|
5
5
|
"mcpName": "io.github.vibeview/vibeview",
|
|
6
6
|
"type": "module",
|