@ramonclaudio/create-vexpo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +183 -0
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
  5. package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
  6. package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
  7. package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
  8. package/dist/templates/default/.eas/workflows/release.yml +44 -0
  9. package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
  10. package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
  11. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
  12. package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
  13. package/dist/templates/default/.github/workflows/check.yml +28 -0
  14. package/dist/templates/default/.maestro/launch.yaml +18 -0
  15. package/dist/templates/default/AGENTS.md +79 -0
  16. package/dist/templates/default/DESIGN.md +331 -0
  17. package/dist/templates/default/LICENSE +21 -0
  18. package/dist/templates/default/README.md +153 -0
  19. package/dist/templates/default/SETUP.md +618 -0
  20. package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
  21. package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
  22. package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
  23. package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
  24. package/dist/templates/default/_easignore +22 -0
  25. package/dist/templates/default/_editorconfig +9 -0
  26. package/dist/templates/default/_env.example +34 -0
  27. package/dist/templates/default/_fingerprintignore +24 -0
  28. package/dist/templates/default/_gitattributes +7 -0
  29. package/dist/templates/default/_gitignore +69 -0
  30. package/dist/templates/default/_oxfmtrc.json +3 -0
  31. package/dist/templates/default/_oxlintrc.json +34 -0
  32. package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
  33. package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
  34. package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
  35. package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
  36. package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
  37. package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
  38. package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
  39. package/dist/templates/default/app/(app)/_layout.tsx +73 -0
  40. package/dist/templates/default/app/(app)/debug.tsx +389 -0
  41. package/dist/templates/default/app/(app)/help.tsx +254 -0
  42. package/dist/templates/default/app/(app)/linked.tsx +116 -0
  43. package/dist/templates/default/app/(app)/privacy.tsx +159 -0
  44. package/dist/templates/default/app/(app)/profile.tsx +915 -0
  45. package/dist/templates/default/app/(app)/sessions.tsx +191 -0
  46. package/dist/templates/default/app/(app)/welcome.tsx +140 -0
  47. package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
  48. package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
  49. package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
  50. package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
  51. package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
  52. package/dist/templates/default/app/+native-intent.tsx +14 -0
  53. package/dist/templates/default/app/+not-found.tsx +51 -0
  54. package/dist/templates/default/app/_layout.tsx +102 -0
  55. package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
  56. package/dist/templates/default/app-store/screenshots/README.md +13 -0
  57. package/dist/templates/default/app.config.ts +201 -0
  58. package/dist/templates/default/app.json +11 -0
  59. package/dist/templates/default/assets/brand-icon-dark.png +0 -0
  60. package/dist/templates/default/assets/brand-icon-light.png +0 -0
  61. package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
  62. package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
  63. package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
  64. package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
  65. package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
  66. package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  67. package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
  68. package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
  69. package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
  70. package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
  71. package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
  72. package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
  73. package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
  74. package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
  75. package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
  76. package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
  77. package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
  78. package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
  79. package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
  80. package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
  81. package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
  82. package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
  83. package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
  84. package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
  85. package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
  86. package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
  87. package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
  88. package/dist/templates/default/assets/icon.png +0 -0
  89. package/dist/templates/default/assets/sounds/notification.wav +0 -0
  90. package/dist/templates/default/assets/splash-image-dark.png +0 -0
  91. package/dist/templates/default/assets/splash-image-light.png +0 -0
  92. package/dist/templates/default/bun.lock +1860 -0
  93. package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
  94. package/dist/templates/default/components/auth/password-field.tsx +121 -0
  95. package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
  96. package/dist/templates/default/components/ui/convex-error.tsx +32 -0
  97. package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
  98. package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
  99. package/dist/templates/default/components/ui/material.tsx +94 -0
  100. package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
  101. package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
  102. package/dist/templates/default/components/ui/skeleton.tsx +107 -0
  103. package/dist/templates/default/components/ui/status-text.tsx +49 -0
  104. package/dist/templates/default/components/ui/update-banner.tsx +82 -0
  105. package/dist/templates/default/constants/layout.ts +102 -0
  106. package/dist/templates/default/constants/theme.ts +401 -0
  107. package/dist/templates/default/constants/ui.ts +77 -0
  108. package/dist/templates/default/convex/_generated/api.d.ts +77 -0
  109. package/dist/templates/default/convex/_generated/api.js +23 -0
  110. package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
  111. package/dist/templates/default/convex/_generated/server.d.ts +143 -0
  112. package/dist/templates/default/convex/_generated/server.js +93 -0
  113. package/dist/templates/default/convex/admin.ts +102 -0
  114. package/dist/templates/default/convex/auth.config.ts +6 -0
  115. package/dist/templates/default/convex/auth.ts +335 -0
  116. package/dist/templates/default/convex/constants.ts +46 -0
  117. package/dist/templates/default/convex/convex.config.ts +11 -0
  118. package/dist/templates/default/convex/crons.ts +42 -0
  119. package/dist/templates/default/convex/email.ts +109 -0
  120. package/dist/templates/default/convex/env.ts +31 -0
  121. package/dist/templates/default/convex/errors.ts +33 -0
  122. package/dist/templates/default/convex/functions.ts +54 -0
  123. package/dist/templates/default/convex/http.ts +176 -0
  124. package/dist/templates/default/convex/log.ts +81 -0
  125. package/dist/templates/default/convex/pushTokens.ts +114 -0
  126. package/dist/templates/default/convex/rateLimit.ts +92 -0
  127. package/dist/templates/default/convex/schema.ts +28 -0
  128. package/dist/templates/default/convex/tsconfig.json +18 -0
  129. package/dist/templates/default/convex/users.ts +279 -0
  130. package/dist/templates/default/convex/validators.ts +74 -0
  131. package/dist/templates/default/convex/webhook.ts +193 -0
  132. package/dist/templates/default/convex.json +6 -0
  133. package/dist/templates/default/eas.json +56 -0
  134. package/dist/templates/default/fingerprint.config.js +9 -0
  135. package/dist/templates/default/hooks/use-debounce.ts +20 -0
  136. package/dist/templates/default/hooks/use-deep-link.ts +43 -0
  137. package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
  138. package/dist/templates/default/hooks/use-network.ts +11 -0
  139. package/dist/templates/default/hooks/use-notifications.ts +107 -0
  140. package/dist/templates/default/hooks/use-onboarding.ts +15 -0
  141. package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
  142. package/dist/templates/default/hooks/use-theme.ts +53 -0
  143. package/dist/templates/default/hooks/use-updates.ts +86 -0
  144. package/dist/templates/default/lib/a11y.ts +5 -0
  145. package/dist/templates/default/lib/app.ts +14 -0
  146. package/dist/templates/default/lib/assets.ts +17 -0
  147. package/dist/templates/default/lib/auth-client.ts +21 -0
  148. package/dist/templates/default/lib/convex-auth.tsx +79 -0
  149. package/dist/templates/default/lib/deep-link.ts +71 -0
  150. package/dist/templates/default/lib/dev-menu.ts +119 -0
  151. package/dist/templates/default/lib/device.ts +40 -0
  152. package/dist/templates/default/lib/dynamic-font.ts +49 -0
  153. package/dist/templates/default/lib/env.ts +10 -0
  154. package/dist/templates/default/lib/haptics.ts +24 -0
  155. package/dist/templates/default/lib/notifications.ts +276 -0
  156. package/dist/templates/default/lib/preferences.ts +45 -0
  157. package/dist/templates/default/lib/schemas.ts +137 -0
  158. package/dist/templates/default/lib/storage.ts +47 -0
  159. package/dist/templates/default/lib/updates.ts +107 -0
  160. package/dist/templates/default/metro.config.js +14 -0
  161. package/dist/templates/default/package.json +129 -0
  162. package/dist/templates/default/patches/PR-368.patch +91 -0
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. package/dist/templates/default/plugins/README.md +9 -0
  165. package/dist/templates/default/plugins/with-auto-signing.js +45 -0
  166. package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
  167. package/dist/templates/default/scripts/README.md +36 -0
  168. package/dist/templates/default/scripts/_run.mjs +77 -0
  169. package/dist/templates/default/scripts/clean.ts +543 -0
  170. package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
  171. package/dist/templates/default/store.config.json +58 -0
  172. package/dist/templates/default/tsconfig.json +13 -0
  173. package/dist/templates/default/vitest.config.ts +21 -0
  174. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # create-vexpo
2
+
3
+ [![npm](https://img.shields.io/npm/v/@ramonclaudio/create-vexpo)](https://www.npmjs.com/package/@ramonclaudio/create-vexpo)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Scaffold a new [vexpo](https://github.com/ramonclaudio/vexpo) project: Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Real auth (email + password, email OTP, Apple Sign In), APNs push, OTA updates, App Store submission. Strict TypeScript, native SwiftUI via `@expo/ui/swift-ui`, no NativeWind.
7
+
8
+ ## Usage
9
+
10
+ ```bash
11
+ npm create @ramonclaudio/vexpo@latest my-app
12
+ # or
13
+ bunx @ramonclaudio/create-vexpo@latest my-app
14
+ ```
15
+
16
+ After scaffold:
17
+
18
+ ```bash
19
+ cd my-app
20
+
21
+ bunx vexpo lite # 60 seconds: Convex + Better Auth, simulator-ready
22
+ bunx vexpo lite --new # same + Convex signup walkthrough for first-time users
23
+ bunx vexpo full # full provisioning: TestFlight-ready (Convex, Better Auth, Resend, Apple Sign In, EAS, rebrand)
24
+ bunx vexpo full --new # same + Apple/Convex/Expo/Resend signup walkthrough
25
+ ```
26
+
27
+ `bunx vexpo lite` is the dev-mode shortcut. No Apple Developer account, no domain, no EAS, no Resend. Boots in the iOS Simulator in ~60 seconds. Add `--new` if you don't have a Convex account yet.
28
+
29
+ `bunx vexpo full` walks Apple Developer / Expo / Convex / Resend signups (with `--new`), validates each, provisions everything in order. ~30 minutes hands-on plus Apple-side wait times. Prints the canonical `eas build` command at the end. You run it when ready.
30
+
31
+ ## Options
32
+
33
+ | Flag | Behavior |
34
+ | --------------- | --------------------------------------------------------------------------------------------- |
35
+ | `[directory]` | Project directory name (positional). Defaults to `my-vexpo-app` with `-y`, otherwise prompts. |
36
+ | `--no-install` | Skip running `<pm> install` after copying the template. |
37
+ | `--no-git` | Skip `git init` after install. |
38
+ | `--no-setup` | Skip the post-install `bunx vexpo lite` / `bunx vexpo full` prompt. |
39
+ | `-y, --yes` | Accept defaults, skip prompts. |
40
+ | `-v, --version` | Print version, exit. |
41
+
42
+ ## What gets scaffolded
43
+
44
+ The CLI copies `templates/default/` from the published tarball, restores npm-stripped dotfiles (`.gitignore`, `.env.example`, etc.), rewrites `package.json` (project name, version, drops monorepo metadata, swaps `vexpo` workspace ref for the published version), runs `bun install`, and initializes a fresh git repo with `feat: initial commit`.
45
+
46
+ After that, the project is standalone. The operational CLI (`vexpo`) is installed as a devDependency, so `bunx vexpo <subcommand>` resolves to the local pinned version. Setup commands aren't in `package.json`. They're one-shot CLI invocations, not runtime scripts.
47
+
48
+ ## Repo
49
+
50
+ [github.com/ramonclaudio/vexpo](https://github.com/ramonclaudio/vexpo)
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'fs';
3
+ import { cp, rename, readFile, writeFile } from 'fs/promises';
4
+ import { dirname, join, resolve, relative, basename } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { Command } from 'commander';
7
+ import { execa } from 'execa';
8
+ import kleur from 'kleur';
9
+ import ora from 'ora';
10
+ import prompts from 'prompts';
11
+
12
+ // package.json
13
+ var package_default = {
14
+ version: "0.1.0"};
15
+
16
+ // src/index.ts
17
+ var here = dirname(fileURLToPath(import.meta.url));
18
+ var TEMPLATE_DIR = join(here, "templates", "default");
19
+ async function main() {
20
+ const program = new Command().name("create-vexpo").description(
21
+ "Scaffold a new vexpo project. Expo SDK 56 + Convex + Better Auth + Resend, wired for iOS."
22
+ ).argument("[directory]", "project directory name").option("--no-install", "skip installing dependencies").option("--no-git", "skip git init").option("--no-setup", "skip the `bunx vexpo lite` / `bunx vexpo full` prompt after install").option("-y, --yes", "accept defaults, skip prompts").version(package_default.version, "-v, --version").parse();
23
+ const flags = program.opts();
24
+ const argDir = program.args[0];
25
+ intro();
26
+ const name = await resolveName(argDir, flags.yes);
27
+ const target = resolve(process.cwd(), name);
28
+ if (existsSync(target)) {
29
+ console.error(kleur.red(`
30
+ Target ${target} already exists. Pick a different name.`));
31
+ process.exit(1);
32
+ }
33
+ const pm = detectPackageManager();
34
+ const copySpin = ora(
35
+ `Copying template to ${kleur.cyan(relative(process.cwd(), target) || ".")}`
36
+ ).start();
37
+ try {
38
+ await cp(TEMPLATE_DIR, target, { recursive: true });
39
+ await restoreStrippedDotfiles(target);
40
+ await rewritePackage(target, name);
41
+ copySpin.succeed("Template copied");
42
+ } catch (err) {
43
+ copySpin.fail("Template copy failed");
44
+ throw err;
45
+ }
46
+ if (flags.install) {
47
+ const installSpin = ora(`Installing dependencies with ${kleur.cyan(pm)}`).start();
48
+ try {
49
+ await execa(pm, ["install"], { cwd: target, stdio: "ignore" });
50
+ installSpin.succeed(`Installed with ${pm}`);
51
+ } catch {
52
+ installSpin.warn(`Install skipped. Run ${kleur.cyan(`${pm} install`)} manually.`);
53
+ }
54
+ }
55
+ if (flags.git) {
56
+ const gitSpin = ora("Initializing git").start();
57
+ try {
58
+ await execa("git", ["init", "--initial-branch=main"], { cwd: target, stdio: "ignore" });
59
+ await execa("git", ["add", "-A"], { cwd: target, stdio: "ignore" });
60
+ await execa("git", ["commit", "-m", "feat: initial commit", "--no-gpg-sign"], {
61
+ cwd: target,
62
+ stdio: "ignore"
63
+ });
64
+ gitSpin.succeed("Git repo initialized");
65
+ } catch {
66
+ gitSpin.warn("Git init skipped");
67
+ }
68
+ }
69
+ nextSteps(target, flags, pm);
70
+ }
71
+ var NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
72
+ var NAME_HINT = "lowercase letters, numbers, dashes; must start alphanumeric";
73
+ function validateNameSegment(target) {
74
+ const segment = basename(target);
75
+ if (!NAME_RE.test(segment)) return { ok: false, reason: NAME_HINT };
76
+ return { ok: true };
77
+ }
78
+ async function resolveName(argDir, yes) {
79
+ if (argDir) {
80
+ const check = validateNameSegment(argDir);
81
+ if (!check.ok) {
82
+ console.error(kleur.red(`
83
+ Invalid project directory '${argDir}'. ${check.reason}.`));
84
+ console.error(
85
+ kleur.gray(`Examples: my-app, my-cool-app, project1. Avoid spaces, unicode, npm scopes.`)
86
+ );
87
+ process.exit(1);
88
+ }
89
+ return argDir;
90
+ }
91
+ if (yes) return "my-vexpo-app";
92
+ const res = await prompts(
93
+ {
94
+ type: "text",
95
+ name: "name",
96
+ message: "Project directory",
97
+ initial: "my-vexpo-app",
98
+ validate: (v) => {
99
+ const check = validateNameSegment(v);
100
+ return check.ok ? true : check.reason;
101
+ }
102
+ },
103
+ { onCancel: () => process.exit(1) }
104
+ );
105
+ return res.name;
106
+ }
107
+ async function restoreStrippedDotfiles(target) {
108
+ const renames = [
109
+ ["_gitignore", ".gitignore"],
110
+ ["_env.example", ".env.example"],
111
+ ["_oxfmtrc.json", ".oxfmtrc.json"],
112
+ ["_oxlintrc.json", ".oxlintrc.json"],
113
+ ["_editorconfig", ".editorconfig"],
114
+ ["_gitattributes", ".gitattributes"],
115
+ ["_easignore", ".easignore"],
116
+ ["_fingerprintignore", ".fingerprintignore"],
117
+ ["_env.convex.local", ".env.convex.local"]
118
+ ];
119
+ for (const [from, to] of renames) {
120
+ const src = join(target, from);
121
+ if (existsSync(src)) await rename(src, join(target, to));
122
+ }
123
+ }
124
+ async function rewritePackage(target, requestedName) {
125
+ const pkgPath = join(target, "package.json");
126
+ const raw = await readFile(pkgPath, "utf8");
127
+ const parsed = JSON.parse(raw);
128
+ parsed.name = toPackageName(requestedName);
129
+ parsed.version = "0.0.0";
130
+ parsed.private = true;
131
+ delete parsed.author;
132
+ delete parsed.repository;
133
+ delete parsed.bugs;
134
+ delete parsed.homepage;
135
+ delete parsed.license;
136
+ delete parsed.publishConfig;
137
+ await writeFile(pkgPath, `${JSON.stringify(parsed, null, 2)}
138
+ `);
139
+ }
140
+ function toPackageName(raw) {
141
+ const name = basename(raw).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
142
+ return name || "my-vexpo-app";
143
+ }
144
+ function detectPackageManager() {
145
+ const ua = process.env.npm_config_user_agent ?? "";
146
+ if (ua.startsWith("bun")) return "bun";
147
+ if (ua.startsWith("pnpm")) return "pnpm";
148
+ if (ua.startsWith("yarn")) return "yarn";
149
+ if (ua.startsWith("npm")) return "npm";
150
+ return "bun";
151
+ }
152
+ function intro() {
153
+ console.log();
154
+ console.log(kleur.bold().cyan("create-vexpo") + kleur.gray(` v${package_default.version}`));
155
+ }
156
+ function nextSteps(target, flags, pm) {
157
+ const cdPath = relative(process.cwd(), target) || ".";
158
+ console.log();
159
+ console.log(kleur.bold("Next steps:"));
160
+ console.log(kleur.gray(" cd ") + kleur.cyan(cdPath));
161
+ if (!flags.install) console.log(kleur.gray(` ${pm} install`));
162
+ console.log(
163
+ kleur.gray(
164
+ ` bunx vexpo lite ${kleur.dim("# dev mode: Convex + Better Auth, 60s to simulator")}`
165
+ )
166
+ );
167
+ console.log(
168
+ kleur.gray(
169
+ ` bunx vexpo full ${kleur.dim("# real setup: TestFlight-ready (add --new if you're new)")}`
170
+ )
171
+ );
172
+ console.log();
173
+ console.log(kleur.bold("Then in two terminals:"));
174
+ console.log(kleur.gray(` ${pm} run convex:dev ${kleur.dim("# terminal 1")}`));
175
+ console.log(kleur.gray(` ${pm} run ios ${kleur.dim("# terminal 2")}`));
176
+ console.log();
177
+ console.log(kleur.gray("Docs: ") + kleur.cyan("https://github.com/ramonclaudio/vexpo"));
178
+ console.log();
179
+ }
180
+ main().catch((err) => {
181
+ console.error(err);
182
+ process.exit(1);
183
+ });
@@ -0,0 +1,84 @@
1
+ name: App Store Connect events
2
+
3
+ # Reacts to App Store Connect events using the `on.app_store_connect` trigger
4
+ # (https://docs.expo.dev/eas/workflows/syntax/#onapp_store_connect). EAS
5
+ # proxies ASC webhooks so we don't have to set up our own listener.
6
+ #
7
+ # Setup:
8
+ # 1. Open EAS dashboard → Project settings → General → Connections
9
+ # 2. Connect your App Store Connect app
10
+ # 3. Paste your Slack incoming-webhook URL into each `webhook_url:` field
11
+ # below (or strip the slack jobs entirely if you don't use Slack).
12
+
13
+ on:
14
+ app_store_connect:
15
+ app_version:
16
+ states:
17
+ - waiting_for_review
18
+ - in_review
19
+ - rejected
20
+ - accepted
21
+ - ready_for_distribution
22
+ build_upload:
23
+ states:
24
+ - complete
25
+ - failed
26
+ external_beta:
27
+ states:
28
+ - ready_for_beta_testing
29
+ - beta_approved
30
+ - beta_rejected
31
+ beta_feedback:
32
+ types:
33
+ - crash
34
+
35
+ jobs:
36
+ # NOTE: EAS's `app_store_connect.*` workflow context only documents
37
+ # `app.id` and `build_upload.id`. Other event-specific properties (state
38
+ # values, beta-feedback details) aren't in the public context shape today.
39
+ # The `if:` guards below check whether the corresponding event key exists,
40
+ # so each notify only fires on the matching trigger. The message bodies
41
+ # link straight to App Store Connect rather than interpolating fields that
42
+ # may not resolve.
43
+
44
+ notify_review_state:
45
+ name: Slack notify on review state change
46
+ if: ${{ app_store_connect.app_version }}
47
+ type: slack
48
+ params:
49
+ # `webhook_url` only accepts hardcoded strings today; EAS env-stored
50
+ # interpolation for this field is upcoming. Paste your URL below.
51
+ webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
52
+ message: |
53
+ App Store version state changed. App Store Connect → My Apps → App Store tab.
54
+ Workflow: ${{ workflow.url }}
55
+
56
+ notify_upload_state:
57
+ name: Slack notify on build upload
58
+ if: ${{ app_store_connect.build_upload }}
59
+ type: slack
60
+ params:
61
+ webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
62
+ message: |
63
+ Build upload completed: ${{ app_store_connect.build_upload.id }}.
64
+ Workflow: ${{ workflow.url }}
65
+
66
+ notify_beta_state:
67
+ name: Slack notify on external beta state
68
+ if: ${{ app_store_connect.external_beta }}
69
+ type: slack
70
+ params:
71
+ webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
72
+ message: |
73
+ External beta state changed. App Store Connect → TestFlight.
74
+ Workflow: ${{ workflow.url }}
75
+
76
+ notify_crash:
77
+ name: Slack notify on TestFlight crash
78
+ if: ${{ app_store_connect.beta_feedback }}
79
+ type: slack
80
+ params:
81
+ webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
82
+ message: |
83
+ 🔴 TestFlight crash reported. App Store Connect → TestFlight → Feedback.
84
+ Workflow: ${{ workflow.url }}
@@ -0,0 +1,129 @@
1
+ name: Deploy to production
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ workflow_dispatch: {}
7
+
8
+ concurrency:
9
+ cancel_in_progress: true
10
+ group: ${{ workflow.filename }}-${{ github.ref }}
11
+
12
+ defaults:
13
+ tools:
14
+ bun: latest
15
+
16
+ jobs:
17
+ precheck:
18
+ name: Precheck
19
+ type: custom
20
+ runs_on: linux-medium
21
+ steps:
22
+ - uses: eas/checkout
23
+ - uses: eas/install_node_modules
24
+ - name: Typecheck
25
+ run: bun run typecheck
26
+ - name: Lint
27
+ run: bun run lint
28
+ - name: Test
29
+ run: bun run test
30
+
31
+ deploy_convex:
32
+ name: Deploy Convex
33
+ needs: [precheck]
34
+ type: custom
35
+ runs_on: linux-medium
36
+ # `production` env pulls CONVEX_DEPLOY_KEY (a production deploy key, set
37
+ # at secret visibility) into the job. `convex deploy` is non-interactive
38
+ # whenever CONVEX_DEPLOY_KEY is present. no --yes / --auto-accept flag.
39
+ # https://docs.convex.dev/cli#deploy-convex-functions-to-production
40
+ # https://docs.convex.dev/cli/deploy-key-types
41
+ environment: production
42
+ steps:
43
+ - uses: eas/checkout
44
+ - uses: eas/install_node_modules
45
+ - name: Deploy Convex backend
46
+ run: bunx convex deploy
47
+
48
+ fingerprint:
49
+ name: Fingerprint
50
+ needs: [precheck]
51
+ type: fingerprint
52
+ environment: production
53
+
54
+ get_ios_build:
55
+ name: Check existing iOS build
56
+ needs: [fingerprint]
57
+ type: get-build
58
+ params:
59
+ platform: ios
60
+ fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
61
+ profile: production
62
+
63
+ build_ios:
64
+ name: Build iOS
65
+ needs: [get_ios_build, deploy_convex]
66
+ if: ${{ !needs.get_ios_build.outputs.build_id }}
67
+ type: build
68
+ params:
69
+ platform: ios
70
+ profile: production
71
+
72
+ submit_ios:
73
+ name: Submit to App Store
74
+ needs: [build_ios]
75
+ type: submit
76
+ params:
77
+ build_id: ${{ needs.build_ios.outputs.build_id }}
78
+
79
+ update_ios:
80
+ name: OTA update
81
+ needs: [get_ios_build, deploy_convex]
82
+ if: ${{ needs.get_ios_build.outputs.build_id }}
83
+ type: update
84
+ environment: production
85
+ params:
86
+ platform: ios
87
+ branch: production
88
+
89
+ # Post-deploy notification with EAS Insights link. `update_ios` exposes
90
+ # `first_update_group_id` (https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#update-outputs);
91
+ # we feed it into a Slack message that links to:
92
+ # - the update group page (rollout %, runtime version, builds touched)
93
+ # - the deployments dashboard (adoption curves, crash-free %, unique users)
94
+ # - the channel insights page (filtered to production)
95
+ #
96
+ # `webhook_url` only accepts hardcoded strings today (env-stored webhooks
97
+ # are upcoming per https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#slack);
98
+ # paste yours in `SLACK_WEBHOOK_URL` below and uncomment, or strip this job
99
+ # if you don't use Slack. The `print_insights` job prints `update:insights`
100
+ # JSON to workflow logs so you can audit adoption metrics directly.
101
+ print_insights:
102
+ name: Capture update:insights output
103
+ needs: [update_ios]
104
+ if: ${{ needs.update_ios.outputs.first_update_group_id }}
105
+ runs_on: linux-medium
106
+ steps:
107
+ - uses: eas/checkout
108
+ - uses: eas/install_node_modules
109
+ - name: Print insights JSON
110
+ run: |
111
+ GROUP="${{ needs.update_ios.outputs.first_update_group_id }}"
112
+ echo "Update group: $GROUP"
113
+ bunx eas-cli update:insights "$GROUP" --json --non-interactive || true
114
+
115
+ notify_update_published:
116
+ name: Notify Slack with insights link
117
+ needs: [update_ios]
118
+ if: ${{ needs.update_ios.outputs.first_update_group_id }}
119
+ type: slack
120
+ params:
121
+ # Replace with your Slack incoming-webhook URL. EAS does not yet
122
+ # support reading this from EAS env (see job comment above). interpolation
123
+ # of `${{ env.* }}` for `webhook_url` lands in a future EAS release.
124
+ webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
125
+ message: |
126
+ :rocket: OTA update published to production.
127
+ Group: ${{ needs.update_ios.outputs.first_update_group_id }}
128
+ Workflow: ${{ workflow.url }}
129
+ Open the EAS dashboard for adoption %, crash-free %, and unique users.
@@ -0,0 +1,19 @@
1
+ name: Development builds
2
+
3
+ on:
4
+ workflow_dispatch: {}
5
+
6
+ jobs:
7
+ ios_device:
8
+ name: iOS device
9
+ type: build
10
+ params:
11
+ platform: ios
12
+ profile: development
13
+
14
+ ios_simulator:
15
+ name: iOS simulator
16
+ type: build
17
+ params:
18
+ platform: ios
19
+ profile: development:simulator
@@ -0,0 +1,42 @@
1
+ name: E2E tests
2
+
3
+ # Builds an iOS Simulator binary and runs Maestro flows against it. Mirrors
4
+ # the canonical EAS workflow at
5
+ # https://docs.expo.dev/eas/workflows/examples/e2e-tests.
6
+ #
7
+ # Maestro flows live under .maestro/ at the repo root. The build profile
8
+ # `development:simulator` already exists in eas.json. it produces a
9
+ # .app bundle the Maestro runner can install on an iOS Simulator.
10
+ #
11
+ # `MAESTRO_APP_ID` resolves to the project's bundle id from the EAS development
12
+ # environment (populated by `vexpo full` or `eas env:create`). The flow
13
+ # yaml at .maestro/launch.yaml reads it as `appId: ${MAESTRO_APP_ID}`.
14
+
15
+ on:
16
+ pull_request:
17
+ branches: ["main"]
18
+ workflow_dispatch: {}
19
+
20
+ concurrency:
21
+ cancel_in_progress: true
22
+ group: ${{ workflow.filename }}-${{ github.ref }}
23
+
24
+ jobs:
25
+ build_ios_simulator:
26
+ name: Build iOS Simulator
27
+ type: build
28
+ params:
29
+ platform: ios
30
+ profile: development:simulator
31
+
32
+ maestro:
33
+ name: Run Maestro flows
34
+ needs: [build_ios_simulator]
35
+ type: maestro
36
+ environment: development
37
+ env:
38
+ MAESTRO_APP_ID: ${{ env.EXPO_PUBLIC_APP_BUNDLE_ID }}
39
+ params:
40
+ build_id: ${{ needs.build_ios_simulator.outputs.build_id }}
41
+ flow_path: [".maestro"]
42
+ retries: 2
@@ -0,0 +1,98 @@
1
+ name: PR preview
2
+
3
+ # Builds + auto-posts a comment on the PR with build/update IDs and QR codes
4
+ # via the `github-comment` job. Mirrors the canonical EAS workflow pattern at
5
+ # https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#github-comment.
6
+ #
7
+ # Flow (canonical fingerprint-gated branch model):
8
+ # 1. fingerprint → compute the iOS fingerprint for the PR's dev env.
9
+ # 2. get_existing_build → check if any development:simulator build with
10
+ # that fingerprint already exists (returns build_id if so).
11
+ # 3a. Path A (no compatible build): build a fresh dev-client simulator
12
+ # binary + publish a preview OTA to branch `pr-<n>` so the build
13
+ # picks it up on launch.
14
+ # 3b. Path B (compatible build exists): publish a preview OTA only.
15
+ # 4. github-comment → post the QR / build / update IDs.
16
+ #
17
+ # Either path publishes an update so a) the PR is always testable via a QR
18
+ # scan and b) `pr-<n>` is a real EAS branch from the first commit, not the
19
+ # second. Uses development:simulator (subscribed to the `development`
20
+ # channel). vexpo doesn't ship a separate `preview` build profile.
21
+
22
+ on:
23
+ pull_request:
24
+ branches: ["main"]
25
+
26
+ concurrency:
27
+ cancel_in_progress: true
28
+ group: ${{ workflow.filename }}-${{ github.ref }}
29
+
30
+ defaults:
31
+ tools:
32
+ bun: latest
33
+
34
+ jobs:
35
+ precheck:
36
+ name: Precheck
37
+ type: custom
38
+ runs_on: linux-medium
39
+ steps:
40
+ - uses: eas/checkout
41
+ - uses: eas/install_node_modules
42
+ - name: Typecheck
43
+ run: bun run typecheck
44
+ - name: Lint
45
+ run: bun run lint
46
+ - name: Test
47
+ run: bun run test
48
+
49
+ fingerprint:
50
+ name: Fingerprint
51
+ needs: [precheck]
52
+ type: fingerprint
53
+ environment: development
54
+
55
+ get_existing_build:
56
+ name: Find compatible build
57
+ needs: [fingerprint]
58
+ type: get-build
59
+ params:
60
+ platform: ios
61
+ fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
62
+ profile: development:simulator
63
+ wait_for_in_progress: true
64
+
65
+ build_ios:
66
+ name: Build iOS Simulator (only if no compatible build exists)
67
+ needs: [get_existing_build]
68
+ if: ${{ !needs.get_existing_build.outputs.build_id }}
69
+ type: build
70
+ params:
71
+ platform: ios
72
+ profile: development:simulator
73
+
74
+ publish_update_new:
75
+ name: Publish preview update (new fingerprint path)
76
+ needs: [build_ios]
77
+ type: update
78
+ environment: development
79
+ params:
80
+ platform: ios
81
+ branch: pr-${{ github.event.pull_request.number }}
82
+
83
+ publish_update_existing:
84
+ name: Publish preview update (existing build path)
85
+ needs: [get_existing_build]
86
+ if: ${{ needs.get_existing_build.outputs.build_id }}
87
+ type: update
88
+ environment: development
89
+ params:
90
+ platform: ios
91
+ branch: pr-${{ github.event.pull_request.number }}
92
+
93
+ comment:
94
+ name: Post results to PR
95
+ after: [build_ios, publish_update_new, publish_update_existing]
96
+ type: github-comment
97
+ params:
98
+ message: "🚀 Preview ready. Scan the QR code or open the link to test."
@@ -0,0 +1,44 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ concurrency:
8
+ cancel_in_progress: true
9
+ group: ${{ workflow.filename }}-${{ github.ref }}
10
+
11
+ defaults:
12
+ tools:
13
+ bun: latest
14
+
15
+ jobs:
16
+ precheck:
17
+ name: Precheck
18
+ type: custom
19
+ runs_on: linux-medium
20
+ steps:
21
+ - uses: eas/checkout
22
+ - uses: eas/install_node_modules
23
+ - name: Typecheck
24
+ run: bun run typecheck
25
+ - name: Lint
26
+ run: bun run lint
27
+ - name: Test
28
+ run: bun run test
29
+
30
+ build_ios:
31
+ name: Build iOS
32
+ needs: [precheck]
33
+ type: build
34
+ params:
35
+ platform: ios
36
+ profile: production
37
+
38
+ submit_ios:
39
+ name: Submit to App Store
40
+ needs: [build_ios]
41
+ type: submit
42
+ params:
43
+ build_id: ${{ needs.build_ios.outputs.build_id }}
44
+ profile: production