@ramonclaudio/vexpo 0.1.3 → 0.1.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ramon Claudio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,73 +3,79 @@
3
3
  [![npm](https://img.shields.io/npm/v/@ramonclaudio/vexpo)](https://www.npmjs.com/package/@ramonclaudio/vexpo)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Operational CLI for [vexpo](https://github.com/ramonclaudio/vexpo) projects: Expo + Convex + Better Auth + Resend, end-to-end iOS. Provisions the stack, validates every credential, keeps env values in sync across `.env.local` / Convex env / EAS env, and covers the last App Store Connect mile to a first ship: TestFlight delivery plus the privacy and accessibility labels Apple requires before review.
6
+ The setup CLI for [vexpo](https://github.com/ramonclaudio/vexpo) projects (Expo + Convex + Better Auth + Resend, end-to-end iOS). It creates or links your Convex deployment, signs and rotates the Apple keys (the P8 dance), and keeps your env in sync everywhere. It covers the App Store Connect last mile to a first ship. EAS does the heavy lifting (builds, updates, submission), vexpo covers the setup around it.
7
7
 
8
- Scaffolded by [`create-vexpo`](https://www.npmjs.com/package/@ramonclaudio/create-vexpo) into your project's devDependencies. Invoke via `npx vexpo`.
8
+ Scaffolded by [`create-vexpo`](https://www.npmjs.com/package/@ramonclaudio/create-vexpo) into your devDependencies. Run it with `npx vexpo`.
9
9
 
10
- ## Design rule: don't reinvent EAS
11
-
12
- If `eas <subcommand>` is the canonical answer, the recipe is `npx eas <subcommand>`, not `vexpo`. This CLI only surfaces what `eas-cli` doesn't do: setup orchestration, cross-source drift detection, Apple SIWA work, and the last App Store Connect mile to a first ship. Scope test for any command: does it help an empty directory become a first shipped, authenticated, backed iOS app? Post-launch ops (customer reviews, IAP sandbox testing, release management) are out, that's `eas` and App Store Connect once you have users. Wrapping every `eas` command would add no value over `eas-cli` itself, expand the maintenance surface, and signal a lack of trust in the platform.
10
+ <p align="center">
11
+ <img src="https://raw.githubusercontent.com/ramonclaudio/vexpo/main/docs/assets/demo-doctor.gif" width="720" alt="vexpo doctor auth-checking every credential against the live services">
12
+ </p>
13
13
 
14
14
  ## Setup
15
15
 
16
16
  ```
17
- vexpo lite Convex + Better Auth only, simulator-ready (60s)
18
- vexpo lite --new same + Convex signup walkthrough for first-time users
17
+ vexpo lite Convex + Better Auth only, simulator-ready (~60s)
18
+ vexpo lite --new same + Convex signup walkthrough for first-timers
19
19
  vexpo full full provisioning (Convex + Better Auth + Resend + Apple + EAS init + rebrand)
20
20
  vexpo full --new same + walks Apple/Convex/Expo/Resend signups
21
21
  vexpo full --skip-rebrand full setup, skip the rebrand wizard
22
22
 
23
- vexpo doctor Cross-source drift detection
24
- vexpo doctor --json Machine-readable output
25
- vexpo doctor --strict Exit non-zero on any warn
26
-
27
- vexpo accounts Walk Apple/Expo/Convex/Resend signups (standalone)
28
- vexpo rebrand Replace template defaults with your identity
29
- vexpo review-account Seed the App Review demo account on Convex
30
- vexpo convex Provision Convex deployment + write .env.local
31
- vexpo better-auth Set BETTER_AUTH_SECRET, SITE_URL, APP_NAME on Convex
32
- vexpo resend Provision Resend sending key + webhook, flip REQUIRE_EMAIL_VERIFICATION=true
33
- vexpo env push .env.local + .env.prod Convex + EAS (one pass)
34
- vexpo env convex-key Sync Convex deploy key + selector to EAS env (post-migration fix)
35
- vexpo adopt Finish a project created by `eas integrations:convex:connect`
36
- vexpo convex:migrate Copy server-side Convex env from another deployment
37
- vexpo asc:connect Re-link the EAS project to its ASC app (standalone, wraps `eas integrations:asc:connect`)
23
+ vexpo doctor cross-source drift detection
24
+ vexpo doctor --json machine-readable output
25
+ vexpo doctor --strict exit non-zero on any warn
26
+ vexpo doctor --redact mask identifying values (screenshots, issue reports)
27
+
28
+ vexpo accounts walk Apple/Expo/Convex/Resend signups (standalone)
29
+ vexpo rebrand replace template defaults with your identity
30
+ vexpo review-account seed the App Review demo account on Convex
31
+ vexpo convex provision or connect a Convex deployment
32
+ vexpo better-auth set SITE_URL, BETTER_AUTH_SECRET, APP_NAME on Convex
33
+ vexpo resend provision Resend sending key + webhook, write to Convex env
34
+ vexpo env push push .env.local + .env.prod to Convex + EAS env
35
+ vexpo env convex-key sync Convex deploy key + selector to EAS (post-migration fix)
36
+ vexpo adopt finish a project created by `eas integrations:convex:connect`
37
+ vexpo convex:migrate copy server-side Convex env from another deployment
38
+ vexpo asc:connect link the EAS project to its ASC app (wraps `eas integrations:asc:connect`)
38
39
  ```
39
40
 
40
41
  ## Apple
41
42
 
42
43
  ```
43
- vexpo apple asc-key Validate ASC API key against /v1/apps
44
- vexpo apple credentials Wraps `eas credentials:configure-build` with cached ASC env vars (skips Apple Developer login prompt)
45
- vexpo apple services-id Detect SIWA Services ID + attach APPLE_ID_AUTH capability
46
- vexpo apple jwt Sign SIWA ES256 client_secret JWT (180-day expiry)
47
- vexpo apple jwt --rotate Re-sign the JWT only
48
- vexpo apple eas-rotation-secrets Push the 5 EAS production secrets the JWT cron needs
44
+ vexpo apple asc-key validate an ASC API key against /v1/apps
45
+ vexpo apple asc-key --revalidate re-check the cached key without re-prompting
46
+ vexpo apple credentials wrap `eas credentials:configure-build` with the cached ASC key
47
+ vexpo apple services-id detect SIWA Services ID + attach APPLE_ID_AUTH capability
48
+ vexpo apple jwt sign the SIWA ES256 client_secret JWT (180-day expiry)
49
+ vexpo apple jwt --rotate re-sign the JWT only
50
+ vexpo apple eas-rotation-secrets push the 5 EAS production secrets the JWT cron needs
49
51
  ```
50
52
 
51
- ## App Store Connect (the last mile to a first ship)
53
+ ## App Store Connect
52
54
 
53
- TestFlight delivery, plus the privacy and accessibility labels Apple requires before a submission clears review. `eas-cli` hands a build to TestFlight and stops there.
55
+ Picks up after `eas submit` hands a build to TestFlight: groups, testers, release notes, plus the privacy and accessibility labels Apple requires before review.
54
56
 
55
57
  ```
56
- vexpo testflight groups list List beta groups
57
- vexpo testflight groups create <name> Create a beta group
58
- vexpo testflight groups view <id> View a beta group + its testers
59
- vexpo testflight groups delete <id> Delete a beta group
60
- vexpo testflight testers list List beta testers
61
- vexpo testflight invite <email> Add a tester + send invite
62
- vexpo testflight whats-new <buildId> <text> Set "What's new" notes
63
-
64
- vexpo asc:privacy show [file] Show the declared privacy nutrition label
65
- vexpo asc:privacy lint <file> Validate privacy.config.json against Apple's enums
66
- vexpo asc:accessibility show Fetch the app's accessibility declarations
67
- vexpo asc:accessibility lint <file> Validate accessibility.config.json against Apple's enums
58
+ vexpo testflight groups list list beta groups
59
+ vexpo testflight groups create <name> create a beta group
60
+ vexpo testflight groups view <id> view a beta group + its testers
61
+ vexpo testflight groups delete <id> delete a beta group
62
+ vexpo testflight testers list list beta testers
63
+ vexpo testflight invite <email> add a tester + send a TestFlight invite
64
+ vexpo testflight whats-new <buildId> <text> set the "What's new" notes
65
+
66
+ vexpo asc:privacy show [file] show the declared privacy.config.json
67
+ vexpo asc:privacy lint <file> validate privacy.config.json against Apple's enums
68
+ vexpo asc:accessibility show fetch the app's accessibility declarations
69
+ vexpo asc:accessibility lint <file> validate accessibility.config.json against Apple's enums
68
70
  ```
69
71
 
72
+ ## Design rule: don't reinvent EAS
73
+
74
+ vexpo only covers what `eas-cli` doesn't: setup orchestration, cross-source drift detection, Apple SIWA work, and the last App Store Connect mile to a first ship. If `eas <subcommand>` is the canonical answer, run `npx eas <subcommand>`.
75
+
70
76
  ## What `vexpo` doesn't wrap
71
77
 
72
- For canonical EAS surface, use `eas` directly. Wrapping these would add no value over `eas-cli` itself.
78
+ Reach for `eas` directly for the canonical platform surface.
73
79
 
74
80
  ```bash
75
81
  npx eas init # EAS project init
@@ -89,26 +95,23 @@ npx eas env [...] # env:push, env:pull, env:get, env:delete, env:
89
95
  npx eas integrations:asc [...] # status, connect, disconnect
90
96
  ```
91
97
 
92
- `vexpo full` orchestrates `eas init`, `eas env:push`, `eas credentials -p ios` (via `eas credentials:configure-build`), and `eas integrations:asc:connect` internally as setup steps. Only the ASC link step is also exposed standalone as `vexpo asc:connect` (re-link without re-running `full`); the rest are setup-only. The ASC API key flows through to both wizards via `EXPO_ASC_API_KEY_PATH` / `EXPO_ASC_KEY_ID` / `EXPO_ASC_ISSUER_ID` env vars pre-set from the cached `asc-key` state. These env vars set `AppStoreApi.defaultAuthenticationMode = API_KEY` inside eas-cli, so when the wizard reaches the Apple auth step during ASC key generation, it uses our cached key instead of prompting for Apple ID + password. The manual paste step doesn't auto-fill — the wizard skips it by auto-generating the key instead.
93
-
94
- Earlier vexpo versions passed `--api-key-id <apple-key-id>` to `integrations:asc:connect`. That flag matches against EAS's uploaded key resources, not Apple-side identifiers, so it failed with `No App Store Connect API key found with Apple key identifier ...` whenever the key hadn't been uploaded to EAS yet (the common case on fresh projects). The current orchestration drops the flag and relies on the env vars + the wizard's "Create new or use existing" prompt instead.
98
+ `vexpo full` drives `eas init`, `eas env:push`, `eas credentials`, and the ASC link internally using the cached ASC key. Only the ASC link is also standalone, as `vexpo asc:connect`.
95
99
 
96
100
  ## Architecture
97
101
 
98
- - Commander-based command tree in `src/cli.ts`.
99
- - One file per top-level command in `src/commands/`. Each exports `run<Name>(options)` returning a numeric exit code.
100
- - `src/lib/eas-cli.ts` is the shared shell-out helper: `easJson<T>(argv)` parses `--json --non-interactive` output, `easSpawn(argv)` forwards stdio for interactive commands, `easText(argv)` returns raw streams.
101
- - Built with tsup → single ESM bundle in `dist/`. Node 20+.
102
+ Commander tree in `src/cli.ts`, one file per top-level command in `src/commands/`. `src/lib/eas-cli.ts` shells out to `eas-cli`. Built with tsup to ESM, a `cli.js` entry plus shared chunks. Node 20+.
102
103
 
103
104
  ## Apple ASC API workarounds
104
105
 
105
- Apple changed several ASC API behaviors after the initial CLI release. The CLI handles each one:
106
+ Apple changed several ASC API behaviors after the initial CLI release. The CLI handles each one.
106
107
 
107
- - `POST /v1/bundleIds` rejects `platform: "SERVICES"`. `services-id` walks the user through manual creation in the developer portal, then re-polls.
108
- - App bundle IDs report `platform: "UNIVERSAL"` for newer accounts. `findOrCreateBundleId` matches any non-SERVICES platform when looking up an App ID.
109
- - Relationship endpoints reject `limit`. `bundleIdCapabilities.list` fetches without pagination.
110
- - `filter[platform]=SERVICES` returns 400. `doctor`'s `services-id-exists` check filters by identifier alone.
108
+ - `POST /v1/bundleIds` rejects `platform: "SERVICES"`. `services-id` walks you through manual creation in the developer portal, then re-polls.
109
+ - App bundle IDs report `platform: "UNIVERSAL"` for newer accounts. The App ID lookup matches any non-SERVICES platform.
110
+ - Relationship endpoints reject `limit`. The capability list fetches without pagination.
111
+ - `filter[platform]=SERVICES` returns 400. `doctor` filters by identifier alone.
111
112
 
112
113
  ## Repo
113
114
 
114
115
  [github.com/ramonclaudio/vexpo](https://github.com/ramonclaudio/vexpo)
116
+
117
+ Working on the CLI itself? See [CONTRIBUTING.md](https://github.com/ramonclaudio/vexpo/blob/main/CONTRIBUTING.md).
@@ -19,19 +19,6 @@ async function detectPackageManager() {
19
19
  function dlx() {
20
20
  return process.versions.bun ? "bunx" : "npx";
21
21
  }
22
- function dlxFor(pm) {
23
- switch (pm) {
24
- case "bun":
25
- return "bunx";
26
- case "pnpm":
27
- return "pnpm dlx";
28
- case "yarn":
29
- return "yarn dlx";
30
- case "npm":
31
- default:
32
- return "npx";
33
- }
34
- }
35
22
  function installCmdFor(pm) {
36
23
  switch (pm) {
37
24
  case "bun":
@@ -45,19 +32,6 @@ function installCmdFor(pm) {
45
32
  return "npm install";
46
33
  }
47
34
  }
48
- function runCmdFor(pm) {
49
- switch (pm) {
50
- case "bun":
51
- return "bun run";
52
- case "pnpm":
53
- return "pnpm run";
54
- case "yarn":
55
- return "yarn";
56
- case "npm":
57
- default:
58
- return "npm run";
59
- }
60
- }
61
35
  function currentRuntime() {
62
36
  return process.versions.bun ? "bun" : "node";
63
37
  }
@@ -65,4 +39,4 @@ function currentRuntimeVersion() {
65
39
  return process.versions.bun ?? process.versions.node ?? "?";
66
40
  }
67
41
 
68
- export { currentRuntime, currentRuntimeVersion, detectPackageManager, dlx, dlxFor, installCmdFor, runCmdFor };
42
+ export { currentRuntime, currentRuntimeVersion, detectPackageManager, dlx, installCmdFor };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn as spawn$1, spawnSync as spawnSync$1 } from 'child_process';
2
+ import { spawn as spawn$1 } from 'child_process';
3
3
 
4
4
  function spawn(argv, opts = {}) {
5
5
  const stdio = opts.stdio ?? [
@@ -25,24 +25,6 @@ function spawn(argv, opts = {}) {
25
25
  kill: (signal) => proc.kill(signal)
26
26
  };
27
27
  }
28
- function spawnSync(argv, opts = {}) {
29
- const stdio = opts.stdio ?? [
30
- opts.stdin ?? "inherit",
31
- opts.stdout ?? "pipe",
32
- opts.stderr ?? "pipe"
33
- ];
34
- const result = spawnSync$1(argv[0], argv.slice(1), {
35
- stdio,
36
- env: opts.env ? { ...process.env, ...opts.env } : process.env,
37
- cwd: opts.cwd,
38
- encoding: "utf8"
39
- });
40
- return {
41
- code: result.status ?? (result.signal ? 1 : 0),
42
- stdout: typeof result.stdout === "string" ? result.stdout : "",
43
- stderr: typeof result.stderr === "string" ? result.stderr : ""
44
- };
45
- }
46
28
  async function streamText(stream) {
47
29
  if (!stream) return "";
48
30
  const chunks = [];
@@ -56,7 +38,11 @@ async function run(argv, opts = {}) {
56
38
  stdin: opts.stdin ?? "ignore",
57
39
  stdout: "pipe",
58
40
  stderr: "pipe",
59
- env: opts.env,
41
+ // run() exists to PARSE output. A FORCE_COLOR=1 in the caller's shell
42
+ // (CI, recordings) makes child CLIs wrap fields in ANSI codes and every
43
+ // regex parser downstream silently misses. Force color off; an explicit
44
+ // opts.env can still override.
45
+ env: { FORCE_COLOR: "0", NO_COLOR: "1", ...opts.env },
60
46
  cwd: opts.cwd
61
47
  });
62
48
  const [code, stdout, stderr] = await Promise.all([
@@ -67,4 +53,4 @@ async function run(argv, opts = {}) {
67
53
  return { code, stdout, stderr };
68
54
  }
69
55
 
70
- export { run, spawn, spawnSync, streamText };
56
+ export { run, spawn, streamText };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { dlx } from './chunk-PYXH4J77.js';
3
- import { run } from './chunk-QFP5R25M.js';
2
+ import { dlx } from './chunk-3RDUQUJW.js';
3
+ import { run } from './chunk-5BTLX335.js';
4
4
 
5
5
  // src/lib/eas-cli.ts
6
6
  function compact(argv) {
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { ensureLine, readOne, readAll, removeLines, ENV_FILE } from './chunk-3TT4CDAJ.js';
3
- import { ascStatus } from './chunk-VOL7YISA.js';
4
- import { dlx, detectPackageManager, installCmdFor } from './chunk-PYXH4J77.js';
5
- import { run, spawn } from './chunk-QFP5R25M.js';
3
+ import { ascStatus } from './chunk-BRSFTWP2.js';
4
+ import { dlx, detectPackageManager, installCmdFor } from './chunk-3RDUQUJW.js';
5
+ import { run, spawn } from './chunk-5BTLX335.js';
6
6
  import { Command } from 'commander';
7
- import { readFile, access, writeFile, unlink, stat, mkdir, rename } from 'fs/promises';
7
+ import { readFile, access, unlink, stat, mkdir, writeFile, rename } from 'fs/promises';
8
8
  import { createInterface } from 'readline/promises';
9
9
  import { homedir } from 'os';
10
10
  import { join } from 'path';
@@ -13,7 +13,7 @@ import { createSign } from 'crypto';
13
13
 
14
14
  // package.json
15
15
  var package_default = {
16
- version: "0.1.3"};
16
+ version: "0.1.5"};
17
17
  function deploymentName(value) {
18
18
  if (!value) return void 0;
19
19
  const m = /^(?:dev|prod|preview):(.+)$/.exec(value);
@@ -470,7 +470,7 @@ async function askYesNo(question, defaultYes) {
470
470
  return raw === "y" || raw === "yes";
471
471
  }
472
472
  async function openUrlExternal(url) {
473
- const { spawn: spawn2 } = await import('./proc-TEOPRZZ2.js');
473
+ const { spawn: spawn2 } = await import('./proc-L3ORJMPB.js');
474
474
  const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
475
475
  spawn2(cmd, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
476
476
  }
@@ -1228,7 +1228,7 @@ async function ensureIdentity(localEnv) {
1228
1228
  ok(`wrote EXPO_PUBLIC_APP_BUNDLE_ID=${bundleId}`);
1229
1229
  }
1230
1230
  } else {
1231
- nop(`EXPO_PUBLIC_APP_BUNDLE_ID already set (${bundleId})`);
1231
+ ok(`EXPO_PUBLIC_APP_BUNDLE_ID=${bundleId} (from .env.local); syncing to Convex`);
1232
1232
  }
1233
1233
  if (!haveTeam) {
1234
1234
  if (!process.stdin.isTTY) {
@@ -1829,13 +1829,6 @@ function makeAscClient(creds) {
1829
1829
  };
1830
1830
  const res = await request("POST", "/v1/bundleIds", body);
1831
1831
  return res.data;
1832
- },
1833
- async get(id) {
1834
- const res = await request("GET", `/v1/bundleIds/${id}`);
1835
- return res.data;
1836
- },
1837
- async delete(id) {
1838
- await request("DELETE", `/v1/bundleIds/${id}`);
1839
1832
  }
1840
1833
  },
1841
1834
  bundleIdCapabilities: {
@@ -1864,80 +1857,17 @@ function makeAscClient(creds) {
1864
1857
  body
1865
1858
  );
1866
1859
  return res.data;
1867
- },
1868
- async delete(id) {
1869
- await request("DELETE", `/v1/bundleIdCapabilities/${id}`);
1870
1860
  }
1871
1861
  },
1872
1862
  apps: {
1873
- async list() {
1874
- return paginatedList("/v1/apps");
1875
- },
1876
- async get(id) {
1877
- const res = await request("GET", `/v1/apps/${id}`);
1878
- return res.data;
1879
- }
1880
- },
1881
- certificates: {
1882
1863
  async list(filter) {
1883
1864
  const query = {};
1884
- if (filter?.certificateType) query["filter[certificateType]"] = filter.certificateType;
1885
- return paginatedList("/v1/certificates", query);
1886
- },
1887
- async create(args) {
1888
- const body = {
1889
- data: {
1890
- type: "certificates",
1891
- attributes: {
1892
- csrContent: args.csrContent,
1893
- certificateType: args.certificateType
1894
- }
1895
- }
1896
- };
1897
- const res = await request("POST", "/v1/certificates", body);
1898
- return res.data;
1865
+ if (filter?.bundleId) query["filter[bundleId]"] = filter.bundleId;
1866
+ return paginatedList("/v1/apps", query);
1899
1867
  },
1900
- async delete(id) {
1901
- await request("DELETE", `/v1/certificates/${id}`);
1902
- }
1903
- },
1904
- profiles: {
1905
- async list(filter) {
1906
- const query = {};
1907
- if (filter?.profileType) query["filter[profileType]"] = filter.profileType;
1908
- if (filter?.name) query["filter[name]"] = filter.name;
1909
- return paginatedList("/v1/profiles", query);
1910
- },
1911
- async create(args) {
1912
- const body = {
1913
- data: {
1914
- type: "profiles",
1915
- attributes: { name: args.name, profileType: args.profileType },
1916
- relationships: {
1917
- bundleId: { data: { type: "bundleIds", id: args.bundleIdResourceId } },
1918
- certificates: {
1919
- data: args.certificateIds.map((id) => ({ type: "certificates", id }))
1920
- },
1921
- ...args.deviceIds && args.deviceIds.length > 0 ? {
1922
- devices: {
1923
- data: args.deviceIds.map((id) => ({ type: "devices", id }))
1924
- }
1925
- } : {}
1926
- }
1927
- }
1928
- };
1929
- const res = await request("POST", "/v1/profiles", body);
1868
+ async get(id) {
1869
+ const res = await request("GET", `/v1/apps/${id}`);
1930
1870
  return res.data;
1931
- },
1932
- async delete(id) {
1933
- await request("DELETE", `/v1/profiles/${id}`);
1934
- }
1935
- },
1936
- devices: {
1937
- async list(filter) {
1938
- const query = {};
1939
- if (filter?.status) query["filter[status]"] = filter.status;
1940
- return paginatedList("/v1/devices", query);
1941
1871
  }
1942
1872
  }
1943
1873
  };
@@ -2367,6 +2297,36 @@ async function runServicesId(options2) {
2367
2297
  return 1;
2368
2298
  }
2369
2299
  }
2300
+ async function loadAscCreds2() {
2301
+ const state = await load();
2302
+ const rec = state.steps["asc-key"];
2303
+ if (!rec?.outputs) return null;
2304
+ const out = rec.outputs;
2305
+ const issuerId = out.issuerId;
2306
+ const keyId = out.keyId;
2307
+ const rawPath = out.p8Path;
2308
+ if (!issuerId || !keyId || !rawPath) return null;
2309
+ const p8Path = expandTilde(rawPath);
2310
+ if (!existsSync(p8Path)) return null;
2311
+ return { issuerId, keyId, privateKey: { path: p8Path } };
2312
+ }
2313
+ async function ascBootstrap() {
2314
+ const creds = await loadAscCreds2();
2315
+ if (!creds) {
2316
+ throw new Error("no cached ASC creds. run `vexpo apple asc-key` first");
2317
+ }
2318
+ const client = makeAscClient(creds);
2319
+ const bundleId = await readOne("EXPO_PUBLIC_APP_BUNDLE_ID") ?? await readOne("APP_BUNDLE_ID") ?? void 0;
2320
+ let ascAppId;
2321
+ if (bundleId) {
2322
+ try {
2323
+ const apps = await client.paginatedList("/v1/apps", { "filter[bundleId]": bundleId }, 5);
2324
+ ascAppId = apps[0]?.id;
2325
+ } catch {
2326
+ }
2327
+ }
2328
+ return { client, bundleId, ascAppId, creds };
2329
+ }
2370
2330
 
2371
2331
  // src/lib/eas-submit.ts
2372
2332
  function isObject(value) {
@@ -2402,6 +2362,16 @@ function withAscAppId(easJson, ascAppId) {
2402
2362
  }
2403
2363
 
2404
2364
  // src/commands/asc.ts
2365
+ async function ascAppExists(bundleId) {
2366
+ const creds = await loadAscCreds2();
2367
+ if (!creds) return "unknown";
2368
+ try {
2369
+ const apps = await makeAscClient(creds).apps.list({ bundleId });
2370
+ return apps.length > 0 ? "proceed" : "defer";
2371
+ } catch {
2372
+ return "unknown";
2373
+ }
2374
+ }
2405
2375
  async function loadAscFromState2() {
2406
2376
  const state = await load();
2407
2377
  const rec = state.steps["asc-key"];
@@ -2415,6 +2385,23 @@ async function loadAscFromState2() {
2415
2385
  if (!existsSync(p8Path)) return null;
2416
2386
  return { issuerId, keyId, p8Path };
2417
2387
  }
2388
+ async function syncAscAppIdToEasJson(ascAppId) {
2389
+ if (!ascAppId || !existsSync("eas.json")) return;
2390
+ try {
2391
+ const before = await readFile("eas.json", "utf8");
2392
+ const after = withAscAppId(before, ascAppId);
2393
+ if (after !== before) {
2394
+ await writeFile("eas.json", after);
2395
+ ok(`wrote ascAppId ${BOLD}${ascAppId}${RESET} to eas.json submit profiles`);
2396
+ note("commit this in your fork: non-interactive `eas submit` (CI) needs it");
2397
+ } else {
2398
+ nop("eas.json submit profiles already carry ascAppId");
2399
+ }
2400
+ } catch (err) {
2401
+ yep(`couldn't write ascAppId to eas.json: ${err instanceof Error ? err.message : err}`);
2402
+ note("non-interactive submit will need `ascAppId` set manually in eas.json");
2403
+ }
2404
+ }
2418
2405
  async function runAscConnect(opts = {}) {
2419
2406
  section("ASC connect");
2420
2407
  if (!opts.force) {
@@ -2429,6 +2416,7 @@ async function runAscConnect(opts = {}) {
2429
2416
  bundleId: status.appStoreConnectApp.bundleIdentifier,
2430
2417
  connectedAt: (/* @__PURE__ */ new Date()).toISOString()
2431
2418
  });
2419
+ await syncAscAppIdToEasJson(status.appStoreConnectApp.ascAppIdentifier);
2432
2420
  return 0;
2433
2421
  }
2434
2422
  } catch {
@@ -2449,6 +2437,15 @@ async function runAscConnect(opts = {}) {
2449
2437
  return 1;
2450
2438
  }
2451
2439
  ok(`bundle id: ${BOLD}${bundleId}${RESET}`);
2440
+ if (await ascAppExists(bundleId) === "defer") {
2441
+ yep("no App Store Connect app record for this bundle id yet, NOT connected");
2442
+ note("the ASC app record only appears after the first `eas submit`. run:");
2443
+ note(
2444
+ ` ${BOLD}npx eas build -p ios --profile production --auto-submit-with-profile testflight${RESET}`
2445
+ );
2446
+ note("then re-run `npx vexpo asc:connect` to finish the EAS\u2194ASC link");
2447
+ return 0;
2448
+ }
2452
2449
  if (!process.stdin.isTTY) {
2453
2450
  bad("ASC connect needs a TTY: eas integrations:asc:connect can't run headless");
2454
2451
  note("run `vexpo asc:connect` in an interactive terminal to finish the EAS\u2194ASC link");
@@ -2485,55 +2482,15 @@ async function runAscConnect(opts = {}) {
2485
2482
  connectedAt: (/* @__PURE__ */ new Date()).toISOString()
2486
2483
  });
2487
2484
  if (existsSync("eas.json")) {
2485
+ let postStatus = null;
2488
2486
  try {
2489
- const ascAppId = (await ascStatus()).appStoreConnectApp?.ascAppIdentifier;
2490
- if (ascAppId) {
2491
- const before = await readFile("eas.json", "utf8");
2492
- const after = withAscAppId(before, ascAppId);
2493
- if (after !== before) {
2494
- await writeFile("eas.json", after);
2495
- ok(`wrote ascAppId ${BOLD}${ascAppId}${RESET} to eas.json submit profiles`);
2496
- note("commit this in your fork: non-interactive `eas submit` (CI) needs it");
2497
- } else {
2498
- nop("eas.json submit profiles already carry ascAppId");
2499
- }
2500
- }
2501
- } catch (err) {
2502
- yep(`couldn't write ascAppId to eas.json: ${err instanceof Error ? err.message : err}`);
2503
- note("non-interactive submit will need `ascAppId` set manually in eas.json");
2504
- }
2505
- }
2506
- return 0;
2507
- }
2508
- async function loadAscCreds2() {
2509
- const state = await load();
2510
- const rec = state.steps["asc-key"];
2511
- if (!rec?.outputs) return null;
2512
- const out = rec.outputs;
2513
- const issuerId = out.issuerId;
2514
- const keyId = out.keyId;
2515
- const rawPath = out.p8Path;
2516
- if (!issuerId || !keyId || !rawPath) return null;
2517
- const p8Path = expandTilde(rawPath);
2518
- if (!existsSync(p8Path)) return null;
2519
- return { issuerId, keyId, privateKey: { path: p8Path } };
2520
- }
2521
- async function ascBootstrap() {
2522
- const creds = await loadAscCreds2();
2523
- if (!creds) {
2524
- throw new Error("no cached ASC creds. run `vexpo apple asc-key` first");
2525
- }
2526
- const client = makeAscClient(creds);
2527
- const bundleId = await readOne("EXPO_PUBLIC_APP_BUNDLE_ID") ?? await readOne("APP_BUNDLE_ID") ?? void 0;
2528
- let ascAppId;
2529
- if (bundleId) {
2530
- try {
2531
- const apps = await client.paginatedList("/v1/apps", { "filter[bundleId]": bundleId }, 5);
2532
- ascAppId = apps[0]?.id;
2487
+ postStatus = await ascStatus();
2533
2488
  } catch {
2489
+ postStatus = null;
2534
2490
  }
2491
+ await syncAscAppIdToEasJson(postStatus?.appStoreConnectApp?.ascAppIdentifier);
2535
2492
  }
2536
- return { client, bundleId, ascAppId, creds };
2493
+ return 0;
2537
2494
  }
2538
2495
 
2539
2496
  // src/lib/asc-accessibility.ts
@@ -3167,6 +3124,9 @@ var ROUTING = {
3167
3124
  RESEND_TEST_MODE: {
3168
3125
  routes: (c) => [{ type: "convex", key: "RESEND_TEST_MODE", channel: c }]
3169
3126
  },
3127
+ REQUIRE_EMAIL_VERIFICATION: {
3128
+ routes: (c) => [{ type: "convex", key: "REQUIRE_EMAIL_VERIFICATION", channel: c }]
3129
+ },
3170
3130
  APP_BUNDLE_ID: { routes: (c) => [{ type: "convex", key: "APP_BUNDLE_ID", channel: c }] },
3171
3131
  APPLE_CLIENT_ID: { routes: (c) => [{ type: "convex", key: "APPLE_CLIENT_ID", channel: c }] },
3172
3132
  APPLE_CLIENT_SECRET: {
@@ -3934,7 +3894,7 @@ async function verifyEas(ctx) {
3934
3894
  "eas",
3935
3895
  "asc-submit-id",
3936
3896
  `submit profile${missing.length === 1 ? "" : "s"} ${missing.join(", ")} missing ascAppId`,
3937
- "run `vexpo asc` to write it; non-interactive `eas submit` (CI) fails without it"
3897
+ "run `vexpo asc:connect` to write it; non-interactive `eas submit` (CI) fails without it"
3938
3898
  )
3939
3899
  );
3940
3900
  } else if (existsSync("eas.json")) {
@@ -4078,7 +4038,7 @@ async function readContext(channel) {
4078
4038
  () => /* @__PURE__ */ new Map()
4079
4039
  ) : Promise.resolve(/* @__PURE__ */ new Map()),
4080
4040
  readAppConfigFacts(),
4081
- loadAscCreds3()
4041
+ loadAscCreds2()
4082
4042
  ]
4083
4043
  );
4084
4044
  return {
@@ -4111,21 +4071,6 @@ async function readAppConfigFacts() {
4111
4071
  return {};
4112
4072
  }
4113
4073
  }
4114
- async function loadAscCreds3() {
4115
- try {
4116
- const state = await load();
4117
- const rec = state.steps["asc-key"];
4118
- if (!rec?.outputs) return null;
4119
- const out = rec.outputs;
4120
- const issuerId = out.issuerId;
4121
- const keyId = out.keyId;
4122
- const p8Path = out.p8Path;
4123
- if (!issuerId || !keyId || !p8Path) return null;
4124
- return { issuerId, keyId, privateKey: { path: p8Path } };
4125
- } catch {
4126
- return null;
4127
- }
4128
- }
4129
4074
  async function verifyAll(ctx) {
4130
4075
  const [files, convex, resend, apple2, eas] = await Promise.all([
4131
4076
  Promise.resolve(verifyFiles(ctx)),
@@ -4170,10 +4115,25 @@ function icon(severity) {
4170
4115
  return `${DIM}-${RESET}`;
4171
4116
  }
4172
4117
  }
4118
+ var REDACTIONS = [
4119
+ [/https?:\/\/[a-z0-9-]+\.convex\.(cloud|site)[^\s]*/g, "https://<deployment>.convex.$1"],
4120
+ [/\b[a-z]+-[a-z]+-\d{3}\b/g, "<deployment>"],
4121
+ [/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "<project-id>"],
4122
+ [/\b[\w.+-]+@[\w-]+\.[\w.]+\b/g, "<email>"],
4123
+ [/\b(?:[a-z0-9-]+\.){1,}[a-z]{2,}\b(?= verified)/g, "<domain>"],
4124
+ [/\b(?:com|io|dev|app|net|org)(?:\.[a-z0-9-]+){2,}\b/gi, "<bundle-id>"],
4125
+ [/\b[A-Z0-9]{10}\b/g, "<id>"],
4126
+ [/(@)[\w-]+(\/)/g, "$1<owner>$2"]
4127
+ ];
4128
+ function redactValue(text) {
4129
+ let out = text;
4130
+ for (const [re, sub] of REDACTIONS) out = out.replace(re, sub);
4131
+ return out;
4132
+ }
4173
4133
  function categoryHeader(c) {
4174
4134
  return c.charAt(0).toUpperCase() + c.slice(1);
4175
4135
  }
4176
- function printResults(checks) {
4136
+ function printResults(checks, redact) {
4177
4137
  const byCategory = /* @__PURE__ */ new Map();
4178
4138
  for (const c of checks) {
4179
4139
  if (!byCategory.has(c.category)) byCategory.set(c.category, []);
@@ -4186,8 +4146,9 @@ function printResults(checks) {
4186
4146
  section(categoryHeader(cat));
4187
4147
  const w = Math.max(...items.map((c) => c.name.length));
4188
4148
  for (const c of items) {
4189
- line(` ${icon(c.severity)} ${BOLD}${c.name.padEnd(w)}${RESET} ${c.message}`);
4190
- if (c.details) line(` ${DIM}${c.details}${RESET}`);
4149
+ const message = redact ? redactValue(c.message) : c.message;
4150
+ line(` ${icon(c.severity)} ${BOLD}${c.name.padEnd(w)}${RESET} ${message}`);
4151
+ if (c.details) line(` ${DIM}${redact ? redactValue(c.details) : c.details}${RESET}`);
4191
4152
  }
4192
4153
  }
4193
4154
  }
@@ -4222,7 +4183,7 @@ async function runDoctor(options2) {
4222
4183
  process.stdout.write(JSON.stringify({ channel, summary, checks }, null, 2) + "\n");
4223
4184
  } else {
4224
4185
  section(`Verify (${channel})`);
4225
- printResults(checks);
4186
+ printResults(checks, options2.redact === true);
4226
4187
  line();
4227
4188
  const parts = [
4228
4189
  `${GREEN}${summary.ok} ok${RESET}`,
@@ -4606,7 +4567,6 @@ async function runEnvPush(options2) {
4606
4567
  return 2;
4607
4568
  }
4608
4569
  ok("nothing to do. all source values match destinations");
4609
- await recordStep("accounts", { mode: "lite", verifiedAt: (/* @__PURE__ */ new Date()).toISOString() });
4610
4570
  return 0;
4611
4571
  }
4612
4572
  const prodConvexWrites = entries.some(
@@ -4651,12 +4611,6 @@ async function runEnvPush(options2) {
4651
4611
  return 1;
4652
4612
  }
4653
4613
  ok(`${appliedTotal} value${appliedTotal === 1 ? "" : "s"} synced`);
4654
- await recordStep("accounts", {
4655
- mode: "lite",
4656
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
4657
- sources: sources.map((s) => ({ path: s.path, channel: s.channel, keys: s.entries.size })),
4658
- applied: appliedTotal
4659
- });
4660
4614
  if (!options2.noVerify) {
4661
4615
  const haveProd = sources.some((s) => s.channel === "prod");
4662
4616
  const verifyChannels = haveProd ? ["dev", "prod"] : ["dev"];
@@ -4716,19 +4670,21 @@ function bundleSlug(value) {
4716
4670
  return value.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "").slice(0, 32);
4717
4671
  }
4718
4672
  async function promptInputs(overrides) {
4719
- if (!process.stdin.isTTY) fail("rebrand wizard needs a TTY");
4720
- line();
4721
- note(
4722
- `${DIM}4 prompts. Everything else is derived. Override any with flags or edit later.${RESET}`
4723
- );
4724
- line();
4725
- const appName2 = overrides.appName ?? (await ask(` ${BOLD}App name${RESET} ${DIM}(e.g. Foobar)${RESET} > `)).trim();
4673
+ const interactive = process.stdin.isTTY === true;
4674
+ if (interactive) {
4675
+ line();
4676
+ note(
4677
+ `${DIM}4 prompts. Everything else is derived. Override any with flags or edit later.${RESET}`
4678
+ );
4679
+ line();
4680
+ }
4681
+ const appName2 = overrides.appName ?? (interactive ? (await ask(` ${BOLD}App name${RESET} ${DIM}(e.g. Foobar)${RESET} > `)).trim() : "");
4726
4682
  if (!appName2) fail("app name required");
4727
4683
  const defaultPkg = slug(appName2);
4728
4684
  const bundleHint = `com.${slug(appName2).replace(/-/g, "")}.${bundleSlug(defaultPkg)}`;
4729
- const bundleId = overrides.bundleId ?? ((await ask(` ${BOLD}Bundle ID${RESET} ${DIM}[${bundleHint}]${RESET} > `)).trim() || bundleHint);
4730
- const ownerName = overrides.ownerName ?? ((await ask(` ${BOLD}Your name${RESET} > `)).trim() || "Owner");
4731
- const reviewEmail = overrides.reviewEmail ?? (await ask(` ${BOLD}Apple review contact email${RESET} > `)).trim();
4685
+ const bundleId = overrides.bundleId ?? (interactive ? (await ask(` ${BOLD}Bundle ID${RESET} ${DIM}[${bundleHint}]${RESET} > `)).trim() || bundleHint : bundleHint);
4686
+ const ownerName = overrides.ownerName ?? (interactive ? (await ask(` ${BOLD}Your name${RESET} > `)).trim() || "Owner" : "Owner");
4687
+ const reviewEmail = overrides.reviewEmail ?? (interactive ? (await ask(` ${BOLD}Apple review contact email${RESET} > `)).trim() : "");
4732
4688
  if (!reviewEmail) fail("review email required");
4733
4689
  const packageName = overrides.packageName ?? defaultPkg;
4734
4690
  const scheme2 = overrides.scheme ?? bundleSlug(packageName);
@@ -4759,6 +4715,20 @@ async function promptInputs(overrides) {
4759
4715
  expoOwner
4760
4716
  };
4761
4717
  }
4718
+ async function syncBundleId(bundleId) {
4719
+ const env2 = await readAll();
4720
+ const current = env2.get("EXPO_PUBLIC_APP_BUNDLE_ID");
4721
+ if (current === bundleId) return;
4722
+ if (current !== void 0) await removeLines(["EXPO_PUBLIC_APP_BUNDLE_ID"]);
4723
+ await ensureLine("EXPO_PUBLIC_APP_BUNDLE_ID", bundleId);
4724
+ ok(`wrote EXPO_PUBLIC_APP_BUNDLE_ID=${bundleId} to .env.local`);
4725
+ if (env2.has("CONVEX_DEPLOYMENT")) {
4726
+ await envSet("APP_BUNDLE_ID", bundleId);
4727
+ ok(`Convex env: APP_BUNDLE_ID=${bundleId}`);
4728
+ } else {
4729
+ note("no Convex deployment yet; the next `vexpo convex` run carries APP_BUNDLE_ID");
4730
+ }
4731
+ }
4762
4732
  async function backup(files, stamp) {
4763
4733
  const dir = `.rebrand-backup/${stamp}`;
4764
4734
  await mkdir(dir, { recursive: true });
@@ -4960,6 +4930,7 @@ async function runRebrand(options2) {
4960
4930
  await rewriteAppJson();
4961
4931
  await rewritePackageJson(inputs);
4962
4932
  await rewriteStoreConfig(inputs);
4933
+ await syncBundleId(inputs.bundleId);
4963
4934
  if (inputs.expoOwner) {
4964
4935
  await ensureLine("EXPO_PUBLIC_EXPO_OWNER", inputs.expoOwner);
4965
4936
  ok(`wrote EXPO_PUBLIC_EXPO_OWNER=${inputs.expoOwner} to .env.local`);
@@ -5337,67 +5308,63 @@ async function runEas(options2) {
5337
5308
  ok(`signed in as ${BOLD}${who}${RESET}`);
5338
5309
  }
5339
5310
  let projectId = await resolveProjectId();
5340
- if (!options2.skipInit) {
5341
- if (projectId) {
5342
- ok(`EAS project linked: ${projectId}`);
5343
- } else {
5344
- const result = await init();
5345
- if (!result.ok) {
5346
- bad("eas init failed");
5347
- return 1;
5311
+ if (projectId) {
5312
+ ok(`EAS project linked: ${projectId}`);
5313
+ } else {
5314
+ const result = await init();
5315
+ if (!result.ok) {
5316
+ bad("eas init failed");
5317
+ return 1;
5318
+ }
5319
+ projectId = result.projectId ?? null;
5320
+ ok(`EAS project created: ${projectId}`);
5321
+ }
5322
+ const channels = ["development", "preview", "production"];
5323
+ const createdChannels = await ensureChannels(channels);
5324
+ if (createdChannels.length > 0) ok(`channels created: ${createdChannels.join(", ")}`);
5325
+ else nop(`channels already exist (${channels.join(", ")})`);
5326
+ const branches = ["development", "preview", "production"];
5327
+ const createdBranches = await ensureBranches(branches);
5328
+ if (createdBranches.length > 0) ok(`branches created: ${createdBranches.join(", ")}`);
5329
+ else nop(`branches already exist (${branches.join(", ")})`);
5330
+ if (await fileExists7(".env.local")) {
5331
+ try {
5332
+ const pushed = await pushEasRoutedKeys(".env.local", ["development"]);
5333
+ if (pushed.length > 0) {
5334
+ ok(
5335
+ `pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (development)`
5336
+ );
5337
+ } else {
5338
+ nop(".env.local has no EAS-routed keys yet (run `vexpo convex` first)");
5348
5339
  }
5349
- projectId = result.projectId ?? null;
5350
- ok(`EAS project created: ${projectId}`);
5340
+ } catch (err) {
5341
+ bad(err instanceof Error ? err.message : String(err));
5351
5342
  }
5352
- const channels = ["development", "preview", "production"];
5353
- const createdChannels = await ensureChannels(channels);
5354
- if (createdChannels.length > 0) ok(`channels created: ${createdChannels.join(", ")}`);
5355
- else nop(`channels already exist (${channels.join(", ")})`);
5356
- const branches = ["development", "preview", "production"];
5357
- const createdBranches = await ensureBranches(branches);
5358
- if (createdBranches.length > 0) ok(`branches created: ${createdBranches.join(", ")}`);
5359
- else nop(`branches already exist (${branches.join(", ")})`);
5360
- }
5361
- if (!options2.skipEnv) {
5362
- if (await fileExists7(".env.local")) {
5343
+ } else {
5344
+ nop(".env.local missing. skipping development env push (run `vexpo convex` first)");
5345
+ }
5346
+ if (options2.withProd) {
5347
+ const prodFile = await fileExists7(".env.prod") ? ".env.prod" : await fileExists7(".env.production") ? ".env.production" : null;
5348
+ if (prodFile) {
5363
5349
  try {
5364
- const pushed = await pushEasRoutedKeys(".env.local", ["development"]);
5350
+ const pushed = await pushEasRoutedKeys(prodFile, ["production", "preview"]);
5365
5351
  if (pushed.length > 0) {
5366
5352
  ok(
5367
- `pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (development)`
5353
+ `pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (production, preview)`
5368
5354
  );
5369
5355
  } else {
5370
- nop(".env.local has no EAS-routed keys yet (run `vexpo convex` first)");
5356
+ nop(`${prodFile} has no EAS-routed keys`);
5371
5357
  }
5372
5358
  } catch (err) {
5373
5359
  bad(err instanceof Error ? err.message : String(err));
5374
5360
  }
5375
5361
  } else {
5376
- nop(".env.local missing. skipping development env push (run `vexpo convex` first)");
5362
+ nop("--with-prod set but no .env.prod or .env.production found");
5377
5363
  }
5378
- if (options2.withProd) {
5379
- const prodFile = await fileExists7(".env.prod") ? ".env.prod" : await fileExists7(".env.production") ? ".env.production" : null;
5380
- if (prodFile) {
5381
- try {
5382
- const pushed = await pushEasRoutedKeys(prodFile, ["production", "preview"]);
5383
- if (pushed.length > 0) {
5384
- ok(
5385
- `pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (production, preview)`
5386
- );
5387
- } else {
5388
- nop(`${prodFile} has no EAS-routed keys`);
5389
- }
5390
- } catch (err) {
5391
- bad(err instanceof Error ? err.message : String(err));
5392
- }
5393
- } else {
5394
- nop("--with-prod set but no .env.prod or .env.production found");
5395
- }
5396
- }
5397
- note(
5398
- `server-side secrets route to Convex, not EAS. run ${BOLD}vexpo env push${RESET} to sync those`
5399
- );
5400
5364
  }
5365
+ note(
5366
+ `server-side secrets route to Convex, not EAS. run ${BOLD}vexpo env push${RESET} to sync those`
5367
+ );
5401
5368
  if (projectId) {
5402
5369
  await recordStep("eas", {
5403
5370
  projectId,
@@ -5556,7 +5523,7 @@ async function liveCheckEas() {
5556
5523
  }
5557
5524
  async function liveCheckAscLink() {
5558
5525
  try {
5559
- const { ascStatus: ascStatus2 } = await import('./eas-integrations-2QVR45NE.js');
5526
+ const { ascStatus: ascStatus2 } = await import('./eas-integrations-ZULIUD4T.js');
5560
5527
  const status = await ascStatus2();
5561
5528
  return status.status === "connected";
5562
5529
  } catch {
@@ -5575,16 +5542,20 @@ async function liveCheckRotationSecrets() {
5575
5542
  "CONVEX_DEPLOY_KEY"
5576
5543
  ].every((k) => eas.has(k));
5577
5544
  }
5545
+ var LOCAL_ENV_LITE_CORE = [
5546
+ "CONVEX_DEPLOYMENT",
5547
+ "EXPO_PUBLIC_CONVEX_URL",
5548
+ "EXPO_PUBLIC_CONVEX_SITE_URL",
5549
+ "EXPO_PUBLIC_SITE_URL",
5550
+ "EXPO_PUBLIC_APP_BUNDLE_ID"
5551
+ ];
5552
+ var LOCAL_ENV_TEAM_ID = "EXPO_PUBLIC_APPLE_TEAM_ID";
5553
+ function classifyLocalEnv(env2) {
5554
+ if (!LOCAL_ENV_LITE_CORE.every((k) => env2.has(k))) return "missing";
5555
+ return env2.has(LOCAL_ENV_TEAM_ID) ? "ok" : "partial";
5556
+ }
5578
5557
  async function liveCheckLocalEnv() {
5579
- const env2 = await readAll();
5580
- return [
5581
- "CONVEX_DEPLOYMENT",
5582
- "EXPO_PUBLIC_CONVEX_URL",
5583
- "EXPO_PUBLIC_CONVEX_SITE_URL",
5584
- "EXPO_PUBLIC_SITE_URL",
5585
- "EXPO_PUBLIC_APP_BUNDLE_ID",
5586
- "EXPO_PUBLIC_APPLE_TEAM_ID"
5587
- ].every((k) => env2.has(k));
5558
+ return classifyLocalEnv(await readAll());
5588
5559
  }
5589
5560
  async function stepPrerequisites() {
5590
5561
  section("Prerequisites");
@@ -5605,12 +5576,17 @@ async function stepPrerequisites() {
5605
5576
  async function stepProbe() {
5606
5577
  section("Probe");
5607
5578
  const installOk = await nodeModulesPresent();
5608
- const localOk = await liveCheckLocalEnv();
5609
- const convex = localOk ? await envMap() : /* @__PURE__ */ new Map();
5579
+ const localEnvState = await liveCheckLocalEnv();
5580
+ const convexLive = localEnvState !== "missing";
5581
+ const convex = convexLive ? await envMap() : /* @__PURE__ */ new Map();
5610
5582
  const rows = /* @__PURE__ */ new Map();
5611
5583
  rows.set("accounts", await shouldRun("accounts", async () => true));
5612
5584
  rows.set("rebrand", await shouldRun("rebrand", async () => false));
5613
- rows.set("convex", { step: "convex", label: "convex", status: localOk ? "live" : "missing" });
5585
+ rows.set("convex", {
5586
+ step: "convex",
5587
+ label: "convex",
5588
+ status: convexLive ? "live" : "missing"
5589
+ });
5614
5590
  rows.set("better-auth", await shouldRun("better-auth", () => liveCheckBetterAuth(convex)));
5615
5591
  rows.set("resend", await shouldRun("resend", () => liveCheckResend(convex)));
5616
5592
  rows.set("asc-key", await shouldRun("asc-key", async () => false));
@@ -5631,9 +5607,8 @@ async function stepProbe() {
5631
5607
  line(
5632
5608
  ` ${BOLD}${"node_modules".padEnd(w)}${RESET} ${installOk ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`}`
5633
5609
  );
5634
- line(
5635
- ` ${BOLD}${".env.local".padEnd(w)}${RESET} ${localOk ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`}`
5636
- );
5610
+ const localEnvMark = localEnvState === "ok" ? `${GREEN}ok${RESET}` : localEnvState === "partial" ? `${YELLOW}partial (lite)${RESET}` : `${RED}missing${RESET}`;
5611
+ line(` ${BOLD}${".env.local".padEnd(w)}${RESET} ${localEnvMark}`);
5637
5612
  for (const [key, row] of rows) {
5638
5613
  const label = key === "convex" ? "Convex / .env.local" : key === "better-auth" ? "Better Auth" : key === "resend" ? "Resend" : key === "asc-key" ? "ASC API key" : key === "apple-services-id" ? "Sign In Services ID" : key === "apple-sign-in" ? "Sign In JWT" : key === "apple-credentials" ? "EAS iOS credentials" : key === "apple-asc-link" ? "EAS \u2194 ASC link" : key === "apple-eas-rotation-secrets" ? "EAS rotation secrets" : key === "eas" ? "EAS project + env" : key === "rebrand" ? "Rebrand" : key === "accounts" ? "Accounts" : key;
5639
5614
  line(` ${BOLD}${label.padEnd(w)}${RESET} ${mark(row.status)}`);
@@ -5643,7 +5618,7 @@ async function stepProbe() {
5643
5618
  );
5644
5619
  const needs = /* @__PURE__ */ new Map();
5645
5620
  for (const [k, row] of rows) needs.set(k, row.status === "missing");
5646
- return { rows, needs, install: !installOk, localEnv: localOk };
5621
+ return { rows, needs, install: !installOk, localEnv: localEnvState };
5647
5622
  }
5648
5623
  async function describePhase(step, probe) {
5649
5624
  const status = probe.rows.get(step)?.status;
@@ -6096,13 +6071,13 @@ async function runStep(name, state) {
6096
6071
  }
6097
6072
  if (state) completed.push(state);
6098
6073
  }
6099
- async function maybeRunStep(name, prompt, state) {
6074
+ async function maybeRunStep(name, prompt, state, defaultYes = true) {
6100
6075
  if (!process.stdin.isTTY) {
6101
6076
  nop(`non-TTY: skipping ${name} (run \`${name}\` later)`);
6102
6077
  if (state) skipped.push(state);
6103
6078
  return;
6104
6079
  }
6105
- if (!await askYesNo(prompt, true)) {
6080
+ if (!await askYesNo(prompt, defaultYes)) {
6106
6081
  nop(`skipped ${name} (run \`${name}\` later)`);
6107
6082
  if (state) skipped.push(state);
6108
6083
  return;
@@ -6124,7 +6099,7 @@ function printShipNextSteps() {
6124
6099
  }
6125
6100
  async function stepExpoDoctor() {
6126
6101
  section("expo-doctor");
6127
- const { dlx: dlx2 } = await import('./pkg-manager-PU7UPAID.js');
6102
+ const { dlx: dlx2 } = await import('./pkg-manager-DOOC6W2C.js');
6128
6103
  const { code, stdout, stderr } = await run([dlx2(), "expo-doctor"]);
6129
6104
  if (stdout.trim()) process.stderr.write(stdout);
6130
6105
  if (stderr.trim()) process.stderr.write(stderr);
@@ -6370,8 +6345,9 @@ async function runSetup(opts) {
6370
6345
  nop("vexpo apple services-id cached");
6371
6346
  }
6372
6347
  const status = probe.rows.get("apple-sign-in")?.status;
6373
- const prompt = status === "live" || status === "cached" ? "Apple Sign In is configured, rotate the JWT now?" : "Sign the Apple Sign In JWT now?";
6374
- await maybeRunStep("vexpo apple jwt", prompt, "apple-sign-in");
6348
+ const healthy = status === "live" || status === "cached";
6349
+ const prompt = healthy ? "Apple Sign In is configured, rotate the JWT now?" : "Sign the Apple Sign In JWT now?";
6350
+ await maybeRunStep("vexpo apple jwt", prompt, "apple-sign-in", !healthy);
6375
6351
  if (options.force || probe.needs.get("apple-eas-rotation-secrets")) {
6376
6352
  await maybeRunStep(
6377
6353
  "vexpo apple eas-rotation-secrets",
@@ -6488,7 +6464,7 @@ program.command("rebrand").description("Replace template defaults with your fork
6488
6464
  program.command("review-account").description("Seed the App Review demo account on Convex.").option("--email <email>", "override demo email").option("--password <password>", "override demo password").option("--name <name>", "override demo display name", "App Review").option("--username <username>", "optional username").action((options2) => exitWith(runReviewAccount(options2)));
6489
6465
  program.command("doctor").description(
6490
6466
  "Cross-source drift detection. Auth-checks every credential, confirms IDs match across `.env.local`, Convex env, EAS env, `app.config.ts`. No eas-cli equivalent."
6491
- ).option("--channel <channel>", "dev | prod", "dev").option("--json", "machine-readable output", false).option("--strict", "exit non-zero on any warn", false).action((options2) => {
6467
+ ).option("--channel <channel>", "dev | prod", "dev").option("--json", "machine-readable output", false).option("--strict", "exit non-zero on any warn", false).option("--redact", "mask identifying values (for screenshots and issue reports)", false).action((options2) => {
6492
6468
  exitWith(runDoctor(options2));
6493
6469
  });
6494
6470
  program.command("adopt").description(
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export { ascStatus } from './chunk-BRSFTWP2.js';
3
+ import './chunk-3RDUQUJW.js';
4
+ import './chunk-5BTLX335.js';
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export { currentRuntime, currentRuntimeVersion, detectPackageManager, dlx, dlxFor, installCmdFor, runCmdFor } from './chunk-PYXH4J77.js';
2
+ export { currentRuntime, currentRuntimeVersion, detectPackageManager, dlx, installCmdFor } from './chunk-3RDUQUJW.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { run, spawn, streamText } from './chunk-5BTLX335.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramonclaudio/vexpo",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Operational CLI for vexpo projects: setup orchestration, drift detection, env sync, Apple JWT signing, ASC API integration.",
5
5
  "keywords": [
6
6
  "apple-sign-in",
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { ascStatus } from './chunk-VOL7YISA.js';
3
- import './chunk-PYXH4J77.js';
4
- import './chunk-QFP5R25M.js';
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export { run, spawn, spawnSync, streamText } from './chunk-QFP5R25M.js';