@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/dist/cli.js
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { ensureLine, readOne, readAll, removeLines, ENV_FILE } from './chunk-3TT4CDAJ.js';
|
|
3
|
+
import { ascStatus } from './chunk-VOL7YISA.js';
|
|
3
4
|
import { dlx, detectPackageManager, installCmdFor } from './chunk-PYXH4J77.js';
|
|
4
|
-
import {
|
|
5
|
-
import { unansweredOlderThan, reviews } from './chunk-5JSZTHAP.js';
|
|
5
|
+
import { run, spawn } from './chunk-QFP5R25M.js';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import { readFile,
|
|
7
|
+
import { readFile, access, writeFile, unlink, stat, mkdir, rename } from 'fs/promises';
|
|
8
8
|
import { createInterface } from 'readline/promises';
|
|
9
|
-
import { existsSync } from 'fs';
|
|
10
9
|
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
12
|
import { createSign } from 'crypto';
|
|
12
13
|
|
|
13
14
|
// package.json
|
|
14
15
|
var package_default = {
|
|
15
|
-
version: "0.1.
|
|
16
|
+
version: "0.1.1"};
|
|
16
17
|
function deploymentName(value) {
|
|
17
18
|
if (!value) return void 0;
|
|
18
19
|
const m = /^(?:dev|prod|preview):(.+)$/.exec(value);
|
|
19
20
|
return m ? m[1] : value;
|
|
20
21
|
}
|
|
21
22
|
function targetArgs(target) {
|
|
22
|
-
if (target?.prod)
|
|
23
|
+
if (target?.prod) {
|
|
24
|
+
return target.envFile ? ["--env-file", target.envFile] : ["--prod"];
|
|
25
|
+
}
|
|
23
26
|
const explicit = target?.deployment ?? deploymentName(process.env.CONVEX_DEPLOYMENT);
|
|
24
27
|
return explicit ? ["--deployment", explicit] : [];
|
|
25
28
|
}
|
|
29
|
+
function unquoteEnvValue(value) {
|
|
30
|
+
const q = value[0];
|
|
31
|
+
if ((q === '"' || q === "'") && value.length >= 2 && value[value.length - 1] === q) {
|
|
32
|
+
const inner = value.slice(1, -1);
|
|
33
|
+
return q === '"' ? inner.replace(/\\n/g, "\n") : inner;
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
26
37
|
async function envMap(target) {
|
|
27
38
|
const argv = [dlx(), "convex", "env", "list", ...targetArgs(target)];
|
|
28
39
|
const { code, stdout } = await run(argv);
|
|
@@ -32,7 +43,7 @@ async function envMap(target) {
|
|
|
32
43
|
const trimmed = raw.trim();
|
|
33
44
|
if (!trimmed) continue;
|
|
34
45
|
const eq = trimmed.indexOf("=");
|
|
35
|
-
if (eq > 0) out.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
|
|
46
|
+
if (eq > 0) out.set(trimmed.slice(0, eq), unquoteEnvValue(trimmed.slice(eq + 1)));
|
|
36
47
|
}
|
|
37
48
|
return out;
|
|
38
49
|
}
|
|
@@ -75,6 +86,9 @@ async function isLoggedIn() {
|
|
|
75
86
|
return false;
|
|
76
87
|
}
|
|
77
88
|
}
|
|
89
|
+
function deploymentSlug(value) {
|
|
90
|
+
return deploymentName(value);
|
|
91
|
+
}
|
|
78
92
|
async function checkCli() {
|
|
79
93
|
const { code, stdout } = await run([dlx(), "eas", "--version"]);
|
|
80
94
|
if (code !== 0) return { ok: false };
|
|
@@ -87,19 +101,26 @@ async function whoami() {
|
|
|
87
101
|
const text = stdout.trim();
|
|
88
102
|
return text ? text.split("\n")[0].trim() : null;
|
|
89
103
|
}
|
|
90
|
-
async function
|
|
104
|
+
async function resolveProjectId() {
|
|
91
105
|
try {
|
|
92
|
-
|
|
93
|
-
await access("app.json");
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
106
|
+
await access("app.json");
|
|
97
107
|
const json = JSON.parse(await readFile("app.json", "utf8"));
|
|
98
108
|
const value = json.expo?.extra?.eas?.projectId;
|
|
99
|
-
|
|
109
|
+
if (value && value.length > 0) return value;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
const fromProcess = process.env.EAS_PROJECT_ID;
|
|
113
|
+
if (fromProcess && fromProcess.length > 0) return fromProcess;
|
|
114
|
+
try {
|
|
115
|
+
const { readOne: readOne2 } = await import('./env-local-TW3T2PZ6.js');
|
|
116
|
+
const fromFile = await readOne2("EAS_PROJECT_ID");
|
|
117
|
+
if (fromFile && fromFile.length > 0) {
|
|
118
|
+
process.env.EAS_PROJECT_ID = fromFile;
|
|
119
|
+
return fromFile;
|
|
120
|
+
}
|
|
100
121
|
} catch {
|
|
101
|
-
return null;
|
|
102
122
|
}
|
|
123
|
+
return null;
|
|
103
124
|
}
|
|
104
125
|
async function envList(environment = "production") {
|
|
105
126
|
const { code, stdout } = await run([
|
|
@@ -155,7 +176,7 @@ async function envUpdate(name, value, visibility, environments = ["production",
|
|
|
155
176
|
visibility
|
|
156
177
|
];
|
|
157
178
|
if (opts?.type) argv.push("--type", opts.type);
|
|
158
|
-
for (const env2 of environments) argv.push("--environment", env2);
|
|
179
|
+
for (const env2 of environments) argv.push("--variable-environment", env2);
|
|
159
180
|
argv.push("--non-interactive");
|
|
160
181
|
const { code, stderr } = await run(argv);
|
|
161
182
|
if (code !== 0) {
|
|
@@ -164,18 +185,18 @@ async function envUpdate(name, value, visibility, environments = ["production",
|
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
async function envPush(opts) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
for (const env2 of opts.environments) {
|
|
189
|
+
const argv = [dlx(), "eas", "env:push", "--environment", env2, "--path", opts.path];
|
|
190
|
+
if (opts.force) argv.push("--force");
|
|
191
|
+
const { code, stderr } = await run(argv);
|
|
192
|
+
if (code !== 0) {
|
|
193
|
+
const tail = stderr.trim().split("\n").pop()?.trim() ?? `exit ${code}`;
|
|
194
|
+
throw new Error(`eas env:push (${env2}) failed: ${tail}`);
|
|
195
|
+
}
|
|
175
196
|
}
|
|
176
197
|
}
|
|
177
198
|
async function init() {
|
|
178
|
-
const existing = await
|
|
199
|
+
const existing = await resolveProjectId();
|
|
179
200
|
const argv = existing ? [dlx(), "eas", "init", "--non-interactive", "--force", "--id", existing] : [dlx(), "eas", "init", "--non-interactive", "--force"];
|
|
180
201
|
const proc = spawn(argv, {
|
|
181
202
|
stdin: "inherit",
|
|
@@ -183,7 +204,7 @@ async function init() {
|
|
|
183
204
|
stderr: "inherit"
|
|
184
205
|
});
|
|
185
206
|
if (await proc.exited !== 0) return { ok: false };
|
|
186
|
-
const id = await
|
|
207
|
+
const id = await resolveProjectId();
|
|
187
208
|
return { ok: !!id, projectId: id ?? void 0 };
|
|
188
209
|
}
|
|
189
210
|
async function diagnostics() {
|
|
@@ -323,13 +344,20 @@ async function call(method, path, key, body) {
|
|
|
323
344
|
throw new Error(`Resend ${method} ${path} \u2192 429 after retries`);
|
|
324
345
|
}
|
|
325
346
|
async function probeAccess(key) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
347
|
+
const ctl = new AbortController();
|
|
348
|
+
const timer = setTimeout(() => ctl.abort(), REQUEST_TIMEOUT_MS);
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch(`${BASE}/api-keys`, {
|
|
351
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
352
|
+
signal: ctl.signal
|
|
353
|
+
});
|
|
354
|
+
if (res.ok) return "full";
|
|
355
|
+
const text = await res.text();
|
|
356
|
+
if (text.includes("restricted_api_key")) return "sending";
|
|
357
|
+
return "invalid";
|
|
358
|
+
} finally {
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
}
|
|
333
361
|
}
|
|
334
362
|
async function listDomains(key) {
|
|
335
363
|
return (await call("GET", "/domains", key)).data;
|
|
@@ -385,7 +413,7 @@ async function provisionWebhook(fullKey, endpoint, events = RESEND_TRANSACTIONAL
|
|
|
385
413
|
await deleteWebhook(fullKey, stale.id);
|
|
386
414
|
}
|
|
387
415
|
const created = await createWebhook(fullKey, { endpoint, events: [...events] });
|
|
388
|
-
return created.signing_secret;
|
|
416
|
+
return { id: created.id, secret: created.signing_secret };
|
|
389
417
|
}
|
|
390
418
|
var RESET = "\x1B[0m";
|
|
391
419
|
var BOLD = "\x1B[1m";
|
|
@@ -567,10 +595,11 @@ function isStepFresh(state, name, ttlHours) {
|
|
|
567
595
|
function checkConcurrentRun(state) {
|
|
568
596
|
const updated = new Date(state.updatedAt).getTime();
|
|
569
597
|
const ageMs = Date.now() - updated;
|
|
570
|
-
|
|
571
|
-
|
|
598
|
+
const hasOtherPid = typeof state.lastPid === "number" && Number.isFinite(state.lastPid) && state.lastPid !== 0 && state.lastPid !== process.pid;
|
|
599
|
+
if (ageMs < PID_WARN_WINDOW_MS && hasOtherPid) {
|
|
600
|
+
return { active: true, otherPid: state.lastPid };
|
|
572
601
|
}
|
|
573
|
-
return {
|
|
602
|
+
return { active: false };
|
|
574
603
|
}
|
|
575
604
|
function fingerprint(value) {
|
|
576
605
|
let h = 0;
|
|
@@ -620,13 +649,13 @@ async function statusResend() {
|
|
|
620
649
|
detail: "no env var"
|
|
621
650
|
};
|
|
622
651
|
}
|
|
623
|
-
const
|
|
624
|
-
if (
|
|
652
|
+
const access17 = await probeAccess(k);
|
|
653
|
+
if (access17 === "full") return { name: "Resend", what: "full-access API key", status: "ok" };
|
|
625
654
|
return {
|
|
626
655
|
name: "Resend",
|
|
627
656
|
what: "full-access API key in RESEND_FULL_ACCESS_KEY env",
|
|
628
657
|
status: "missing",
|
|
629
|
-
detail:
|
|
658
|
+
detail: access17 === "sending" ? "key has only sending access" : "key invalid"
|
|
630
659
|
};
|
|
631
660
|
}
|
|
632
661
|
var ROW_APPLE = {
|
|
@@ -681,7 +710,7 @@ async function walkDomain() {
|
|
|
681
710
|
lines: [
|
|
682
711
|
`${BOLD}what:${RESET} a domain you control DNS for`,
|
|
683
712
|
`${BOLD}where:${RESET} any registrar (Cloudflare, GoDaddy, Route 53, Namecheap, Vercel, \u2026)`,
|
|
684
|
-
`${BOLD}notes:${RESET} after \`
|
|
713
|
+
`${BOLD}notes:${RESET} after \`npx vexpo resend\`, you'll add SPF/DKIM/DMARC records at your registrar`,
|
|
685
714
|
" Resend's dashboard shows them and verifies. vexpo doesn't automate this.",
|
|
686
715
|
"vexpo doesn't register domains for you."
|
|
687
716
|
],
|
|
@@ -706,23 +735,26 @@ async function walkConvex() {
|
|
|
706
735
|
lines: [
|
|
707
736
|
`${BOLD}what:${RESET} logged-in Convex CLI session`,
|
|
708
737
|
`${BOLD}where:${RESET} free tier at dashboard.convex.dev (instant signup)`,
|
|
709
|
-
`${BOLD}how:${RESET} \`
|
|
738
|
+
`${BOLD}how:${RESET} \`npx convex login\` (browser-based OAuth)`
|
|
710
739
|
],
|
|
711
740
|
urls: [{ label: "dashboard", url: "https://dashboard.convex.dev" }]
|
|
712
741
|
});
|
|
713
742
|
if (!process.stdin.isTTY) {
|
|
714
|
-
bad("non-TTY: run `
|
|
743
|
+
bad("non-TTY: run `npx convex login` then re-run");
|
|
715
744
|
return;
|
|
716
745
|
}
|
|
717
746
|
if (await askYesNo(`Run \`${dlx()} convex login\` now?`, false)) {
|
|
718
747
|
const proc = spawn([dlx(), "convex", "login"], {
|
|
719
748
|
stdio: ["inherit", "inherit", "inherit"]
|
|
720
749
|
});
|
|
721
|
-
if (await proc.exited !== 0)
|
|
750
|
+
if (await proc.exited !== 0) {
|
|
751
|
+
yep("convex login did not complete; run `npx convex login` later");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
722
754
|
if ((await statusConvex()).status === "ok") ok("Convex authenticated");
|
|
723
|
-
else
|
|
755
|
+
else yep("still not signed in; run `npx convex login` later");
|
|
724
756
|
} else {
|
|
725
|
-
nop("`
|
|
757
|
+
nop("`npx convex login` will prompt automatically when `npx vexpo convex` runs");
|
|
726
758
|
}
|
|
727
759
|
}
|
|
728
760
|
async function walkExpo() {
|
|
@@ -736,7 +768,7 @@ async function walkExpo() {
|
|
|
736
768
|
lines: [
|
|
737
769
|
`${BOLD}what:${RESET} logged-in EAS CLI session`,
|
|
738
770
|
`${BOLD}where:${RESET} free tier at expo.dev/signup (instant signup)`,
|
|
739
|
-
`${BOLD}how:${RESET} \`
|
|
771
|
+
`${BOLD}how:${RESET} \`npx eas login\` (browser-based OAuth)`
|
|
740
772
|
],
|
|
741
773
|
urls: [
|
|
742
774
|
{ label: "signup", url: "https://expo.dev/signup" },
|
|
@@ -744,17 +776,20 @@ async function walkExpo() {
|
|
|
744
776
|
]
|
|
745
777
|
});
|
|
746
778
|
if (!process.stdin.isTTY) {
|
|
747
|
-
bad("non-TTY: run `
|
|
779
|
+
bad("non-TTY: run `npx eas login` then re-run");
|
|
748
780
|
return;
|
|
749
781
|
}
|
|
750
782
|
if (await askYesNo(`Run \`${dlx()} eas login\` now?`, false)) {
|
|
751
783
|
const proc = spawn([dlx(), "eas", "login"], { stdio: ["inherit", "inherit", "inherit"] });
|
|
752
|
-
if (await proc.exited !== 0)
|
|
784
|
+
if (await proc.exited !== 0) {
|
|
785
|
+
yep("eas login did not complete; run `npx eas login` later");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
753
788
|
const after = await statusExpo();
|
|
754
789
|
if (after.status === "ok") ok(`signed in as ${after.detail}`);
|
|
755
|
-
else
|
|
790
|
+
else yep("still not signed in; run `npx eas login` later");
|
|
756
791
|
} else {
|
|
757
|
-
nop("`
|
|
792
|
+
nop("`npx eas login` will prompt automatically when the EAS phase of `npx vexpo full` runs");
|
|
758
793
|
}
|
|
759
794
|
}
|
|
760
795
|
async function walkResend() {
|
|
@@ -770,14 +805,14 @@ async function walkResend() {
|
|
|
770
805
|
`${BOLD}where:${RESET} free tier at resend.com (instant signup)`,
|
|
771
806
|
`${BOLD}how:${RESET} Create API Key \u2192 Permission: ${BOLD}Full Access${RESET} \u2192 copy \u2192 export`,
|
|
772
807
|
`${BOLD}notes:${RESET} used once to provision a scoped sending key, then discarded.`,
|
|
773
|
-
" `
|
|
808
|
+
" `npx vexpo resend` will prompt for it interactively if env isn't set."
|
|
774
809
|
],
|
|
775
810
|
urls: [
|
|
776
811
|
{ label: "signup", url: "https://resend.com/signup" },
|
|
777
812
|
{ label: "API keys", url: "https://resend.com/api-keys" }
|
|
778
813
|
]
|
|
779
814
|
});
|
|
780
|
-
nop("`
|
|
815
|
+
nop("`npx vexpo resend` handles the key prompt. Nothing to do here");
|
|
781
816
|
}
|
|
782
817
|
async function runAccounts(options2) {
|
|
783
818
|
try {
|
|
@@ -823,7 +858,7 @@ async function runAccounts(options2) {
|
|
|
823
858
|
` where: ${DIM}https://developer.apple.com/account/resources/authkeys/list${RESET}`
|
|
824
859
|
);
|
|
825
860
|
note(
|
|
826
|
-
`${BOLD}DNS records${RESET} added at your registrar after \`
|
|
861
|
+
`${BOLD}DNS records${RESET} added at your registrar after \`npx vexpo resend\``
|
|
827
862
|
);
|
|
828
863
|
note(` Resend dashboard shows them + verifies them`);
|
|
829
864
|
await recordStep("accounts", {
|
|
@@ -841,184 +876,658 @@ async function runAccounts(options2) {
|
|
|
841
876
|
return 1;
|
|
842
877
|
}
|
|
843
878
|
}
|
|
844
|
-
function
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
879
|
+
async function readJsonOrNull(path) {
|
|
880
|
+
try {
|
|
881
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
848
885
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const issuerId = out.issuerId;
|
|
857
|
-
const keyId = out.keyId;
|
|
858
|
-
const rawPath = out.p8Path;
|
|
859
|
-
if (!issuerId || !keyId || !rawPath) return null;
|
|
860
|
-
const p8Path = expandTilde(rawPath);
|
|
861
|
-
if (!existsSync(p8Path)) return null;
|
|
862
|
-
return { issuerId, keyId, p8Path };
|
|
886
|
+
async function readTextOrNull(path) {
|
|
887
|
+
try {
|
|
888
|
+
await access(path);
|
|
889
|
+
return await readFile(path, "utf8");
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
863
893
|
}
|
|
864
|
-
async function
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
894
|
+
async function pkgName() {
|
|
895
|
+
const pkg = await readJsonOrNull("package.json");
|
|
896
|
+
return typeof pkg?.name === "string" && pkg.name ? pkg.name : "app";
|
|
897
|
+
}
|
|
898
|
+
async function appName() {
|
|
899
|
+
const text = await readTextOrNull("app.config.ts");
|
|
900
|
+
if (text) {
|
|
901
|
+
const ternary = /name:\s*IS_DEV\s*\?\s*"[^"]+"\s*:\s*"([^"]+)"/.exec(text)?.[1];
|
|
902
|
+
if (ternary) return ternary;
|
|
903
|
+
const quoted = /\bname:\s*["']([^"']+)["']/.exec(text)?.[1];
|
|
904
|
+
if (quoted) return quoted;
|
|
871
905
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
906
|
+
const name = await pkgName();
|
|
907
|
+
const clean = name.replace(/^@[^/]+\//, "");
|
|
908
|
+
const parts = clean.split(/[-_]/).filter(Boolean);
|
|
909
|
+
if (parts.length === 0) return "App";
|
|
910
|
+
return parts.map((w) => (w[0] ?? "").toUpperCase() + w.slice(1)).join(" ");
|
|
911
|
+
}
|
|
912
|
+
async function scheme() {
|
|
913
|
+
const text = await readTextOrNull("app.config.ts");
|
|
914
|
+
if (!text) return "app";
|
|
915
|
+
return /scheme:\s*["']([^"']+)["']/.exec(text)?.[1] ?? "app";
|
|
916
|
+
}
|
|
917
|
+
async function bundleIdFallback() {
|
|
918
|
+
const text = await readTextOrNull("app.config.ts");
|
|
919
|
+
if (!text) return null;
|
|
920
|
+
return /EXPO_PUBLIC_APP_BUNDLE_ID\s*\?\?\s*"([^"]+)"/.exec(text)?.[1] ?? null;
|
|
921
|
+
}
|
|
922
|
+
async function appleTeamIdFallback() {
|
|
923
|
+
const text = await readTextOrNull("app.config.ts");
|
|
924
|
+
if (!text) return null;
|
|
925
|
+
const value = /EXPO_PUBLIC_APPLE_TEAM_ID\s*\?\?\s*"([^"]+)"/.exec(text)?.[1] ?? null;
|
|
926
|
+
if (!value || value === "ABCDE12345") return null;
|
|
927
|
+
return value;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/commands/better-auth.ts
|
|
931
|
+
function base64Secret() {
|
|
932
|
+
const buf = new Uint8Array(32);
|
|
933
|
+
crypto.getRandomValues(buf);
|
|
934
|
+
return btoa(String.fromCharCode(...buf));
|
|
935
|
+
}
|
|
936
|
+
async function runBetterAuth(options2) {
|
|
937
|
+
section("Better Auth env");
|
|
938
|
+
try {
|
|
939
|
+
const env2 = await envMap();
|
|
940
|
+
const siteUrl = options2.siteUrl ?? `${await scheme()}://`;
|
|
941
|
+
if (env2.has("SITE_URL") && env2.get("SITE_URL") === siteUrl) {
|
|
942
|
+
nop(`SITE_URL already set to ${siteUrl}`);
|
|
943
|
+
} else {
|
|
944
|
+
await envSet("SITE_URL", siteUrl);
|
|
945
|
+
ok(`set SITE_URL=${siteUrl}`);
|
|
891
946
|
}
|
|
892
|
-
|
|
893
|
-
|
|
947
|
+
if (env2.has("BETTER_AUTH_SECRET") && !options2.rotateSecret) {
|
|
948
|
+
nop("BETTER_AUTH_SECRET already set (use --rotate-secret to regenerate)");
|
|
949
|
+
} else {
|
|
950
|
+
await envSet("BETTER_AUTH_SECRET", base64Secret());
|
|
951
|
+
ok(
|
|
952
|
+
options2.rotateSecret === true ? "rotated BETTER_AUTH_SECRET (sessions invalidated)" : "generated BETTER_AUTH_SECRET"
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const desiredAppName = options2.appName ?? await appName();
|
|
956
|
+
if (env2.has("APP_NAME") && env2.get("APP_NAME") === desiredAppName) {
|
|
957
|
+
nop(`APP_NAME already set to ${desiredAppName}`);
|
|
958
|
+
} else {
|
|
959
|
+
await envSet("APP_NAME", desiredAppName);
|
|
960
|
+
ok(`set APP_NAME=${desiredAppName}`);
|
|
961
|
+
}
|
|
962
|
+
await recordStep("better-auth", {
|
|
963
|
+
siteUrl,
|
|
964
|
+
appName: desiredAppName,
|
|
965
|
+
rotated: options2.rotateSecret === true
|
|
966
|
+
});
|
|
894
967
|
return 0;
|
|
968
|
+
} catch (err) {
|
|
969
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
970
|
+
return 1;
|
|
895
971
|
}
|
|
896
|
-
const env2 = {
|
|
897
|
-
...process.env,
|
|
898
|
-
EXPO_ASC_API_KEY_PATH: asc.p8Path,
|
|
899
|
-
EXPO_ASC_KEY_ID: asc.keyId,
|
|
900
|
-
EXPO_ASC_ISSUER_ID: asc.issuerId
|
|
901
|
-
};
|
|
902
|
-
const proc = spawn([dlx(), "eas", "credentials:configure-build", "-p", "ios", "-e", profile], {
|
|
903
|
-
stdin: "inherit",
|
|
904
|
-
stdout: "inherit",
|
|
905
|
-
stderr: "inherit",
|
|
906
|
-
env: env2
|
|
907
|
-
});
|
|
908
|
-
const code = await proc.exited;
|
|
909
|
-
if (code !== 0) {
|
|
910
|
-
bad(`eas credentials exited with code ${code}`);
|
|
911
|
-
return code;
|
|
912
|
-
}
|
|
913
|
-
await recordStep("apple-credentials", {
|
|
914
|
-
profile,
|
|
915
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
916
|
-
ascIssuerId: asc.issuerId,
|
|
917
|
-
ascKeyId: asc.keyId
|
|
918
|
-
});
|
|
919
|
-
line();
|
|
920
|
-
ok("EAS credentials configured");
|
|
921
|
-
yep(`next: ${BOLD}bun run eas:dev:device${RESET} to build the dev client on a registered device`);
|
|
922
|
-
return 0;
|
|
923
972
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const path = expandTilde(source.path);
|
|
973
|
+
var BASE2 = `${process.env.CONVEX_PROVISION_HOST || "https://api.convex.dev"}/v1`;
|
|
974
|
+
async function accessToken() {
|
|
927
975
|
try {
|
|
928
|
-
await
|
|
976
|
+
const raw = await readFile(join(homedir(), ".convex", "config.json"), "utf8");
|
|
977
|
+
const token = JSON.parse(raw).accessToken;
|
|
978
|
+
return token && token.length > 0 ? token : null;
|
|
929
979
|
} catch {
|
|
930
|
-
|
|
980
|
+
return null;
|
|
931
981
|
}
|
|
932
|
-
return readFile(path, "utf8");
|
|
933
982
|
}
|
|
934
|
-
async function
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
signer.update(signingInput);
|
|
951
|
-
signer.end();
|
|
952
|
-
const signature = signer.sign({ key: privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
953
|
-
return `${signingInput}.${signature}`;
|
|
983
|
+
async function checkToken() {
|
|
984
|
+
const token = await accessToken();
|
|
985
|
+
if (!token) return "no-token";
|
|
986
|
+
const ctl = new AbortController();
|
|
987
|
+
const timer = setTimeout(() => ctl.abort(), 8e3);
|
|
988
|
+
try {
|
|
989
|
+
const res = await fetch(`${BASE2}/list_personal_access_tokens`, {
|
|
990
|
+
headers: { Authorization: `Bearer ${token}`, "Convex-Client": "vexpo-cli" },
|
|
991
|
+
signal: ctl.signal
|
|
992
|
+
});
|
|
993
|
+
return res.status === 401 || res.status === 403 ? "unauthorized" : "valid";
|
|
994
|
+
} catch {
|
|
995
|
+
return "valid";
|
|
996
|
+
} finally {
|
|
997
|
+
clearTimeout(timer);
|
|
998
|
+
}
|
|
954
999
|
}
|
|
955
|
-
|
|
956
|
-
|
|
1000
|
+
async function get(token, path) {
|
|
1001
|
+
const ctl = new AbortController();
|
|
1002
|
+
const timer = setTimeout(() => ctl.abort(), 1e4);
|
|
957
1003
|
try {
|
|
958
|
-
await
|
|
959
|
-
|
|
1004
|
+
const res = await fetch(`${BASE2}${path}`, {
|
|
1005
|
+
headers: { Authorization: `Bearer ${token}`, "Convex-Client": "vexpo-cli" },
|
|
1006
|
+
signal: ctl.signal
|
|
1007
|
+
});
|
|
1008
|
+
if (!res.ok) return null;
|
|
1009
|
+
return await res.json();
|
|
960
1010
|
} catch {
|
|
961
|
-
return
|
|
1011
|
+
return null;
|
|
1012
|
+
} finally {
|
|
1013
|
+
clearTimeout(timer);
|
|
962
1014
|
}
|
|
963
1015
|
}
|
|
964
|
-
async function
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
let endIdx = rest.indexOf(quote);
|
|
982
|
-
while (endIdx === -1 && i + 1 < lines.length) {
|
|
983
|
-
i += 1;
|
|
984
|
-
rest += `
|
|
985
|
-
${lines[i]}`;
|
|
986
|
-
endIdx = rest.indexOf(quote);
|
|
987
|
-
}
|
|
988
|
-
value = endIdx === -1 ? rest : rest.slice(0, endIdx);
|
|
989
|
-
out.set(key, value);
|
|
990
|
-
continue;
|
|
1016
|
+
async function post(token, path, body) {
|
|
1017
|
+
const ctl = new AbortController();
|
|
1018
|
+
const timer = setTimeout(() => ctl.abort(), 15e3);
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await fetch(`${BASE2}${path}`, {
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
headers: {
|
|
1023
|
+
Authorization: `Bearer ${token}`,
|
|
1024
|
+
"Convex-Client": "vexpo-cli",
|
|
1025
|
+
"Content-Type": "application/json"
|
|
1026
|
+
},
|
|
1027
|
+
body: JSON.stringify(body),
|
|
1028
|
+
signal: ctl.signal
|
|
1029
|
+
});
|
|
1030
|
+
const text = await res.text();
|
|
1031
|
+
if (!res.ok) {
|
|
1032
|
+
throw new Error(`Convex Platform POST ${path} \u2192 ${res.status}: ${text.slice(0, 200)}`);
|
|
991
1033
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
out.set(key, value);
|
|
1034
|
+
return text ? JSON.parse(text) : void 0;
|
|
1035
|
+
} finally {
|
|
1036
|
+
clearTimeout(timer);
|
|
996
1037
|
}
|
|
997
|
-
return out;
|
|
998
1038
|
}
|
|
999
|
-
async function
|
|
1000
|
-
|
|
1039
|
+
async function requireToken() {
|
|
1040
|
+
const token = await accessToken();
|
|
1041
|
+
if (!token) throw new Error("not logged in to Convex (no ~/.convex/config.json accessToken)");
|
|
1042
|
+
return token;
|
|
1001
1043
|
}
|
|
1002
|
-
async function
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1044
|
+
async function mintDeployKey(deploymentName2, opts) {
|
|
1045
|
+
const token = await requireToken();
|
|
1046
|
+
const body = { name: opts?.name ?? "vexpo" };
|
|
1047
|
+
if (opts?.expiresAtMs) {
|
|
1048
|
+
if (opts.expiresAtMs < Date.now() + 30 * 6e4) {
|
|
1049
|
+
throw new Error("deploy key expiresAtMs must be at least 30 minutes in the future");
|
|
1050
|
+
}
|
|
1051
|
+
body.expiresAt = opts.expiresAtMs;
|
|
1052
|
+
}
|
|
1053
|
+
const res = await post(
|
|
1054
|
+
token,
|
|
1055
|
+
`/deployments/${deploymentName2}/create_deploy_key`,
|
|
1056
|
+
body
|
|
1057
|
+
);
|
|
1058
|
+
if (!res?.deployKey) throw new Error("create_deploy_key returned no deployKey");
|
|
1059
|
+
return res.deployKey;
|
|
1060
|
+
}
|
|
1061
|
+
async function resolveProdDeployment(anyDeploymentName) {
|
|
1062
|
+
const deployments = await listProjectDeployments(anyDeploymentName);
|
|
1063
|
+
if (!deployments) return null;
|
|
1064
|
+
const prods = deploymentsOfType(deployments, "prod");
|
|
1065
|
+
return (prods.find((d) => d.isDefault) ?? prods[0])?.name ?? null;
|
|
1066
|
+
}
|
|
1067
|
+
async function mintProdDeployKey(anyDeploymentName, name = "vexpo") {
|
|
1068
|
+
const deployment = await resolveProdDeployment(anyDeploymentName);
|
|
1069
|
+
if (!deployment) return null;
|
|
1070
|
+
return { key: await mintDeployKey(deployment, { name }), deployment };
|
|
1071
|
+
}
|
|
1072
|
+
async function listProjectDeployments(deploymentName2) {
|
|
1073
|
+
const token = await accessToken();
|
|
1074
|
+
if (!token) return null;
|
|
1075
|
+
const dep = await get(token, `/deployments/${deploymentName2}`);
|
|
1076
|
+
if (!dep?.projectId) return null;
|
|
1077
|
+
const list = await get(
|
|
1078
|
+
token,
|
|
1079
|
+
`/projects/${dep.projectId}/list_deployments`
|
|
1080
|
+
);
|
|
1081
|
+
return Array.isArray(list) ? list : null;
|
|
1082
|
+
}
|
|
1083
|
+
function deploymentsOfType(deployments, type) {
|
|
1084
|
+
return deployments.filter((d) => d.deploymentType === type);
|
|
1008
1085
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1086
|
+
function describeDeployment(d) {
|
|
1087
|
+
return d.reference ? `${d.name} (${d.reference})` : d.name;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/commands/convex.ts
|
|
1091
|
+
var BUNDLE_ID_RE = /^[A-Za-z0-9.-]+$/;
|
|
1092
|
+
var TEAM_ID_RE = /^[A-Z0-9]{10}$/;
|
|
1093
|
+
function planConvexDev(options2, needsProvisioning, projectName) {
|
|
1094
|
+
const devArgs = ["convex", "dev", "--once", "--tail-logs", "disable"];
|
|
1095
|
+
if (needsProvisioning) {
|
|
1096
|
+
devArgs.push("--configure", "new", "--project", projectName);
|
|
1097
|
+
devArgs.push("--dev-deployment", options2.local ? "local" : "cloud");
|
|
1098
|
+
}
|
|
1099
|
+
return { selectLocalFirst: !!options2.local && !needsProvisioning, devArgs };
|
|
1100
|
+
}
|
|
1101
|
+
async function runConvex(options2) {
|
|
1102
|
+
section("Convex deployment");
|
|
1103
|
+
try {
|
|
1104
|
+
const tokenStatus = await checkToken();
|
|
1105
|
+
if (tokenStatus !== "valid") {
|
|
1106
|
+
yep(
|
|
1107
|
+
tokenStatus === "no-token" ? "not signed in to Convex" : "Convex token expired or revoked"
|
|
1108
|
+
);
|
|
1109
|
+
await helpAndWait({
|
|
1110
|
+
body: "Sign in (or refresh) with `npx convex login` in another terminal:",
|
|
1111
|
+
urls: [
|
|
1112
|
+
{ label: "Convex sign-up", url: "https://convex.dev" },
|
|
1113
|
+
{ label: "Convex dashboard", url: "https://dashboard.convex.dev" }
|
|
1114
|
+
],
|
|
1115
|
+
allowSkip: true,
|
|
1116
|
+
skipLabel: "skip"
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
const localEnv = await readAll();
|
|
1120
|
+
const existing = localEnv.get("CONVEX_DEPLOYMENT");
|
|
1121
|
+
if (options2.fresh) {
|
|
1122
|
+
await removeLines([
|
|
1123
|
+
"CONVEX_DEPLOYMENT",
|
|
1124
|
+
"EXPO_PUBLIC_CONVEX_URL",
|
|
1125
|
+
"EXPO_PUBLIC_CONVEX_SITE_URL"
|
|
1126
|
+
]);
|
|
1127
|
+
}
|
|
1128
|
+
const needsProvisioning = options2.fresh === true || !existing;
|
|
1129
|
+
const projectName = options2.name ?? await pkgName();
|
|
1130
|
+
const plan = planConvexDev(options2, needsProvisioning, projectName);
|
|
1131
|
+
if (plan.selectLocalFirst) {
|
|
1132
|
+
const sel = spawn([dlx(), "convex", "deployment", "select", "local"], {
|
|
1133
|
+
stdin: "inherit",
|
|
1134
|
+
stdout: "inherit",
|
|
1135
|
+
stderr: "inherit"
|
|
1136
|
+
});
|
|
1137
|
+
if (await sel.exited !== 0) {
|
|
1138
|
+
bad("convex deployment select local failed");
|
|
1139
|
+
return 1;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const cmd = [dlx(), ...plan.devArgs];
|
|
1143
|
+
if (needsProvisioning) {
|
|
1144
|
+
ok(`provisioning Convex project '${projectName}'`);
|
|
1145
|
+
} else {
|
|
1146
|
+
ok(`connecting to existing deployment ${existing}`);
|
|
1147
|
+
}
|
|
1148
|
+
const proc = spawn(cmd, { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
1149
|
+
if (await proc.exited !== 0) {
|
|
1150
|
+
bad("convex dev exited with a non-zero code");
|
|
1151
|
+
return 1;
|
|
1152
|
+
}
|
|
1153
|
+
const refreshed = await readAll();
|
|
1154
|
+
const deployment = refreshed.get("CONVEX_DEPLOYMENT");
|
|
1155
|
+
if (!deployment) {
|
|
1156
|
+
bad("CONVEX_DEPLOYMENT missing after convex dev ran");
|
|
1157
|
+
return 1;
|
|
1158
|
+
}
|
|
1159
|
+
const slug2 = deployment.split("#")[0].trim().split(":")[1];
|
|
1160
|
+
if (!slug2) {
|
|
1161
|
+
bad(`invalid CONVEX_DEPLOYMENT: ${deployment}`);
|
|
1162
|
+
return 1;
|
|
1163
|
+
}
|
|
1164
|
+
process.env.CONVEX_DEPLOYMENT = deployment;
|
|
1165
|
+
if (refreshed.has("EXPO_PUBLIC_CONVEX_URL")) {
|
|
1166
|
+
nop("EXPO_PUBLIC_CONVEX_URL already set");
|
|
1167
|
+
} else {
|
|
1168
|
+
await ensureLine("EXPO_PUBLIC_CONVEX_URL", `https://${slug2}.convex.cloud`);
|
|
1169
|
+
ok("wrote EXPO_PUBLIC_CONVEX_URL");
|
|
1170
|
+
}
|
|
1171
|
+
if (refreshed.has("EXPO_PUBLIC_CONVEX_SITE_URL")) {
|
|
1172
|
+
nop("EXPO_PUBLIC_CONVEX_SITE_URL already set");
|
|
1173
|
+
} else {
|
|
1174
|
+
await ensureLine("EXPO_PUBLIC_CONVEX_SITE_URL", `https://${slug2}.convex.site`);
|
|
1175
|
+
ok("wrote EXPO_PUBLIC_CONVEX_SITE_URL");
|
|
1176
|
+
}
|
|
1177
|
+
if (refreshed.has("EXPO_PUBLIC_SITE_URL")) {
|
|
1178
|
+
nop("EXPO_PUBLIC_SITE_URL already set");
|
|
1179
|
+
} else {
|
|
1180
|
+
const s = `${await scheme()}://`;
|
|
1181
|
+
await ensureLine("EXPO_PUBLIC_SITE_URL", s);
|
|
1182
|
+
ok(`wrote EXPO_PUBLIC_SITE_URL=${s}`);
|
|
1183
|
+
}
|
|
1184
|
+
await ensureIdentity(refreshed);
|
|
1185
|
+
await recordStep("convex", {
|
|
1186
|
+
deployment,
|
|
1187
|
+
slug: slug2,
|
|
1188
|
+
...options2.local ? { local: true } : {}
|
|
1189
|
+
});
|
|
1190
|
+
line();
|
|
1191
|
+
ok(`Convex deployment ready: ${BOLD}${slug2}${RESET}`);
|
|
1192
|
+
note(`dashboard: https://dashboard.convex.dev/d/${slug2}`);
|
|
1193
|
+
return 0;
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
1196
|
+
return 1;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async function ensureIdentity(localEnv) {
|
|
1200
|
+
const haveBundle = localEnv.has("EXPO_PUBLIC_APP_BUNDLE_ID");
|
|
1201
|
+
const haveTeam = localEnv.has("EXPO_PUBLIC_APPLE_TEAM_ID");
|
|
1202
|
+
let bundleId = localEnv.get("EXPO_PUBLIC_APP_BUNDLE_ID");
|
|
1203
|
+
let teamId = localEnv.get("EXPO_PUBLIC_APPLE_TEAM_ID");
|
|
1204
|
+
if (!haveBundle) {
|
|
1205
|
+
if (!process.stdin.isTTY) {
|
|
1206
|
+
yep("EXPO_PUBLIC_APP_BUNDLE_ID not set (non-TTY); skipping prompt");
|
|
1207
|
+
yep("set it in .env.local before running `vexpo apple` or building");
|
|
1208
|
+
} else {
|
|
1209
|
+
const fromConfig = await bundleIdFallback();
|
|
1210
|
+
const isTemplate = !fromConfig || fromConfig.startsWith("com.example.");
|
|
1211
|
+
const suggested = isTemplate ? `com.example.${await pkgName()}` : fromConfig;
|
|
1212
|
+
const cachedHint = isTemplate ? "" : ` ${DIM}(from app.config.ts)${RESET}`;
|
|
1213
|
+
const raw = (await ask(
|
|
1214
|
+
` iOS bundle id ${DIM}(reverse-DNS, e.g. com.you.app)${RESET}${cachedHint}
|
|
1215
|
+
${DIM}> ${suggested} ${RESET}`
|
|
1216
|
+
)).trim();
|
|
1217
|
+
bundleId = raw || suggested;
|
|
1218
|
+
if (!BUNDLE_ID_RE.test(bundleId)) {
|
|
1219
|
+
throw new Error(`invalid bundle id '${bundleId}' (allowed: A-Z a-z 0-9 . -)`);
|
|
1220
|
+
}
|
|
1221
|
+
await ensureLine("EXPO_PUBLIC_APP_BUNDLE_ID", bundleId);
|
|
1222
|
+
ok(`wrote EXPO_PUBLIC_APP_BUNDLE_ID=${bundleId}`);
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
1225
|
+
nop(`EXPO_PUBLIC_APP_BUNDLE_ID already set (${bundleId})`);
|
|
1226
|
+
}
|
|
1227
|
+
if (!haveTeam) {
|
|
1228
|
+
if (!process.stdin.isTTY) {
|
|
1229
|
+
yep("EXPO_PUBLIC_APPLE_TEAM_ID not set (non-TTY); skipping prompt");
|
|
1230
|
+
} else {
|
|
1231
|
+
const fromConfig = await appleTeamIdFallback();
|
|
1232
|
+
const cachedHint = fromConfig ? ` ${DIM}[${fromConfig} from app.config.ts]${RESET}` : "";
|
|
1233
|
+
const raw = (await ask(
|
|
1234
|
+
` Apple Team id ${DIM}(10-char alphanumeric, find at developer.apple.com/account)${RESET}${cachedHint}
|
|
1235
|
+
${DIM}> ${RESET}`
|
|
1236
|
+
)).trim().toUpperCase();
|
|
1237
|
+
const value = raw || (fromConfig ?? "");
|
|
1238
|
+
if (!TEAM_ID_RE.test(value)) {
|
|
1239
|
+
throw new Error(`invalid Apple Team id '${value}' (must be 10 uppercase alphanumeric)`);
|
|
1240
|
+
}
|
|
1241
|
+
teamId = value;
|
|
1242
|
+
await ensureLine("EXPO_PUBLIC_APPLE_TEAM_ID", teamId);
|
|
1243
|
+
ok(`wrote EXPO_PUBLIC_APPLE_TEAM_ID=${teamId}`);
|
|
1244
|
+
}
|
|
1245
|
+
} else {
|
|
1246
|
+
nop(`EXPO_PUBLIC_APPLE_TEAM_ID already set (${teamId})`);
|
|
1247
|
+
}
|
|
1248
|
+
if (bundleId) {
|
|
1249
|
+
await envSet("APP_BUNDLE_ID", bundleId);
|
|
1250
|
+
ok(`Convex env: APP_BUNDLE_ID=${bundleId}`);
|
|
1251
|
+
}
|
|
1252
|
+
if (teamId) {
|
|
1253
|
+
await envSet("APPLE_TEAM_ID", teamId);
|
|
1254
|
+
ok(`Convex env: APPLE_TEAM_ID=${teamId}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/commands/adopt.ts
|
|
1259
|
+
function buildFinishRunbook(s) {
|
|
1260
|
+
const steps = [];
|
|
1261
|
+
if (!s.hasResend) {
|
|
1262
|
+
steps.push({ cmd: "vexpo resend", desc: "provision the dev sending key + webhook" });
|
|
1263
|
+
}
|
|
1264
|
+
if (!s.hasApple) {
|
|
1265
|
+
steps.push({ cmd: "vexpo apple jwt", desc: "sign Apple Sign In (or --copy-from <old>)" });
|
|
1266
|
+
steps.push({ cmd: "vexpo asc:connect", desc: "link EAS to App Store Connect for submit" });
|
|
1267
|
+
}
|
|
1268
|
+
if (!s.hasProd) {
|
|
1269
|
+
steps.push({ cmd: "npx convex deploy", desc: "provision + push to the prod deployment" });
|
|
1270
|
+
}
|
|
1271
|
+
steps.push({
|
|
1272
|
+
cmd: `vexpo convex:migrate --from ${s.devSlug} --prod`,
|
|
1273
|
+
desc: "mirror server-side env onto prod"
|
|
1274
|
+
});
|
|
1275
|
+
steps.push({ cmd: "vexpo env convex-key", desc: "sync the deploy key + selector to EAS" });
|
|
1276
|
+
if (!s.hasEasProdUrl) {
|
|
1277
|
+
steps.push({ cmd: "vexpo full", desc: "push prod/preview EAS env (or `vexpo eas`)" });
|
|
1278
|
+
}
|
|
1279
|
+
steps.push({ cmd: "vexpo doctor --channel prod", desc: "verify the whole chain" });
|
|
1280
|
+
return steps;
|
|
1281
|
+
}
|
|
1282
|
+
async function runAdopt(options2) {
|
|
1283
|
+
section("Adopt");
|
|
1284
|
+
const deploymentRef = await readOne("CONVEX_DEPLOYMENT");
|
|
1285
|
+
if (!deploymentRef) {
|
|
1286
|
+
bad("no CONVEX_DEPLOYMENT in .env.local. nothing to adopt");
|
|
1287
|
+
note("run `eas integrations:convex:connect` first, or `vexpo full` to provision from scratch");
|
|
1288
|
+
return 1;
|
|
1289
|
+
}
|
|
1290
|
+
const devSlug = deploymentSlug(deploymentRef);
|
|
1291
|
+
if (!devSlug) {
|
|
1292
|
+
bad(`could not parse a deployment slug from CONVEX_DEPLOYMENT=${deploymentRef}`);
|
|
1293
|
+
return 1;
|
|
1294
|
+
}
|
|
1295
|
+
ok(`adopting Convex deployment: ${BOLD}${devSlug}${RESET}`);
|
|
1296
|
+
const deployments = await listProjectDeployments(devSlug);
|
|
1297
|
+
let prodSlug;
|
|
1298
|
+
if (deployments) {
|
|
1299
|
+
line();
|
|
1300
|
+
note("project deployments:");
|
|
1301
|
+
for (const d of deployments) {
|
|
1302
|
+
const mine = d.name === devSlug ? ` ${DIM}\u2190 .env.local${RESET}` : "";
|
|
1303
|
+
note(` ${describeDeployment(d)} ${DIM}[${d.deploymentType}]${RESET}${mine}`);
|
|
1304
|
+
}
|
|
1305
|
+
const devs = deploymentsOfType(deployments, "dev");
|
|
1306
|
+
if (devs.length > 1) {
|
|
1307
|
+
yep(
|
|
1308
|
+
`${devs.length} dev deployments; pick one canonical and delete the rest in the dashboard`
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
prodSlug = deploymentsOfType(deployments, "prod")[0]?.name;
|
|
1312
|
+
} else {
|
|
1313
|
+
nop("deployment enumeration unavailable (offline or not logged in); continuing");
|
|
1314
|
+
}
|
|
1315
|
+
if (!options2.skipDevSteps) {
|
|
1316
|
+
line();
|
|
1317
|
+
const code = await runConvex({});
|
|
1318
|
+
if (code !== 0) return code;
|
|
1319
|
+
const devEnv2 = await envMap();
|
|
1320
|
+
if (!devEnv2.has("BETTER_AUTH_SECRET")) {
|
|
1321
|
+
const baCode = await runBetterAuth({});
|
|
1322
|
+
if (baCode !== 0) return baCode;
|
|
1323
|
+
} else {
|
|
1324
|
+
nop("BETTER_AUTH_SECRET already set on the dev deployment");
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
const devEnv = await envMap();
|
|
1328
|
+
const projectId = await resolveProjectId();
|
|
1329
|
+
const easProd = projectId ? await envList("production").catch(() => null) : null;
|
|
1330
|
+
const steps = buildFinishRunbook({
|
|
1331
|
+
devSlug,
|
|
1332
|
+
hasResend: devEnv.has("RESEND_API_KEY"),
|
|
1333
|
+
hasApple: devEnv.has("APPLE_CLIENT_SECRET"),
|
|
1334
|
+
hasProd: !!prodSlug,
|
|
1335
|
+
hasEasProdUrl: !!easProd?.has("EXPO_PUBLIC_CONVEX_URL")
|
|
1336
|
+
});
|
|
1337
|
+
line();
|
|
1338
|
+
section("Finish");
|
|
1339
|
+
note("adopted the dev deployment. remaining, in order:");
|
|
1340
|
+
const width = Math.max(...steps.map((s) => s.cmd.length));
|
|
1341
|
+
for (const s of steps) note(` ${BOLD}${s.cmd.padEnd(width)}${RESET} ${DIM}${s.desc}${RESET}`);
|
|
1342
|
+
line();
|
|
1343
|
+
nop("prod + Apple + Resend legs need credentials/prompts, so they're listed, not auto-run");
|
|
1344
|
+
return 0;
|
|
1345
|
+
}
|
|
1346
|
+
function expandTilde(p) {
|
|
1347
|
+
if (p === "~") return homedir();
|
|
1348
|
+
if (p.startsWith("~/")) return `${homedir()}${p.slice(1)}`;
|
|
1349
|
+
return p;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/commands/apple/credentials.ts
|
|
1353
|
+
async function resolveBundleId(profile) {
|
|
1354
|
+
const fromConfig = await bundleIdFallback();
|
|
1355
|
+
if (fromConfig && !fromConfig.startsWith("com.example.")) {
|
|
1356
|
+
return { source: "app.config.ts", value: fromConfig, templatePlaceholder: false };
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
const env2 = await envList(profile);
|
|
1360
|
+
const fromEnv = env2.get("EXPO_PUBLIC_APP_BUNDLE_ID");
|
|
1361
|
+
if (fromEnv && !fromEnv.startsWith("com.example.")) {
|
|
1362
|
+
return { source: "EAS env", value: fromEnv, templatePlaceholder: false };
|
|
1363
|
+
}
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
source: fromConfig ? "app.config.ts" : null,
|
|
1368
|
+
value: fromConfig,
|
|
1369
|
+
templatePlaceholder: true
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
async function loadAscFromState() {
|
|
1373
|
+
const state = await load();
|
|
1374
|
+
const rec = state.steps["asc-key"];
|
|
1375
|
+
if (!rec?.outputs) return null;
|
|
1376
|
+
const out = rec.outputs;
|
|
1377
|
+
const issuerId = out.issuerId;
|
|
1378
|
+
const keyId = out.keyId;
|
|
1379
|
+
const rawPath = out.p8Path;
|
|
1380
|
+
if (!issuerId || !keyId || !rawPath) return null;
|
|
1381
|
+
const p8Path = expandTilde(rawPath);
|
|
1382
|
+
if (!existsSync(p8Path)) return null;
|
|
1383
|
+
return { issuerId, keyId, p8Path };
|
|
1384
|
+
}
|
|
1385
|
+
async function runAppleCredentials(options2) {
|
|
1386
|
+
section("EAS iOS credentials");
|
|
1387
|
+
const profile = options2.profile ?? "production";
|
|
1388
|
+
const asc = await loadAscFromState();
|
|
1389
|
+
if (!asc) {
|
|
1390
|
+
yep("no cached ASC creds. Run `vexpo apple asc-key` first to validate one.");
|
|
1391
|
+
return 1;
|
|
1392
|
+
}
|
|
1393
|
+
ok(`cached ASC API key found in state.json`);
|
|
1394
|
+
note(` issuerId: ${BOLD}${asc.issuerId}${RESET}`);
|
|
1395
|
+
note(` keyId: ${BOLD}${asc.keyId}${RESET}`);
|
|
1396
|
+
note(` .p8: ${BOLD}${asc.p8Path}${RESET}`);
|
|
1397
|
+
const bundle = await resolveBundleId(profile);
|
|
1398
|
+
if (bundle.templatePlaceholder) {
|
|
1399
|
+
line();
|
|
1400
|
+
bad("template bundle id detected, refusing to register placeholder credentials");
|
|
1401
|
+
note(
|
|
1402
|
+
bundle.value ? ` app.config.ts still defaults to ${BOLD}${bundle.value}${RESET}` : ` could not resolve a bundle id from app.config.ts`
|
|
1403
|
+
);
|
|
1404
|
+
note(` EAS env (${profile}) does not set EXPO_PUBLIC_APP_BUNDLE_ID either`);
|
|
1405
|
+
line();
|
|
1406
|
+
note("fix by running the rebrand wizard, which bakes your bundle id into app.config.ts:");
|
|
1407
|
+
note(` ${BOLD}npx vexpo rebrand${RESET}`);
|
|
1408
|
+
note("alternatively, push your local env to EAS before running this step:");
|
|
1409
|
+
note(` ${BOLD}npx eas env:push --environment ${profile}${RESET}`);
|
|
1410
|
+
return 1;
|
|
1411
|
+
}
|
|
1412
|
+
ok(`bundle id: ${BOLD}${bundle.value}${RESET} (from ${bundle.source})`);
|
|
1413
|
+
line();
|
|
1414
|
+
note("eas-cli's credentials wizard is interactive (no non-interactive path).");
|
|
1415
|
+
note("It walks through:");
|
|
1416
|
+
note(` 1. ${BOLD}App Store Connect: Manage your API Key${RESET}, Set up a new key`);
|
|
1417
|
+
note(" paste the 3 values above when prompted");
|
|
1418
|
+
note(` 2. ${BOLD}Build Credentials${RESET}, generate dist cert + provisioning profile`);
|
|
1419
|
+
note(` 3. ${BOLD}Push Notifications${RESET}, generate APNs key`);
|
|
1420
|
+
note("");
|
|
1421
|
+
note("After this, every `eas build` + `eas submit` works without Apple Developer");
|
|
1422
|
+
note("login prompts. Existing creds are detected and reused.");
|
|
1423
|
+
line();
|
|
1424
|
+
if (process.stdin.isTTY) {
|
|
1425
|
+
if (!await askYesNo(`Run \`eas credentials -p ios -e ${profile}\` now?`, true)) {
|
|
1426
|
+
nop("skipped (run `npx eas credentials -p ios` later)");
|
|
1427
|
+
return 0;
|
|
1428
|
+
}
|
|
1429
|
+
} else {
|
|
1430
|
+
nop("non-TTY: skipping interactive wizard");
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
const env2 = {
|
|
1434
|
+
...process.env,
|
|
1435
|
+
EXPO_ASC_API_KEY_PATH: asc.p8Path,
|
|
1436
|
+
EXPO_ASC_KEY_ID: asc.keyId,
|
|
1437
|
+
EXPO_ASC_ISSUER_ID: asc.issuerId
|
|
1438
|
+
};
|
|
1439
|
+
const proc = spawn([dlx(), "eas", "credentials:configure-build", "-p", "ios", "-e", profile], {
|
|
1440
|
+
stdin: "inherit",
|
|
1441
|
+
stdout: "inherit",
|
|
1442
|
+
stderr: "inherit",
|
|
1443
|
+
env: env2
|
|
1444
|
+
});
|
|
1445
|
+
const code = await proc.exited;
|
|
1446
|
+
if (code !== 0) {
|
|
1447
|
+
bad(`eas credentials exited with code ${code}`);
|
|
1448
|
+
return code;
|
|
1449
|
+
}
|
|
1450
|
+
await recordStep("apple-credentials", {
|
|
1451
|
+
profile,
|
|
1452
|
+
configuredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1453
|
+
ascIssuerId: asc.issuerId,
|
|
1454
|
+
ascKeyId: asc.keyId
|
|
1455
|
+
});
|
|
1456
|
+
line();
|
|
1457
|
+
ok("EAS credentials configured");
|
|
1458
|
+
yep(`next: ${BOLD}npm run eas:dev:device${RESET} to build the dev client on a registered device`);
|
|
1459
|
+
return 0;
|
|
1460
|
+
}
|
|
1461
|
+
async function readPrivateKey(source) {
|
|
1462
|
+
if ("contents" in source) return source.contents;
|
|
1463
|
+
const path = expandTilde(source.path);
|
|
1464
|
+
try {
|
|
1465
|
+
await access(path);
|
|
1466
|
+
} catch {
|
|
1467
|
+
throw new Error(`p8 file not found at ${path}`);
|
|
1468
|
+
}
|
|
1469
|
+
return readFile(path, "utf8");
|
|
1470
|
+
}
|
|
1471
|
+
async function signClientSecret(opts) {
|
|
1472
|
+
const days = opts.expirationDays;
|
|
1473
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1474
|
+
const header = { alg: "ES256", kid: opts.keyId };
|
|
1475
|
+
const payload = {
|
|
1476
|
+
iss: opts.teamId,
|
|
1477
|
+
iat: now,
|
|
1478
|
+
exp: now + days * 86400,
|
|
1479
|
+
aud: "https://appleid.apple.com",
|
|
1480
|
+
sub: opts.servicesId
|
|
1481
|
+
};
|
|
1482
|
+
const privateKey = await readPrivateKey(opts.privateKey);
|
|
1483
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
1484
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1485
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
1486
|
+
const signer = createSign("SHA256");
|
|
1487
|
+
signer.update(signingInput);
|
|
1488
|
+
signer.end();
|
|
1489
|
+
const signature = signer.sign({ key: privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
1490
|
+
return `${signingInput}.${signature}`;
|
|
1019
1491
|
}
|
|
1020
1492
|
|
|
1021
1493
|
// src/commands/apple/jwt.ts
|
|
1494
|
+
var APPLE_ENV_KEYS = [
|
|
1495
|
+
"APPLE_CLIENT_ID",
|
|
1496
|
+
"APPLE_TEAM_ID",
|
|
1497
|
+
"APPLE_KEY_ID",
|
|
1498
|
+
"APPLE_CLIENT_SECRET"
|
|
1499
|
+
];
|
|
1500
|
+
async function copyAppleEnv(from) {
|
|
1501
|
+
section("Apple Sign In");
|
|
1502
|
+
const slug2 = deploymentSlug(from) ?? from;
|
|
1503
|
+
const src = await envMap({ deployment: slug2 });
|
|
1504
|
+
const present = APPLE_ENV_KEYS.filter((k) => src.has(k) && src.get(k));
|
|
1505
|
+
if (present.length === 0) {
|
|
1506
|
+
bad(`no APPLE_* vars on deployment ${slug2} (unreachable or not provisioned)`);
|
|
1507
|
+
note("pass a deployment slug your account can reach, e.g. `--copy-from old-deployment-123`");
|
|
1508
|
+
return 1;
|
|
1509
|
+
}
|
|
1510
|
+
const dst = await envMap();
|
|
1511
|
+
let copied = 0;
|
|
1512
|
+
for (const key of present) {
|
|
1513
|
+
const value = src.get(key);
|
|
1514
|
+
if (dst.get(key) === value) {
|
|
1515
|
+
nop(`${key} already matches`);
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
await envSet(key, value);
|
|
1519
|
+
ok(`copied ${key} from ${slug2}`);
|
|
1520
|
+
copied += 1;
|
|
1521
|
+
}
|
|
1522
|
+
line();
|
|
1523
|
+
ok(`Apple env copied from ${slug2} (${copied} changed)`);
|
|
1524
|
+
if (!present.includes("APPLE_CLIENT_SECRET")) {
|
|
1525
|
+
yep("source had no APPLE_CLIENT_SECRET; re-sign with `vexpo apple jwt`");
|
|
1526
|
+
} else {
|
|
1527
|
+
note("the copied client_secret keeps the source's expiry; re-sign before it lapses");
|
|
1528
|
+
}
|
|
1529
|
+
return 0;
|
|
1530
|
+
}
|
|
1022
1531
|
async function promptOrEnv(envName, prompt) {
|
|
1023
1532
|
const fromEnv = process.env[envName];
|
|
1024
1533
|
if (fromEnv) return fromEnv;
|
|
@@ -1027,6 +1536,14 @@ async function promptOrEnv(envName, prompt) {
|
|
|
1027
1536
|
return v || void 0;
|
|
1028
1537
|
}
|
|
1029
1538
|
async function runAppleJwt(options2) {
|
|
1539
|
+
if (options2.copyFrom) {
|
|
1540
|
+
try {
|
|
1541
|
+
return await copyAppleEnv(options2.copyFrom);
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1030
1547
|
section("Apple Sign In");
|
|
1031
1548
|
try {
|
|
1032
1549
|
const env2 = await envMap();
|
|
@@ -1119,7 +1636,7 @@ async function runAppleJwt(options2) {
|
|
|
1119
1636
|
});
|
|
1120
1637
|
if (process.stdin.isTTY && !rotateOnly) {
|
|
1121
1638
|
line();
|
|
1122
|
-
if (await askYesNo("
|
|
1639
|
+
if (await askYesNo("Show the renewal date and rotate command?", false)) {
|
|
1123
1640
|
const when = new Date(Date.now() + 150 * 864e5);
|
|
1124
1641
|
note(`renew on or before ${when.toDateString()} by running:`);
|
|
1125
1642
|
note(` ${BOLD}vexpo apple jwt --rotate${RESET}`);
|
|
@@ -1278,11 +1795,6 @@ function makeAscClient(creds) {
|
|
|
1278
1795
|
return out;
|
|
1279
1796
|
}
|
|
1280
1797
|
return {
|
|
1281
|
-
/**
|
|
1282
|
-
* Low-level primitives. Exposed so resource-specific modules
|
|
1283
|
-
* (asc-testflight.ts, asc-reviews.ts, asc-versions.ts, asc-sandbox.ts)
|
|
1284
|
-
* can extend the client without re-implementing auth + retry.
|
|
1285
|
-
*/
|
|
1286
1798
|
request,
|
|
1287
1799
|
paginatedList,
|
|
1288
1800
|
bundleIds: {
|
|
@@ -1421,8 +1933,8 @@ function makeAscClient(creds) {
|
|
|
1421
1933
|
}
|
|
1422
1934
|
async function validate(creds) {
|
|
1423
1935
|
try {
|
|
1424
|
-
const
|
|
1425
|
-
const apps = await
|
|
1936
|
+
const client = makeAscClient(creds);
|
|
1937
|
+
const apps = await client.apps.list();
|
|
1426
1938
|
return { ok: true, appCount: apps.length };
|
|
1427
1939
|
} catch (err) {
|
|
1428
1940
|
if (err instanceof AscApiError) {
|
|
@@ -1435,7 +1947,7 @@ async function validate(creds) {
|
|
|
1435
1947
|
var SIGN_IN_WITH_APPLE_CAPABILITY = "APPLE_ID_AUTH";
|
|
1436
1948
|
|
|
1437
1949
|
// src/commands/apple/asc-key.ts
|
|
1438
|
-
async function
|
|
1950
|
+
async function fileExists2(p) {
|
|
1439
1951
|
try {
|
|
1440
1952
|
await access(expandTilde(p));
|
|
1441
1953
|
return true;
|
|
@@ -1479,7 +1991,7 @@ async function promptCredsInteractive() {
|
|
|
1479
1991
|
return null;
|
|
1480
1992
|
}
|
|
1481
1993
|
const p8Path = expandTilde(rawP8);
|
|
1482
|
-
if (!await
|
|
1994
|
+
if (!await fileExists2(p8Path)) {
|
|
1483
1995
|
bad(`.p8 not found at ${p8Path}`);
|
|
1484
1996
|
return null;
|
|
1485
1997
|
}
|
|
@@ -1490,20 +2002,12 @@ async function readEnvCreds() {
|
|
|
1490
2002
|
const keyId = process.env.APPLE_ASC_KEY_ID;
|
|
1491
2003
|
const p8Path = process.env.APPLE_ASC_P8_PATH;
|
|
1492
2004
|
if (!issuerId || !keyId || !p8Path) return null;
|
|
1493
|
-
if (!await
|
|
2005
|
+
if (!await fileExists2(p8Path)) {
|
|
1494
2006
|
bad(`APPLE_ASC_P8_PATH=${p8Path} not found`);
|
|
1495
2007
|
return null;
|
|
1496
2008
|
}
|
|
1497
2009
|
return { issuerId, keyId, privateKey: { path: p8Path } };
|
|
1498
2010
|
}
|
|
1499
|
-
async function easAscStatus() {
|
|
1500
|
-
try {
|
|
1501
|
-
const status = await ascStatus();
|
|
1502
|
-
return status.connected ? "connected" : "disconnected";
|
|
1503
|
-
} catch {
|
|
1504
|
-
return "unknown";
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
2011
|
async function readStateCreds() {
|
|
1508
2012
|
const state = await load();
|
|
1509
2013
|
const rec = state.steps["asc-key"];
|
|
@@ -1513,7 +2017,7 @@ async function readStateCreds() {
|
|
|
1513
2017
|
const keyId = out.keyId;
|
|
1514
2018
|
const p8Path = out.p8Path;
|
|
1515
2019
|
if (!issuerId || !keyId || !p8Path) return null;
|
|
1516
|
-
if (!await
|
|
2020
|
+
if (!await fileExists2(p8Path)) return null;
|
|
1517
2021
|
return { issuerId, keyId, privateKey: { path: p8Path } };
|
|
1518
2022
|
}
|
|
1519
2023
|
async function runAscKey(options2) {
|
|
@@ -1525,25 +2029,12 @@ async function runAscKey(options2) {
|
|
|
1525
2029
|
bad("no cached ASC key in state.json; run without --revalidate first");
|
|
1526
2030
|
return 1;
|
|
1527
2031
|
}
|
|
1528
|
-
const easSays = await easAscStatus();
|
|
1529
|
-
if (easSays === "connected") {
|
|
1530
|
-
ok("cached key valid (verified via `eas integrations:asc:status`)");
|
|
1531
|
-
await recordStep("asc-key", {
|
|
1532
|
-
issuerId: cached2.issuerId,
|
|
1533
|
-
keyId: cached2.keyId,
|
|
1534
|
-
p8Path: "path" in cached2.privateKey ? cached2.privateKey.path : void 0
|
|
1535
|
-
});
|
|
1536
|
-
return 0;
|
|
1537
|
-
}
|
|
1538
2032
|
const result = await validate(cached2);
|
|
1539
2033
|
if (!result.ok) {
|
|
1540
2034
|
bad(`cached key invalid: ${result.reason}`);
|
|
1541
2035
|
return 1;
|
|
1542
2036
|
}
|
|
1543
2037
|
ok(`cached key still valid (${result.appCount} app${result.appCount === 1 ? "" : "s"})`);
|
|
1544
|
-
if (easSays === "disconnected") {
|
|
1545
|
-
note("EAS reports no ASC link; run `vexpo asc connect` to wire it up");
|
|
1546
|
-
}
|
|
1547
2038
|
await recordStep("asc-key", {
|
|
1548
2039
|
issuerId: cached2.issuerId,
|
|
1549
2040
|
keyId: cached2.keyId,
|
|
@@ -1592,7 +2083,7 @@ async function runAscKey(options2) {
|
|
|
1592
2083
|
note(
|
|
1593
2084
|
`${BOLD}This step is purely validation${RESET} ${DIM}- EAS still needs the same key uploaded:${RESET}`
|
|
1594
2085
|
);
|
|
1595
|
-
note(` ${BOLD}
|
|
2086
|
+
note(` ${BOLD}npx eas credentials -p ios${RESET}`);
|
|
1596
2087
|
note(` \u2192 Build Credentials \u2192 'Use existing App Store Connect API Key'`);
|
|
1597
2088
|
note(` \u2192 'Set up a new key' if no existing match, paste:`);
|
|
1598
2089
|
note(` issuer=${creds.issuerId}, keyId=${creds.keyId}`);
|
|
@@ -1611,7 +2102,7 @@ async function runEasRotationSecrets(options2) {
|
|
|
1611
2102
|
existing = await envList("production");
|
|
1612
2103
|
} catch (err) {
|
|
1613
2104
|
bad(`could not list EAS production env: ${err instanceof Error ? err.message : err}`);
|
|
1614
|
-
note("run `
|
|
2105
|
+
note("run `npx eas login` and `npx eas init` first");
|
|
1615
2106
|
return 1;
|
|
1616
2107
|
}
|
|
1617
2108
|
const teamId = await readOne("APPLE_TEAM_ID");
|
|
@@ -1635,15 +2126,14 @@ async function runEasRotationSecrets(options2) {
|
|
|
1635
2126
|
note("re-run with APPLE_P8_PATH=/path/to/AuthKey.p8");
|
|
1636
2127
|
return 1;
|
|
1637
2128
|
}
|
|
1638
|
-
let pem;
|
|
1639
2129
|
try {
|
|
1640
|
-
|
|
2130
|
+
await access(p8Path);
|
|
1641
2131
|
} catch {
|
|
1642
2132
|
bad(`.p8 file not found at ${p8Path}`);
|
|
1643
2133
|
return 1;
|
|
1644
2134
|
}
|
|
1645
2135
|
const apple2 = [
|
|
1646
|
-
{ name: "APPLE_P8_PRIVATE_KEY", value:
|
|
2136
|
+
{ name: "APPLE_P8_PRIVATE_KEY", value: p8Path, type: "file" },
|
|
1647
2137
|
{ name: "APPLE_TEAM_ID", value: teamId },
|
|
1648
2138
|
{ name: "APPLE_KEY_ID", value: keyId },
|
|
1649
2139
|
{ name: "APPLE_SERVICES_ID", value: servicesId }
|
|
@@ -1675,98 +2165,59 @@ async function runEasRotationSecrets(options2) {
|
|
|
1675
2165
|
}
|
|
1676
2166
|
if (!existing.has("CONVEX_DEPLOY_KEY") || options2.force) {
|
|
1677
2167
|
line();
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
{
|
|
1683
|
-
label: "Convex dashboard (Settings \u2192 Deploy keys)",
|
|
1684
|
-
url: "https://dashboard.convex.dev"
|
|
1685
|
-
}
|
|
1686
|
-
],
|
|
1687
|
-
allowSkip: true,
|
|
1688
|
-
skipLabel: "skip"
|
|
1689
|
-
});
|
|
1690
|
-
if (process.stdin.isTTY) {
|
|
1691
|
-
const key = (await ask(` Paste Convex prod deploy key ${DIM}(or Enter to skip)${RESET} > `)).trim();
|
|
1692
|
-
if (key) {
|
|
1693
|
-
try {
|
|
1694
|
-
if (existing.has("CONVEX_DEPLOY_KEY")) {
|
|
1695
|
-
await envUpdate("CONVEX_DEPLOY_KEY", key, "secret", ENVS);
|
|
1696
|
-
updated += 1;
|
|
1697
|
-
} else {
|
|
1698
|
-
await envCreate("CONVEX_DEPLOY_KEY", key, "secret", ENVS);
|
|
1699
|
-
applied += 1;
|
|
1700
|
-
}
|
|
1701
|
-
ok("CONVEX_DEPLOY_KEY set");
|
|
1702
|
-
} catch (err) {
|
|
1703
|
-
bad(`CONVEX_DEPLOY_KEY: ${err instanceof Error ? err.message : err}`);
|
|
1704
|
-
return 1;
|
|
1705
|
-
}
|
|
2168
|
+
const setKey = async (key) => {
|
|
2169
|
+
if (existing.has("CONVEX_DEPLOY_KEY")) {
|
|
2170
|
+
await envUpdate("CONVEX_DEPLOY_KEY", key, "secret", ENVS);
|
|
2171
|
+
updated += 1;
|
|
1706
2172
|
} else {
|
|
1707
|
-
|
|
1708
|
-
|
|
2173
|
+
await envCreate("CONVEX_DEPLOY_KEY", key, "secret", ENVS);
|
|
2174
|
+
applied += 1;
|
|
1709
2175
|
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
|
|
2176
|
+
};
|
|
2177
|
+
let minted = false;
|
|
2178
|
+
try {
|
|
2179
|
+
const result = await mintProdDeployKey(
|
|
2180
|
+
deploymentSlug(await readOne("CONVEX_DEPLOYMENT")) ?? "",
|
|
2181
|
+
"eas-rotation"
|
|
2182
|
+
);
|
|
2183
|
+
if (result) {
|
|
2184
|
+
await setKey(result.key);
|
|
2185
|
+
ok(`minted + set CONVEX_DEPLOY_KEY for prod ${BOLD}${result.deployment}${RESET}`);
|
|
2186
|
+
minted = true;
|
|
2187
|
+
} else {
|
|
2188
|
+
yep("couldn't resolve the prod deployment (offline or not logged in)");
|
|
2189
|
+
}
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
yep(`couldn't mint a deploy key: ${err instanceof Error ? err.message : err}`);
|
|
1713
2192
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
async function pkgName() {
|
|
1739
|
-
const pkg = await readJsonOrNull("package.json");
|
|
1740
|
-
return typeof pkg?.name === "string" && pkg.name ? pkg.name : "app";
|
|
1741
|
-
}
|
|
1742
|
-
async function appName() {
|
|
1743
|
-
const text = await readTextOrNull("app.config.ts");
|
|
1744
|
-
if (text) {
|
|
1745
|
-
const match = /\bname:\s*["']([^"']+)["']/.exec(text);
|
|
1746
|
-
if (match) return match[1];
|
|
2193
|
+
if (!minted) {
|
|
2194
|
+
if (process.stdin.isTTY) {
|
|
2195
|
+
const key = (await ask(` Paste a Convex prod deploy key ${DIM}(or Enter to skip)${RESET} > `)).trim();
|
|
2196
|
+
if (key) {
|
|
2197
|
+
try {
|
|
2198
|
+
await setKey(key);
|
|
2199
|
+
ok("CONVEX_DEPLOY_KEY set");
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
bad(`CONVEX_DEPLOY_KEY: ${err instanceof Error ? err.message : err}`);
|
|
2202
|
+
return 1;
|
|
2203
|
+
}
|
|
2204
|
+
} else {
|
|
2205
|
+
yep("skipped CONVEX_DEPLOY_KEY (set later with `eas env:create`)");
|
|
2206
|
+
skipped2 += 1;
|
|
2207
|
+
}
|
|
2208
|
+
} else {
|
|
2209
|
+
yep("skipped CONVEX_DEPLOY_KEY (non-interactive, mint unavailable)");
|
|
2210
|
+
skipped2 += 1;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
nop("CONVEX_DEPLOY_KEY already set");
|
|
2215
|
+
skipped2 += 1;
|
|
1747
2216
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
return parts.map((w) => (w[0] ?? "").toUpperCase() + w.slice(1)).join(" ");
|
|
1753
|
-
}
|
|
1754
|
-
async function scheme() {
|
|
1755
|
-
const text = await readTextOrNull("app.config.ts");
|
|
1756
|
-
if (!text) return "app";
|
|
1757
|
-
return /scheme:\s*["']([^"']+)["']/.exec(text)?.[1] ?? "app";
|
|
1758
|
-
}
|
|
1759
|
-
async function bundleIdFallback() {
|
|
1760
|
-
const text = await readTextOrNull("app.config.ts");
|
|
1761
|
-
if (!text) return null;
|
|
1762
|
-
return /EXPO_PUBLIC_APP_BUNDLE_ID\s*\?\?\s*"([^"]+)"/.exec(text)?.[1] ?? null;
|
|
1763
|
-
}
|
|
1764
|
-
async function appleTeamIdFallback() {
|
|
1765
|
-
const text = await readTextOrNull("app.config.ts");
|
|
1766
|
-
if (!text) return null;
|
|
1767
|
-
const value = /EXPO_PUBLIC_APPLE_TEAM_ID\s*\?\?\s*"([^"]+)"/.exec(text)?.[1] ?? null;
|
|
1768
|
-
if (!value || value === "ABCDE12345") return null;
|
|
1769
|
-
return value;
|
|
2217
|
+
line();
|
|
2218
|
+
ok(`${applied} created, ${updated} updated, ${skipped2} skipped (of ${apple2.length + 1} secrets)`);
|
|
2219
|
+
yep(`${BOLD}rotation cron${RESET} reads these on the next fire (every 3 months on the 1st)`);
|
|
2220
|
+
return 0;
|
|
1770
2221
|
}
|
|
1771
2222
|
|
|
1772
2223
|
// src/commands/apple/services-id.ts
|
|
@@ -1787,23 +2238,23 @@ async function loadAscCreds() {
|
|
|
1787
2238
|
if (!sIssuer || !sKey || !sPath) return null;
|
|
1788
2239
|
return { issuerId: sIssuer, keyId: sKey, privateKey: { path: sPath } };
|
|
1789
2240
|
}
|
|
1790
|
-
async function findOrCreateBundleId(
|
|
1791
|
-
const all = await
|
|
2241
|
+
async function findOrCreateBundleId(client, args) {
|
|
2242
|
+
const all = await client.bundleIds.list({ identifier: args.identifier });
|
|
1792
2243
|
const existing = all.find((b) => {
|
|
1793
2244
|
if (b.attributes.identifier !== args.identifier) return false;
|
|
1794
2245
|
if (args.platform === "SERVICES") return b.attributes.platform === "SERVICES";
|
|
1795
2246
|
return b.attributes.platform !== "SERVICES";
|
|
1796
2247
|
});
|
|
1797
2248
|
if (existing) return existing;
|
|
1798
|
-
return
|
|
2249
|
+
return client.bundleIds.create({
|
|
1799
2250
|
identifier: args.identifier,
|
|
1800
2251
|
name: args.name,
|
|
1801
2252
|
platform: args.platform
|
|
1802
2253
|
});
|
|
1803
2254
|
}
|
|
1804
|
-
async function findServicesIdOrPromptManual(
|
|
2255
|
+
async function findServicesIdOrPromptManual(client, identifier) {
|
|
1805
2256
|
const lookup = async () => {
|
|
1806
|
-
const matches = await
|
|
2257
|
+
const matches = await client.bundleIds.list({ identifier });
|
|
1807
2258
|
return matches.find((b) => b.attributes.identifier === identifier) ?? null;
|
|
1808
2259
|
};
|
|
1809
2260
|
const found = await lookup();
|
|
@@ -1862,26 +2313,26 @@ async function runServicesId(options2) {
|
|
|
1862
2313
|
ok(
|
|
1863
2314
|
`ASC API authenticated (${validation.appCount} app${validation.appCount === 1 ? "" : "s"} on team)`
|
|
1864
2315
|
);
|
|
1865
|
-
const
|
|
2316
|
+
const client = makeAscClient(creds);
|
|
1866
2317
|
const servicesId = options2.servicesId ?? process.env.APPLE_SERVICES_ID ?? `${bundleId}.signin`;
|
|
1867
2318
|
const name = await appName();
|
|
1868
|
-
const appBundle = await findOrCreateBundleId(
|
|
2319
|
+
const appBundle = await findOrCreateBundleId(client, {
|
|
1869
2320
|
identifier: bundleId,
|
|
1870
2321
|
name,
|
|
1871
2322
|
platform: "IOS"
|
|
1872
2323
|
});
|
|
1873
2324
|
ok(`app bundle id resource: ${appBundle.id} (${appBundle.attributes.identifier})`);
|
|
1874
|
-
const sid = await findServicesIdOrPromptManual(
|
|
2325
|
+
const sid = await findServicesIdOrPromptManual(client, servicesId);
|
|
1875
2326
|
if (!sid) return 1;
|
|
1876
2327
|
ok(`services id resource: ${sid.id} (${servicesId})`);
|
|
1877
|
-
const caps = await
|
|
2328
|
+
const caps = await client.bundleIdCapabilities.list(appBundle.id);
|
|
1878
2329
|
const siwaCap = caps.find((c) => c.attributes.capabilityType === SIGN_IN_WITH_APPLE_CAPABILITY);
|
|
1879
2330
|
let siwaCapId;
|
|
1880
2331
|
if (siwaCap) {
|
|
1881
2332
|
siwaCapId = siwaCap.id;
|
|
1882
2333
|
nop("Sign In with Apple capability already enabled on app bundle id");
|
|
1883
2334
|
} else {
|
|
1884
|
-
const created = await
|
|
2335
|
+
const created = await client.bundleIdCapabilities.create({
|
|
1885
2336
|
bundleIdResourceId: appBundle.id,
|
|
1886
2337
|
capabilityType: SIGN_IN_WITH_APPLE_CAPABILITY
|
|
1887
2338
|
});
|
|
@@ -1905,6 +2356,144 @@ async function runServicesId(options2) {
|
|
|
1905
2356
|
return 1;
|
|
1906
2357
|
}
|
|
1907
2358
|
}
|
|
2359
|
+
|
|
2360
|
+
// src/lib/eas-submit.ts
|
|
2361
|
+
function isObject(value) {
|
|
2362
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2363
|
+
}
|
|
2364
|
+
function submitProfilesMissingAscAppId(easJson) {
|
|
2365
|
+
let cfg;
|
|
2366
|
+
try {
|
|
2367
|
+
cfg = JSON.parse(easJson);
|
|
2368
|
+
} catch {
|
|
2369
|
+
return [];
|
|
2370
|
+
}
|
|
2371
|
+
return Object.entries(cfg.submit ?? {}).filter(([, p]) => isObject(p?.ios) && !p.ios.ascAppId).map(([name]) => name);
|
|
2372
|
+
}
|
|
2373
|
+
function needsAscAppId(easJson, ascAppId) {
|
|
2374
|
+
let cfg;
|
|
2375
|
+
try {
|
|
2376
|
+
cfg = JSON.parse(easJson);
|
|
2377
|
+
} catch {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
return Object.values(cfg.submit ?? {}).some(
|
|
2381
|
+
(p) => isObject(p?.ios) && p.ios.ascAppId !== ascAppId
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
function withAscAppId(easJson, ascAppId) {
|
|
2385
|
+
if (!needsAscAppId(easJson, ascAppId)) return easJson;
|
|
2386
|
+
const cfg = JSON.parse(easJson);
|
|
2387
|
+
for (const profile of Object.values(cfg.submit ?? {})) {
|
|
2388
|
+
if (isObject(profile?.ios)) profile.ios.ascAppId = ascAppId;
|
|
2389
|
+
}
|
|
2390
|
+
return JSON.stringify(cfg, null, 2) + "\n";
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/commands/asc.ts
|
|
2394
|
+
async function loadAscFromState2() {
|
|
2395
|
+
const state = await load();
|
|
2396
|
+
const rec = state.steps["asc-key"];
|
|
2397
|
+
if (!rec?.outputs) return null;
|
|
2398
|
+
const out = rec.outputs;
|
|
2399
|
+
const issuerId = out.issuerId;
|
|
2400
|
+
const keyId = out.keyId;
|
|
2401
|
+
const rawPath = out.p8Path;
|
|
2402
|
+
if (!issuerId || !keyId || !rawPath) return null;
|
|
2403
|
+
const p8Path = expandTilde(rawPath);
|
|
2404
|
+
if (!existsSync(p8Path)) return null;
|
|
2405
|
+
return { issuerId, keyId, p8Path };
|
|
2406
|
+
}
|
|
2407
|
+
async function runAscConnect(opts = {}) {
|
|
2408
|
+
section("ASC connect");
|
|
2409
|
+
if (!opts.force) {
|
|
2410
|
+
try {
|
|
2411
|
+
const status = await ascStatus();
|
|
2412
|
+
if (status.status === "connected" && status.appStoreConnectApp) {
|
|
2413
|
+
const label = status.appStoreConnectApp.bundleIdentifier ?? status.appStoreConnectApp.ascAppIdentifier;
|
|
2414
|
+
nop(`already connected (${label})`);
|
|
2415
|
+
await recordStep("apple-asc-link", {
|
|
2416
|
+
ascAppId: status.appStoreConnectApp.ascAppIdentifier,
|
|
2417
|
+
ascAppEasId: status.appStoreConnectApp.id,
|
|
2418
|
+
bundleId: status.appStoreConnectApp.bundleIdentifier,
|
|
2419
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2420
|
+
});
|
|
2421
|
+
return 0;
|
|
2422
|
+
}
|
|
2423
|
+
} catch {
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const asc = await loadAscFromState2();
|
|
2427
|
+
if (!asc) {
|
|
2428
|
+
bad("no cached ASC creds. Run `vexpo apple asc-key` first to validate one.");
|
|
2429
|
+
return 1;
|
|
2430
|
+
}
|
|
2431
|
+
ok("cached ASC API key found in state.json");
|
|
2432
|
+
note(` issuerId: ${BOLD}${asc.issuerId}${RESET}`);
|
|
2433
|
+
note(` keyId: ${BOLD}${asc.keyId}${RESET}`);
|
|
2434
|
+
note(` .p8: ${BOLD}${asc.p8Path}${RESET}`);
|
|
2435
|
+
const bundleId = await readOne("EXPO_PUBLIC_APP_BUNDLE_ID");
|
|
2436
|
+
if (!bundleId) {
|
|
2437
|
+
bad("no EXPO_PUBLIC_APP_BUNDLE_ID in .env.local. Run `vexpo convex` first.");
|
|
2438
|
+
return 1;
|
|
2439
|
+
}
|
|
2440
|
+
ok(`bundle id: ${BOLD}${bundleId}${RESET}`);
|
|
2441
|
+
if (!process.stdin.isTTY) {
|
|
2442
|
+
bad("ASC connect needs a TTY: eas integrations:asc:connect can't run headless");
|
|
2443
|
+
note("run `vexpo asc:connect` in an interactive terminal to finish the EAS\u2194ASC link");
|
|
2444
|
+
return 1;
|
|
2445
|
+
}
|
|
2446
|
+
const env2 = {
|
|
2447
|
+
...process.env,
|
|
2448
|
+
EXPO_ASC_API_KEY_PATH: asc.p8Path,
|
|
2449
|
+
EXPO_ASC_KEY_ID: asc.keyId,
|
|
2450
|
+
EXPO_ASC_ISSUER_ID: asc.issuerId
|
|
2451
|
+
};
|
|
2452
|
+
line();
|
|
2453
|
+
note("spawning `eas integrations:asc:connect`. Most likely flow:");
|
|
2454
|
+
note(" 1. Press Y to generate a new ASC API key (default)");
|
|
2455
|
+
note(" 2. Press Enter to accept ADMIN role (default)");
|
|
2456
|
+
note("EXPO_ASC_API_KEY_* env vars are set so eas-cli uses our cached key");
|
|
2457
|
+
note("for the Apple auth step, no Apple ID + password prompt.");
|
|
2458
|
+
const proc = spawn([dlx(), "eas", "integrations:asc:connect", "--bundle-id", bundleId], {
|
|
2459
|
+
stdin: "inherit",
|
|
2460
|
+
stdout: "inherit",
|
|
2461
|
+
stderr: "inherit",
|
|
2462
|
+
env: env2
|
|
2463
|
+
});
|
|
2464
|
+
const code = await proc.exited;
|
|
2465
|
+
if (code !== 0) {
|
|
2466
|
+
bad(`eas integrations:asc:connect exited with code ${code}`);
|
|
2467
|
+
return code;
|
|
2468
|
+
}
|
|
2469
|
+
ok("EAS project linked to ASC app");
|
|
2470
|
+
await recordStep("apple-asc-link", {
|
|
2471
|
+
bundleId,
|
|
2472
|
+
ascIssuerId: asc.issuerId,
|
|
2473
|
+
ascKeyId: asc.keyId,
|
|
2474
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2475
|
+
});
|
|
2476
|
+
if (existsSync("eas.json")) {
|
|
2477
|
+
try {
|
|
2478
|
+
const ascAppId = (await ascStatus()).appStoreConnectApp?.ascAppIdentifier;
|
|
2479
|
+
if (ascAppId) {
|
|
2480
|
+
const before = await readFile("eas.json", "utf8");
|
|
2481
|
+
const after = withAscAppId(before, ascAppId);
|
|
2482
|
+
if (after !== before) {
|
|
2483
|
+
await writeFile("eas.json", after);
|
|
2484
|
+
ok(`wrote ascAppId ${BOLD}${ascAppId}${RESET} to eas.json submit profiles`);
|
|
2485
|
+
note("commit this in your fork: non-interactive `eas submit` (CI) needs it");
|
|
2486
|
+
} else {
|
|
2487
|
+
nop("eas.json submit profiles already carry ascAppId");
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
yep(`couldn't write ascAppId to eas.json: ${err instanceof Error ? err.message : err}`);
|
|
2492
|
+
note("non-interactive submit will need `ascAppId` set manually in eas.json");
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return 0;
|
|
2496
|
+
}
|
|
1908
2497
|
async function loadAscCreds2() {
|
|
1909
2498
|
const state = await load();
|
|
1910
2499
|
const rec = state.steps["asc-key"];
|
|
@@ -1923,459 +2512,330 @@ async function ascBootstrap() {
|
|
|
1923
2512
|
if (!creds) {
|
|
1924
2513
|
throw new Error("no cached ASC creds. run `vexpo apple asc-key` first");
|
|
1925
2514
|
}
|
|
1926
|
-
const
|
|
2515
|
+
const client = makeAscClient(creds);
|
|
1927
2516
|
const bundleId = await readOne("EXPO_PUBLIC_APP_BUNDLE_ID") ?? await readOne("APP_BUNDLE_ID") ?? void 0;
|
|
1928
2517
|
let ascAppId;
|
|
1929
2518
|
if (bundleId) {
|
|
1930
2519
|
try {
|
|
1931
|
-
const apps = await
|
|
2520
|
+
const apps = await client.paginatedList("/v1/apps", { "filter[bundleId]": bundleId }, 5);
|
|
1932
2521
|
ascAppId = apps[0]?.id;
|
|
1933
2522
|
} catch {
|
|
1934
2523
|
}
|
|
1935
2524
|
}
|
|
1936
|
-
return { client
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// src/lib/asc-versions.ts
|
|
1940
|
-
function versions(client2) {
|
|
1941
|
-
return {
|
|
1942
|
-
/* app store versions ------------------------------------------------ */
|
|
1943
|
-
appStoreVersions: {
|
|
1944
|
-
list(filter) {
|
|
1945
|
-
const query = {};
|
|
1946
|
-
if (filter?.platform) query["filter[platform]"] = filter.platform;
|
|
1947
|
-
if (filter?.versionString) query["filter[versionString]"] = filter.versionString;
|
|
1948
|
-
if (filter?.state) query["filter[appStoreState]"] = filter.state;
|
|
1949
|
-
const path = filter?.appId ? `/v1/apps/${filter.appId}/appStoreVersions` : "/v1/appStoreVersions";
|
|
1950
|
-
return client2.paginatedList(path, query, filter?.limit ?? 25);
|
|
1951
|
-
},
|
|
1952
|
-
async get(id) {
|
|
1953
|
-
const res = await client2.request(
|
|
1954
|
-
"GET",
|
|
1955
|
-
`/v1/appStoreVersions/${id}`
|
|
1956
|
-
);
|
|
1957
|
-
return res.data;
|
|
1958
|
-
},
|
|
1959
|
-
async getBuild(versionId) {
|
|
1960
|
-
try {
|
|
1961
|
-
return await client2.request(
|
|
1962
|
-
"GET",
|
|
1963
|
-
`/v1/appStoreVersions/${versionId}/relationships/build`
|
|
1964
|
-
);
|
|
1965
|
-
} catch {
|
|
1966
|
-
return null;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
},
|
|
1970
|
-
/* review submissions ------------------------------------------------ */
|
|
1971
|
-
reviewSubmissions: {
|
|
1972
|
-
list(filter) {
|
|
1973
|
-
const query = {};
|
|
1974
|
-
if (filter?.appId) query["filter[app]"] = filter.appId;
|
|
1975
|
-
if (filter?.platform) query["filter[platform]"] = filter.platform;
|
|
1976
|
-
if (filter?.state) query["filter[state]"] = filter.state;
|
|
1977
|
-
return client2.paginatedList("/v1/reviewSubmissions", query);
|
|
1978
|
-
},
|
|
1979
|
-
async get(id) {
|
|
1980
|
-
const res = await client2.request(
|
|
1981
|
-
"GET",
|
|
1982
|
-
`/v1/reviewSubmissions/${id}`
|
|
1983
|
-
);
|
|
1984
|
-
return res.data;
|
|
1985
|
-
}
|
|
1986
|
-
},
|
|
1987
|
-
/* phased release ---------------------------------------------------- */
|
|
1988
|
-
phasedReleases: {
|
|
1989
|
-
async getForVersion(versionId) {
|
|
1990
|
-
try {
|
|
1991
|
-
const res = await client2.request(
|
|
1992
|
-
"GET",
|
|
1993
|
-
`/v1/appStoreVersions/${versionId}/appStoreVersionPhasedRelease`
|
|
1994
|
-
);
|
|
1995
|
-
return res.data;
|
|
1996
|
-
} catch {
|
|
1997
|
-
return null;
|
|
1998
|
-
}
|
|
1999
|
-
},
|
|
2000
|
-
async pause(id) {
|
|
2001
|
-
const body = {
|
|
2002
|
-
data: {
|
|
2003
|
-
type: "appStoreVersionPhasedReleases",
|
|
2004
|
-
id,
|
|
2005
|
-
attributes: { phasedReleaseState: "PAUSED" }
|
|
2006
|
-
}
|
|
2007
|
-
};
|
|
2008
|
-
const res = await client2.request(
|
|
2009
|
-
"PATCH",
|
|
2010
|
-
`/v1/appStoreVersionPhasedReleases/${id}`,
|
|
2011
|
-
body
|
|
2012
|
-
);
|
|
2013
|
-
return res.data;
|
|
2014
|
-
},
|
|
2015
|
-
async resume(id) {
|
|
2016
|
-
const body = {
|
|
2017
|
-
data: {
|
|
2018
|
-
type: "appStoreVersionPhasedReleases",
|
|
2019
|
-
id,
|
|
2020
|
-
attributes: { phasedReleaseState: "ACTIVE" }
|
|
2021
|
-
}
|
|
2022
|
-
};
|
|
2023
|
-
const res = await client2.request(
|
|
2024
|
-
"PATCH",
|
|
2025
|
-
`/v1/appStoreVersionPhasedReleases/${id}`,
|
|
2026
|
-
body
|
|
2027
|
-
);
|
|
2028
|
-
return res.data;
|
|
2029
|
-
},
|
|
2030
|
-
async complete(id) {
|
|
2031
|
-
const body = {
|
|
2032
|
-
data: {
|
|
2033
|
-
type: "appStoreVersionPhasedReleases",
|
|
2034
|
-
id,
|
|
2035
|
-
attributes: { phasedReleaseState: "COMPLETE" }
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
|
-
const res = await client2.request(
|
|
2039
|
-
"PATCH",
|
|
2040
|
-
`/v1/appStoreVersionPhasedReleases/${id}`,
|
|
2041
|
-
body
|
|
2042
|
-
);
|
|
2043
|
-
return res.data;
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
};
|
|
2525
|
+
return { client, bundleId, ascAppId, creds };
|
|
2047
2526
|
}
|
|
2048
2527
|
|
|
2049
|
-
// src/
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
try {
|
|
2090
|
-
const { v } = await bootstrap();
|
|
2091
|
-
const ver = await v.appStoreVersions.get(versionId);
|
|
2092
|
-
const phased = await v.phasedReleases.getForVersion(versionId).catch(() => null);
|
|
2093
|
-
if (opts.json) {
|
|
2094
|
-
process.stdout.write(JSON.stringify({ version: ver, phasedRelease: phased }, null, 2) + "\n");
|
|
2095
|
-
return 0;
|
|
2096
|
-
}
|
|
2097
|
-
section(`Version ${ver.attributes.versionString ?? versionId}`);
|
|
2098
|
-
line(` state: ${ver.attributes.appStoreState ?? "?"}`);
|
|
2099
|
-
line(` platform: ${ver.attributes.platform ?? "?"}`);
|
|
2100
|
-
if (ver.attributes.releaseType) line(` release: ${ver.attributes.releaseType}`);
|
|
2101
|
-
if (ver.attributes.earliestReleaseDate)
|
|
2102
|
-
line(` earliest: ${ver.attributes.earliestReleaseDate}`);
|
|
2103
|
-
if (phased) {
|
|
2104
|
-
line(` phased: ${phased.attributes.phasedReleaseState ?? "?"}`);
|
|
2105
|
-
if (phased.attributes.startDate) line(` started ${phased.attributes.startDate}`);
|
|
2106
|
-
}
|
|
2107
|
-
return 0;
|
|
2108
|
-
} catch (err) {
|
|
2109
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2110
|
-
return 1;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
async function runSubmissionsList(opts) {
|
|
2114
|
-
try {
|
|
2115
|
-
const { v, ascAppId } = await bootstrap();
|
|
2116
|
-
const submissions = await v.reviewSubmissions.list({
|
|
2117
|
-
appId: ascAppId,
|
|
2118
|
-
platform: opts.platform,
|
|
2119
|
-
state: opts.state
|
|
2528
|
+
// src/lib/asc-accessibility.ts
|
|
2529
|
+
var ACCESSIBILITY_FEATURES = [
|
|
2530
|
+
"VOICE_OVER",
|
|
2531
|
+
"VOICE_CONTROL",
|
|
2532
|
+
"LARGER_TEXT",
|
|
2533
|
+
"DARK_INTERFACE",
|
|
2534
|
+
"SUFFICIENT_CONTRAST",
|
|
2535
|
+
"DIFFERENTIATION_WITHOUT_COLOR_ALONE",
|
|
2536
|
+
"REDUCED_MOTION",
|
|
2537
|
+
"CAPTIONS",
|
|
2538
|
+
"AUDIO_DESCRIPTIONS"
|
|
2539
|
+
];
|
|
2540
|
+
var ACCESSIBILITY_LEVELS = [
|
|
2541
|
+
"FULLY_SUPPORTS",
|
|
2542
|
+
"PARTIAL",
|
|
2543
|
+
"DOES_NOT_SUPPORT",
|
|
2544
|
+
"NOT_APPLICABLE"
|
|
2545
|
+
];
|
|
2546
|
+
var ACCESSIBILITY_DEVICE_FAMILIES = [
|
|
2547
|
+
"IPHONE",
|
|
2548
|
+
"IPAD",
|
|
2549
|
+
"MAC",
|
|
2550
|
+
"APPLE_TV",
|
|
2551
|
+
"APPLE_WATCH",
|
|
2552
|
+
"VISION"
|
|
2553
|
+
];
|
|
2554
|
+
function lintAccessibilityConfig(config) {
|
|
2555
|
+
const issues = [];
|
|
2556
|
+
if (!isRecord(config)) {
|
|
2557
|
+
issues.push({ severity: "error", message: "config must be a JSON object" });
|
|
2558
|
+
return issues;
|
|
2559
|
+
}
|
|
2560
|
+
if (!Array.isArray(config.entries)) {
|
|
2561
|
+
issues.push({ severity: "error", message: "`entries` must be an array" });
|
|
2562
|
+
return issues;
|
|
2563
|
+
}
|
|
2564
|
+
if (config.entries.length === 0) {
|
|
2565
|
+
issues.push({
|
|
2566
|
+
severity: "warning",
|
|
2567
|
+
message: "`entries` is empty; declare at least one device family."
|
|
2120
2568
|
});
|
|
2121
|
-
if (opts.json) {
|
|
2122
|
-
process.stdout.write(JSON.stringify(submissions, null, 2) + "\n");
|
|
2123
|
-
return 0;
|
|
2124
|
-
}
|
|
2125
|
-
section("Review submissions");
|
|
2126
|
-
if (submissions.length === 0) {
|
|
2127
|
-
nop("none");
|
|
2128
|
-
return 0;
|
|
2129
|
-
}
|
|
2130
|
-
for (const s of submissions) {
|
|
2131
|
-
line(
|
|
2132
|
-
` ${BOLD}${s.id.slice(0, 8)}${RESET} ${s.attributes.state ?? "?"} ${DIM}${s.attributes.submittedDate ?? ""}${RESET}`
|
|
2133
|
-
);
|
|
2134
|
-
}
|
|
2135
|
-
return 0;
|
|
2136
|
-
} catch (err) {
|
|
2137
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2138
|
-
return 1;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
async function runPhasedRelease(opts) {
|
|
2142
|
-
try {
|
|
2143
|
-
const { v } = await bootstrap();
|
|
2144
|
-
const phased = await v.phasedReleases.getForVersion(opts.versionId);
|
|
2145
|
-
if (!phased) {
|
|
2146
|
-
bad(`no phased release for version ${opts.versionId}`);
|
|
2147
|
-
return 1;
|
|
2148
|
-
}
|
|
2149
|
-
const next = opts.action === "pause" ? await v.phasedReleases.pause(phased.id) : opts.action === "resume" ? await v.phasedReleases.resume(phased.id) : await v.phasedReleases.complete(phased.id);
|
|
2150
|
-
section(`Phased release ${opts.action}d`);
|
|
2151
|
-
ok(next.attributes.phasedReleaseState ?? "ok");
|
|
2152
|
-
return 0;
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2155
|
-
return 1;
|
|
2156
2569
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
try {
|
|
2177
|
-
const { r, ascAppId } = await bootstrap2();
|
|
2178
|
-
const list = await r.customerReviews.list({
|
|
2179
|
-
appId: ascAppId,
|
|
2180
|
-
territory: opts.territory,
|
|
2181
|
-
rating: opts.rating,
|
|
2182
|
-
limit: opts.limit
|
|
2183
|
-
});
|
|
2184
|
-
if (opts.json) {
|
|
2185
|
-
process.stdout.write(JSON.stringify(list, null, 2) + "\n");
|
|
2186
|
-
return 0;
|
|
2570
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2571
|
+
config.entries.forEach((raw, index) => {
|
|
2572
|
+
if (!isRecord(raw)) {
|
|
2573
|
+
issues.push({ severity: "error", message: `entry[${index}] must be an object` });
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const family = raw.deviceFamily;
|
|
2577
|
+
if (typeof family !== "string" || !ACCESSIBILITY_DEVICE_FAMILIES.includes(family)) {
|
|
2578
|
+
issues.push({
|
|
2579
|
+
severity: "error",
|
|
2580
|
+
message: `entry[${index}].deviceFamily '${String(family)}' is not a valid AccessibilityDeviceFamily. Allowed: ${ACCESSIBILITY_DEVICE_FAMILIES.join(", ")}`
|
|
2581
|
+
});
|
|
2582
|
+
} else if (seen.has(family)) {
|
|
2583
|
+
issues.push({
|
|
2584
|
+
severity: "warning",
|
|
2585
|
+
message: `entry[${index}].deviceFamily '${family}' is duplicated; only the last entry counts.`
|
|
2586
|
+
});
|
|
2587
|
+
} else {
|
|
2588
|
+
seen.add(family);
|
|
2187
2589
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
return 0;
|
|
2590
|
+
if (!isRecord(raw.features)) {
|
|
2591
|
+
issues.push({ severity: "error", message: `entry[${index}].features must be an object` });
|
|
2592
|
+
return;
|
|
2192
2593
|
}
|
|
2193
|
-
for (const
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2594
|
+
for (const [feature, level] of Object.entries(raw.features)) {
|
|
2595
|
+
if (!ACCESSIBILITY_FEATURES.includes(feature)) {
|
|
2596
|
+
issues.push({
|
|
2597
|
+
severity: "error",
|
|
2598
|
+
message: `entry[${index}].features['${feature}'] is not a valid AccessibilityFeature. Allowed: ${ACCESSIBILITY_FEATURES.join(", ")}`
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
if (typeof level !== "string" || !ACCESSIBILITY_LEVELS.includes(level)) {
|
|
2602
|
+
issues.push({
|
|
2603
|
+
severity: "error",
|
|
2604
|
+
message: `entry[${index}].features['${feature}'] level '${String(level)}' is not a valid AccessibilityLevel. Allowed: ${ACCESSIBILITY_LEVELS.join(", ")}`
|
|
2605
|
+
});
|
|
2204
2606
|
}
|
|
2205
|
-
line(` ${DIM}id ${rev.id}${RESET}`);
|
|
2206
2607
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
}
|
|
2608
|
+
});
|
|
2609
|
+
return issues;
|
|
2610
|
+
}
|
|
2611
|
+
async function fetchAccessibilityDeclarations(client, appId) {
|
|
2612
|
+
return client.request("GET", `/v1/apps/${appId}/accessibilityDeclarations`);
|
|
2613
|
+
}
|
|
2614
|
+
function isRecord(value) {
|
|
2615
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2212
2616
|
}
|
|
2213
|
-
|
|
2617
|
+
|
|
2618
|
+
// src/commands/asc-accessibility.ts
|
|
2619
|
+
async function runAccessibilityShow(opts) {
|
|
2214
2620
|
try {
|
|
2215
|
-
const {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
process.stdout.write(JSON.stringify(unresponded, null, 2) + "\n");
|
|
2220
|
-
return 0;
|
|
2621
|
+
const { client, ascAppId, bundleId } = await ascBootstrap();
|
|
2622
|
+
if (!ascAppId) {
|
|
2623
|
+
bad(`no ASC app for bundle id ${bundleId ?? "(unset)"}`);
|
|
2624
|
+
return 1;
|
|
2221
2625
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
if (unresponded.length === 0) {
|
|
2226
|
-
ok("all caught up");
|
|
2626
|
+
const decls = await fetchAccessibilityDeclarations(client, ascAppId);
|
|
2627
|
+
if (opts.json) {
|
|
2628
|
+
process.stdout.write(JSON.stringify(decls, null, 2) + "\n");
|
|
2227
2629
|
return 0;
|
|
2228
2630
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
line(
|
|
2232
|
-
` ${stars(rev.attributes.rating)} ${DIM}${created} ${rev.attributes.territory ?? ""}${RESET} ${BOLD}${rev.attributes.title ?? ""}${RESET}`
|
|
2233
|
-
);
|
|
2234
|
-
line(` ${DIM}respond: vexpo reviews respond ${rev.id} "..."${RESET}`);
|
|
2235
|
-
}
|
|
2631
|
+
section("Accessibility declarations");
|
|
2632
|
+
line(JSON.stringify(decls, null, 2));
|
|
2236
2633
|
return 0;
|
|
2237
2634
|
} catch (err) {
|
|
2238
2635
|
bad(err instanceof Error ? err.message : String(err));
|
|
2239
2636
|
return 1;
|
|
2240
2637
|
}
|
|
2241
2638
|
}
|
|
2242
|
-
async function
|
|
2639
|
+
async function runAccessibilityLint(filePath) {
|
|
2640
|
+
let parsed;
|
|
2243
2641
|
try {
|
|
2244
|
-
|
|
2245
|
-
const response = await r.customerReviewResponses.create({
|
|
2246
|
-
reviewId: opts.reviewId,
|
|
2247
|
-
responseBody: opts.body
|
|
2248
|
-
});
|
|
2249
|
-
section(`Responded to ${opts.reviewId}`);
|
|
2250
|
-
ok(`response ${response.id} ${DIM}${response.attributes.state ?? ""}${RESET}`);
|
|
2251
|
-
return 0;
|
|
2642
|
+
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
2252
2643
|
} catch (err) {
|
|
2253
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2644
|
+
bad(`failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2254
2645
|
return 1;
|
|
2255
2646
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2647
|
+
const issues = lintAccessibilityConfig(parsed);
|
|
2648
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
2649
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2650
|
+
section(`Accessibility lint: ${filePath}`);
|
|
2651
|
+
for (const i of issues) {
|
|
2652
|
+
const tag = i.severity === "error" ? `${RED}error${RESET}` : `${YELLOW}warn${RESET}`;
|
|
2653
|
+
line(` ${tag} ${i.message}`);
|
|
2654
|
+
}
|
|
2655
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
2656
|
+
ok("clean");
|
|
2263
2657
|
return 0;
|
|
2264
|
-
} catch (err) {
|
|
2265
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2266
|
-
return 1;
|
|
2267
2658
|
}
|
|
2659
|
+
line(`${BOLD}${errors.length}${RESET} error(s), ${BOLD}${warnings.length}${RESET} warning(s)`);
|
|
2660
|
+
return errors.length > 0 ? 1 : 0;
|
|
2268
2661
|
}
|
|
2269
2662
|
|
|
2270
|
-
// src/lib/asc-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2663
|
+
// src/lib/asc-privacy.ts
|
|
2664
|
+
var PRIVACY_DATA_TYPES = [
|
|
2665
|
+
"CONTACT_INFO",
|
|
2666
|
+
"HEALTH_FITNESS",
|
|
2667
|
+
"FINANCIAL_INFO",
|
|
2668
|
+
"LOCATION",
|
|
2669
|
+
"SENSITIVE_INFO",
|
|
2670
|
+
"CONTACTS",
|
|
2671
|
+
"USER_CONTENT",
|
|
2672
|
+
"BROWSING_HISTORY",
|
|
2673
|
+
"SEARCH_HISTORY",
|
|
2674
|
+
"IDENTIFIERS",
|
|
2675
|
+
"PURCHASES",
|
|
2676
|
+
"USAGE_DATA",
|
|
2677
|
+
"DIAGNOSTICS",
|
|
2678
|
+
"OTHER_DATA"
|
|
2679
|
+
];
|
|
2680
|
+
var PRIVACY_PURPOSES = [
|
|
2681
|
+
"THIRD_PARTY_ADVERTISING",
|
|
2682
|
+
"DEVELOPER_ADVERTISING",
|
|
2683
|
+
"ANALYTICS",
|
|
2684
|
+
"PRODUCT_PERSONALIZATION",
|
|
2685
|
+
"APP_FUNCTIONALITY",
|
|
2686
|
+
"OTHER"
|
|
2687
|
+
];
|
|
2688
|
+
function lintPrivacyConfig(config) {
|
|
2689
|
+
const issues = [];
|
|
2690
|
+
if (!isRecord2(config)) {
|
|
2691
|
+
issues.push({ severity: "error", message: "config must be a JSON object" });
|
|
2692
|
+
return issues;
|
|
2693
|
+
}
|
|
2694
|
+
if (typeof config.collectsData !== "boolean") {
|
|
2695
|
+
issues.push({ severity: "error", message: "`collectsData` must be a boolean" });
|
|
2696
|
+
}
|
|
2697
|
+
if (!Array.isArray(config.entries)) {
|
|
2698
|
+
issues.push({ severity: "error", message: "`entries` must be an array" });
|
|
2699
|
+
return issues;
|
|
2700
|
+
}
|
|
2701
|
+
if (config.collectsData === false && config.entries.length > 0) {
|
|
2702
|
+
issues.push({
|
|
2703
|
+
severity: "warning",
|
|
2704
|
+
message: "`collectsData` is false but `entries` is non-empty; entries will be ignored."
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
if (config.collectsData === true && config.entries.length === 0) {
|
|
2708
|
+
issues.push({
|
|
2709
|
+
severity: "error",
|
|
2710
|
+
message: "`collectsData` is true but `entries` is empty; declare at least one data type."
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
const seenCategories = /* @__PURE__ */ new Set();
|
|
2714
|
+
config.entries.forEach((raw, index) => {
|
|
2715
|
+
if (!isRecord2(raw)) {
|
|
2716
|
+
issues.push({ severity: "error", message: `entry[${index}] must be an object` });
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
const category = raw.category;
|
|
2720
|
+
if (typeof category !== "string" || !PRIVACY_DATA_TYPES.includes(category)) {
|
|
2721
|
+
issues.push({
|
|
2722
|
+
severity: "error",
|
|
2723
|
+
message: `entry[${index}].category '${String(category)}' is not a valid PrivacyDataType. Allowed: ${PRIVACY_DATA_TYPES.join(", ")}`
|
|
2724
|
+
});
|
|
2725
|
+
} else if (seenCategories.has(category)) {
|
|
2726
|
+
issues.push({
|
|
2727
|
+
severity: "warning",
|
|
2728
|
+
message: `entry[${index}].category '${category}' is duplicated; only the last entry counts.`
|
|
2729
|
+
});
|
|
2730
|
+
} else {
|
|
2731
|
+
seenCategories.add(category);
|
|
2307
2732
|
}
|
|
2308
|
-
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// src/commands/sandbox.ts
|
|
2312
|
-
async function client() {
|
|
2313
|
-
const { client: ascClient } = await ascBootstrap();
|
|
2314
|
-
return sandbox(ascClient);
|
|
2315
|
-
}
|
|
2316
|
-
async function runSandboxList(opts = {}) {
|
|
2317
|
-
try {
|
|
2318
|
-
const s = await client();
|
|
2319
|
-
const list = await s.sandboxTesters.list();
|
|
2320
|
-
if (opts.json) {
|
|
2321
|
-
process.stdout.write(JSON.stringify(list, null, 2) + "\n");
|
|
2322
|
-
return 0;
|
|
2733
|
+
if (typeof raw.collected !== "boolean") {
|
|
2734
|
+
issues.push({ severity: "error", message: `entry[${index}].collected must be a boolean` });
|
|
2323
2735
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2736
|
+
if (typeof raw.usedForTracking !== "boolean") {
|
|
2737
|
+
issues.push({
|
|
2738
|
+
severity: "error",
|
|
2739
|
+
message: `entry[${index}].usedForTracking must be a boolean`
|
|
2740
|
+
});
|
|
2328
2741
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
`
|
|
2333
|
-
);
|
|
2742
|
+
if (typeof raw.linkedToUser !== "boolean") {
|
|
2743
|
+
issues.push({
|
|
2744
|
+
severity: "error",
|
|
2745
|
+
message: `entry[${index}].linkedToUser must be a boolean`
|
|
2746
|
+
});
|
|
2334
2747
|
}
|
|
2748
|
+
if (!Array.isArray(raw.purposes)) {
|
|
2749
|
+
issues.push({ severity: "error", message: `entry[${index}].purposes must be an array` });
|
|
2750
|
+
} else {
|
|
2751
|
+
raw.purposes.forEach((purpose, j) => {
|
|
2752
|
+
if (typeof purpose !== "string" || !PRIVACY_PURPOSES.includes(purpose)) {
|
|
2753
|
+
issues.push({
|
|
2754
|
+
severity: "error",
|
|
2755
|
+
message: `entry[${index}].purposes[${j}] '${String(purpose)}' is not a valid PrivacyPurpose. Allowed: ${PRIVACY_PURPOSES.join(", ")}`
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
return issues;
|
|
2762
|
+
}
|
|
2763
|
+
function isRecord2(value) {
|
|
2764
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/commands/asc-privacy.ts
|
|
2768
|
+
var ASC_PRIVACY_URL = "https://appstoreconnect.apple.com";
|
|
2769
|
+
async function runPrivacyShow(file, opts = {}) {
|
|
2770
|
+
if (!existsSync(file)) {
|
|
2771
|
+
section("Privacy details");
|
|
2772
|
+
note(`no local ${file}. Apple's API can't read the live label; set it in App Store Connect:`);
|
|
2773
|
+
note(` ${ASC_PRIVACY_URL} -> your app -> App Privacy`);
|
|
2335
2774
|
return 0;
|
|
2336
|
-
} catch (err) {
|
|
2337
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2338
|
-
return 1;
|
|
2339
2775
|
}
|
|
2340
|
-
|
|
2341
|
-
async function runSandboxCreate(opts) {
|
|
2776
|
+
let parsed;
|
|
2342
2777
|
try {
|
|
2343
|
-
|
|
2344
|
-
const created = await s.sandboxTesters.create(opts);
|
|
2345
|
-
section(`Sandbox tester ${opts.email}`);
|
|
2346
|
-
ok(`id ${created.id}`);
|
|
2347
|
-
return 0;
|
|
2778
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
2348
2779
|
} catch (err) {
|
|
2349
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2780
|
+
bad(`failed to read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2350
2781
|
return 1;
|
|
2351
2782
|
}
|
|
2783
|
+
if (opts.json) {
|
|
2784
|
+
process.stdout.write(JSON.stringify(parsed, null, 2) + "\n");
|
|
2785
|
+
return 0;
|
|
2786
|
+
}
|
|
2787
|
+
section(`Privacy details (declared in ${file})`);
|
|
2788
|
+
const config = parsed;
|
|
2789
|
+
if (!config.collectsData) {
|
|
2790
|
+
line(` ${BOLD}Data Not Collected${RESET}`);
|
|
2791
|
+
return 0;
|
|
2792
|
+
}
|
|
2793
|
+
for (const e of config.entries ?? []) {
|
|
2794
|
+
const flags = [
|
|
2795
|
+
e.usedForTracking ? "tracking" : "",
|
|
2796
|
+
e.linkedToUser ? "linked" : "",
|
|
2797
|
+
Array.isArray(e.purposes) ? e.purposes.join(",") : ""
|
|
2798
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2799
|
+
line(` ${BOLD}${String(e.category)}${RESET} ${DIM}${flags}${RESET}`);
|
|
2800
|
+
}
|
|
2801
|
+
return 0;
|
|
2352
2802
|
}
|
|
2353
|
-
async function
|
|
2803
|
+
async function runPrivacyLint(filePath) {
|
|
2804
|
+
let parsed;
|
|
2354
2805
|
try {
|
|
2355
|
-
|
|
2356
|
-
await s.sandboxTesters.delete(id);
|
|
2357
|
-
section(`Deleted sandbox tester ${id}`);
|
|
2358
|
-
ok("done");
|
|
2359
|
-
return 0;
|
|
2806
|
+
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
2360
2807
|
} catch (err) {
|
|
2361
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2808
|
+
bad(`failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2362
2809
|
return 1;
|
|
2363
2810
|
}
|
|
2811
|
+
const issues = lintPrivacyConfig(parsed);
|
|
2812
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
2813
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
2814
|
+
section(`Privacy lint: ${filePath}`);
|
|
2815
|
+
for (const i of issues) {
|
|
2816
|
+
const tag = i.severity === "error" ? `${RED}error${RESET}` : `${YELLOW}warn${RESET}`;
|
|
2817
|
+
line(` ${tag} ${i.message}`);
|
|
2818
|
+
}
|
|
2819
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
2820
|
+
ok("clean");
|
|
2821
|
+
return 0;
|
|
2822
|
+
}
|
|
2823
|
+
line(`${BOLD}${errors.length}${RESET} error(s), ${BOLD}${warnings.length}${RESET} warning(s)`);
|
|
2824
|
+
return errors.length > 0 ? 1 : 0;
|
|
2364
2825
|
}
|
|
2365
2826
|
|
|
2366
2827
|
// src/lib/asc-testflight.ts
|
|
2367
|
-
function testflight(
|
|
2828
|
+
function testflight(client) {
|
|
2368
2829
|
return {
|
|
2369
|
-
/* beta groups -------------------------------------------------------- */
|
|
2370
2830
|
betaGroups: {
|
|
2371
2831
|
list(filter) {
|
|
2372
2832
|
const query = {};
|
|
2373
2833
|
if (filter?.appId) query["filter[app]"] = filter.appId;
|
|
2374
2834
|
if (filter?.name) query["filter[name]"] = filter.name;
|
|
2375
|
-
return
|
|
2835
|
+
return client.paginatedList("/v1/betaGroups", query);
|
|
2376
2836
|
},
|
|
2377
2837
|
async get(id) {
|
|
2378
|
-
const res = await
|
|
2838
|
+
const res = await client.request("GET", `/v1/betaGroups/${id}`);
|
|
2379
2839
|
return res.data;
|
|
2380
2840
|
},
|
|
2381
2841
|
async create(args) {
|
|
@@ -2384,8 +2844,6 @@ function testflight(client2) {
|
|
|
2384
2844
|
type: "betaGroups",
|
|
2385
2845
|
attributes: {
|
|
2386
2846
|
name: args.name,
|
|
2387
|
-
...args.publicLinkEnabled !== void 0 ? { publicLinkEnabled: args.publicLinkEnabled } : {},
|
|
2388
|
-
...args.publicLinkLimit !== void 0 ? { publicLinkLimit: args.publicLinkLimit, publicLinkLimitEnabled: true } : {},
|
|
2389
2847
|
...args.feedbackEnabled !== void 0 ? { feedbackEnabled: args.feedbackEnabled } : {}
|
|
2390
2848
|
},
|
|
2391
2849
|
relationships: {
|
|
@@ -2393,39 +2851,30 @@ function testflight(client2) {
|
|
|
2393
2851
|
}
|
|
2394
2852
|
}
|
|
2395
2853
|
};
|
|
2396
|
-
const res = await
|
|
2854
|
+
const res = await client.request("POST", "/v1/betaGroups", body);
|
|
2397
2855
|
return res.data;
|
|
2398
2856
|
},
|
|
2399
2857
|
async delete(id) {
|
|
2400
|
-
await
|
|
2858
|
+
await client.request("DELETE", `/v1/betaGroups/${id}`);
|
|
2401
2859
|
},
|
|
2402
2860
|
async listTesters(groupId) {
|
|
2403
|
-
return
|
|
2861
|
+
return client.paginatedList(`/v1/betaGroups/${groupId}/betaTesters`);
|
|
2404
2862
|
},
|
|
2405
2863
|
async addTesters(groupId, testerIds) {
|
|
2406
2864
|
const body = { data: testerIds.map((id) => ({ type: "betaTesters", id })) };
|
|
2407
|
-
await
|
|
2865
|
+
await client.request(
|
|
2408
2866
|
"POST",
|
|
2409
2867
|
`/v1/betaGroups/${groupId}/relationships/betaTesters`,
|
|
2410
2868
|
body
|
|
2411
2869
|
);
|
|
2412
|
-
},
|
|
2413
|
-
async removeTesters(groupId, testerIds) {
|
|
2414
|
-
const body = { data: testerIds.map((id) => ({ type: "betaTesters", id })) };
|
|
2415
|
-
await client2.request(
|
|
2416
|
-
"DELETE",
|
|
2417
|
-
`/v1/betaGroups/${groupId}/relationships/betaTesters`,
|
|
2418
|
-
body
|
|
2419
|
-
);
|
|
2420
2870
|
}
|
|
2421
2871
|
},
|
|
2422
|
-
/* beta testers ------------------------------------------------------- */
|
|
2423
2872
|
betaTesters: {
|
|
2424
2873
|
list(filter) {
|
|
2425
2874
|
const query = {};
|
|
2426
2875
|
if (filter?.email) query["filter[email]"] = filter.email;
|
|
2427
2876
|
if (filter?.appId) query["filter[apps]"] = filter.appId;
|
|
2428
|
-
return
|
|
2877
|
+
return client.paginatedList("/v1/betaTesters", query);
|
|
2429
2878
|
},
|
|
2430
2879
|
async create(args) {
|
|
2431
2880
|
const body = {
|
|
@@ -2450,14 +2899,10 @@ function testflight(client2) {
|
|
|
2450
2899
|
}
|
|
2451
2900
|
}
|
|
2452
2901
|
};
|
|
2453
|
-
const res = await
|
|
2902
|
+
const res = await client.request("POST", "/v1/betaTesters", body);
|
|
2454
2903
|
return res.data;
|
|
2455
|
-
},
|
|
2456
|
-
async delete(id) {
|
|
2457
|
-
await client2.request("DELETE", `/v1/betaTesters/${id}`);
|
|
2458
2904
|
}
|
|
2459
2905
|
},
|
|
2460
|
-
/* invitations -------------------------------------------------------- */
|
|
2461
2906
|
betaTesterInvitations: {
|
|
2462
2907
|
async create(args) {
|
|
2463
2908
|
const body = {
|
|
@@ -2469,7 +2914,7 @@ function testflight(client2) {
|
|
|
2469
2914
|
}
|
|
2470
2915
|
}
|
|
2471
2916
|
};
|
|
2472
|
-
const res = await
|
|
2917
|
+
const res = await client.request(
|
|
2473
2918
|
"POST",
|
|
2474
2919
|
"/v1/betaTesterInvitations",
|
|
2475
2920
|
body
|
|
@@ -2477,15 +2922,9 @@ function testflight(client2) {
|
|
|
2477
2922
|
return res.data;
|
|
2478
2923
|
}
|
|
2479
2924
|
},
|
|
2480
|
-
/* beta build localizations ------------------------------------------ */
|
|
2481
2925
|
betaBuildLocalizations: {
|
|
2482
|
-
list(buildId) {
|
|
2483
|
-
return client2.paginatedList(
|
|
2484
|
-
`/v1/builds/${buildId}/betaBuildLocalizations`
|
|
2485
|
-
);
|
|
2486
|
-
},
|
|
2487
2926
|
async upsert(args) {
|
|
2488
|
-
const existing = await
|
|
2927
|
+
const existing = await client.paginatedList(
|
|
2489
2928
|
`/v1/builds/${args.buildId}/betaBuildLocalizations`,
|
|
2490
2929
|
{ "filter[locale]": args.locale }
|
|
2491
2930
|
);
|
|
@@ -2497,7 +2936,7 @@ function testflight(client2) {
|
|
|
2497
2936
|
attributes: { whatsNew: args.whatsNew }
|
|
2498
2937
|
}
|
|
2499
2938
|
};
|
|
2500
|
-
const res2 = await
|
|
2939
|
+
const res2 = await client.request(
|
|
2501
2940
|
"PATCH",
|
|
2502
2941
|
`/v1/betaBuildLocalizations/${existing[0].id}`,
|
|
2503
2942
|
body2
|
|
@@ -2513,44 +2952,30 @@ function testflight(client2) {
|
|
|
2513
2952
|
}
|
|
2514
2953
|
}
|
|
2515
2954
|
};
|
|
2516
|
-
const res = await
|
|
2955
|
+
const res = await client.request(
|
|
2517
2956
|
"POST",
|
|
2518
2957
|
"/v1/betaBuildLocalizations",
|
|
2519
2958
|
body
|
|
2520
2959
|
);
|
|
2521
2960
|
return res.data;
|
|
2522
2961
|
}
|
|
2523
|
-
},
|
|
2524
|
-
/* builds (read-only) ------------------------------------------------- */
|
|
2525
|
-
builds: {
|
|
2526
|
-
list(filter) {
|
|
2527
|
-
const query = {};
|
|
2528
|
-
if (filter?.appId) query["filter[app]"] = filter.appId;
|
|
2529
|
-
if (filter?.version) query["filter[version]"] = filter.version;
|
|
2530
|
-
if (filter?.processingState) query["filter[processingState]"] = filter.processingState;
|
|
2531
|
-
return client2.paginatedList("/v1/builds", query, filter?.limit ?? 50);
|
|
2532
|
-
},
|
|
2533
|
-
async get(id) {
|
|
2534
|
-
const res = await client2.request("GET", `/v1/builds/${id}`);
|
|
2535
|
-
return res.data;
|
|
2536
|
-
}
|
|
2537
2962
|
}
|
|
2538
2963
|
};
|
|
2539
2964
|
}
|
|
2540
2965
|
|
|
2541
2966
|
// src/commands/testflight.ts
|
|
2542
|
-
async function
|
|
2543
|
-
const { client
|
|
2967
|
+
async function bootstrap() {
|
|
2968
|
+
const { client, ascAppId, bundleId } = await ascBootstrap();
|
|
2544
2969
|
if (!ascAppId) {
|
|
2545
2970
|
throw new Error(
|
|
2546
2971
|
`no ASC app found for bundle id ${bundleId ?? "(unset)"}; run \`vexpo apple credentials\` first`
|
|
2547
2972
|
);
|
|
2548
2973
|
}
|
|
2549
|
-
return { tf: testflight(
|
|
2974
|
+
return { tf: testflight(client), ascAppId };
|
|
2550
2975
|
}
|
|
2551
2976
|
async function runTestflightGroupsList(opts = {}) {
|
|
2552
2977
|
try {
|
|
2553
|
-
const { tf, ascAppId } = await
|
|
2978
|
+
const { tf, ascAppId } = await bootstrap();
|
|
2554
2979
|
const groups = await tf.betaGroups.list({ appId: ascAppId });
|
|
2555
2980
|
if (opts.json) {
|
|
2556
2981
|
process.stdout.write(JSON.stringify(groups, null, 2) + "\n");
|
|
@@ -2574,17 +2999,14 @@ async function runTestflightGroupsList(opts = {}) {
|
|
|
2574
2999
|
}
|
|
2575
3000
|
async function runTestflightGroupsCreate(opts) {
|
|
2576
3001
|
try {
|
|
2577
|
-
const { tf, ascAppId } = await
|
|
3002
|
+
const { tf, ascAppId } = await bootstrap();
|
|
2578
3003
|
const created = await tf.betaGroups.create({
|
|
2579
3004
|
name: opts.name,
|
|
2580
3005
|
appId: ascAppId,
|
|
2581
|
-
publicLinkEnabled: opts.publicLink,
|
|
2582
|
-
publicLinkLimit: opts.publicLimit,
|
|
2583
3006
|
feedbackEnabled: opts.feedback
|
|
2584
3007
|
});
|
|
2585
3008
|
section(`Beta group ${created.attributes.name}`);
|
|
2586
3009
|
ok(`id ${created.id}`);
|
|
2587
|
-
if (created.attributes.publicLink) line(` public link: ${created.attributes.publicLink}`);
|
|
2588
3010
|
return 0;
|
|
2589
3011
|
} catch (err) {
|
|
2590
3012
|
bad(err instanceof Error ? err.message : String(err));
|
|
@@ -2593,7 +3015,7 @@ async function runTestflightGroupsCreate(opts) {
|
|
|
2593
3015
|
}
|
|
2594
3016
|
async function runTestflightGroupsView(groupId, opts) {
|
|
2595
3017
|
try {
|
|
2596
|
-
const { tf } = await
|
|
3018
|
+
const { tf } = await bootstrap();
|
|
2597
3019
|
const [group, testers] = await Promise.all([
|
|
2598
3020
|
tf.betaGroups.get(groupId),
|
|
2599
3021
|
tf.betaGroups.listTesters(groupId).catch(() => [])
|
|
@@ -2618,298 +3040,89 @@ async function runTestflightGroupsView(groupId, opts) {
|
|
|
2618
3040
|
return 1;
|
|
2619
3041
|
}
|
|
2620
3042
|
}
|
|
2621
|
-
async function runTestflightGroupsDelete(groupId) {
|
|
2622
|
-
try {
|
|
2623
|
-
const { tf } = await bootstrap3();
|
|
2624
|
-
await tf.betaGroups.delete(groupId);
|
|
2625
|
-
section(`Group ${groupId} deleted`);
|
|
2626
|
-
ok("done");
|
|
2627
|
-
return 0;
|
|
2628
|
-
} catch (err) {
|
|
2629
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2630
|
-
return 1;
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
async function runTestflightTestersList(opts) {
|
|
2634
|
-
try {
|
|
2635
|
-
const { tf, ascAppId } = await bootstrap3();
|
|
2636
|
-
const testers = await tf.betaTesters.list({ appId: ascAppId, email: opts.email });
|
|
2637
|
-
if (opts.json) {
|
|
2638
|
-
process.stdout.write(JSON.stringify(testers, null, 2) + "\n");
|
|
2639
|
-
return 0;
|
|
2640
|
-
}
|
|
2641
|
-
section("Beta testers");
|
|
2642
|
-
if (testers.length === 0) {
|
|
2643
|
-
nop("none");
|
|
2644
|
-
return 0;
|
|
2645
|
-
}
|
|
2646
|
-
for (const t of testers) {
|
|
2647
|
-
const name = `${t.attributes.firstName ?? ""} ${t.attributes.lastName ?? ""}`.trim();
|
|
2648
|
-
line(
|
|
2649
|
-
` ${BOLD}${t.attributes.email ?? "(no email)"}${RESET} ${name ? DIM + name + RESET + " " : ""}${DIM}${t.attributes.state ?? ""}${RESET}`
|
|
2650
|
-
);
|
|
2651
|
-
}
|
|
2652
|
-
return 0;
|
|
2653
|
-
} catch (err) {
|
|
2654
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2655
|
-
return 1;
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
async function runTestflightInvite(opts) {
|
|
2659
|
-
try {
|
|
2660
|
-
const { tf, ascAppId } = await bootstrap3();
|
|
2661
|
-
const existing = await tf.betaTesters.list({ email: opts.email, appId: ascAppId });
|
|
2662
|
-
let testerId = existing[0]?.id;
|
|
2663
|
-
if (!testerId) {
|
|
2664
|
-
const created = await tf.betaTesters.create({
|
|
2665
|
-
email: opts.email,
|
|
2666
|
-
firstName: opts.firstName,
|
|
2667
|
-
lastName: opts.lastName,
|
|
2668
|
-
appIds: [ascAppId],
|
|
2669
|
-
groupIds: opts.groupId ? [opts.groupId] : []
|
|
2670
|
-
});
|
|
2671
|
-
testerId = created.id;
|
|
2672
|
-
ok(`tester ${opts.email} added`);
|
|
2673
|
-
} else {
|
|
2674
|
-
ok(`tester ${opts.email} already exists (${testerId})`);
|
|
2675
|
-
if (opts.groupId) await tf.betaGroups.addTesters(opts.groupId, [testerId]);
|
|
2676
|
-
}
|
|
2677
|
-
const inv = await tf.betaTesterInvitations.create({ appId: ascAppId, testerId });
|
|
2678
|
-
section(`Invited ${opts.email}`);
|
|
2679
|
-
ok(`invitation ${inv.id}`);
|
|
2680
|
-
return 0;
|
|
2681
|
-
} catch (err) {
|
|
2682
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2683
|
-
return 1;
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
async function runTestflightRemove(email) {
|
|
2687
|
-
try {
|
|
2688
|
-
const { tf, ascAppId } = await bootstrap3();
|
|
2689
|
-
const matches = await tf.betaTesters.list({ email, appId: ascAppId });
|
|
2690
|
-
if (matches.length === 0) {
|
|
2691
|
-
bad(`no tester with email ${email}`);
|
|
2692
|
-
return 1;
|
|
2693
|
-
}
|
|
2694
|
-
for (const t of matches) await tf.betaTesters.delete(t.id);
|
|
2695
|
-
section(`Removed ${matches.length} tester${matches.length === 1 ? "" : "s"}`);
|
|
2696
|
-
ok("done");
|
|
2697
|
-
return 0;
|
|
2698
|
-
} catch (err) {
|
|
2699
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2700
|
-
return 1;
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
async function runTestflightWhatsNew(opts) {
|
|
2704
|
-
try {
|
|
2705
|
-
const { tf } = await bootstrap3();
|
|
2706
|
-
const loc = await tf.betaBuildLocalizations.upsert({
|
|
2707
|
-
buildId: opts.buildId,
|
|
2708
|
-
locale: opts.locale,
|
|
2709
|
-
whatsNew: opts.text
|
|
2710
|
-
});
|
|
2711
|
-
section(`What's new for build ${opts.buildId}`);
|
|
2712
|
-
ok(`upserted (${loc.attributes.locale})`);
|
|
2713
|
-
return 0;
|
|
2714
|
-
} catch (err) {
|
|
2715
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2716
|
-
return 1;
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
// src/commands/better-auth.ts
|
|
2721
|
-
function base64Secret() {
|
|
2722
|
-
const buf = new Uint8Array(32);
|
|
2723
|
-
crypto.getRandomValues(buf);
|
|
2724
|
-
return btoa(String.fromCharCode(...buf));
|
|
2725
|
-
}
|
|
2726
|
-
async function runBetterAuth(options2) {
|
|
2727
|
-
section("Better Auth env");
|
|
2728
|
-
try {
|
|
2729
|
-
const env2 = await envMap();
|
|
2730
|
-
const siteUrl = options2.siteUrl ?? `${await scheme()}://`;
|
|
2731
|
-
if (env2.has("SITE_URL") && env2.get("SITE_URL") === siteUrl) {
|
|
2732
|
-
nop(`SITE_URL already set to ${siteUrl}`);
|
|
2733
|
-
} else {
|
|
2734
|
-
await envSet("SITE_URL", siteUrl);
|
|
2735
|
-
ok(`set SITE_URL=${siteUrl}`);
|
|
2736
|
-
}
|
|
2737
|
-
if (env2.has("BETTER_AUTH_SECRET") && !options2.rotateSecret) {
|
|
2738
|
-
nop("BETTER_AUTH_SECRET already set (use --rotate-secret to regenerate)");
|
|
2739
|
-
} else {
|
|
2740
|
-
await envSet("BETTER_AUTH_SECRET", base64Secret());
|
|
2741
|
-
ok(
|
|
2742
|
-
options2.rotateSecret === true ? "rotated BETTER_AUTH_SECRET (sessions invalidated)" : "generated BETTER_AUTH_SECRET"
|
|
2743
|
-
);
|
|
2744
|
-
}
|
|
2745
|
-
const desiredAppName = options2.appName ?? await appName();
|
|
2746
|
-
if (env2.has("APP_NAME") && env2.get("APP_NAME") === desiredAppName) {
|
|
2747
|
-
nop(`APP_NAME already set to ${desiredAppName}`);
|
|
2748
|
-
} else {
|
|
2749
|
-
await envSet("APP_NAME", desiredAppName);
|
|
2750
|
-
ok(`set APP_NAME=${desiredAppName}`);
|
|
2751
|
-
}
|
|
2752
|
-
await recordStep("better-auth", {
|
|
2753
|
-
siteUrl,
|
|
2754
|
-
appName: desiredAppName,
|
|
2755
|
-
rotated: options2.rotateSecret === true
|
|
2756
|
-
});
|
|
2757
|
-
return 0;
|
|
2758
|
-
} catch (err) {
|
|
2759
|
-
bad(err instanceof Error ? err.message : String(err));
|
|
2760
|
-
return 1;
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
// src/commands/convex.ts
|
|
2765
|
-
var BUNDLE_ID_RE = /^[A-Za-z0-9.-]+$/;
|
|
2766
|
-
var TEAM_ID_RE = /^[A-Z0-9]{10}$/;
|
|
2767
|
-
async function runConvex(options2) {
|
|
2768
|
-
section("Convex deployment");
|
|
3043
|
+
async function runTestflightGroupsDelete(groupId) {
|
|
2769
3044
|
try {
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
const
|
|
2784
|
-
if (
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
"EXPO_PUBLIC_CONVEX_URL",
|
|
2788
|
-
"EXPO_PUBLIC_CONVEX_SITE_URL"
|
|
2789
|
-
]);
|
|
2790
|
-
}
|
|
2791
|
-
const needsProvisioning = options2.fresh === true || !existing;
|
|
2792
|
-
const projectName = options2.name ?? await pkgName();
|
|
2793
|
-
const cmd = [dlx(), "convex", "dev", "--once", "--tail-logs", "disable"];
|
|
2794
|
-
if (options2.local) cmd.push("--local");
|
|
2795
|
-
if (needsProvisioning) cmd.push("--configure", "new", "--project", projectName);
|
|
2796
|
-
if (needsProvisioning) {
|
|
2797
|
-
ok(`provisioning Convex project '${projectName}'`);
|
|
2798
|
-
} else {
|
|
2799
|
-
ok(`connecting to existing deployment ${existing}`);
|
|
2800
|
-
}
|
|
2801
|
-
const proc = spawn(cmd, { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
2802
|
-
if (await proc.exited !== 0) {
|
|
2803
|
-
bad("convex dev exited with a non-zero code");
|
|
2804
|
-
return 1;
|
|
2805
|
-
}
|
|
2806
|
-
const refreshed = await readAll();
|
|
2807
|
-
const deployment = refreshed.get("CONVEX_DEPLOYMENT");
|
|
2808
|
-
if (!deployment) {
|
|
2809
|
-
bad("CONVEX_DEPLOYMENT missing after convex dev ran");
|
|
2810
|
-
return 1;
|
|
2811
|
-
}
|
|
2812
|
-
const slug2 = deployment.split("#")[0].trim().split(":")[1];
|
|
2813
|
-
if (!slug2) {
|
|
2814
|
-
bad(`invalid CONVEX_DEPLOYMENT: ${deployment}`);
|
|
2815
|
-
return 1;
|
|
2816
|
-
}
|
|
2817
|
-
process.env.CONVEX_DEPLOYMENT = deployment;
|
|
2818
|
-
if (refreshed.has("EXPO_PUBLIC_CONVEX_URL")) {
|
|
2819
|
-
nop("EXPO_PUBLIC_CONVEX_URL already set");
|
|
2820
|
-
} else {
|
|
2821
|
-
await ensureLine("EXPO_PUBLIC_CONVEX_URL", `https://${slug2}.convex.cloud`);
|
|
2822
|
-
ok("wrote EXPO_PUBLIC_CONVEX_URL");
|
|
3045
|
+
const { tf } = await bootstrap();
|
|
3046
|
+
await tf.betaGroups.delete(groupId);
|
|
3047
|
+
section(`Group ${groupId} deleted`);
|
|
3048
|
+
ok("done");
|
|
3049
|
+
return 0;
|
|
3050
|
+
} catch (err) {
|
|
3051
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
3052
|
+
return 1;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
async function runTestflightTestersList(opts) {
|
|
3056
|
+
try {
|
|
3057
|
+
const { tf, ascAppId } = await bootstrap();
|
|
3058
|
+
const testers = await tf.betaTesters.list({ appId: ascAppId, email: opts.email });
|
|
3059
|
+
if (opts.json) {
|
|
3060
|
+
process.stdout.write(JSON.stringify(testers, null, 2) + "\n");
|
|
3061
|
+
return 0;
|
|
2823
3062
|
}
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
ok("wrote EXPO_PUBLIC_CONVEX_SITE_URL");
|
|
3063
|
+
section("Beta testers");
|
|
3064
|
+
if (testers.length === 0) {
|
|
3065
|
+
nop("none");
|
|
3066
|
+
return 0;
|
|
2829
3067
|
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
ok(`wrote EXPO_PUBLIC_SITE_URL=${s}`);
|
|
3068
|
+
for (const t of testers) {
|
|
3069
|
+
const name = `${t.attributes.firstName ?? ""} ${t.attributes.lastName ?? ""}`.trim();
|
|
3070
|
+
line(
|
|
3071
|
+
` ${BOLD}${t.attributes.email ?? "(no email)"}${RESET} ${name ? DIM + name + RESET + " " : ""}${DIM}${t.attributes.state ?? ""}${RESET}`
|
|
3072
|
+
);
|
|
2836
3073
|
}
|
|
2837
|
-
await ensureIdentity(refreshed);
|
|
2838
|
-
await recordStep("convex", {
|
|
2839
|
-
deployment,
|
|
2840
|
-
slug: slug2,
|
|
2841
|
-
...options2.local ? { local: true } : {}
|
|
2842
|
-
});
|
|
2843
|
-
line();
|
|
2844
|
-
ok(`Convex deployment ready: ${BOLD}${slug2}${RESET}`);
|
|
2845
|
-
note(`dashboard: https://dashboard.convex.dev/d/${slug2}`);
|
|
2846
3074
|
return 0;
|
|
2847
3075
|
} catch (err) {
|
|
2848
3076
|
bad(err instanceof Error ? err.message : String(err));
|
|
2849
3077
|
return 1;
|
|
2850
3078
|
}
|
|
2851
3079
|
}
|
|
2852
|
-
async function
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
` iOS bundle id ${DIM}(reverse-DNS, e.g. com.you.app)${RESET}${cachedHint}
|
|
2868
|
-
${DIM}> ${suggested} ${RESET}`
|
|
2869
|
-
)).trim();
|
|
2870
|
-
bundleId = raw || suggested;
|
|
2871
|
-
if (!BUNDLE_ID_RE.test(bundleId)) {
|
|
2872
|
-
throw new Error(`invalid bundle id '${bundleId}' (allowed: A-Z a-z 0-9 . -)`);
|
|
2873
|
-
}
|
|
2874
|
-
await ensureLine("EXPO_PUBLIC_APP_BUNDLE_ID", bundleId);
|
|
2875
|
-
ok(`wrote EXPO_PUBLIC_APP_BUNDLE_ID=${bundleId}`);
|
|
2876
|
-
}
|
|
2877
|
-
} else {
|
|
2878
|
-
nop(`EXPO_PUBLIC_APP_BUNDLE_ID already set (${bundleId})`);
|
|
2879
|
-
}
|
|
2880
|
-
if (!haveTeam) {
|
|
2881
|
-
if (!process.stdin.isTTY) {
|
|
2882
|
-
yep("EXPO_PUBLIC_APPLE_TEAM_ID not set (non-TTY); skipping prompt");
|
|
3080
|
+
async function runTestflightInvite(opts) {
|
|
3081
|
+
try {
|
|
3082
|
+
const { tf, ascAppId } = await bootstrap();
|
|
3083
|
+
const existing = await tf.betaTesters.list({ email: opts.email, appId: ascAppId });
|
|
3084
|
+
let testerId = existing[0]?.id;
|
|
3085
|
+
if (!testerId) {
|
|
3086
|
+
const created = await tf.betaTesters.create({
|
|
3087
|
+
email: opts.email,
|
|
3088
|
+
firstName: opts.firstName,
|
|
3089
|
+
lastName: opts.lastName,
|
|
3090
|
+
appIds: [ascAppId],
|
|
3091
|
+
groupIds: opts.groupId ? [opts.groupId] : []
|
|
3092
|
+
});
|
|
3093
|
+
testerId = created.id;
|
|
3094
|
+
ok(`tester ${opts.email} added`);
|
|
2883
3095
|
} else {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
const raw = (await ask(
|
|
2887
|
-
` Apple Team id ${DIM}(10-char alphanumeric, find at developer.apple.com/account)${RESET}${cachedHint}
|
|
2888
|
-
${DIM}> ${RESET}`
|
|
2889
|
-
)).trim().toUpperCase();
|
|
2890
|
-
const value = raw || (fromConfig ?? "");
|
|
2891
|
-
if (!TEAM_ID_RE.test(value)) {
|
|
2892
|
-
throw new Error(`invalid Apple Team id '${value}' (must be 10 uppercase alphanumeric)`);
|
|
2893
|
-
}
|
|
2894
|
-
teamId = value;
|
|
2895
|
-
await ensureLine("EXPO_PUBLIC_APPLE_TEAM_ID", teamId);
|
|
2896
|
-
ok(`wrote EXPO_PUBLIC_APPLE_TEAM_ID=${teamId}`);
|
|
3096
|
+
ok(`tester ${opts.email} already exists (${testerId})`);
|
|
3097
|
+
if (opts.groupId) await tf.betaGroups.addTesters(opts.groupId, [testerId]);
|
|
2897
3098
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3099
|
+
const inv = await tf.betaTesterInvitations.create({ appId: ascAppId, testerId });
|
|
3100
|
+
section(`Invited ${opts.email}`);
|
|
3101
|
+
ok(`invitation ${inv.id}`);
|
|
3102
|
+
return 0;
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
3105
|
+
return 1;
|
|
2904
3106
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3107
|
+
}
|
|
3108
|
+
async function runTestflightWhatsNew(opts) {
|
|
3109
|
+
try {
|
|
3110
|
+
const { tf } = await bootstrap();
|
|
3111
|
+
const loc = await tf.betaBuildLocalizations.upsert({
|
|
3112
|
+
buildId: opts.buildId,
|
|
3113
|
+
locale: opts.locale,
|
|
3114
|
+
whatsNew: opts.text
|
|
3115
|
+
});
|
|
3116
|
+
section(`What's new for build ${opts.buildId}`);
|
|
3117
|
+
ok(`upserted (${loc.attributes.locale})`);
|
|
3118
|
+
return 0;
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
bad(err instanceof Error ? err.message : String(err));
|
|
3121
|
+
return 1;
|
|
2908
3122
|
}
|
|
2909
3123
|
}
|
|
2910
3124
|
var easEnvFor = (channel) => channel === "prod" ? ["production", "preview"] : ["development"];
|
|
2911
3125
|
var ROUTING = {
|
|
2912
|
-
// EAS-only (build-time public)
|
|
2913
3126
|
EXPO_PUBLIC_CONVEX_URL: {
|
|
2914
3127
|
routes: (c) => [{ type: "eas", key: "EXPO_PUBLIC_CONVEX_URL", environments: easEnvFor(c) }]
|
|
2915
3128
|
},
|
|
@@ -2930,7 +3143,6 @@ var ROUTING = {
|
|
|
2930
3143
|
EXPO_PUBLIC_EXPO_OWNER: {
|
|
2931
3144
|
routes: (c) => [{ type: "eas", key: "EXPO_PUBLIC_EXPO_OWNER", environments: easEnvFor(c) }]
|
|
2932
3145
|
},
|
|
2933
|
-
// Convex-bound server-side
|
|
2934
3146
|
SITE_URL: { routes: (c) => [{ type: "convex", key: "SITE_URL", channel: c }] },
|
|
2935
3147
|
BETTER_AUTH_SECRET: {
|
|
2936
3148
|
routes: (c) => [{ type: "convex", key: "BETTER_AUTH_SECRET", channel: c }]
|
|
@@ -2967,7 +3179,7 @@ var MANUAL_EAS_SECRETS = {
|
|
|
2967
3179
|
APPLE_P8_PRIVATE_KEY: "eas env:create --name APPLE_P8_PRIVATE_KEY --value-file <path>.p8 --environment production --visibility secret",
|
|
2968
3180
|
CONVEX_DEPLOY_KEY: "eas env:create --name CONVEX_DEPLOY_KEY --value <prod-deploy-key> --environment production --visibility secret"
|
|
2969
3181
|
};
|
|
2970
|
-
async function
|
|
3182
|
+
async function fileExists3(p) {
|
|
2971
3183
|
try {
|
|
2972
3184
|
await access(p);
|
|
2973
3185
|
return true;
|
|
@@ -2977,7 +3189,7 @@ async function fileExists4(p) {
|
|
|
2977
3189
|
}
|
|
2978
3190
|
async function readEnvFile(path) {
|
|
2979
3191
|
const out = /* @__PURE__ */ new Map();
|
|
2980
|
-
if (!await
|
|
3192
|
+
if (!await fileExists3(path)) return out;
|
|
2981
3193
|
const text = await readFile(path, "utf8");
|
|
2982
3194
|
let buffer = "";
|
|
2983
3195
|
let pendingKey = null;
|
|
@@ -3026,13 +3238,13 @@ async function readSources(paths) {
|
|
|
3026
3238
|
const local = paths?.local ?? ".env.local";
|
|
3027
3239
|
const prodCandidates = paths?.prod ? [paths.prod] : [".env.prod", ".env.production"];
|
|
3028
3240
|
const sources = [];
|
|
3029
|
-
if (await
|
|
3241
|
+
if (await fileExists3(local)) {
|
|
3030
3242
|
sources.push({ path: local, channel: "dev", entries: await readEnvFile(local) });
|
|
3031
3243
|
} else if (paths?.local) {
|
|
3032
3244
|
throw new Error(`--local-file path does not exist: ${paths.local}`);
|
|
3033
3245
|
}
|
|
3034
3246
|
for (const p of prodCandidates) {
|
|
3035
|
-
if (await
|
|
3247
|
+
if (await fileExists3(p)) {
|
|
3036
3248
|
sources.push({ path: p, channel: "prod", entries: await readEnvFile(p) });
|
|
3037
3249
|
break;
|
|
3038
3250
|
}
|
|
@@ -3082,7 +3294,90 @@ function missingKeys(sources) {
|
|
|
3082
3294
|
return { dev: [...dev].toSorted(), prod: [...prod].toSorted() };
|
|
3083
3295
|
}
|
|
3084
3296
|
|
|
3085
|
-
// src/
|
|
3297
|
+
// src/commands/convex-migrate.ts
|
|
3298
|
+
function selectMigratableEnv(src, dst) {
|
|
3299
|
+
const out = [];
|
|
3300
|
+
for (const [key, value] of src) {
|
|
3301
|
+
if (key.startsWith("CONVEX_")) continue;
|
|
3302
|
+
if (dst.get(key) === value) continue;
|
|
3303
|
+
out.push([key, value]);
|
|
3304
|
+
}
|
|
3305
|
+
return out;
|
|
3306
|
+
}
|
|
3307
|
+
async function fileExists4(p) {
|
|
3308
|
+
try {
|
|
3309
|
+
await access(p);
|
|
3310
|
+
return true;
|
|
3311
|
+
} catch {
|
|
3312
|
+
return false;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
async function runConvexMigrate(options2) {
|
|
3316
|
+
const channel = options2.prod ? "prod" : "dev";
|
|
3317
|
+
section(`Convex migrate (${channel})`);
|
|
3318
|
+
if (!options2.from) {
|
|
3319
|
+
bad("--from <deployment> is required (the source deployment slug)");
|
|
3320
|
+
return 1;
|
|
3321
|
+
}
|
|
3322
|
+
const fromSlug = deploymentSlug(options2.from) ?? options2.from;
|
|
3323
|
+
let target;
|
|
3324
|
+
if (options2.prod) {
|
|
3325
|
+
const prodFile = await fileExists4(".env.prod") ? ".env.prod" : ".env.production";
|
|
3326
|
+
const prodEnv = await readEnvFile(prodFile);
|
|
3327
|
+
const deployKey = prodEnv.get("CONVEX_DEPLOY_KEY") ?? "";
|
|
3328
|
+
const selector = prodEnv.get("CONVEX_DEPLOYMENT") ?? "";
|
|
3329
|
+
if (!deployKey.startsWith("prod:") && !selector.startsWith("prod:")) {
|
|
3330
|
+
bad(`${prodFile} has no prod-scoped CONVEX_DEPLOY_KEY or CONVEX_DEPLOYMENT`);
|
|
3331
|
+
note("the copy would land on the DEV deployment (the dev key shadows --prod)");
|
|
3332
|
+
return 1;
|
|
3333
|
+
}
|
|
3334
|
+
target = { prod: true, envFile: prodFile };
|
|
3335
|
+
}
|
|
3336
|
+
const src = await envMap({ deployment: fromSlug });
|
|
3337
|
+
if (src.size === 0) {
|
|
3338
|
+
bad(`no env on source deployment ${fromSlug} (unreachable or empty)`);
|
|
3339
|
+
note("pass a deployment slug your account can reach, e.g. `--from old-deployment-123`");
|
|
3340
|
+
return 1;
|
|
3341
|
+
}
|
|
3342
|
+
const dst = await envMap(target);
|
|
3343
|
+
const toMove = selectMigratableEnv(src, dst);
|
|
3344
|
+
if (toMove.length === 0) {
|
|
3345
|
+
ok(`target already matches ${fromSlug} (nothing to copy)`);
|
|
3346
|
+
return 0;
|
|
3347
|
+
}
|
|
3348
|
+
line();
|
|
3349
|
+
note(
|
|
3350
|
+
`${BOLD}${toMove.length}${RESET} server-side var${toMove.length === 1 ? "" : "s"} to copy from ${fromSlug}:`
|
|
3351
|
+
);
|
|
3352
|
+
for (const [key] of toMove) note(` ${key}`);
|
|
3353
|
+
if (options2.dryRun) {
|
|
3354
|
+
line();
|
|
3355
|
+
note("--dry-run; exiting without changes");
|
|
3356
|
+
return 0;
|
|
3357
|
+
}
|
|
3358
|
+
let failed = 0;
|
|
3359
|
+
for (const [key, value] of toMove) {
|
|
3360
|
+
try {
|
|
3361
|
+
await envSet(key, value, target);
|
|
3362
|
+
ok(`copied ${key}`);
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
bad(`${key} failed: ${err instanceof Error ? err.message : err}`);
|
|
3365
|
+
failed += 1;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
line();
|
|
3369
|
+
if (failed > 0) {
|
|
3370
|
+
bad(`${failed} write${failed === 1 ? "" : "s"} failed`);
|
|
3371
|
+
return 1;
|
|
3372
|
+
}
|
|
3373
|
+
ok(
|
|
3374
|
+
`migrated ${toMove.length} var${toMove.length === 1 ? "" : "s"} onto the ${channel} deployment`
|
|
3375
|
+
);
|
|
3376
|
+
note(`next: ${BOLD}vexpo env convex-key${RESET} (EAS deploy key + selector)`);
|
|
3377
|
+
note(` ${BOLD}vexpo resend --repoint${options2.prod ? " --prod" : ""}${RESET} (webhook)`);
|
|
3378
|
+
note(`then: ${BOLD}vexpo doctor --channel ${channel}${RESET}`);
|
|
3379
|
+
return 0;
|
|
3380
|
+
}
|
|
3086
3381
|
var ok2 = (category, name, message, details) => ({
|
|
3087
3382
|
category,
|
|
3088
3383
|
name,
|
|
@@ -3150,6 +3445,16 @@ async function verifyConvex(ctx) {
|
|
|
3150
3445
|
const checks = [];
|
|
3151
3446
|
const env2 = ctx.channel === "prod" ? ctx.convexProdEnv : ctx.convexEnv;
|
|
3152
3447
|
const local = ctx.channel === "prod" ? ctx.envProd : ctx.envLocal;
|
|
3448
|
+
if (local.get("CONVEX_DEPLOYMENT")) {
|
|
3449
|
+
const status = await checkToken();
|
|
3450
|
+
if (status === "unauthorized") {
|
|
3451
|
+
checks.push(
|
|
3452
|
+
fail2("convex", "login", "Convex token expired or revoked", "run `npx convex login`")
|
|
3453
|
+
);
|
|
3454
|
+
} else if (status === "valid") {
|
|
3455
|
+
checks.push(ok2("convex", "login", "token valid"));
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3153
3458
|
const cloudUrl = local.get("EXPO_PUBLIC_CONVEX_URL");
|
|
3154
3459
|
const siteUrl = local.get("EXPO_PUBLIC_CONVEX_SITE_URL");
|
|
3155
3460
|
if (cloudUrl) {
|
|
@@ -3201,6 +3506,25 @@ async function verifyConvex(ctx) {
|
|
|
3201
3506
|
} else {
|
|
3202
3507
|
checks.push(fail2("convex", "better-auth-secret", `not set on Convex (${ctx.channel})`));
|
|
3203
3508
|
}
|
|
3509
|
+
const deploymentName2 = deploymentSlug(local.get("CONVEX_DEPLOYMENT"));
|
|
3510
|
+
if (deploymentName2) {
|
|
3511
|
+
const deployments = await listProjectDeployments(deploymentName2);
|
|
3512
|
+
if (deployments) {
|
|
3513
|
+
const devs = deploymentsOfType(deployments, "dev");
|
|
3514
|
+
if (devs.length > 1) {
|
|
3515
|
+
checks.push(
|
|
3516
|
+
warn(
|
|
3517
|
+
"convex",
|
|
3518
|
+
"deployments",
|
|
3519
|
+
`${devs.length} dev deployments in this project`,
|
|
3520
|
+
`${devs.map(describeDeployment).join(", ")} \u2014 pick one canonical, delete the others`
|
|
3521
|
+
)
|
|
3522
|
+
);
|
|
3523
|
+
} else {
|
|
3524
|
+
checks.push(ok2("convex", "deployments", `${deployments.length} total, 1 dev`));
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3204
3528
|
return checks;
|
|
3205
3529
|
}
|
|
3206
3530
|
async function verifyResend(ctx) {
|
|
@@ -3213,21 +3537,21 @@ async function verifyResend(ctx) {
|
|
|
3213
3537
|
if (!apiKey) {
|
|
3214
3538
|
const requireEmailVerification = env2.get("REQUIRE_EMAIL_VERIFICATION");
|
|
3215
3539
|
if (!requireEmailVerification || requireEmailVerification === "false") {
|
|
3216
|
-
checks.push(skip("resend", "api-key-set", "lite mode (run `
|
|
3540
|
+
checks.push(skip("resend", "api-key-set", "lite mode (run `npx vexpo full` to provision)"));
|
|
3217
3541
|
return checks;
|
|
3218
3542
|
}
|
|
3219
3543
|
checks.push(fail2("resend", "api-key-set", `RESEND_API_KEY not set on Convex (${ctx.channel})`));
|
|
3220
3544
|
return checks;
|
|
3221
3545
|
}
|
|
3222
|
-
const
|
|
3223
|
-
if (
|
|
3546
|
+
const access17 = await probeAccess(apiKey);
|
|
3547
|
+
if (access17 === "invalid") {
|
|
3224
3548
|
checks.push(fail2("resend", "api-key-valid", "RESEND_API_KEY rejected by Resend"));
|
|
3225
3549
|
return checks;
|
|
3226
3550
|
}
|
|
3227
|
-
checks.push(ok2("resend", "api-key-valid", `key authenticated (access=${
|
|
3551
|
+
checks.push(ok2("resend", "api-key-valid", `key authenticated (access=${access17})`));
|
|
3228
3552
|
let domains = [];
|
|
3229
3553
|
let webhooks = [];
|
|
3230
|
-
if (
|
|
3554
|
+
if (access17 === "full") {
|
|
3231
3555
|
try {
|
|
3232
3556
|
domains = await listDomains(apiKey);
|
|
3233
3557
|
} catch (e) {
|
|
@@ -3295,15 +3619,29 @@ async function verifyResend(ctx) {
|
|
|
3295
3619
|
const expectedEndpoint = `${expectedSiteUrl.replace(/\/$/, "")}/resend-webhook`;
|
|
3296
3620
|
const match = webhooks.find((w) => w.endpoint === expectedEndpoint);
|
|
3297
3621
|
if (!match) {
|
|
3298
|
-
const others = webhooks.map((w) => w.endpoint)
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
"resend",
|
|
3302
|
-
"webhook-endpoint",
|
|
3303
|
-
`no webhook pointing at ${expectedEndpoint}`,
|
|
3304
|
-
others ? `existing: ${others}` : void 0
|
|
3305
|
-
)
|
|
3622
|
+
const others = webhooks.map((w) => w.endpoint);
|
|
3623
|
+
const staleConvex = others.filter(
|
|
3624
|
+
(e) => e.includes(".convex.site") && e.endsWith("/resend-webhook")
|
|
3306
3625
|
);
|
|
3626
|
+
if (staleConvex.length > 0) {
|
|
3627
|
+
checks.push(
|
|
3628
|
+
warn(
|
|
3629
|
+
"resend",
|
|
3630
|
+
"webhook-endpoint",
|
|
3631
|
+
`no webhook for this deployment; ${staleConvex.length} point at other convex.site deployments (stale after a deployment migration)`,
|
|
3632
|
+
`run \`vexpo resend --repoint${ctx.channel === "prod" ? " --prod" : ""}\` to move it to ${expectedEndpoint} and realign RESEND_WEBHOOK_SECRET. stale: ${staleConvex.join(", ")}`
|
|
3633
|
+
)
|
|
3634
|
+
);
|
|
3635
|
+
} else {
|
|
3636
|
+
checks.push(
|
|
3637
|
+
warn(
|
|
3638
|
+
"resend",
|
|
3639
|
+
"webhook-endpoint",
|
|
3640
|
+
`no webhook pointing at ${expectedEndpoint}`,
|
|
3641
|
+
others.length ? `existing: ${others.join(", ")}` : void 0
|
|
3642
|
+
)
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3307
3645
|
} else if (match.status !== "enabled" && match.status !== "active") {
|
|
3308
3646
|
checks.push(warn("resend", "webhook-endpoint", `webhook ${match.id} status=${match.status}`));
|
|
3309
3647
|
} else {
|
|
@@ -3321,7 +3659,7 @@ async function verifyResend(ctx) {
|
|
|
3321
3659
|
"resend",
|
|
3322
3660
|
"webhook-events",
|
|
3323
3661
|
`webhook missing ${missing.join(", ")}`,
|
|
3324
|
-
"re-run `
|
|
3662
|
+
"re-run `npx vexpo resend` to refresh subscription"
|
|
3325
3663
|
)
|
|
3326
3664
|
);
|
|
3327
3665
|
}
|
|
@@ -3398,10 +3736,10 @@ async function verifyApple(ctx) {
|
|
|
3398
3736
|
const v = await validate(ctx.ascCreds);
|
|
3399
3737
|
if (v.ok) {
|
|
3400
3738
|
checks.push(ok2("apple", "asc-key-valid", `${v.appCount} app${v.appCount === 1 ? "" : "s"}`));
|
|
3401
|
-
const
|
|
3739
|
+
const client = makeAscClient(ctx.ascCreds);
|
|
3402
3740
|
if (servicesId) {
|
|
3403
3741
|
try {
|
|
3404
|
-
const matches = await
|
|
3742
|
+
const matches = await client.bundleIds.list({ identifier: servicesId });
|
|
3405
3743
|
if (matches.length > 0)
|
|
3406
3744
|
checks.push(ok2("apple", "services-id-exists", `${servicesId} found in ASC`));
|
|
3407
3745
|
else
|
|
@@ -3410,7 +3748,7 @@ async function verifyApple(ctx) {
|
|
|
3410
3748
|
"apple",
|
|
3411
3749
|
"services-id-exists",
|
|
3412
3750
|
`${servicesId} not found in App Store Connect`,
|
|
3413
|
-
"run `
|
|
3751
|
+
"run `npx vexpo apple services-id` to provision it"
|
|
3414
3752
|
)
|
|
3415
3753
|
);
|
|
3416
3754
|
} catch (e) {
|
|
@@ -3423,44 +3761,12 @@ async function verifyApple(ctx) {
|
|
|
3423
3761
|
);
|
|
3424
3762
|
}
|
|
3425
3763
|
}
|
|
3426
|
-
const bundleId = ctx.envLocal.get("EXPO_PUBLIC_APP_BUNDLE_ID") ?? ctx.appConfig.bundleIdFallback ?? void 0;
|
|
3427
|
-
if (bundleId) {
|
|
3428
|
-
try {
|
|
3429
|
-
const apps = await client2.paginatedList(
|
|
3430
|
-
"/v1/apps",
|
|
3431
|
-
{ "filter[bundleId]": bundleId },
|
|
3432
|
-
5
|
|
3433
|
-
);
|
|
3434
|
-
const ascAppId = apps[0]?.id;
|
|
3435
|
-
if (ascAppId) {
|
|
3436
|
-
const { reviews: reviewsApi, unansweredOlderThan: unansweredOlderThan2 } = await import('./asc-reviews-OPKN34SB.js');
|
|
3437
|
-
const all = await reviewsApi(client2).customerReviews.list({
|
|
3438
|
-
appId: ascAppId,
|
|
3439
|
-
limit: 100
|
|
3440
|
-
});
|
|
3441
|
-
const stale = unansweredOlderThan2(all, 7);
|
|
3442
|
-
if (stale.length === 0)
|
|
3443
|
-
checks.push(ok2("apple", "reviews-answered", "no stale reviews"));
|
|
3444
|
-
else
|
|
3445
|
-
checks.push(
|
|
3446
|
-
warn(
|
|
3447
|
-
"apple",
|
|
3448
|
-
"reviews-answered",
|
|
3449
|
-
`${stale.length} review${stale.length === 1 ? "" : "s"} unanswered for >7 days`,
|
|
3450
|
-
"run `vexpo reviews unanswered --days 7` to triage"
|
|
3451
|
-
)
|
|
3452
|
-
);
|
|
3453
|
-
}
|
|
3454
|
-
} catch {
|
|
3455
|
-
checks.push(skip("apple", "reviews-answered", "could not query customer reviews"));
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
3764
|
} else {
|
|
3459
3765
|
checks.push(fail2("apple", "asc-key-valid", v.reason));
|
|
3460
3766
|
}
|
|
3461
3767
|
} else {
|
|
3462
3768
|
checks.push(
|
|
3463
|
-
skip("apple", "asc-key-valid", "no cached ASC creds (run `
|
|
3769
|
+
skip("apple", "asc-key-valid", "no cached ASC creds (run `npx vexpo apple asc-key`)")
|
|
3464
3770
|
);
|
|
3465
3771
|
}
|
|
3466
3772
|
return checks;
|
|
@@ -3469,100 +3775,84 @@ async function verifyEas(ctx) {
|
|
|
3469
3775
|
const checks = [];
|
|
3470
3776
|
let projectId = null;
|
|
3471
3777
|
try {
|
|
3472
|
-
projectId = await
|
|
3778
|
+
projectId = await resolveProjectId();
|
|
3473
3779
|
} catch {
|
|
3474
|
-
checks.push(skip("eas", "project-id", "couldn't read app.json"));
|
|
3475
|
-
return checks;
|
|
3476
3780
|
}
|
|
3477
3781
|
if (!projectId) {
|
|
3478
3782
|
const env2 = ctx.channel === "prod" ? ctx.convexProdEnv : ctx.convexEnv;
|
|
3479
|
-
const
|
|
3480
|
-
if (!
|
|
3481
|
-
checks.push(skip("eas", "project-id", "lite mode (run `
|
|
3783
|
+
const rev = env2.get("REQUIRE_EMAIL_VERIFICATION");
|
|
3784
|
+
if (!rev || rev === "false") {
|
|
3785
|
+
checks.push(skip("eas", "project-id", "lite mode (run `npx vexpo full` to init EAS)"));
|
|
3482
3786
|
return checks;
|
|
3483
3787
|
}
|
|
3484
|
-
checks.push(fail2("eas", "project-id", "no projectId in app.json"));
|
|
3485
|
-
return checks;
|
|
3486
3788
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3789
|
+
const envNames = ["production", "preview", "development"];
|
|
3790
|
+
const envMaps = /* @__PURE__ */ new Map();
|
|
3791
|
+
for (const e of envNames) {
|
|
3792
|
+
try {
|
|
3793
|
+
envMaps.set(e, await envList(e));
|
|
3794
|
+
} catch {
|
|
3795
|
+
envMaps.set(e, null);
|
|
3796
|
+
}
|
|
3494
3797
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3798
|
+
const provisioned = [...envMaps.values()].some((m) => m !== null && m.size > 0);
|
|
3799
|
+
if (projectId) {
|
|
3800
|
+
checks.push(ok2("eas", "project-id", projectId));
|
|
3801
|
+
} else if (provisioned) {
|
|
3802
|
+
checks.push(
|
|
3803
|
+
warn(
|
|
3804
|
+
"eas",
|
|
3805
|
+
"project-id",
|
|
3806
|
+
"EAS env is provisioned but projectId is unresolved",
|
|
3807
|
+
"set EAS_PROJECT_ID in .env.local (app.json is intentionally stubbed)"
|
|
3808
|
+
)
|
|
3809
|
+
);
|
|
3810
|
+
} else {
|
|
3811
|
+
checks.push(
|
|
3812
|
+
fail2("eas", "project-id", "no projectId in app.json, EAS_PROJECT_ID env, or .env.local")
|
|
3813
|
+
);
|
|
3497
3814
|
return checks;
|
|
3498
3815
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3816
|
+
if (projectId) {
|
|
3817
|
+
try {
|
|
3818
|
+
const who = await whoami();
|
|
3819
|
+
checks.push(
|
|
3820
|
+
who ? ok2("eas", "signed-in", who) : warn("eas", "signed-in", "not signed in (run `npx eas login`)")
|
|
3821
|
+
);
|
|
3822
|
+
} catch {
|
|
3823
|
+
checks.push(skip("eas", "signed-in", "eas CLI not available"));
|
|
3824
|
+
}
|
|
3825
|
+
try {
|
|
3826
|
+
const info = await projectInfo();
|
|
3827
|
+
if (info && info.id === projectId) checks.push(ok2("eas", "project-info", info.fullName));
|
|
3828
|
+
else if (info)
|
|
3506
3829
|
checks.push(
|
|
3507
3830
|
fail2(
|
|
3508
3831
|
"eas",
|
|
3509
3832
|
"project-info",
|
|
3510
|
-
`
|
|
3833
|
+
`local projectId (${projectId}) doesn't match resolved project (${info.id})`,
|
|
3511
3834
|
"run `vexpo eas` to re-link"
|
|
3512
3835
|
)
|
|
3513
3836
|
);
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
"eas project:info failed (project may have been deleted or transferred)"
|
|
3521
|
-
)
|
|
3522
|
-
);
|
|
3523
|
-
}
|
|
3524
|
-
} catch {
|
|
3525
|
-
checks.push(skip("eas", "project-info", "eas-cli not available"));
|
|
3526
|
-
}
|
|
3527
|
-
try {
|
|
3528
|
-
const diag = await diagnostics();
|
|
3529
|
-
if (diag.ok) {
|
|
3530
|
-
checks.push(ok2("eas", "diagnostics", "eas-cli health ok"));
|
|
3531
|
-
} else {
|
|
3532
|
-
checks.push(warn("eas", "diagnostics", diag.error));
|
|
3837
|
+
else
|
|
3838
|
+
checks.push(
|
|
3839
|
+
warn("eas", "project-info", "eas project:info failed (project deleted or transferred?)")
|
|
3840
|
+
);
|
|
3841
|
+
} catch {
|
|
3842
|
+
checks.push(skip("eas", "project-info", "eas-cli not available"));
|
|
3533
3843
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
}
|
|
3537
|
-
try {
|
|
3538
|
-
const { ascStatus: ascStatus2 } = await import('./eas-integrations-TIYBWWKC.js');
|
|
3539
|
-
const asc = await ascStatus2();
|
|
3540
|
-
if (asc.connected) {
|
|
3541
|
-
const label = asc.ascApp?.bundleId ?? asc.ascApp?.id ?? "ok";
|
|
3542
|
-
checks.push(ok2("eas", "asc-integration", String(label)));
|
|
3543
|
-
} else {
|
|
3844
|
+
try {
|
|
3845
|
+
const diag = await diagnostics();
|
|
3544
3846
|
checks.push(
|
|
3545
|
-
warn(
|
|
3546
|
-
"eas",
|
|
3547
|
-
"asc-integration",
|
|
3548
|
-
"no ASC integration on EAS",
|
|
3549
|
-
"run `vexpo asc connect` to link"
|
|
3550
|
-
)
|
|
3847
|
+
diag.ok ? ok2("eas", "diagnostics", "eas-cli health ok") : warn("eas", "diagnostics", diag.error)
|
|
3551
3848
|
);
|
|
3849
|
+
} catch {
|
|
3850
|
+
checks.push(skip("eas", "diagnostics", "eas-cli not available"));
|
|
3552
3851
|
}
|
|
3553
|
-
} catch {
|
|
3554
|
-
checks.push(skip("eas", "asc-integration", "eas integrations:asc:status unavailable"));
|
|
3555
3852
|
}
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
"development"
|
|
3560
|
-
];
|
|
3561
|
-
for (const env2 of envs) {
|
|
3562
|
-
let list;
|
|
3563
|
-
try {
|
|
3564
|
-
list = await envList(env2);
|
|
3565
|
-
} catch {
|
|
3853
|
+
for (const env2 of envNames) {
|
|
3854
|
+
const list = envMaps.get(env2) ?? null;
|
|
3855
|
+
if (!list) {
|
|
3566
3856
|
checks.push(skip("eas", `env-${env2}`, "eas env:list unavailable"));
|
|
3567
3857
|
continue;
|
|
3568
3858
|
}
|
|
@@ -3575,9 +3865,29 @@ async function verifyEas(ctx) {
|
|
|
3575
3865
|
"eas",
|
|
3576
3866
|
`env-${env2}`,
|
|
3577
3867
|
`missing ${missing.join(", ")}`,
|
|
3578
|
-
"run `
|
|
3868
|
+
"run `npx vexpo full` to init EAS + mirror env"
|
|
3579
3869
|
)
|
|
3580
3870
|
);
|
|
3871
|
+
const expected = (env2 === "development" ? ctx.envLocal : ctx.envProd).get(
|
|
3872
|
+
"EXPO_PUBLIC_CONVEX_URL"
|
|
3873
|
+
);
|
|
3874
|
+
const actual = list.get("EXPO_PUBLIC_CONVEX_URL");
|
|
3875
|
+
if (expected && actual) {
|
|
3876
|
+
const expSlug = deploymentSlugFromHost(hostnameOf(expected) ?? "");
|
|
3877
|
+
const actSlug = deploymentSlugFromHost(hostnameOf(actual) ?? "");
|
|
3878
|
+
if (expSlug && actSlug && expSlug !== actSlug) {
|
|
3879
|
+
checks.push(
|
|
3880
|
+
fail2(
|
|
3881
|
+
"eas",
|
|
3882
|
+
`convex-url-${env2}`,
|
|
3883
|
+
`EAS points at ${actSlug}, local at ${expSlug}`,
|
|
3884
|
+
"run `vexpo env push` + `vexpo env convex-key` to repoint EAS at the active deployment"
|
|
3885
|
+
)
|
|
3886
|
+
);
|
|
3887
|
+
} else if (expSlug && actSlug) {
|
|
3888
|
+
checks.push(ok2("eas", `convex-url-${env2}`, `points at ${actSlug}`));
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3581
3891
|
if (env2 === "production") {
|
|
3582
3892
|
const rotationSecrets = [
|
|
3583
3893
|
"CONVEX_DEPLOY_KEY",
|
|
@@ -3600,6 +3910,38 @@ async function verifyEas(ctx) {
|
|
|
3600
3910
|
);
|
|
3601
3911
|
}
|
|
3602
3912
|
}
|
|
3913
|
+
try {
|
|
3914
|
+
const status = await ascStatus();
|
|
3915
|
+
if (status.status === "connected") {
|
|
3916
|
+
checks.push(
|
|
3917
|
+
ok2("eas", "asc-integration", status.appStoreConnectApp?.bundleIdentifier ?? "connected")
|
|
3918
|
+
);
|
|
3919
|
+
const missing = existsSync("eas.json") ? submitProfilesMissingAscAppId(readFileSync("eas.json", "utf8")) : [];
|
|
3920
|
+
if (missing.length > 0) {
|
|
3921
|
+
checks.push(
|
|
3922
|
+
warn(
|
|
3923
|
+
"eas",
|
|
3924
|
+
"asc-submit-id",
|
|
3925
|
+
`submit profile${missing.length === 1 ? "" : "s"} ${missing.join(", ")} missing ascAppId`,
|
|
3926
|
+
"run `vexpo asc` to write it; non-interactive `eas submit` (CI) fails without it"
|
|
3927
|
+
)
|
|
3928
|
+
);
|
|
3929
|
+
} else if (existsSync("eas.json")) {
|
|
3930
|
+
checks.push(ok2("eas", "asc-submit-id", "submit profiles carry ascAppId"));
|
|
3931
|
+
}
|
|
3932
|
+
} else {
|
|
3933
|
+
checks.push(
|
|
3934
|
+
warn(
|
|
3935
|
+
"eas",
|
|
3936
|
+
"asc-integration",
|
|
3937
|
+
`not connected (${status.status})`,
|
|
3938
|
+
"run `vexpo asc:connect` so `eas submit` resolves the app from the bundle id"
|
|
3939
|
+
)
|
|
3940
|
+
);
|
|
3941
|
+
}
|
|
3942
|
+
} catch {
|
|
3943
|
+
checks.push(skip("eas", "asc-integration", "eas integrations:asc:status unavailable"));
|
|
3944
|
+
}
|
|
3603
3945
|
return checks;
|
|
3604
3946
|
}
|
|
3605
3947
|
function verifyCoherence(ctx) {
|
|
@@ -3715,12 +4057,15 @@ function verifyFiles(ctx) {
|
|
|
3715
4057
|
return checks;
|
|
3716
4058
|
}
|
|
3717
4059
|
async function readContext(channel) {
|
|
4060
|
+
const prodEnvFile = existsSync(".env.prod") ? ".env.prod" : existsSync(".env.production") ? ".env.production" : void 0;
|
|
3718
4061
|
const [envLocal, envProd, convexEnv, convexProdEnv, appConfigFacts, ascCreds] = await Promise.all(
|
|
3719
4062
|
[
|
|
3720
4063
|
readEnvFile(".env.local"),
|
|
3721
4064
|
readEnvFile(".env.prod").then(async (m) => m.size > 0 ? m : readEnvFile(".env.production")),
|
|
3722
4065
|
envMap().catch(() => /* @__PURE__ */ new Map()),
|
|
3723
|
-
envMap({ prod: true }).catch(
|
|
4066
|
+
prodEnvFile ? envMap({ prod: true, envFile: prodEnvFile }).catch(
|
|
4067
|
+
() => /* @__PURE__ */ new Map()
|
|
4068
|
+
) : Promise.resolve(/* @__PURE__ */ new Map()),
|
|
3724
4069
|
readAppConfigFacts(),
|
|
3725
4070
|
loadAscCreds3()
|
|
3726
4071
|
]
|
|
@@ -3884,6 +4229,113 @@ async function runDoctor(options2) {
|
|
|
3884
4229
|
return 2;
|
|
3885
4230
|
}
|
|
3886
4231
|
}
|
|
4232
|
+
async function fileExists5(p) {
|
|
4233
|
+
try {
|
|
4234
|
+
await access(p);
|
|
4235
|
+
return true;
|
|
4236
|
+
} catch {
|
|
4237
|
+
return false;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
async function upsert(name, value, visibility, env2) {
|
|
4241
|
+
const existing = await envList(env2);
|
|
4242
|
+
if (existing.has(name)) await envUpdate(name, value, visibility, [env2]);
|
|
4243
|
+
else await envCreate(name, value, visibility, [env2]);
|
|
4244
|
+
}
|
|
4245
|
+
async function runConvexKey(options2) {
|
|
4246
|
+
section("EAS Convex key");
|
|
4247
|
+
const projectId = await resolveProjectId();
|
|
4248
|
+
if (!projectId) {
|
|
4249
|
+
bad("no EAS projectId. run `eas init` (or `vexpo full`) first");
|
|
4250
|
+
return 1;
|
|
4251
|
+
}
|
|
4252
|
+
ok(`EAS project: ${BOLD}${projectId}${RESET}`);
|
|
4253
|
+
const localFile = options2.localFile ?? ".env.local";
|
|
4254
|
+
const prodFile = options2.prodFile ?? (await fileExists5(".env.prod") ? ".env.prod" : ".env.production");
|
|
4255
|
+
const local = await readEnvFile(localFile);
|
|
4256
|
+
const prod = await readEnvFile(prodFile);
|
|
4257
|
+
const devKey = options2.devKey ?? local.get("CONVEX_DEPLOY_KEY");
|
|
4258
|
+
let prodKey = options2.prodKey ?? prod.get("CONVEX_DEPLOY_KEY");
|
|
4259
|
+
const devSel = local.get("CONVEX_DEPLOYMENT");
|
|
4260
|
+
const prodSel = prod.get("CONVEX_DEPLOYMENT");
|
|
4261
|
+
if (options2.mint && !prodKey) {
|
|
4262
|
+
const easProd = await envList("production").catch(() => /* @__PURE__ */ new Map());
|
|
4263
|
+
if (easProd.has("CONVEX_DEPLOY_KEY")) {
|
|
4264
|
+
note("prod CONVEX_DEPLOY_KEY already on EAS; skipping mint");
|
|
4265
|
+
} else {
|
|
4266
|
+
const slug2 = deploymentSlug(prodSel ?? devSel);
|
|
4267
|
+
const minted = slug2 ? await mintProdDeployKey(slug2, "convex-key").catch(() => null) : null;
|
|
4268
|
+
if (minted) {
|
|
4269
|
+
prodKey = minted.key;
|
|
4270
|
+
ok(`minted prod deploy key for ${BOLD}${minted.deployment}${RESET}`);
|
|
4271
|
+
} else {
|
|
4272
|
+
yep("--mint: couldn't resolve the prod deployment to mint a key");
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
if (devKey && !devKey.startsWith("dev:"))
|
|
4277
|
+
yep("dev deploy key is not dev-scoped (expected dev:\u2026)");
|
|
4278
|
+
if (prodKey && !prodKey.startsWith("prod:"))
|
|
4279
|
+
yep("prod deploy key is not prod-scoped (expected prod:\u2026)");
|
|
4280
|
+
const writes = [];
|
|
4281
|
+
if (devKey)
|
|
4282
|
+
writes.push({
|
|
4283
|
+
name: "CONVEX_DEPLOY_KEY",
|
|
4284
|
+
value: devKey,
|
|
4285
|
+
visibility: "secret",
|
|
4286
|
+
envs: ["development"],
|
|
4287
|
+
label: "dev deploy key"
|
|
4288
|
+
});
|
|
4289
|
+
if (prodKey)
|
|
4290
|
+
writes.push({
|
|
4291
|
+
name: "CONVEX_DEPLOY_KEY",
|
|
4292
|
+
value: prodKey,
|
|
4293
|
+
visibility: "secret",
|
|
4294
|
+
envs: ["production"],
|
|
4295
|
+
label: "prod deploy key"
|
|
4296
|
+
});
|
|
4297
|
+
if (devSel)
|
|
4298
|
+
writes.push({
|
|
4299
|
+
name: "CONVEX_DEPLOYMENT",
|
|
4300
|
+
value: devSel,
|
|
4301
|
+
visibility: "plaintext",
|
|
4302
|
+
envs: ["development"],
|
|
4303
|
+
label: "dev selector"
|
|
4304
|
+
});
|
|
4305
|
+
if (prodSel)
|
|
4306
|
+
writes.push({
|
|
4307
|
+
name: "CONVEX_DEPLOYMENT",
|
|
4308
|
+
value: prodSel,
|
|
4309
|
+
visibility: "plaintext",
|
|
4310
|
+
envs: ["production", "preview"],
|
|
4311
|
+
label: "prod selector"
|
|
4312
|
+
});
|
|
4313
|
+
if (writes.length === 0) {
|
|
4314
|
+
yep("no CONVEX_DEPLOY_KEY / CONVEX_DEPLOYMENT found in env files or flags");
|
|
4315
|
+
note("pass --dev-key / --prod-key, or set them in .env.local / .env.prod");
|
|
4316
|
+
return 1;
|
|
4317
|
+
}
|
|
4318
|
+
let failed = 0;
|
|
4319
|
+
for (const w of writes) {
|
|
4320
|
+
for (const env2 of w.envs) {
|
|
4321
|
+
try {
|
|
4322
|
+
await upsert(w.name, w.value, w.visibility, env2);
|
|
4323
|
+
ok(`${env2}: ${w.name} ${DIM}(${w.label})${RESET}`);
|
|
4324
|
+
} catch (err) {
|
|
4325
|
+
bad(`${env2}: ${w.name} failed: ${err instanceof Error ? err.message : err}`);
|
|
4326
|
+
failed += 1;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
line();
|
|
4331
|
+
if (failed > 0) {
|
|
4332
|
+
bad(`${failed} write${failed === 1 ? "" : "s"} failed`);
|
|
4333
|
+
return 1;
|
|
4334
|
+
}
|
|
4335
|
+
ok("EAS Convex key + selector synced");
|
|
4336
|
+
note("`vexpo doctor` to confirm EAS now points at the active deployment");
|
|
4337
|
+
return 0;
|
|
4338
|
+
}
|
|
3887
4339
|
|
|
3888
4340
|
// src/commands/env/push.ts
|
|
3889
4341
|
function shortValue(v) {
|
|
@@ -3894,12 +4346,12 @@ function describeDest(d) {
|
|
|
3894
4346
|
if (d.type === "convex") return `convex env (${d.channel}) \u2192 ${d.key}`;
|
|
3895
4347
|
return `eas env (${d.environments.join(",")}) \u2192 ${d.key}`;
|
|
3896
4348
|
}
|
|
3897
|
-
async function readRemoteState() {
|
|
3898
|
-
const projectId = await
|
|
4349
|
+
async function readRemoteState(prodEnvFile) {
|
|
4350
|
+
const projectId = await resolveProjectId();
|
|
3899
4351
|
const hasEasProject = !!projectId;
|
|
3900
4352
|
const [convexDev, convexProd, easDev, easPreview, easProd] = await Promise.all([
|
|
3901
4353
|
envMap().catch(() => /* @__PURE__ */ new Map()),
|
|
3902
|
-
envMap({ prod: true }).catch(() => /* @__PURE__ */ new Map()),
|
|
4354
|
+
envMap({ prod: true, envFile: prodEnvFile }).catch(() => /* @__PURE__ */ new Map()),
|
|
3903
4355
|
hasEasProject ? envList("development").catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map()),
|
|
3904
4356
|
hasEasProject ? envList("preview").catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map()),
|
|
3905
4357
|
hasEasProject ? envList("production").catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map())
|
|
@@ -3997,15 +4449,22 @@ async function applyPlan(plan, opts = {}) {
|
|
|
3997
4449
|
}
|
|
3998
4450
|
let applied = 0;
|
|
3999
4451
|
let failed = 0;
|
|
4000
|
-
const { writeFile: writeFile4, unlink: unlink2 } = await import('fs/promises');
|
|
4452
|
+
const { writeFile: writeFile4, unlink: unlink2, mkdtemp, rmdir } = await import('fs/promises');
|
|
4453
|
+
const { tmpdir } = await import('os');
|
|
4454
|
+
const { join: join2 } = await import('path');
|
|
4001
4455
|
for (const [channel, entries] of convexBatches) {
|
|
4002
4456
|
if (entries.length === 0) continue;
|
|
4003
|
-
const
|
|
4457
|
+
const dir = await mkdtemp(join2(tmpdir(), "vexpo-env-"));
|
|
4458
|
+
const tmp = join2(dir, "convex.env");
|
|
4004
4459
|
try {
|
|
4005
|
-
await writeFile4(tmp, entries.map(([k, v]) => `${k}=${v}`).join("\n") + "\n"
|
|
4006
|
-
|
|
4007
|
-
force: opts.force ?? false
|
|
4460
|
+
await writeFile4(tmp, entries.map(([k, v]) => `${k}=${v}`).join("\n") + "\n", {
|
|
4461
|
+
mode: 384
|
|
4008
4462
|
});
|
|
4463
|
+
await envSetFromFile(
|
|
4464
|
+
tmp,
|
|
4465
|
+
channel === "prod" ? { prod: true, envFile: plan.sourceFile } : void 0,
|
|
4466
|
+
{ force: opts.force ?? false }
|
|
4467
|
+
);
|
|
4009
4468
|
ok(`convex(${channel}) bulk-set ${entries.length} var${entries.length === 1 ? "" : "s"}`);
|
|
4010
4469
|
for (const [k] of entries) note(` ${k}`);
|
|
4011
4470
|
applied += entries.length;
|
|
@@ -4015,13 +4474,18 @@ async function applyPlan(plan, opts = {}) {
|
|
|
4015
4474
|
} finally {
|
|
4016
4475
|
await unlink2(tmp).catch(() => {
|
|
4017
4476
|
});
|
|
4477
|
+
await rmdir(dir).catch(() => {
|
|
4478
|
+
});
|
|
4018
4479
|
}
|
|
4019
4480
|
}
|
|
4020
4481
|
for (const { envs, entries } of easBatches.values()) {
|
|
4021
4482
|
if (entries.length === 0) continue;
|
|
4022
|
-
const
|
|
4483
|
+
const dir = await mkdtemp(join2(tmpdir(), "vexpo-env-"));
|
|
4484
|
+
const tmp = join2(dir, "eas.env");
|
|
4023
4485
|
try {
|
|
4024
|
-
await writeFile4(tmp, entries.map(([k, v]) => `${k}=${v}`).join("\n") + "\n"
|
|
4486
|
+
await writeFile4(tmp, entries.map(([k, v]) => `${k}=${v}`).join("\n") + "\n", {
|
|
4487
|
+
mode: 384
|
|
4488
|
+
});
|
|
4025
4489
|
await envPush({ path: tmp, environments: envs, force: true });
|
|
4026
4490
|
ok(`eas(${envs.join(",")}) pushed ${entries.length} var${entries.length === 1 ? "" : "s"}`);
|
|
4027
4491
|
for (const [k] of entries) note(` ${k}`);
|
|
@@ -4032,12 +4496,19 @@ async function applyPlan(plan, opts = {}) {
|
|
|
4032
4496
|
} finally {
|
|
4033
4497
|
await unlink2(tmp).catch(() => {
|
|
4034
4498
|
});
|
|
4499
|
+
await rmdir(dir).catch(() => {
|
|
4500
|
+
});
|
|
4035
4501
|
}
|
|
4036
4502
|
}
|
|
4037
4503
|
return { applied, failed };
|
|
4038
4504
|
}
|
|
4039
4505
|
async function runEnvPush(options2) {
|
|
4040
4506
|
section("Env push");
|
|
4507
|
+
if (await checkToken() === "unauthorized") {
|
|
4508
|
+
bad("Convex login expired or revoked");
|
|
4509
|
+
note("run `npx convex login` to refresh, then re-run");
|
|
4510
|
+
return 1;
|
|
4511
|
+
}
|
|
4041
4512
|
const sources = await readSources({ local: options2.localFile, prod: options2.prodFile });
|
|
4042
4513
|
if (sources.length === 0) {
|
|
4043
4514
|
yep("no source files found");
|
|
@@ -4069,18 +4540,23 @@ async function runEnvPush(options2) {
|
|
|
4069
4540
|
);
|
|
4070
4541
|
}
|
|
4071
4542
|
}
|
|
4072
|
-
const
|
|
4543
|
+
const prodEnvFile = sources.find((s) => s.channel === "prod")?.path;
|
|
4544
|
+
const remote = await readRemoteState(prodEnvFile);
|
|
4073
4545
|
if (!remote.hasEasProject) yep("no EAS projectId in app.json. EAS env routes will be blocked");
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4546
|
+
const manualHits = [];
|
|
4547
|
+
for (const s of sources) {
|
|
4548
|
+
for (const k of Object.keys(MANUAL_EAS_SECRETS)) {
|
|
4549
|
+
if (s.entries.has(k)) manualHits.push({ key: k, file: s.path });
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4076
4552
|
if (manualHits.length > 0) {
|
|
4077
4553
|
line();
|
|
4078
4554
|
yep(
|
|
4079
4555
|
`${manualHits.length} secret-visibility key${manualHits.length === 1 ? "" : "s"} detected. set manually:`
|
|
4080
4556
|
);
|
|
4081
|
-
for (const
|
|
4082
|
-
note(` ${BOLD}${
|
|
4083
|
-
note(` ${DIM}${MANUAL_EAS_SECRETS[
|
|
4557
|
+
for (const { key, file } of manualHits) {
|
|
4558
|
+
note(` ${BOLD}${key}${RESET} ${DIM}(${file})${RESET}`);
|
|
4559
|
+
note(` ${DIM}${MANUAL_EAS_SECRETS[key]}${RESET}`);
|
|
4084
4560
|
}
|
|
4085
4561
|
note(`${DIM}lite skips these to avoid pushing secrets at default visibility${RESET}`);
|
|
4086
4562
|
}
|
|
@@ -4122,6 +4598,21 @@ async function runEnvPush(options2) {
|
|
|
4122
4598
|
await recordStep("accounts", { mode: "lite", verifiedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4123
4599
|
return 0;
|
|
4124
4600
|
}
|
|
4601
|
+
const prodConvexWrites = entries.some(
|
|
4602
|
+
(e) => e.channel === "prod" && e.destinations.some((d) => d.type === "convex")
|
|
4603
|
+
);
|
|
4604
|
+
if (prodConvexWrites) {
|
|
4605
|
+
const pf = sources.find((s) => s.channel === "prod");
|
|
4606
|
+
const deployKey = pf?.entries.get("CONVEX_DEPLOY_KEY") ?? "";
|
|
4607
|
+
const selector = pf?.entries.get("CONVEX_DEPLOYMENT") ?? "";
|
|
4608
|
+
if (!deployKey.startsWith("prod:") && !selector.startsWith("prod:")) {
|
|
4609
|
+
line();
|
|
4610
|
+
bad(`${pf?.path ?? "prod source"} has no prod-scoped CONVEX_DEPLOY_KEY or CONVEX_DEPLOYMENT`);
|
|
4611
|
+
note("prod env would silently write to the DEV deployment (the dev key shadows --prod)");
|
|
4612
|
+
note("add a `prod:` CONVEX_DEPLOY_KEY (or CONVEX_DEPLOYMENT) to the prod file and re-run");
|
|
4613
|
+
return 1;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4125
4616
|
line();
|
|
4126
4617
|
if (totalConflicts > 0) {
|
|
4127
4618
|
note(
|
|
@@ -4190,7 +4681,7 @@ async function runEnvPush(options2) {
|
|
|
4190
4681
|
}
|
|
4191
4682
|
function printVerifyResults(checks) {
|
|
4192
4683
|
const w = Math.max(...checks.map((c) => c.name.length));
|
|
4193
|
-
const order = ["files", "convex", "resend", "apple", "eas", "
|
|
4684
|
+
const order = ["files", "convex", "resend", "apple", "eas", "coherence"];
|
|
4194
4685
|
const grouped = /* @__PURE__ */ new Map();
|
|
4195
4686
|
for (const c of checks) {
|
|
4196
4687
|
if (!grouped.has(c.category)) grouped.set(c.category, []);
|
|
@@ -4458,6 +4949,10 @@ async function runRebrand(options2) {
|
|
|
4458
4949
|
await rewriteAppJson();
|
|
4459
4950
|
await rewritePackageJson(inputs);
|
|
4460
4951
|
await rewriteStoreConfig(inputs);
|
|
4952
|
+
if (inputs.expoOwner) {
|
|
4953
|
+
await ensureLine("EXPO_PUBLIC_EXPO_OWNER", inputs.expoOwner);
|
|
4954
|
+
ok(`wrote EXPO_PUBLIC_EXPO_OWNER=${inputs.expoOwner} to .env.local`);
|
|
4955
|
+
}
|
|
4461
4956
|
await recordStep("rebrand", {
|
|
4462
4957
|
appName: inputs.appName,
|
|
4463
4958
|
packageName: inputs.packageName,
|
|
@@ -4508,7 +5003,27 @@ function formatElapsed(ms) {
|
|
|
4508
5003
|
}
|
|
4509
5004
|
|
|
4510
5005
|
// src/commands/resend.ts
|
|
5006
|
+
async function fileExists6(p) {
|
|
5007
|
+
try {
|
|
5008
|
+
await access(p);
|
|
5009
|
+
return true;
|
|
5010
|
+
} catch {
|
|
5011
|
+
return false;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
async function resolveFullKey() {
|
|
5015
|
+
const fromEnv = process.env.RESEND_FULL_ACCESS_KEY;
|
|
5016
|
+
if (fromEnv) return fromEnv;
|
|
5017
|
+
if (!process.stdin.isTTY) return null;
|
|
5018
|
+
line();
|
|
5019
|
+
note("Need a Resend full-access API key. Create one at:");
|
|
5020
|
+
note(` ${BOLD}https://resend.com/api-keys${RESET} \u2192 Create API Key \u2192 Permission: Full Access`);
|
|
5021
|
+
note("Used once, never persisted.");
|
|
5022
|
+
const pasted = await ask(` RESEND_FULL_ACCESS_KEY > `);
|
|
5023
|
+
return pasted || null;
|
|
5024
|
+
}
|
|
4511
5025
|
async function runResend(options2) {
|
|
5026
|
+
if (options2.repoint) return runResendRepoint(options2);
|
|
4512
5027
|
section("Resend provisioning");
|
|
4513
5028
|
const siteUrl = await readOne("EXPO_PUBLIC_CONVEX_SITE_URL");
|
|
4514
5029
|
if (!siteUrl) {
|
|
@@ -4535,9 +5050,9 @@ async function runResend(options2) {
|
|
|
4535
5050
|
return 1;
|
|
4536
5051
|
}
|
|
4537
5052
|
}
|
|
4538
|
-
const
|
|
4539
|
-
if (
|
|
4540
|
-
bad(`provided key has '${
|
|
5053
|
+
const keyAccess = await probeAccess(fullKey);
|
|
5054
|
+
if (keyAccess !== "full") {
|
|
5055
|
+
bad(`provided key has '${keyAccess}' access; need 'full'`);
|
|
4541
5056
|
return 1;
|
|
4542
5057
|
}
|
|
4543
5058
|
ok("full-access key verified");
|
|
@@ -4619,7 +5134,7 @@ async function runResend(options2) {
|
|
|
4619
5134
|
const token = await provisionSendingKey(fullKey, name, domain.id);
|
|
4620
5135
|
ok(`scoped sending key '${name}' provisioned`);
|
|
4621
5136
|
const endpoint = `${siteUrl.replace(/\/$/, "")}/resend-webhook`;
|
|
4622
|
-
const secret = await provisionWebhook(fullKey, endpoint);
|
|
5137
|
+
const { id: webhookId, secret } = await provisionWebhook(fullKey, endpoint);
|
|
4623
5138
|
ok(`webhook \u2192 ${endpoint}`);
|
|
4624
5139
|
const fromAddr = options2.from ?? `${name}@${domain.name}`;
|
|
4625
5140
|
await envSet("RESEND_API_KEY", token);
|
|
@@ -4637,7 +5152,8 @@ async function runResend(options2) {
|
|
|
4637
5152
|
domainName: domain.name,
|
|
4638
5153
|
keyName: name,
|
|
4639
5154
|
fromAddress: fromAddr,
|
|
4640
|
-
webhookEndpoint: endpoint
|
|
5155
|
+
webhookEndpoint: endpoint,
|
|
5156
|
+
webhookId
|
|
4641
5157
|
});
|
|
4642
5158
|
line();
|
|
4643
5159
|
ok("Resend provisioning complete");
|
|
@@ -4648,6 +5164,64 @@ async function runResend(options2) {
|
|
|
4648
5164
|
);
|
|
4649
5165
|
return 0;
|
|
4650
5166
|
}
|
|
5167
|
+
async function runResendRepoint(options2) {
|
|
5168
|
+
const channel = options2.prod ? "prod" : "dev";
|
|
5169
|
+
section(`Resend repoint (${channel})`);
|
|
5170
|
+
let siteUrl;
|
|
5171
|
+
let convexTarget;
|
|
5172
|
+
if (options2.prod) {
|
|
5173
|
+
const prodFile = await fileExists6(".env.prod") ? ".env.prod" : ".env.production";
|
|
5174
|
+
siteUrl = (await readEnvFile(prodFile)).get("EXPO_PUBLIC_CONVEX_SITE_URL");
|
|
5175
|
+
convexTarget = { prod: true, envFile: prodFile };
|
|
5176
|
+
} else {
|
|
5177
|
+
siteUrl = await readOne("EXPO_PUBLIC_CONVEX_SITE_URL") ?? void 0;
|
|
5178
|
+
}
|
|
5179
|
+
if (!siteUrl) {
|
|
5180
|
+
bad(`EXPO_PUBLIC_CONVEX_SITE_URL missing from ${options2.prod ? ".env.prod" : ".env.local"}`);
|
|
5181
|
+
note("run `vexpo convex` (and a prod deploy) so the site URL is populated, then re-run");
|
|
5182
|
+
return 1;
|
|
5183
|
+
}
|
|
5184
|
+
const endpoint = `${siteUrl.replace(/\/$/, "")}/resend-webhook`;
|
|
5185
|
+
ok(`target endpoint: ${endpoint}`);
|
|
5186
|
+
const fullKey = await resolveFullKey();
|
|
5187
|
+
if (!fullKey) {
|
|
5188
|
+
bad("no RESEND_FULL_ACCESS_KEY env var and no TTY for paste");
|
|
5189
|
+
return 1;
|
|
5190
|
+
}
|
|
5191
|
+
if (await probeAccess(fullKey) !== "full") {
|
|
5192
|
+
bad("provided key does not have full access");
|
|
5193
|
+
return 1;
|
|
5194
|
+
}
|
|
5195
|
+
const hooks = await listWebhooks(fullKey);
|
|
5196
|
+
const atNew = hooks.find((w) => w.endpoint === endpoint);
|
|
5197
|
+
const stale = hooks.filter(
|
|
5198
|
+
(w) => w.endpoint !== endpoint && w.endpoint.endsWith("/resend-webhook")
|
|
5199
|
+
);
|
|
5200
|
+
let webhookId;
|
|
5201
|
+
if (atNew && !options2.force) {
|
|
5202
|
+
ok(`webhook already points at ${endpoint}`);
|
|
5203
|
+
note("its secret can't be read back; pass --force to recreate + realign RESEND_WEBHOOK_SECRET");
|
|
5204
|
+
webhookId = atNew.id;
|
|
5205
|
+
} else {
|
|
5206
|
+
const { id, secret } = await provisionWebhook(fullKey, endpoint);
|
|
5207
|
+
webhookId = id;
|
|
5208
|
+
ok(`webhook \u2192 ${endpoint}`);
|
|
5209
|
+
await envSet("RESEND_WEBHOOK_SECRET", secret, convexTarget);
|
|
5210
|
+
ok(`RESEND_WEBHOOK_SECRET aligned on the ${channel} deployment`);
|
|
5211
|
+
}
|
|
5212
|
+
let retired = 0;
|
|
5213
|
+
for (const w of stale) {
|
|
5214
|
+
await deleteWebhook(fullKey, w.id);
|
|
5215
|
+
note(`retired stale webhook \u2192 ${w.endpoint}`);
|
|
5216
|
+
retired += 1;
|
|
5217
|
+
}
|
|
5218
|
+
const prev = (await load()).steps.resend?.outputs ?? {};
|
|
5219
|
+
await recordStep("resend", { ...prev, webhookEndpoint: endpoint, webhookId });
|
|
5220
|
+
line();
|
|
5221
|
+
ok(`repoint complete${retired ? ` (${retired} stale retired)` : ""}`);
|
|
5222
|
+
nop("sending key and REQUIRE_EMAIL_VERIFICATION left unchanged");
|
|
5223
|
+
return 0;
|
|
5224
|
+
}
|
|
4651
5225
|
async function runReviewAccount(options2) {
|
|
4652
5226
|
section("App Review demo account");
|
|
4653
5227
|
try {
|
|
@@ -4668,27 +5242,17 @@ async function runReviewAccount(options2) {
|
|
|
4668
5242
|
name,
|
|
4669
5243
|
...options2.username ? { username: options2.username } : {}
|
|
4670
5244
|
});
|
|
4671
|
-
const
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
const code = await proc.exited;
|
|
4677
|
-
return {
|
|
4678
|
-
ok: code === 0,
|
|
4679
|
-
out: await streamText(proc.stdout),
|
|
4680
|
-
err: await streamText(proc.stderr)
|
|
4681
|
-
};
|
|
4682
|
-
};
|
|
4683
|
-
let result = await tryRun(["--component-function"]);
|
|
4684
|
-
if (!result.ok) result = await tryRun([]);
|
|
4685
|
-
if (!result.ok) {
|
|
5245
|
+
const { code, stdout, stderr } = await run(
|
|
5246
|
+
[dlx(), "convex", "run", "admin:createReviewAccount", payload],
|
|
5247
|
+
{ stdin: "ignore" }
|
|
5248
|
+
);
|
|
5249
|
+
if (code !== 0) {
|
|
4686
5250
|
bad("convex run failed");
|
|
4687
|
-
const
|
|
4688
|
-
if (
|
|
5251
|
+
const trimmed = stderr.trim();
|
|
5252
|
+
if (trimmed) note(trimmed);
|
|
4689
5253
|
return 1;
|
|
4690
5254
|
}
|
|
4691
|
-
process.stderr.write(
|
|
5255
|
+
process.stderr.write(stdout);
|
|
4692
5256
|
line();
|
|
4693
5257
|
ok("review account ready, Apple's reviewer can now sign in");
|
|
4694
5258
|
note(`email: ${email}`);
|
|
@@ -4700,61 +5264,7 @@ async function runReviewAccount(options2) {
|
|
|
4700
5264
|
return 1;
|
|
4701
5265
|
}
|
|
4702
5266
|
}
|
|
4703
|
-
|
|
4704
|
-
// src/commands/asc.ts
|
|
4705
|
-
async function runAscConnect(opts) {
|
|
4706
|
-
section("ASC connect");
|
|
4707
|
-
if (!opts.force) {
|
|
4708
|
-
try {
|
|
4709
|
-
const status = await ascStatus();
|
|
4710
|
-
if (status.connected) {
|
|
4711
|
-
const label = status.ascApp?.bundleId ?? status.ascApp?.id ?? "ok";
|
|
4712
|
-
nop(`already connected (${label})`);
|
|
4713
|
-
await recordStep("apple-asc-link", {
|
|
4714
|
-
ascAppId: status.ascApp?.id,
|
|
4715
|
-
bundleId: status.ascApp?.bundleId,
|
|
4716
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4717
|
-
});
|
|
4718
|
-
return 0;
|
|
4719
|
-
}
|
|
4720
|
-
} catch {
|
|
4721
|
-
}
|
|
4722
|
-
}
|
|
4723
|
-
const apiKeyId = opts.apiKeyId ?? await ascKeyIdFromState();
|
|
4724
|
-
const bundleId = opts.bundleId ?? await readOne("EXPO_PUBLIC_APP_BUNDLE_ID");
|
|
4725
|
-
if (!apiKeyId) {
|
|
4726
|
-
yep("no --api-key-id and no cached ASC key id in state.json");
|
|
4727
|
-
note("run `vexpo apple asc-key` first, or pass --api-key-id");
|
|
4728
|
-
}
|
|
4729
|
-
if (!bundleId) {
|
|
4730
|
-
yep("no --bundle-id and no EXPO_PUBLIC_APP_BUNDLE_ID in .env.local");
|
|
4731
|
-
note("run `vexpo convex` first, or pass --bundle-id");
|
|
4732
|
-
}
|
|
4733
|
-
const exit = await ascConnect({
|
|
4734
|
-
apiKeyId,
|
|
4735
|
-
ascAppId: opts.ascAppId,
|
|
4736
|
-
bundleId
|
|
4737
|
-
});
|
|
4738
|
-
if (exit !== 0) {
|
|
4739
|
-
bad(`eas integrations:asc:connect exited with ${exit}`);
|
|
4740
|
-
return exit;
|
|
4741
|
-
}
|
|
4742
|
-
ok("EAS project linked to ASC app");
|
|
4743
|
-
await recordStep("apple-asc-link", {
|
|
4744
|
-
ascApiKeyId: apiKeyId,
|
|
4745
|
-
bundleId,
|
|
4746
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4747
|
-
});
|
|
4748
|
-
return 0;
|
|
4749
|
-
}
|
|
4750
|
-
async function ascKeyIdFromState() {
|
|
4751
|
-
const state = await load();
|
|
4752
|
-
const rec = state.steps["asc-key"];
|
|
4753
|
-
if (!rec?.outputs) return void 0;
|
|
4754
|
-
const keyId = rec.outputs.keyId;
|
|
4755
|
-
return typeof keyId === "string" ? keyId : void 0;
|
|
4756
|
-
}
|
|
4757
|
-
async function fileExists5(p) {
|
|
5267
|
+
async function fileExists7(p) {
|
|
4758
5268
|
try {
|
|
4759
5269
|
await access(p);
|
|
4760
5270
|
return true;
|
|
@@ -4762,19 +5272,42 @@ async function fileExists5(p) {
|
|
|
4762
5272
|
return false;
|
|
4763
5273
|
}
|
|
4764
5274
|
}
|
|
5275
|
+
async function pushEasRoutedKeys(file, environments) {
|
|
5276
|
+
const entries = await readEnvFile(file);
|
|
5277
|
+
const easKeys = [];
|
|
5278
|
+
for (const [key, value] of entries) {
|
|
5279
|
+
if (ROUTING[key]?.routes("dev").some((d) => d.type === "eas")) easKeys.push([key, value]);
|
|
5280
|
+
}
|
|
5281
|
+
if (easKeys.length === 0) return [];
|
|
5282
|
+
const { writeFile: writeFile4, unlink: unlink2, mkdtemp, rmdir } = await import('fs/promises');
|
|
5283
|
+
const { tmpdir } = await import('os');
|
|
5284
|
+
const { join: join2 } = await import('path');
|
|
5285
|
+
const dir = await mkdtemp(join2(tmpdir(), "vexpo-env-"));
|
|
5286
|
+
const tmp = join2(dir, "eas.env");
|
|
5287
|
+
try {
|
|
5288
|
+
await writeFile4(tmp, easKeys.map(([k, v]) => `${k}=${v}`).join("\n") + "\n", { mode: 384 });
|
|
5289
|
+
await envPush({ path: tmp, environments, force: true });
|
|
5290
|
+
return easKeys.map(([k]) => k);
|
|
5291
|
+
} finally {
|
|
5292
|
+
await unlink2(tmp).catch(() => {
|
|
5293
|
+
});
|
|
5294
|
+
await rmdir(dir).catch(() => {
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
4765
5298
|
async function runEas(options2) {
|
|
4766
5299
|
section("EAS");
|
|
4767
5300
|
try {
|
|
4768
5301
|
const cli = await checkCli();
|
|
4769
5302
|
if (!cli.ok) {
|
|
4770
|
-
bad("eas CLI not available. install with `
|
|
5303
|
+
bad("eas CLI not available. install with `npm install -g eas-cli`");
|
|
4771
5304
|
return 1;
|
|
4772
5305
|
}
|
|
4773
5306
|
ok(`eas-cli ${cli.version}`);
|
|
4774
5307
|
const who = await whoami();
|
|
4775
5308
|
if (!who) {
|
|
4776
5309
|
if (!process.stdin.isTTY) {
|
|
4777
|
-
bad("non-TTY: run `
|
|
5310
|
+
bad("non-TTY: run `npx eas login` then re-run");
|
|
4778
5311
|
return 1;
|
|
4779
5312
|
}
|
|
4780
5313
|
yep("not signed in to Expo");
|
|
@@ -4792,7 +5325,7 @@ async function runEas(options2) {
|
|
|
4792
5325
|
} else {
|
|
4793
5326
|
ok(`signed in as ${BOLD}${who}${RESET}`);
|
|
4794
5327
|
}
|
|
4795
|
-
let projectId = await
|
|
5328
|
+
let projectId = await resolveProjectId();
|
|
4796
5329
|
if (!options2.skipInit) {
|
|
4797
5330
|
if (projectId) {
|
|
4798
5331
|
ok(`EAS project linked: ${projectId}`);
|
|
@@ -4815,10 +5348,16 @@ async function runEas(options2) {
|
|
|
4815
5348
|
else nop(`branches already exist (${branches.join(", ")})`);
|
|
4816
5349
|
}
|
|
4817
5350
|
if (!options2.skipEnv) {
|
|
4818
|
-
if (await
|
|
5351
|
+
if (await fileExists7(".env.local")) {
|
|
4819
5352
|
try {
|
|
4820
|
-
await
|
|
4821
|
-
|
|
5353
|
+
const pushed = await pushEasRoutedKeys(".env.local", ["development"]);
|
|
5354
|
+
if (pushed.length > 0) {
|
|
5355
|
+
ok(
|
|
5356
|
+
`pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (development)`
|
|
5357
|
+
);
|
|
5358
|
+
} else {
|
|
5359
|
+
nop(".env.local has no EAS-routed keys yet (run `vexpo convex` first)");
|
|
5360
|
+
}
|
|
4822
5361
|
} catch (err) {
|
|
4823
5362
|
bad(err instanceof Error ? err.message : String(err));
|
|
4824
5363
|
}
|
|
@@ -4826,15 +5365,17 @@ async function runEas(options2) {
|
|
|
4826
5365
|
nop(".env.local missing. skipping development env push (run `vexpo convex` first)");
|
|
4827
5366
|
}
|
|
4828
5367
|
if (options2.withProd) {
|
|
4829
|
-
const prodFile = await
|
|
5368
|
+
const prodFile = await fileExists7(".env.prod") ? ".env.prod" : await fileExists7(".env.production") ? ".env.production" : null;
|
|
4830
5369
|
if (prodFile) {
|
|
4831
5370
|
try {
|
|
4832
|
-
await
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
5371
|
+
const pushed = await pushEasRoutedKeys(prodFile, ["production", "preview"]);
|
|
5372
|
+
if (pushed.length > 0) {
|
|
5373
|
+
ok(
|
|
5374
|
+
`pushed ${pushed.length} EXPO_PUBLIC_* var${pushed.length === 1 ? "" : "s"} \u2192 EAS env (production, preview)`
|
|
5375
|
+
);
|
|
5376
|
+
} else {
|
|
5377
|
+
nop(`${prodFile} has no EAS-routed keys`);
|
|
5378
|
+
}
|
|
4838
5379
|
} catch (err) {
|
|
4839
5380
|
bad(err instanceof Error ? err.message : String(err));
|
|
4840
5381
|
}
|
|
@@ -4842,6 +5383,9 @@ async function runEas(options2) {
|
|
|
4842
5383
|
nop("--with-prod set but no .env.prod or .env.production found");
|
|
4843
5384
|
}
|
|
4844
5385
|
}
|
|
5386
|
+
note(
|
|
5387
|
+
`server-side secrets route to Convex, not EAS. run ${BOLD}vexpo env push${RESET} to sync those`
|
|
5388
|
+
);
|
|
4845
5389
|
}
|
|
4846
5390
|
if (projectId) {
|
|
4847
5391
|
await recordStep("eas", {
|
|
@@ -4853,16 +5397,14 @@ async function runEas(options2) {
|
|
|
4853
5397
|
line();
|
|
4854
5398
|
note(`${BOLD}Next, eas-cli (we don't replace these)${RESET}`);
|
|
4855
5399
|
note(
|
|
4856
|
-
` ${BOLD}
|
|
5400
|
+
` ${BOLD}npx eas credentials -p ios${RESET} dist cert + profile + push key + ASC API key`
|
|
4857
5401
|
);
|
|
4858
|
-
note(` ${BOLD}
|
|
5402
|
+
note(` ${BOLD}npx eas build -p ios --profile production${RESET}`);
|
|
4859
5403
|
note(
|
|
4860
|
-
` ${BOLD}
|
|
4861
|
-
);
|
|
4862
|
-
note(` ${BOLD}bunx eas metadata:push${RESET} push store.config.json`);
|
|
4863
|
-
note(
|
|
4864
|
-
` ${BOLD}bunx eas workflow:run .eas/workflows/<file>${RESET} trigger a workflow locally`
|
|
5404
|
+
` ${BOLD}npx eas submit -p ios --profile production${RESET} (auto-creates App Store record)`
|
|
4865
5405
|
);
|
|
5406
|
+
note(` ${BOLD}npx eas metadata:push${RESET} push store.config.json`);
|
|
5407
|
+
note(` ${BOLD}npx eas workflow:run .eas/workflows/<file>${RESET} trigger a workflow locally`);
|
|
4866
5408
|
line();
|
|
4867
5409
|
note(`${BOLD}Stack-specific (ours, not eas-cli's)${RESET}`);
|
|
4868
5410
|
note(` ${BOLD}vexpo apple asc-key${RESET} validate ASC API key against /v1/apps`);
|
|
@@ -4880,10 +5422,6 @@ function computeScope(o) {
|
|
|
4880
5422
|
const lite = o.lite === true;
|
|
4881
5423
|
const isNew = o.isNew === true;
|
|
4882
5424
|
return {
|
|
4883
|
-
// Accounts walkthrough is educational only. Default mode skips it because
|
|
4884
|
-
// users with existing accounts don't need it. `--new` opts in. In lite
|
|
4885
|
-
// mode + --new, the walkthrough is limited to the Convex signup (runAccounts
|
|
4886
|
-
// gets `lite: true` and short-circuits past Apple/Domain/Expo/Resend).
|
|
4887
5425
|
accounts: isNew,
|
|
4888
5426
|
rebrand: !lite && !o.skipRebrand,
|
|
4889
5427
|
resend: !lite,
|
|
@@ -4901,7 +5439,7 @@ async function isXcodeInstalled() {
|
|
|
4901
5439
|
});
|
|
4902
5440
|
return await proc.exited === 0;
|
|
4903
5441
|
}
|
|
4904
|
-
async function
|
|
5442
|
+
async function fileExists8(p) {
|
|
4905
5443
|
try {
|
|
4906
5444
|
await access(p);
|
|
4907
5445
|
return true;
|
|
@@ -4928,7 +5466,7 @@ async function trashPaths(paths) {
|
|
|
4928
5466
|
}
|
|
4929
5467
|
async function wipeMetroCaches(tmpdir) {
|
|
4930
5468
|
const { readdir } = await import('fs/promises');
|
|
4931
|
-
const { join } = await import('path');
|
|
5469
|
+
const { join: join2 } = await import('path');
|
|
4932
5470
|
let entries = [];
|
|
4933
5471
|
try {
|
|
4934
5472
|
entries = await readdir(tmpdir);
|
|
@@ -4936,11 +5474,11 @@ async function wipeMetroCaches(tmpdir) {
|
|
|
4936
5474
|
return;
|
|
4937
5475
|
}
|
|
4938
5476
|
const matchers = [/^metro-/, /^haste-map-/, /^react-/, /^node-compile-cache$/];
|
|
4939
|
-
const targets = entries.filter((e) => matchers.some((m) => m.test(e))).map((e) =>
|
|
5477
|
+
const targets = entries.filter((e) => matchers.some((m) => m.test(e))).map((e) => join2(tmpdir, e));
|
|
4940
5478
|
await trashPaths(targets);
|
|
4941
5479
|
}
|
|
4942
5480
|
async function nodeModulesPresent() {
|
|
4943
|
-
return await
|
|
5481
|
+
return await fileExists8("node_modules/.package-lock.json") || await fileExists8("node_modules/.bin/expo") || await fileExists8("node_modules/expo/package.json");
|
|
4944
5482
|
}
|
|
4945
5483
|
var STEP_TTL_HOURS = {
|
|
4946
5484
|
accounts: 24,
|
|
@@ -4954,8 +5492,8 @@ var STEP_TTL_HOURS = {
|
|
|
4954
5492
|
// EAS credentials don't drift; once configured, they stay until you rotate.
|
|
4955
5493
|
"apple-credentials": Infinity,
|
|
4956
5494
|
// ASC project link via `eas integrations:asc:connect`. Live-checked through
|
|
4957
|
-
// `eas integrations:asc:status` so
|
|
4958
|
-
//
|
|
5495
|
+
// `eas integrations:asc:status` so cache TTL is short. Drift would mean
|
|
5496
|
+
// someone disconnected via the EAS dashboard.
|
|
4959
5497
|
"apple-asc-link": 24,
|
|
4960
5498
|
// No cache for the rotation secrets phase. EAS env state is the source of
|
|
4961
5499
|
// truth, and the secrets list query takes ~1s.
|
|
@@ -4976,6 +5514,9 @@ async function shouldRun(step, liveCheck) {
|
|
|
4976
5514
|
return { step, label: step, status: "cached" };
|
|
4977
5515
|
}
|
|
4978
5516
|
const live = await liveCheck();
|
|
5517
|
+
if (live && !options.dryRun && !options.plan && !options.noState) {
|
|
5518
|
+
await recordStep(step, { source: "live-check" });
|
|
5519
|
+
}
|
|
4979
5520
|
return { step, label: step, status: live ? "live" : "missing" };
|
|
4980
5521
|
}
|
|
4981
5522
|
async function liveCheckBetterAuth(env2) {
|
|
@@ -4995,7 +5536,7 @@ async function liveCheckApple(env2) {
|
|
|
4995
5536
|
);
|
|
4996
5537
|
}
|
|
4997
5538
|
async function liveCheckEas() {
|
|
4998
|
-
const projectId = await
|
|
5539
|
+
const projectId = await resolveProjectId();
|
|
4999
5540
|
if (!projectId) return false;
|
|
5000
5541
|
const eas = await envList("production").catch(() => /* @__PURE__ */ new Map());
|
|
5001
5542
|
return ["EXPO_PUBLIC_CONVEX_URL", "EXPO_PUBLIC_CONVEX_SITE_URL", "EXPO_PUBLIC_SITE_URL"].every(
|
|
@@ -5004,15 +5545,15 @@ async function liveCheckEas() {
|
|
|
5004
5545
|
}
|
|
5005
5546
|
async function liveCheckAscLink() {
|
|
5006
5547
|
try {
|
|
5007
|
-
const { ascStatus: ascStatus2 } = await import('./eas-integrations-
|
|
5548
|
+
const { ascStatus: ascStatus2 } = await import('./eas-integrations-2QVR45NE.js');
|
|
5008
5549
|
const status = await ascStatus2();
|
|
5009
|
-
return
|
|
5550
|
+
return status.status === "connected";
|
|
5010
5551
|
} catch {
|
|
5011
5552
|
return false;
|
|
5012
5553
|
}
|
|
5013
5554
|
}
|
|
5014
5555
|
async function liveCheckRotationSecrets() {
|
|
5015
|
-
const projectId = await
|
|
5556
|
+
const projectId = await resolveProjectId();
|
|
5016
5557
|
if (!projectId) return false;
|
|
5017
5558
|
const eas = await envList("production").catch(() => /* @__PURE__ */ new Map());
|
|
5018
5559
|
return [
|
|
@@ -5044,11 +5585,11 @@ async function stepPrerequisites() {
|
|
|
5044
5585
|
else yep("Xcode not detected (install from Mac App Store)");
|
|
5045
5586
|
const [easV, convexV] = await Promise.all([version2(), version()]);
|
|
5046
5587
|
if (easV) ok(`eas-cli ${easV}`);
|
|
5047
|
-
else nop("eas-cli not on PATH (
|
|
5588
|
+
else nop("eas-cli not on PATH (npx will fetch on demand)");
|
|
5048
5589
|
if (convexV) ok(`convex ${convexV}`);
|
|
5049
|
-
else nop("convex CLI not on PATH (
|
|
5590
|
+
else nop("convex CLI not on PATH (npx will fetch on demand)");
|
|
5050
5591
|
if (await isLoggedIn()) ok("Convex auth detected");
|
|
5051
|
-
else yep("not signed in to Convex (`
|
|
5592
|
+
else yep("not signed in to Convex (`npx vexpo accounts` will prompt)");
|
|
5052
5593
|
}
|
|
5053
5594
|
async function stepProbe() {
|
|
5054
5595
|
section("Probe");
|
|
@@ -5087,7 +5628,7 @@ async function stepProbe() {
|
|
|
5087
5628
|
line(` ${BOLD}${label.padEnd(w)}${RESET} ${mark(row.status)}`);
|
|
5088
5629
|
}
|
|
5089
5630
|
line(
|
|
5090
|
-
` ${BOLD}${"Review account".padEnd(w)}${RESET} ${DIM}unknown (run \`
|
|
5631
|
+
` ${BOLD}${"Review account".padEnd(w)}${RESET} ${DIM}unknown (run \`npx vexpo review-account\` to seed)${RESET}`
|
|
5091
5632
|
);
|
|
5092
5633
|
const needs = /* @__PURE__ */ new Map();
|
|
5093
5634
|
for (const [k, row] of rows) needs.set(k, row.status === "missing");
|
|
@@ -5195,7 +5736,7 @@ async function describePhase(step, probe) {
|
|
|
5195
5736
|
details: [
|
|
5196
5737
|
"validates an ASC API key against ASC's GET /v1/apps before EAS uses it",
|
|
5197
5738
|
"we do NOT upload to EAS. that's `eas credentials`. We only validate + cache",
|
|
5198
|
-
"cache (issuer, keyId, p8 path) in state.json so `
|
|
5739
|
+
"cache (issuer, keyId, p8 path) in state.json so `npx vexpo apple services-id` can reuse",
|
|
5199
5740
|
"fast-fail: catches a bad key in <1s instead of waiting for an EAS build to fail"
|
|
5200
5741
|
]
|
|
5201
5742
|
};
|
|
@@ -5239,23 +5780,24 @@ async function describePhase(step, probe) {
|
|
|
5239
5780
|
return {
|
|
5240
5781
|
step,
|
|
5241
5782
|
label: "setup:asc:connect",
|
|
5242
|
-
action: cached && !options.force ? "skip (already connected)" : "run (
|
|
5783
|
+
action: cached && !options.force ? "skip (already connected)" : "run (eas-cli interactive)",
|
|
5243
5784
|
details: [
|
|
5244
|
-
"
|
|
5245
|
-
"
|
|
5246
|
-
"
|
|
5785
|
+
"spawns `eas integrations:asc:connect --bundle-id <bundle>`",
|
|
5786
|
+
"pre-sets EXPO_ASC_API_KEY_* env vars from cached asc-key state",
|
|
5787
|
+
"wizard prompts once when no key is uploaded yet (Create new / Use existing)",
|
|
5788
|
+
"after this, `eas submit` skips ASC app discovery"
|
|
5247
5789
|
]
|
|
5248
5790
|
};
|
|
5249
5791
|
case "apple-eas-rotation-secrets":
|
|
5250
5792
|
return {
|
|
5251
5793
|
step,
|
|
5252
5794
|
label: "setup:apple:eas-rotation-secrets",
|
|
5253
|
-
action: cached && !options.force ? "skip (all 5 set)" : "run (
|
|
5795
|
+
action: cached && !options.force ? "skip (all 5 set)" : "run (mints CONVEX_DEPLOY_KEY)",
|
|
5254
5796
|
details: [
|
|
5255
5797
|
"push the 5 EAS production secrets the JWT rotation cron needs",
|
|
5256
|
-
"APPLE_P8_PRIVATE_KEY (
|
|
5798
|
+
"APPLE_P8_PRIVATE_KEY (.p8 path; EAS reads + base64-encodes it)",
|
|
5257
5799
|
"APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_SERVICES_ID (from .env.local)",
|
|
5258
|
-
"CONVEX_DEPLOY_KEY (
|
|
5800
|
+
"CONVEX_DEPLOY_KEY (minted via the Convex Platform API; paste fallback if offline)"
|
|
5259
5801
|
]
|
|
5260
5802
|
};
|
|
5261
5803
|
}
|
|
@@ -5282,7 +5824,7 @@ async function printDryRunPlan(probe) {
|
|
|
5282
5824
|
section("Dry run plan");
|
|
5283
5825
|
if (options.fresh)
|
|
5284
5826
|
note(
|
|
5285
|
-
`${YELLOW}--fresh${RESET}: would wipe .setup-state.json + node_modules + ios/ +
|
|
5827
|
+
`${YELLOW}--fresh${RESET}: would wipe .setup-state.json + node_modules + ios/ + package-lock.json + build artifacts before reprovisioning`
|
|
5286
5828
|
);
|
|
5287
5829
|
if (options.force) note(`${YELLOW}--force${RESET}: would re-run every step regardless of cache`);
|
|
5288
5830
|
if (probe.install) note(`would run install (no node_modules)`);
|
|
@@ -5305,7 +5847,7 @@ async function printDryRunPlan(probe) {
|
|
|
5305
5847
|
);
|
|
5306
5848
|
line();
|
|
5307
5849
|
note(`drop ${DIM}--dry-run${RESET} to actually do it`);
|
|
5308
|
-
note(`single-phase: ${DIM}
|
|
5850
|
+
note(`single-phase: ${DIM}npx vexpo <phase>${RESET} (e.g. ${DIM}npx vexpo resend${RESET})`);
|
|
5309
5851
|
}
|
|
5310
5852
|
var JOURNEY = {
|
|
5311
5853
|
async: [
|
|
@@ -5348,8 +5890,8 @@ var JOURNEY = {
|
|
|
5348
5890
|
},
|
|
5349
5891
|
{
|
|
5350
5892
|
label: "Convex production deploy key",
|
|
5351
|
-
cost: "
|
|
5352
|
-
description: "For the JWT rotation cron and the deploy_convex step in deploy-production.yml.
|
|
5893
|
+
cost: "auto",
|
|
5894
|
+
description: "For the JWT rotation cron and the deploy_convex step in deploy-production.yml. Minted automatically via the Convex Platform API; paste only as an offline fallback.",
|
|
5353
5895
|
url: "https://dashboard.convex.dev"
|
|
5354
5896
|
},
|
|
5355
5897
|
{
|
|
@@ -5397,7 +5939,7 @@ var JOURNEY = {
|
|
|
5397
5939
|
{
|
|
5398
5940
|
label: "EAS rotation secrets",
|
|
5399
5941
|
cost: "auto",
|
|
5400
|
-
description: "Pushes the 5 EAS production secrets the JWT rotation cron needs (4 from .env.local + state, plus CONVEX_DEPLOY_KEY
|
|
5942
|
+
description: "Pushes the 5 EAS production secrets the JWT rotation cron needs (4 from .env.local + state, plus CONVEX_DEPLOY_KEY minted via the Platform API)."
|
|
5401
5943
|
}
|
|
5402
5944
|
]
|
|
5403
5945
|
};
|
|
@@ -5405,7 +5947,7 @@ function printJourneyPlan(lite) {
|
|
|
5405
5947
|
if (lite) {
|
|
5406
5948
|
section("Setup journey (lite)");
|
|
5407
5949
|
line(
|
|
5408
|
-
` ${DIM}Lite mode provisions only what the iOS Simulator needs. No Apple Developer account, no domain, no Resend, no EAS account. ~60 seconds from start to \`
|
|
5950
|
+
` ${DIM}Lite mode provisions only what the iOS Simulator needs. No Apple Developer account, no domain, no Resend, no EAS account. ~60 seconds from start to \`npm run ios\`.${RESET}`
|
|
5409
5951
|
);
|
|
5410
5952
|
line();
|
|
5411
5953
|
line(` ${BOLD}${GREEN}Auto${RESET} ${DIM}(CLI does it, no input needed)${RESET}`);
|
|
@@ -5464,7 +6006,19 @@ var LITE_AUTO_LABELS = /* @__PURE__ */ new Set([
|
|
|
5464
6006
|
]);
|
|
5465
6007
|
function isComplete(result) {
|
|
5466
6008
|
if (result.install) return false;
|
|
5467
|
-
for (const required of [
|
|
6009
|
+
for (const required of [
|
|
6010
|
+
"rebrand",
|
|
6011
|
+
"convex",
|
|
6012
|
+
"better-auth",
|
|
6013
|
+
"resend",
|
|
6014
|
+
"asc-key",
|
|
6015
|
+
"apple-credentials",
|
|
6016
|
+
"apple-asc-link",
|
|
6017
|
+
"apple-services-id",
|
|
6018
|
+
"apple-sign-in",
|
|
6019
|
+
"apple-eas-rotation-secrets",
|
|
6020
|
+
"eas"
|
|
6021
|
+
]) {
|
|
5468
6022
|
if (result.needs.get(required)) return false;
|
|
5469
6023
|
}
|
|
5470
6024
|
return true;
|
|
@@ -5474,6 +6028,7 @@ async function stepCleanup(fresh) {
|
|
|
5474
6028
|
const tmpdir = process.env.TMPDIR ?? "/tmp";
|
|
5475
6029
|
const targets = [
|
|
5476
6030
|
"node_modules",
|
|
6031
|
+
"package-lock.json",
|
|
5477
6032
|
"bun.lock",
|
|
5478
6033
|
"ios",
|
|
5479
6034
|
".expo",
|
|
@@ -5502,6 +6057,7 @@ async function stepInstallOnly() {
|
|
|
5502
6057
|
}
|
|
5503
6058
|
var completed = [];
|
|
5504
6059
|
var skipped = [];
|
|
6060
|
+
var failedStep = null;
|
|
5505
6061
|
var STEP_RUNNERS = {
|
|
5506
6062
|
"vexpo accounts": () => runAccounts({ lite: options.lite }),
|
|
5507
6063
|
"vexpo rebrand": () => runRebrand({}),
|
|
@@ -5514,14 +6070,19 @@ var STEP_RUNNERS = {
|
|
|
5514
6070
|
"vexpo apple jwt": () => runAppleJwt({}),
|
|
5515
6071
|
"vexpo apple eas-rotation-secrets": () => runEasRotationSecrets({}),
|
|
5516
6072
|
"vexpo asc connect": () => runAscConnect({}),
|
|
5517
|
-
"vexpo eas": () => runEas({}),
|
|
6073
|
+
"vexpo eas": async () => runEas({ withProd: await fileExists8(".env.prod") || await fileExists8(".env.production") }),
|
|
5518
6074
|
"vexpo review-account": () => runReviewAccount({})
|
|
5519
6075
|
};
|
|
5520
6076
|
async function runStep(name, state) {
|
|
5521
6077
|
const runner = STEP_RUNNERS[name];
|
|
5522
6078
|
if (!runner) throw new Error(`unknown setup step: ${name}`);
|
|
5523
|
-
|
|
5524
|
-
|
|
6079
|
+
try {
|
|
6080
|
+
const code = await runner();
|
|
6081
|
+
if (code !== 0) throw new Error(`${name} exited with code ${code}`);
|
|
6082
|
+
} catch (err) {
|
|
6083
|
+
if (state) failedStep = state;
|
|
6084
|
+
throw err;
|
|
6085
|
+
}
|
|
5525
6086
|
if (state) completed.push(state);
|
|
5526
6087
|
}
|
|
5527
6088
|
async function maybeRunStep(name, prompt, state) {
|
|
@@ -5543,7 +6104,7 @@ function printShipNextSteps() {
|
|
|
5543
6104
|
line(` Run this when you're ready:`);
|
|
5544
6105
|
line();
|
|
5545
6106
|
line(
|
|
5546
|
-
` ${BOLD}
|
|
6107
|
+
` ${BOLD}npx eas build -p ios --profile production --auto-submit-with-profile testflight${RESET}`
|
|
5547
6108
|
);
|
|
5548
6109
|
line();
|
|
5549
6110
|
line(
|
|
@@ -5563,7 +6124,7 @@ async function printSummary(useLocal, elapsedMs) {
|
|
|
5563
6124
|
section("Summary");
|
|
5564
6125
|
const [localEnv, convexEnv] = await Promise.all([readAll(), envMap()]);
|
|
5565
6126
|
const easEnv = await envList("production").catch(() => /* @__PURE__ */ new Map());
|
|
5566
|
-
const projectId = await
|
|
6127
|
+
const projectId = await resolveProjectId();
|
|
5567
6128
|
const state = await load();
|
|
5568
6129
|
const localKeys = [
|
|
5569
6130
|
"CONVEX_DEPLOYMENT",
|
|
@@ -5601,9 +6162,13 @@ async function printSummary(useLocal, elapsedMs) {
|
|
|
5601
6162
|
"convex",
|
|
5602
6163
|
"better-auth",
|
|
5603
6164
|
"resend",
|
|
6165
|
+
"review-account",
|
|
5604
6166
|
"asc-key",
|
|
6167
|
+
"apple-credentials",
|
|
6168
|
+
"apple-asc-link",
|
|
5605
6169
|
"apple-services-id",
|
|
5606
6170
|
"apple-sign-in",
|
|
6171
|
+
"apple-eas-rotation-secrets",
|
|
5607
6172
|
"eas"
|
|
5608
6173
|
];
|
|
5609
6174
|
const width = Math.max(
|
|
@@ -5639,16 +6204,18 @@ async function printSummary(useLocal, elapsedMs) {
|
|
|
5639
6204
|
${GREEN}ok${RESET} setup complete in ${(elapsedMs / 1e3).toFixed(2)}s`);
|
|
5640
6205
|
line(
|
|
5641
6206
|
`
|
|
5642
|
-
next: ${BOLD}${useLocal ? "
|
|
6207
|
+
next: ${BOLD}${useLocal ? "npx convex dev" : "npm run convex:dev"}${RESET} ${DIM}then${RESET} ${BOLD}npm run ios${RESET}
|
|
5643
6208
|
`
|
|
5644
6209
|
);
|
|
5645
6210
|
}
|
|
5646
6211
|
async function runSetup(opts) {
|
|
5647
6212
|
options = opts;
|
|
6213
|
+
failedStep = null;
|
|
6214
|
+
completed.length = 0;
|
|
6215
|
+
skipped.length = 0;
|
|
5648
6216
|
const startedAtPerf = performance.now();
|
|
5649
6217
|
const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5650
6218
|
let failureMessage = null;
|
|
5651
|
-
let failureStep = null;
|
|
5652
6219
|
try {
|
|
5653
6220
|
if (options.fresh) {
|
|
5654
6221
|
await clearAll();
|
|
@@ -5656,7 +6223,7 @@ async function runSetup(opts) {
|
|
|
5656
6223
|
if (!options.dryRun && !options.plan && !options.noState) {
|
|
5657
6224
|
const existing = await load();
|
|
5658
6225
|
const concurrent = checkConcurrentRun(existing);
|
|
5659
|
-
if (concurrent.
|
|
6226
|
+
if (concurrent.active && concurrent.otherPid !== void 0) {
|
|
5660
6227
|
yep(
|
|
5661
6228
|
`another vexpo run (pid ${concurrent.otherPid}) touched .setup-state.json recently; if you're not running in another terminal, ignore this`
|
|
5662
6229
|
);
|
|
@@ -5844,7 +6411,7 @@ async function runSetup(opts) {
|
|
|
5844
6411
|
cwd: process.cwd(),
|
|
5845
6412
|
completed,
|
|
5846
6413
|
skipped,
|
|
5847
|
-
...failureMessage ? { failed: { step:
|
|
6414
|
+
...failureMessage ? { failed: { step: failedStep ?? "convex", message: failureMessage } } : {}
|
|
5848
6415
|
});
|
|
5849
6416
|
} catch {
|
|
5850
6417
|
}
|
|
@@ -5913,13 +6480,29 @@ program.command("doctor").description(
|
|
|
5913
6480
|
).option("--channel <channel>", "dev | prod", "dev").option("--json", "machine-readable output", false).option("--strict", "exit non-zero on any warn", false).action((options2) => {
|
|
5914
6481
|
exitWith(runDoctor(options2));
|
|
5915
6482
|
});
|
|
6483
|
+
program.command("adopt").description(
|
|
6484
|
+
"Finish a project created by `eas integrations:convex:connect`: adopt the existing dev deployment (never a fresh one), backfill site URLs + Better Auth, report the deployment topology (flagging a duplicate dev deployment), and print the exact commands left to finish."
|
|
6485
|
+
).option("--skip-dev-steps", "report topology + runbook only, don't run convex/better-auth", false).action((options2) => exitWith(runAdopt(options2)));
|
|
5916
6486
|
program.command("convex").description("Provision or connect a Convex deployment.").option("--fresh", "provision a NEW deployment", false).option("--local", "self-hosted / local backend", false).option("--name <name>", "override Convex project name").action(
|
|
5917
6487
|
(options2) => exitWith(runConvex(options2))
|
|
5918
6488
|
);
|
|
6489
|
+
program.command("convex:migrate").description(
|
|
6490
|
+
"Copy server-side Convex env (BETTER_AUTH_SECRET, RESEND_*, APPLE_*, APP_*, ...) from another deployment onto the current one. The piece a deployment migration can't get off disk; CONVEX_* are left untouched."
|
|
6491
|
+
).requiredOption("--from <deployment>", "source deployment slug to copy env from").option("--prod", "target the prod deployment (reads prod creds from .env.prod)").option("--dry-run", "show what would be copied, exit without changes", false).action(
|
|
6492
|
+
(options2) => exitWith(runConvexMigrate(options2))
|
|
6493
|
+
);
|
|
5919
6494
|
program.command("better-auth").description("Set SITE_URL, BETTER_AUTH_SECRET, APP_NAME on Convex.").option("--rotate-secret", "regenerate BETTER_AUTH_SECRET", false).option("--site-url <url>", "override SITE_URL").option("--app-name <name>", "override APP_NAME").action(
|
|
5920
6495
|
(options2) => exitWith(runBetterAuth(options2))
|
|
5921
6496
|
);
|
|
5922
|
-
program.command("resend").description("Provision Resend sending key + webhook, write to Convex env.").option("--name <name>", "override sending key name").option("--from <address>", "override EMAIL_FROM").
|
|
6497
|
+
program.command("resend").description("Provision Resend sending key + webhook, write to Convex env.").option("--name <name>", "override sending key name").option("--from <address>", "override EMAIL_FROM").option(
|
|
6498
|
+
"--repoint",
|
|
6499
|
+
"move the webhook to the current convex.site + realign the secret, without rotating the sending key or changing auth policy"
|
|
6500
|
+
).option("--prod", "with --repoint, target the prod deployment + .env.prod site URL").option(
|
|
6501
|
+
"--force",
|
|
6502
|
+
"with --repoint, recreate the webhook even if it already points at the endpoint"
|
|
6503
|
+
).action(
|
|
6504
|
+
(options2) => exitWith(runResend(options2))
|
|
6505
|
+
);
|
|
5923
6506
|
var apple = program.command("apple").description("Apple-side provisioning.");
|
|
5924
6507
|
apple.command("asc-key").description("Validate an App Store Connect API key against `/v1/apps`. No eas-cli equivalent.").option("--revalidate", "re-check the cached key still works", false).action((options2) => exitWith(runAscKey(options2)));
|
|
5925
6508
|
apple.command("services-id").description(
|
|
@@ -5927,7 +6510,10 @@ apple.command("services-id").description(
|
|
|
5927
6510
|
).option("--services-id <id>", "override Services ID").action((options2) => exitWith(runServicesId(options2)));
|
|
5928
6511
|
apple.command("jwt").description(
|
|
5929
6512
|
"Sign the Sign In with Apple ES256 client_secret JWT (180-day expiry, Apple's max). Quarterly auto-rotation runs as an EAS Workflow cron. No eas-cli equivalent."
|
|
5930
|
-
).option("--rotate", "re-sign the JWT only", false).
|
|
6513
|
+
).option("--rotate", "re-sign the JWT only", false).option(
|
|
6514
|
+
"--copy-from <deployment>",
|
|
6515
|
+
"copy APPLE_* env from another deployment (slug) instead of signing; no .p8 needed"
|
|
6516
|
+
).action((options2) => exitWith(runAppleJwt(options2)));
|
|
5931
6517
|
apple.command("credentials").description(
|
|
5932
6518
|
"Provision iOS build credentials by wrapping `eas credentials:configure-build` with the cached ASC API key passed via env vars (skips the Apple Developer login prompt in the wizard). EAS generates the dist cert + provisioning profile + push key."
|
|
5933
6519
|
).option("-e, --profile <name>", "build profile", "production").action((options2) => exitWith(runAppleCredentials(options2)));
|
|
@@ -5951,37 +6537,35 @@ env.command("push").description(
|
|
|
5951
6537
|
);
|
|
5952
6538
|
}
|
|
5953
6539
|
);
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
(
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
}
|
|
5965
|
-
exitWith(
|
|
5966
|
-
runPhasedRelease({
|
|
5967
|
-
versionId,
|
|
5968
|
-
action
|
|
6540
|
+
env.command("convex-key").description(
|
|
6541
|
+
"Sync the Convex deploy key + deployment selector to EAS env (dev \u2192 development, prod \u2192 production/preview). Fixes a stale EAS deploy key after a deployment migration; env push skips these on purpose."
|
|
6542
|
+
).option("--dev-key <key>", "dev deploy key (default: CONVEX_DEPLOY_KEY in .env.local)").option("--prod-key <key>", "prod deploy key (default: CONVEX_DEPLOY_KEY in .env.prod)").option("--mint", "mint the prod deploy key via the Platform API if EAS lacks one", false).option("--local-file <path>", "override .env.local path").option("--prod-file <path>", "override .env.prod path").action(
|
|
6543
|
+
(options2) => exitWith(
|
|
6544
|
+
runConvexKey({
|
|
6545
|
+
devKey: options2.devKey,
|
|
6546
|
+
prodKey: options2.prodKey,
|
|
6547
|
+
mint: options2.mint,
|
|
6548
|
+
localFile: options2.localFile,
|
|
6549
|
+
prodFile: options2.prodFile
|
|
5969
6550
|
})
|
|
5970
|
-
)
|
|
5971
|
-
|
|
5972
|
-
program.command("asc:
|
|
6551
|
+
)
|
|
6552
|
+
);
|
|
6553
|
+
program.command("asc:connect").description(
|
|
6554
|
+
"Link the EAS project to its App Store Connect app (wraps `eas integrations:asc:connect` with the cached ASC key). Lets `eas submit` resolve the app from the bundle id, so eas.json needs no committed ascAppId. Needs an interactive terminal."
|
|
6555
|
+
).option("--force", "re-run even if already connected", false).action((options2) => exitWith(runAscConnect(options2)));
|
|
6556
|
+
var ascPrivacy = program.command("asc:privacy").description("Privacy nutrition labels (local).");
|
|
6557
|
+
ascPrivacy.command("show [file]").description("Show the declared privacy.config.json (Apple has no live read API; set it in ASC).").option("--json", "JSON output", false).action(
|
|
6558
|
+
(file, options2) => exitWith(runPrivacyShow(file ?? "app-store/privacy.config.json", options2))
|
|
6559
|
+
);
|
|
6560
|
+
ascPrivacy.command("lint <file>").description("Validate a local privacy.config.json against Apple's enums.").action((file) => exitWith(runPrivacyLint(file)));
|
|
6561
|
+
var ascA11y = program.command("asc:accessibility").description("Accessibility nutrition labels (iOS 26+).");
|
|
6562
|
+
ascA11y.command("show").description("Fetch the app's current accessibility declarations.").option("--json", "JSON output", false).action((options2) => exitWith(runAccessibilityShow(options2)));
|
|
6563
|
+
ascA11y.command("lint <file>").description("Validate a local accessibility.config.json against Apple's enums.").action((file) => exitWith(runAccessibilityLint(file)));
|
|
5973
6564
|
var testflight2 = program.command("testflight").description("TestFlight beta groups + testers via ASC API.");
|
|
5974
6565
|
var tfGroups = testflight2.command("groups").description("Beta groups.");
|
|
5975
6566
|
tfGroups.command("list").description("List beta groups for the current app.").option("--json", "JSON output", false).action((options2) => exitWith(runTestflightGroupsList(options2)));
|
|
5976
|
-
tfGroups.command("create <name>").description("Create a beta group.").option("--
|
|
5977
|
-
(name, options2) => exitWith(
|
|
5978
|
-
runTestflightGroupsCreate({
|
|
5979
|
-
name,
|
|
5980
|
-
publicLink: options2.publicLink,
|
|
5981
|
-
publicLimit: options2.publicLimit,
|
|
5982
|
-
feedback: options2.feedback
|
|
5983
|
-
})
|
|
5984
|
-
)
|
|
6567
|
+
tfGroups.command("create <name>").description("Create a beta group.").option("--feedback", "enable in-app feedback", false).action(
|
|
6568
|
+
(name, options2) => exitWith(runTestflightGroupsCreate({ name, feedback: options2.feedback }))
|
|
5985
6569
|
);
|
|
5986
6570
|
tfGroups.command("view <id>").description("View a beta group + its testers.").option("--json", "JSON output", false).action(
|
|
5987
6571
|
(id, options2) => exitWith(runTestflightGroupsView(id, options2))
|
|
@@ -5999,19 +6583,7 @@ testflight2.command("invite <email>").description("Add a tester + send a TestFli
|
|
|
5999
6583
|
})
|
|
6000
6584
|
)
|
|
6001
6585
|
);
|
|
6002
|
-
testflight2.command("remove <email>").description("Remove a beta tester.").action((email) => exitWith(runTestflightRemove(email)));
|
|
6003
6586
|
testflight2.command("whats-new <buildId> <text>").description(`Set the "What's new" release notes for a TestFlight build.`).option("--locale <locale>", "ISO locale", "en-US").action(
|
|
6004
6587
|
(buildId, text, options2) => exitWith(runTestflightWhatsNew({ buildId, locale: options2.locale ?? "en-US", text }))
|
|
6005
6588
|
);
|
|
6006
|
-
var reviewsCmd = program.command("reviews").description("Customer reviews + responses via ASC API.");
|
|
6007
|
-
reviewsCmd.command("list").description("List customer reviews.").option("--territory <code>", "filter by territory (e.g. US)").option("--rating <n>", "filter by rating (1-5)", (v) => parseInt(v, 10)).option("--limit <n>", "max items", (v) => parseInt(v, 10), 50).option("--json", "JSON output", false).action((options2) => exitWith(runReviewsList(options2)));
|
|
6008
|
-
reviewsCmd.command("unanswered").description("List reviews without a response.").option("--days <n>", "older than N days", (v) => parseInt(v, 10)).option("--limit <n>", "max items to scan", (v) => parseInt(v, 10), 200).option("--json", "JSON output", false).action((options2) => exitWith(runReviewsUnanswered(options2)));
|
|
6009
|
-
reviewsCmd.command("respond <reviewId> <body>").description("Post a response to a customer review.").action((reviewId, body) => exitWith(runReviewsRespond({ reviewId, body })));
|
|
6010
|
-
reviewsCmd.command("delete-response <responseId>").description("Delete a review response.").action((responseId) => exitWith(runReviewsDeleteResponse(responseId)));
|
|
6011
|
-
var sandboxCmd = program.command("sandbox").description("Sandbox testers for IAP testing via ASC API.");
|
|
6012
|
-
sandboxCmd.command("list").description("List sandbox testers.").option("--json", "JSON output", false).action((options2) => exitWith(runSandboxList(options2)));
|
|
6013
|
-
sandboxCmd.command("create").description("Create a sandbox tester.").requiredOption("--email <email>").requiredOption("--password <password>", "8+ chars; this is the App Store sandbox password").requiredOption("--first-name <name>").requiredOption("--last-name <name>").requiredOption("--territory <code>", "ISO territory code (e.g. USA)").action(
|
|
6014
|
-
(options2) => exitWith(runSandboxCreate(options2))
|
|
6015
|
-
);
|
|
6016
|
-
sandboxCmd.command("delete <id>").description("Delete a sandbox tester.").action((id) => exitWith(runSandboxDelete(id)));
|
|
6017
6589
|
program.parse();
|