@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 +116 -0
- package/dist/asc-reviews-OPKN34SB.js +2 -0
- package/dist/chunk-5JSZTHAP.js +68 -0
- package/dist/chunk-A43VGOE3.js +61 -0
- package/dist/chunk-PYXH4J77.js +68 -0
- package/dist/chunk-QFP5R25M.js +70 -0
- package/dist/cli.js +6017 -0
- package/dist/eas-integrations-TIYBWWKC.js +4 -0
- package/dist/index.js +7 -0
- package/dist/pkg-manager-PU7UPAID.js +2 -0
- package/dist/proc-TEOPRZZ2.js +2 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# vexpo
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@ramonclaudio/vexpo)
|
|
4
|
+
[](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,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 };
|