@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 +28 -34
- package/dist/chunk-3TT4CDAJ.js +70 -0
- package/dist/{chunk-A43VGOE3.js → chunk-VOL7YISA.js} +2 -24
- package/dist/cli.js +1966 -1394
- package/dist/{eas-integrations-TIYBWWKC.js → eas-integrations-2QVR45NE.js} +1 -1
- package/dist/env-local-TW3T2PZ6.js +2 -0
- package/dist/index.js +2 -2
- package/package.json +8 -6
- package/dist/asc-reviews-OPKN34SB.js +0 -2
- package/dist/chunk-5JSZTHAP.js +0 -68
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ramonclaudio/vexpo)
|
|
4
4
|
[](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
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
60
|
-
vexpo
|
|
61
|
-
vexpo
|
|
62
|
-
vexpo
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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 {
|
|
39
|
+
export { ascStatus };
|