@ramonclaudio/vexpo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # vexpo
2
+
3
+ [![npm](https://img.shields.io/npm/v/@ramonclaudio/vexpo)](https://www.npmjs.com/package/@ramonclaudio/vexpo)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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.
7
+
8
+ Scaffolded by [`create-vexpo`](https://www.npmjs.com/package/create-vexpo) into your project's devDependencies. Invoke via `bunx vexpo`.
9
+
10
+ ## Design rule: don't reinvent EAS
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.
13
+
14
+ ## Setup
15
+
16
+ ```
17
+ vexpo lite Convex + Better Auth only, simulator-ready (60s)
18
+ vexpo lite --new same + Convex signup walkthrough for first-time users
19
+ vexpo full full provisioning (Convex + Better Auth + Resend + Apple + EAS init + rebrand)
20
+ vexpo full --new same + walks Apple/Convex/Expo/Resend signups
21
+ vexpo full --skip-rebrand full setup, skip the rebrand wizard
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
+ ```
35
+
36
+ ## Apple
37
+
38
+ ```
39
+ vexpo apple asc-key Validate ASC API key against /v1/apps
40
+ vexpo apple credentials Wraps `eas credentials:configure-build` with cached ASC env vars (skips Apple Developer login prompt)
41
+ vexpo apple services-id Detect SIWA Services ID + attach APPLE_ID_AUTH capability
42
+ vexpo apple jwt Sign SIWA ES256 client_secret JWT (180-day expiry)
43
+ vexpo apple jwt --rotate Re-sign the JWT only
44
+ vexpo apple eas-rotation-secrets Push the 5 EAS production secrets the JWT cron needs
45
+ ```
46
+
47
+ ## App Store Connect API (endpoints `eas-cli` doesn't expose)
48
+
49
+ ```
50
+ vexpo testflight groups list List beta groups
51
+ vexpo testflight groups create <name> Create a beta group
52
+ vexpo testflight groups view <id> View a beta group + its testers
53
+ vexpo testflight groups delete <id> Delete a beta group
54
+ vexpo testflight testers list List beta testers
55
+ vexpo testflight invite <email> Add a tester + send invite
56
+ vexpo testflight remove <email> Remove a tester
57
+ vexpo testflight whats-new <buildId> <text> Set "What's new" notes
58
+
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
72
+ ```
73
+
74
+ ## What `vexpo` doesn't wrap
75
+
76
+ For canonical EAS surface, use `eas` directly. Wrapping these would add no value over `eas-cli` itself.
77
+
78
+ ```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
94
+ ```
95
+
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.
97
+
98
+ ## Architecture
99
+
100
+ - Commander-based command tree in `src/cli.ts`.
101
+ - One file per top-level command in `src/commands/`. Each exports `run<Name>(options)` returning a numeric exit code.
102
+ - `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.
103
+ - Built with tsup → single ESM bundle in `dist/`. Node 20+.
104
+
105
+ ## Apple ASC API workarounds
106
+
107
+ Apple changed several ASC API behaviors after the initial CLI release. The CLI handles each one:
108
+
109
+ - `POST /v1/bundleIds` rejects `platform: "SERVICES"`. `services-id` walks the user through manual creation in the developer portal, then re-polls.
110
+ - App bundle IDs report `platform: "UNIVERSAL"` for newer accounts. `findOrCreateBundleId` matches any non-SERVICES platform when looking up an App ID.
111
+ - Relationship endpoints reject `limit`. `bundleIdCapabilities.list` fetches without pagination.
112
+ - `filter[platform]=SERVICES` returns 400. `doctor`'s `services-id-exists` check filters by identifier alone.
113
+
114
+ ## Repo
115
+
116
+ [github.com/ramonclaudio/vexpo](https://github.com/ramonclaudio/vexpo)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { reviews, unansweredOlderThan } from './chunk-5JSZTHAP.js';
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // src/lib/asc-reviews.ts
3
+ function reviews(client) {
4
+ return {
5
+ customerReviews: {
6
+ list(filter) {
7
+ const query = {};
8
+ if (filter?.territory) query["filter[territory]"] = filter.territory;
9
+ if (filter?.rating) query["filter[rating]"] = String(filter.rating);
10
+ query["include"] = "response";
11
+ const path = filter?.appId ? `/v1/apps/${filter.appId}/customerReviews` : "/v1/customerReviews";
12
+ return client.paginatedList(path, query, filter?.limit ?? 50);
13
+ },
14
+ async get(id) {
15
+ const res = await client.request(
16
+ "GET",
17
+ `/v1/customerReviews/${id}`,
18
+ void 0,
19
+ { include: "response" }
20
+ );
21
+ return res.data;
22
+ },
23
+ async getResponse(reviewId) {
24
+ try {
25
+ const res = await client.request(
26
+ "GET",
27
+ `/v1/customerReviews/${reviewId}/response`
28
+ );
29
+ return res.data;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ },
35
+ customerReviewResponses: {
36
+ async create(args) {
37
+ const body = {
38
+ data: {
39
+ type: "customerReviewResponses",
40
+ attributes: { responseBody: args.responseBody },
41
+ relationships: {
42
+ review: { data: { type: "customerReviews", id: args.reviewId } }
43
+ }
44
+ }
45
+ };
46
+ const res = await client.request(
47
+ "POST",
48
+ "/v1/customerReviewResponses",
49
+ body
50
+ );
51
+ return res.data;
52
+ },
53
+ async delete(id) {
54
+ await client.request("DELETE", `/v1/customerReviewResponses/${id}`);
55
+ }
56
+ }
57
+ };
58
+ }
59
+ function unansweredOlderThan(list, daysAgo) {
60
+ const cutoff = Date.now() - daysAgo * 864e5;
61
+ return list.filter((r) => {
62
+ const created = r.attributes.createdDate ? Date.parse(r.attributes.createdDate) : NaN;
63
+ if (!Number.isFinite(created) || created > cutoff) return false;
64
+ return !r.relationships?.response?.data;
65
+ });
66
+ }
67
+
68
+ export { reviews, unansweredOlderThan };
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { dlx } from './chunk-PYXH4J77.js';
3
+ import { run, spawn } from './chunk-QFP5R25M.js';
4
+
5
+ // src/lib/eas-cli.ts
6
+ function compact(argv) {
7
+ const out = [];
8
+ for (const item of argv) {
9
+ if (item === void 0 || item === null || item === false) continue;
10
+ if (item === true) continue;
11
+ out.push(String(item));
12
+ }
13
+ return out;
14
+ }
15
+ async function easJson(argv) {
16
+ const flat = compact(argv);
17
+ if (!flat.includes("--json")) flat.push("--json");
18
+ if (!flat.includes("--non-interactive")) flat.push("--non-interactive");
19
+ const { code, stdout, stderr } = await run([dlx(), "eas", ...flat]);
20
+ if (code !== 0) {
21
+ const tail = (stderr || stdout).trim().split("\n").pop()?.trim() ?? `exit ${code}`;
22
+ throw new Error(`eas ${flat[0]} failed: ${tail}`);
23
+ }
24
+ try {
25
+ return JSON.parse(stdout);
26
+ } catch (err) {
27
+ throw new Error(
28
+ `eas ${flat[0]} returned non-JSON output: ${err instanceof Error ? err.message : err}`,
29
+ { cause: err }
30
+ );
31
+ }
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
+
45
+ // src/lib/eas-integrations.ts
46
+ async function ascStatus() {
47
+ return easJson(["integrations:asc:status"]);
48
+ }
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
+
61
+ export { ascConnect, ascStatus };
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { access } from 'fs/promises';
3
+
4
+ async function fileExists(p) {
5
+ try {
6
+ await access(p);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function detectPackageManager() {
13
+ if (await fileExists("bun.lock")) return "bun";
14
+ if (await fileExists("bun.lockb")) return "bun";
15
+ if (await fileExists("pnpm-lock.yaml")) return "pnpm";
16
+ if (await fileExists("yarn.lock")) return "yarn";
17
+ return "npm";
18
+ }
19
+ function dlx() {
20
+ return process.versions.bun ? "bunx" : "npx";
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
+ function installCmdFor(pm) {
36
+ switch (pm) {
37
+ case "bun":
38
+ return "bun install";
39
+ case "pnpm":
40
+ return "pnpm install";
41
+ case "yarn":
42
+ return "yarn install";
43
+ case "npm":
44
+ default:
45
+ return "npm install";
46
+ }
47
+ }
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
+ function currentRuntime() {
62
+ return process.versions.bun ? "bun" : "node";
63
+ }
64
+ function currentRuntimeVersion() {
65
+ return process.versions.bun ?? process.versions.node ?? "?";
66
+ }
67
+
68
+ export { currentRuntime, currentRuntimeVersion, detectPackageManager, dlx, dlxFor, installCmdFor, runCmdFor };
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { spawn as spawn$1, spawnSync as spawnSync$1 } from 'child_process';
3
+
4
+ function spawn(argv, opts = {}) {
5
+ const stdio = opts.stdio ?? [
6
+ opts.stdin ?? "inherit",
7
+ opts.stdout ?? "inherit",
8
+ opts.stderr ?? "inherit"
9
+ ];
10
+ const proc = spawn$1(argv[0], argv.slice(1), {
11
+ stdio,
12
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
13
+ cwd: opts.cwd
14
+ });
15
+ const exited = new Promise((resolve) => {
16
+ proc.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
17
+ proc.once("error", () => resolve(1));
18
+ });
19
+ return {
20
+ exited,
21
+ stdout: proc.stdout,
22
+ stderr: proc.stderr,
23
+ stdin: proc.stdin,
24
+ pid: proc.pid ?? -1,
25
+ kill: (signal) => proc.kill(signal)
26
+ };
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
+ async function streamText(stream) {
47
+ if (!stream) return "";
48
+ const chunks = [];
49
+ for await (const chunk of stream) {
50
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
51
+ }
52
+ return Buffer.concat(chunks).toString("utf8");
53
+ }
54
+ async function run(argv, opts = {}) {
55
+ const proc = spawn(argv, {
56
+ stdin: opts.stdin ?? "ignore",
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ env: opts.env,
60
+ cwd: opts.cwd
61
+ });
62
+ const [code, stdout, stderr] = await Promise.all([
63
+ proc.exited,
64
+ streamText(proc.stdout),
65
+ streamText(proc.stderr)
66
+ ]);
67
+ return { code, stdout, stderr };
68
+ }
69
+
70
+ export { run, spawn, spawnSync, streamText };