@ramonclaudio/vexpo 0.1.0 → 0.1.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
@@ -3,13 +3,13 @@
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 exposes the App Store Connect API endpoints `eas-cli` doesn't.
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.
7
7
 
8
- Scaffolded by [`create-vexpo`](https://www.npmjs.com/package/create-vexpo) into your project's devDependencies. Invoke via `bunx vexpo`.
8
+ Scaffolded by [`create-vexpo`](https://www.npmjs.com/package/@ramonclaudio/create-vexpo) into your project's devDependencies. Invoke via `npx vexpo`.
9
9
 
10
10
  ## Design rule: don't reinvent EAS
11
11
 
12
- If `eas <subcommand>` is the canonical answer, the recipe is `bunx 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 App Store Connect API endpoints that aren't in `eas-cli`. 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.
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.
13
13
 
14
14
  ## Setup
15
15
 
@@ -44,7 +44,9 @@ vexpo apple jwt --rotate Re-sign the JWT only
44
44
  vexpo apple eas-rotation-secrets Push the 5 EAS production secrets the JWT cron needs
45
45
  ```
46
46
 
47
- ## App Store Connect API (endpoints `eas-cli` doesn't expose)
47
+ ## App Store Connect (the last mile to a first ship)
48
+
49
+ 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.
48
50
 
49
51
  ```
50
52
  vexpo testflight groups list List beta groups
@@ -53,22 +55,12 @@ vexpo testflight groups view <id> View a beta group + its testers
53
55
  vexpo testflight groups delete <id> Delete a beta group
54
56
  vexpo testflight testers list List beta testers
55
57
  vexpo testflight invite <email> Add a tester + send invite
56
- vexpo testflight remove <email> Remove a tester
57
58
  vexpo testflight whats-new <buildId> <text> Set "What's new" notes
58
59
 
59
- vexpo reviews list List customer reviews
60
- vexpo reviews unanswered Reviews without a response
61
- vexpo reviews respond <reviewId> <body> Post a response
62
- vexpo reviews delete-response <responseId> Delete a response
63
-
64
- vexpo sandbox list List sandbox testers
65
- vexpo sandbox create --email <e> ... Create a sandbox tester
66
- vexpo sandbox delete <id> Delete a sandbox tester
67
-
68
- vexpo asc:version list List App Store versions
69
- vexpo asc:version view <versionId> Phased-release state
70
- vexpo asc:version phased <id> <action> Pause | resume | complete the phased release
71
- vexpo asc:submissions List review submissions
60
+ vexpo asc:privacy show [file] Show the declared privacy nutrition label
61
+ vexpo asc:privacy lint <file> Validate privacy.config.json against Apple's enums
62
+ vexpo asc:accessibility show Fetch the app's accessibility declarations
63
+ vexpo asc:accessibility lint <file> Validate accessibility.config.json against Apple's enums
72
64
  ```
73
65
 
74
66
  ## What `vexpo` doesn't wrap
@@ -76,24 +68,26 @@ vexpo asc:submissions List review submissions
76
68
  For canonical EAS surface, use `eas` directly. Wrapping these would add no value over `eas-cli` itself.
77
69
 
78
70
  ```bash
79
- bunx eas init # EAS project init
80
- bunx eas build [...] # builds, list, view, cancel, delete, download, run, resign
81
- bunx eas submit
82
- bunx eas update [...] # publish OTAs, --rollout-percentage, etc.
83
- bunx eas update:list / update:view / update:edit / update:rollback / update:republish
84
- bunx eas channel [...] # CRUD + rollouts + insights
85
- bunx eas branch [...] # CRUD
86
- bunx eas deploy [...] # EAS Hosting
87
- bunx eas webhook [...] # BUILD/SUBMIT webhook CRUD
88
- bunx eas workflow [...] # run, validate, logs, cancel
89
- bunx eas fingerprint [...]
90
- bunx eas device [...] # list, create, view, rename, delete
91
- bunx eas metadata [...]
92
- bunx eas env [...] # env:push, env:pull, env:get, env:delete, env:create, env:list
93
- bunx eas integrations:asc [...] # status, connect, disconnect
71
+ npx eas init # EAS project init
72
+ npx eas build [...] # builds, list, view, cancel, delete, download, run, resign
73
+ npx eas submit
74
+ npx eas update [...] # publish OTAs, --rollout-percentage, etc.
75
+ npx eas update:list / update:view / update:edit / update:rollback / update:republish
76
+ npx eas channel [...] # CRUD + rollouts + insights
77
+ npx eas branch [...] # CRUD
78
+ npx eas deploy [...] # EAS Hosting
79
+ npx eas webhook [...] # BUILD/SUBMIT webhook CRUD
80
+ npx eas workflow [...] # run, validate, logs, cancel
81
+ npx eas fingerprint [...]
82
+ npx eas device [...] # list, create, view, rename, delete
83
+ npx eas metadata [...]
84
+ npx eas env [...] # env:push, env:pull, env:get, env:delete, env:create, env:list
85
+ npx eas integrations:asc [...] # status, connect, disconnect
94
86
  ```
95
87
 
96
- `vexpo full` orchestrates `eas init`, `eas env:push`, `eas credentials:configure-build`, and `eas integrations:asc:connect` internally as setup steps, none are exposed as standalone `vexpo` commands. That's `eas-cli`'s surface.
88
+ `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, none are exposed as standalone `vexpo` commands. 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.
89
+
90
+ 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.
97
91
 
98
92
  ## Architecture
99
93
 
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, access } from 'fs/promises';
3
+
4
+ var ENV_FILE = ".env.local";
5
+ async function fileExists(p) {
6
+ try {
7
+ await access(p);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ async function readAll() {
14
+ const out = /* @__PURE__ */ new Map();
15
+ if (!await fileExists(ENV_FILE)) return out;
16
+ const text = (await readFile(ENV_FILE, "utf8")).replace(/^/, "").replace(/\r\n/g, "\n");
17
+ const lines = text.split("\n");
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const raw = lines[i];
20
+ const trimmed = raw.trim();
21
+ if (!trimmed || trimmed.startsWith("#")) continue;
22
+ const eq = trimmed.indexOf("=");
23
+ if (eq <= 0) continue;
24
+ const key = trimmed.slice(0, eq).trim();
25
+ let value = trimmed.slice(eq + 1);
26
+ const startQuote = value.match(/^\s*(['"])/);
27
+ if (startQuote) {
28
+ const quote = startQuote[1];
29
+ let rest = value.replace(/^\s*['"]/, "");
30
+ let endIdx = rest.indexOf(quote);
31
+ while (endIdx === -1 && i + 1 < lines.length) {
32
+ i += 1;
33
+ rest += `
34
+ ${lines[i]}`;
35
+ endIdx = rest.indexOf(quote);
36
+ }
37
+ value = endIdx === -1 ? rest : rest.slice(0, endIdx);
38
+ out.set(key, value);
39
+ continue;
40
+ }
41
+ value = value.trim();
42
+ const hashAt = value.search(/\s#/);
43
+ if (hashAt >= 0) value = value.slice(0, hashAt).trim();
44
+ out.set(key, value);
45
+ }
46
+ return out;
47
+ }
48
+ async function readOne(key) {
49
+ return (await readAll()).get(key);
50
+ }
51
+ async function ensureLine(key, value) {
52
+ const current = await fileExists(ENV_FILE) ? await readFile(ENV_FILE, "utf8") : "";
53
+ if (new RegExp(`^${key}=`, "m").test(current)) return;
54
+ const needsNewline = current !== "" && !current.endsWith("\n");
55
+ await writeFile(ENV_FILE, `${current}${needsNewline ? "\n" : ""}${key}=${value}
56
+ `);
57
+ }
58
+ async function removeLines(keys) {
59
+ if (!await fileExists(ENV_FILE)) return;
60
+ const text = await readFile(ENV_FILE, "utf8");
61
+ const drop = new Set(keys);
62
+ const next = text.split("\n").filter((l) => {
63
+ const eq = l.indexOf("=");
64
+ if (eq <= 0) return true;
65
+ return !drop.has(l.slice(0, eq).trim());
66
+ }).join("\n").replace(/\n{3,}/g, "\n\n");
67
+ await writeFile(ENV_FILE, next);
68
+ }
69
+
70
+ export { ENV_FILE, ensureLine, readAll, readOne, removeLines };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { dlx } from './chunk-PYXH4J77.js';
3
- import { run, spawn } from './chunk-QFP5R25M.js';
3
+ import { run } from './chunk-QFP5R25M.js';
4
4
 
5
5
  // src/lib/eas-cli.ts
6
6
  function compact(argv) {
@@ -30,32 +30,10 @@ async function easJson(argv) {
30
30
  );
31
31
  }
32
32
  }
33
- async function easSpawn(argv, opts = {}) {
34
- const flat = compact(argv);
35
- const proc = spawn([dlx(), "eas", ...flat], {
36
- stdin: "inherit",
37
- stdout: "inherit",
38
- stderr: "inherit",
39
- env: opts.env,
40
- cwd: opts.cwd
41
- });
42
- return proc.exited;
43
- }
44
33
 
45
34
  // src/lib/eas-integrations.ts
46
35
  async function ascStatus() {
47
36
  return easJson(["integrations:asc:status"]);
48
37
  }
49
- async function ascConnect(opts = {}) {
50
- return easSpawn([
51
- "integrations:asc:connect",
52
- opts.apiKeyId ? "--api-key-id" : null,
53
- opts.apiKeyId,
54
- opts.ascAppId ? "--asc-app-id" : null,
55
- opts.ascAppId,
56
- opts.bundleId ? "--bundle-id" : null,
57
- opts.bundleId
58
- ]);
59
- }
60
38
 
61
- export { ascConnect, ascStatus };
39
+ export { ascStatus };