umpordez 1.0.2 → 1.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 (135) hide show
  1. package/.claude/settings.local.json +33 -1
  2. package/README.md +160 -52
  3. package/cli.mjs +75 -0
  4. package/create.mjs +376 -50
  5. package/icons.mjs +257 -0
  6. package/package.json +4 -3
  7. package/template/.claude/settings.json +49 -0
  8. package/template/CLAUDE.md +162 -108
  9. package/template/README.md +153 -85
  10. package/template/dev.sh +62 -25
  11. package/template/doc/index.html +638 -0
  12. package/template/install.sh +76 -19
  13. package/template/mobile/{{PROJECT_SLUG}}/App.tsx +75 -0
  14. package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +168 -0
  15. package/template/mobile/{{PROJECT_SLUG}}/README.md +149 -0
  16. package/template/mobile/{{PROJECT_SLUG}}/app.json +78 -0
  17. package/template/mobile/{{PROJECT_SLUG}}/assets/README.md +32 -0
  18. package/template/mobile/{{PROJECT_SLUG}}/babel.config.js +10 -0
  19. package/template/mobile/{{PROJECT_SLUG}}/eas.json +36 -0
  20. package/template/mobile/{{PROJECT_SLUG}}/eslint.config.js +18 -0
  21. package/template/mobile/{{PROJECT_SLUG}}/index.ts +28 -0
  22. package/template/mobile/{{PROJECT_SLUG}}/metro.config.js +5 -0
  23. package/template/mobile/{{PROJECT_SLUG}}/native/README.md +106 -0
  24. package/template/mobile/{{PROJECT_SLUG}}/native/android/AppNotificationReceiver.kt +65 -0
  25. package/template/mobile/{{PROJECT_SLUG}}/native/android/AppNotificationsModule.kt +219 -0
  26. package/template/mobile/{{PROJECT_SLUG}}/native/android/AppNotificationsPackage.kt +27 -0
  27. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/hello-android/HelloModule.kt +58 -0
  28. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/hello-android/HelloPackage.kt +28 -0
  29. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/hello-ios/HelloModule.m +25 -0
  30. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/hello-ios/HelloModule.swift +61 -0
  31. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/live-activity/AppLiveActivityAttributes.swift +25 -0
  32. package/template/mobile/{{PROJECT_SLUG}}/native/cookbook/live-activity/README.md +73 -0
  33. package/template/mobile/{{PROJECT_SLUG}}/native/ios/AppNotificationsModule.m +28 -0
  34. package/template/mobile/{{PROJECT_SLUG}}/native/ios/AppNotificationsModule.swift +168 -0
  35. package/template/mobile/{{PROJECT_SLUG}}/package.json +56 -0
  36. package/template/mobile/{{PROJECT_SLUG}}/plugins/withAndroidHealthConnect.js +79 -0
  37. package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +216 -0
  38. package/template/mobile/{{PROJECT_SLUG}}/src/components/LanguagePicker.tsx +224 -0
  39. package/template/mobile/{{PROJECT_SLUG}}/src/components/PremiumGate.tsx +123 -0
  40. package/template/mobile/{{PROJECT_SLUG}}/src/components/ToastConfig.tsx +100 -0
  41. package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +60 -0
  42. package/template/mobile/{{PROJECT_SLUG}}/src/contexts/AuthContext.tsx +595 -0
  43. package/template/mobile/{{PROJECT_SLUG}}/src/contexts/ThemeContext.tsx +67 -0
  44. package/template/mobile/{{PROJECT_SLUG}}/src/i18n/index.ts +88 -0
  45. package/template/mobile/{{PROJECT_SLUG}}/src/i18n/locales/en.ts +274 -0
  46. package/template/mobile/{{PROJECT_SLUG}}/src/i18n/locales/pt-BR.ts +458 -0
  47. package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx +530 -0
  48. package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/ForgotPassword.tsx +219 -0
  49. package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/Login.tsx +274 -0
  50. package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/Signup.tsx +291 -0
  51. package/template/mobile/{{PROJECT_SLUG}}/src/pages/main/Action.tsx +113 -0
  52. package/template/mobile/{{PROJECT_SLUG}}/src/pages/main/Explore.tsx +137 -0
  53. package/template/mobile/{{PROJECT_SLUG}}/src/pages/main/Home.tsx +127 -0
  54. package/template/mobile/{{PROJECT_SLUG}}/src/pages/main/Library.tsx +208 -0
  55. package/template/mobile/{{PROJECT_SLUG}}/src/pages/main/Profile.tsx +440 -0
  56. package/template/mobile/{{PROJECT_SLUG}}/src/pages/onboarding/FirstAction.tsx +168 -0
  57. package/template/mobile/{{PROJECT_SLUG}}/src/pages/onboarding/Onboarding.tsx +297 -0
  58. package/template/mobile/{{PROJECT_SLUG}}/src/pages/onboarding/Welcome.tsx +227 -0
  59. package/template/mobile/{{PROJECT_SLUG}}/src/routes/index.tsx +410 -0
  60. package/template/mobile/{{PROJECT_SLUG}}/src/routes/types.ts +31 -0
  61. package/template/mobile/{{PROJECT_SLUG}}/src/services/api.ts +72 -0
  62. package/template/mobile/{{PROJECT_SLUG}}/src/services/backup/export.ts +79 -0
  63. package/template/mobile/{{PROJECT_SLUG}}/src/services/backup/index.ts +131 -0
  64. package/template/mobile/{{PROJECT_SLUG}}/src/services/content/assetCache.ts +74 -0
  65. package/template/mobile/{{PROJECT_SLUG}}/src/services/content/index.ts +65 -0
  66. package/template/mobile/{{PROJECT_SLUG}}/src/services/content/signing.ts +44 -0
  67. package/template/mobile/{{PROJECT_SLUG}}/src/services/content/sync.ts +147 -0
  68. package/template/mobile/{{PROJECT_SLUG}}/src/services/db/index.ts +157 -0
  69. package/template/mobile/{{PROJECT_SLUG}}/src/services/db/migrations.ts +84 -0
  70. package/template/mobile/{{PROJECT_SLUG}}/src/services/db/prefs.ts +84 -0
  71. package/template/mobile/{{PROJECT_SLUG}}/src/services/fetch-api.ts +90 -0
  72. package/template/mobile/{{PROJECT_SLUG}}/src/services/health/index.ts +238 -0
  73. package/template/mobile/{{PROJECT_SLUG}}/src/services/iap/index.ts +409 -0
  74. package/template/mobile/{{PROJECT_SLUG}}/src/services/notifications/index.ts +129 -0
  75. package/template/mobile/{{PROJECT_SLUG}}/src/theme/colors.ts +61 -0
  76. package/template/mobile/{{PROJECT_SLUG}}/tsconfig.json +18 -0
  77. package/template/scripts/README.md +37 -0
  78. package/template/scripts/bump-version.sh +105 -0
  79. package/template/scripts/extract-play-key.js +64 -0
  80. package/template/scripts/generate-icons.sh +50 -0
  81. package/template/scripts/install-native.sh +169 -0
  82. package/template/scripts/prebuild.sh +28 -0
  83. package/template/scripts/release-android.sh +79 -0
  84. package/template/scripts/run-ios.sh +89 -0
  85. package/template/seed.sh +3 -2
  86. package/template/server/.env.sample +26 -1
  87. package/template/server/CLAUDE.md +135 -0
  88. package/template/server/apps/api/app.ts +28 -4
  89. package/template/server/apps/api/routes/account.ts +2 -2
  90. package/template/server/apps/api/routes/admin.ts +2 -2
  91. package/template/server/apps/api/routes/auth.ts +266 -6
  92. package/template/server/apps/api/routes/backup.ts +134 -0
  93. package/template/server/apps/api/routes/content.ts +142 -0
  94. package/template/server/apps/api/routes/files.ts +6 -2
  95. package/template/server/apps/api/routes/health.ts +55 -0
  96. package/template/server/apps/api/routes/iap.ts +159 -0
  97. package/template/server/apps/api/routes/index.ts +11 -4
  98. package/template/server/apps/shared/middlewares/demand-account-access.ts +9 -1
  99. package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +25 -4
  100. package/template/server/apps/site-api/app.ts +19 -5
  101. package/template/server/content/catalog.json +17 -0
  102. package/template/server/core/content.ts +69 -0
  103. package/template/server/core/email.ts +3 -5
  104. package/template/server/core/iap-verify.ts +66 -0
  105. package/template/server/core/knexfile.ts +6 -6
  106. package/template/server/core/logger.ts +34 -12
  107. package/template/server/core/models/auth.ts +151 -1
  108. package/template/server/core/models/user.ts +72 -4
  109. package/template/server/core/oauth.ts +277 -0
  110. package/template/server/core/s3.ts +78 -1
  111. package/template/server/dirname.ts +14 -0
  112. package/template/server/knex.sh +20 -2
  113. package/template/server/migrations/20260208000003_add-mobile-auth.ts +66 -0
  114. package/template/server/package.json +7 -5
  115. package/template/server/tests/core/content.test.ts +19 -0
  116. package/template/server/tests/core/context.test.ts +12 -0
  117. package/template/server/tests/core/db.test.ts +10 -0
  118. package/template/server/tests/core/oauth.test.ts +19 -0
  119. package/template/server/tests/test.sh +10 -0
  120. package/template/ui/admin/CLAUDE.md +101 -0
  121. package/template/ui/admin/index.html +10 -0
  122. package/template/ui/site/.env.sample +5 -0
  123. package/template/ui/site/CLAUDE.md +102 -0
  124. package/template/ui/site/app.ts +73 -18
  125. package/template/ui/site/i18n/index.ts +167 -0
  126. package/template/ui/site/i18n/locales/en.ts +119 -0
  127. package/template/ui/site/i18n/locales/pt-BR.ts +182 -0
  128. package/template/ui/site/package.json +1 -0
  129. package/template/ui/site/views/pages/home.ejs +60 -118
  130. package/template/ui/site/views/pages/not-found.ejs +22 -0
  131. package/template/ui/site/views/partials/footer.ejs +10 -10
  132. package/template/ui/site/views/partials/header.ejs +52 -22
  133. package/template-variables.mjs +59 -0
  134. package/update.mjs +555 -0
  135. package/template/server/migrations/20260208000001_seed-admin.ts +0 -42
@@ -14,7 +14,39 @@
14
14
  "WebFetch(domain:github.com)",
15
15
  "WebFetch(domain:raw.githubusercontent.com)",
16
16
  "Bash(gh api:*)",
17
- "WebFetch(domain:api.github.com)"
17
+ "WebFetch(domain:api.github.com)",
18
+ "Bash(git checkout *)",
19
+ "Bash(cp /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/assets/icon.png)",
20
+ "Bash(cp /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/adaptive-icon.png /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/assets/adaptive-icon.png)",
21
+ "Bash(cp /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/splash-icon.png /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/assets/splash-icon.png)",
22
+ "Bash(cp /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/favicon.png /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/assets/favicon.png)",
23
+ "Bash(git add *)",
24
+ "Bash(git commit -m ' *)",
25
+ "Bash(npm install *)",
26
+ "Read(//private/tmp/**)",
27
+ "Bash(rm -rf icon-test)",
28
+ "Bash(mkdir icon-test *)",
29
+ "Read(//tmp/**)",
30
+ "Bash(git commit *)",
31
+ "Bash(mv /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/scripts/*.sh /Users/ligeiro/dev/umpordez-cli/template/scripts/)",
32
+ "Bash(rmdir /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/scripts)",
33
+ "Read(//Users/ligeiro/dev/umpordez-aulass/**)",
34
+ "Bash(npx tsc *)",
35
+ "Bash(sed -i '' 's|^import React, { |import { |' App.tsx src/components/*.tsx src/contexts/*.tsx src/pages/auth/*.tsx src/pages/main/*.tsx)",
36
+ "Bash(sed -i '' 's|^import React from .react.;||' App.tsx src/components/*.tsx src/contexts/*.tsx src/pages/auth/*.tsx src/pages/main/*.tsx)",
37
+ "Bash(sed -i '' 's|^import React, { ReactNode }|import { ReactNode }|' src/contexts/*.tsx)",
38
+ "Bash(grep -l \"^import React\" App.tsx src/components/*.tsx src/contexts/*.tsx src/pages/auth/*.tsx src/pages/main/*.tsx)",
39
+ "Bash(npm run *)",
40
+ "Bash(npx expo *)",
41
+ "Bash(bash scripts/install-native.sh)",
42
+ "Bash(npm test *)",
43
+ "Bash(rm -rf /tmp/icon-test)",
44
+ "Bash(mkdir /tmp/icon-test)",
45
+ "Bash(md5 /Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/assets/icon.png /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png)",
46
+ "Bash(md5 /tmp/umptest/everything/mobile/testapp/assets/icon.png /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png)",
47
+ "Bash(git merge *)",
48
+ "Bash(git tag *)",
49
+ "Bash(npm pack *)"
18
50
  ]
19
51
  }
20
52
  }
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # umpordez
2
2
 
3
- SaaS starter kit generator.
3
+ SaaS starter kit generator. Server, admin SPA, public site (i18n + SEO),
4
+ mobile app — all wired up, single command.
4
5
 
5
6
  ---
6
7
 
@@ -12,45 +13,70 @@ access... and then you still haven't written a single line of your
12
13
  actual product.
13
14
 
14
15
  I've built this same architecture across multiple production apps
15
- (different domains, same bones). At some point I got tired of
16
- copying between repos, adapting folder names, and forgetting to
17
- update that one hardcoded port somewhere.
16
+ (different domains, same bones). At some point I got tired of copying
17
+ between repos, adapting folder names, and forgetting to update that
18
+ one hardcoded port somewhere.
18
19
 
19
- So I made a CLI that generates the whole thing. One command, answer a
20
- few questions, and you get a production-ready multi-tenant SaaS with
21
- everything wired up.
20
+ So I made a CLI that generates the whole thing. One command, answer
21
+ a few questions, and you get a production-ready multi-tenant SaaS
22
+ with **server + admin + site + mobile** already talking to each
23
+ other.
22
24
 
23
25
  **No magic, no hidden abstractions.** The generated code is yours;
24
- plain TypeScript, plain React, plain SQL. Read it, change it, own it.
26
+ plain TypeScript, plain React, plain SQL, plain Expo. Read it,
27
+ change it, own it.
25
28
 
26
29
  ## What you get
27
30
 
28
31
  ```
29
- server/
30
- apps/api/ Admin API (Express + TypeScript)
31
- apps/site-api/ Public API (Express + TypeScript)
32
- apps/shared/ Shared middlewares + utilities
33
- core/ Models, DB, S3, email, auth
34
- console/ Task runner (migrations, seeds, custom tasks)
35
- migrations/ Raw SQL migrations (Knex)
36
-
37
- ui/admin/ React SPA (Vite + shadcn/ui + Tailwind)
38
- ui/site/ Public site (Express + EJS + Tailwind)
32
+ <project>/
33
+ ├── server/ Admin API + Site API + console + DB
34
+ (Express + TypeScript + PostgreSQL +
35
+ │ Knex + Zod)
36
+ ├── ui/admin/ React SPA (Vite + shadcn/ui + Tailwind)
37
+ ├── ui/site/ Public site (Express + EJS + Tailwind +
38
+ │ i18n with SEO best practices)
39
+ ├── mobile/<slug>/ Expo + React Native + TypeScript
40
+ │ with auth, offline SQLite, content
41
+ │ sync, IAP, push (custom-native),
42
+ │ Apple Health + Health Connect
43
+ ├── scripts/ All helpers in one place (mobile, deploy,
44
+ │ version bumping, icon generation)
45
+ ├── doc/index.html HTML docs themed in your primary color
46
+ ├── .claude/ Claude Code permission allowlist
47
+ └── CLAUDE.md (× 5) Per-app rules for AI agents
39
48
  ```
40
49
 
41
- The architecture:
42
-
43
- - **Multi-tenant**; users > accounts with role-based access
44
- (admin, owner, manager, user)
45
- - **Multi-API**; separate Express servers sharing core logic,
46
- add more as you need
47
- - **Cookie auth**; httpOnly JWT, 30-day expiry, no tokens
48
- in localStorage
49
- - **S3 uploads**; multer-s3 with signed URLs (never public-read)
50
- - **Context DI**; fresh context per request with all models
51
- instantiated, no singletons
52
- - **PostgreSQL**; Knex.js with raw SQL migrations, because ORMs
53
- lie to you eventually :X
50
+ ## Architecture (the bones)
51
+
52
+ - **Multi-tenant** users user_in_accounts → accounts domain
53
+ entities. Roles: admin / owner / manager / user.
54
+ - **Multi-API** separate Express servers sharing one `core/`
55
+ layer. Add more APIs by copying `apps/api/`.
56
+ - **Hybrid auth** httpOnly JWT cookies for web, Bearer tokens for
57
+ mobile, refresh-token rotation single-flighted to prevent false
58
+ logouts on concurrent refresh.
59
+ - **Apple Sign-In + Google OAuth** pre-wired. Apple via JWKS verify
60
+ (zero-dep), Google via server-side flow that keeps the
61
+ `client_secret` out of the app bundle.
62
+ - **Free-first mobile** anyone can use the app without sign-in.
63
+ Auth reached via Profile or via PremiumGate's onUpgrade.
64
+ - **Offline-first SQLite** with USER vs SHARED table classes,
65
+ cloud backup/restore via S3 (two-phase upload survives
66
+ mid-failure).
67
+ - **Content sync** — versioned catalog + HMAC-signed presigned
68
+ asset URLs cached for 55 min, server out of the streaming path.
69
+ - **i18n done right** — pt-BR default + en, every string via
70
+ `t()`. Site uses URL prefixes (`/` vs `/en/`) for SEO with full
71
+ hreflang + canonical + sitemap.xml.
72
+ - **Custom-native push** with cold-start tap dispatch (the bit
73
+ expo-notifications can't do reliably).
74
+ - **Context DI** — fresh `Context` per request, all models
75
+ instantiated, no singletons, no shared state.
76
+ - **Routes go through models** — never knex from a route. Path
77
+ aliases (`#core/*`, `#shared/*`, `@/*`) over relative paths.
78
+ - **PostgreSQL with raw SQL migrations** — because ORMs lie to you
79
+ eventually.
54
80
 
55
81
  ## Install
56
82
 
@@ -66,22 +92,70 @@ npm install -g umpordez
66
92
  umpordez
67
93
  ```
68
94
 
69
- That's it. It asks you a few questions (name, domain, ports, colors)
70
- and generates everything. Then:
95
+ It asks **what to generate** first:
96
+
97
+ | Preset | Includes |
98
+ |---|---|
99
+ | everything | server + admin UI + site + mobile |
100
+ | web-only | server + admin UI + site (no mobile) |
101
+ | mobile-and-server | server + mobile (no UI) |
102
+ | just-mobile | mobile only (assumes external API) |
103
+
104
+ Then a few questions (name, domain, ports, primary color, mobile
105
+ bundle ID if applicable). After scaffold:
71
106
 
72
107
  ```bash
73
108
  cd my-project
74
- ./install.sh # install deps
75
- ./seed.sh # create db + migrate + seed admin
76
- ./dev.sh # start all services
109
+ ./install.sh # install all node deps + seed .env files
110
+ ./install.sh --native # also expo prebuild + pod install + native
111
+ ./seed.sh # create db + run migrations + seed admin
112
+ ./dev.sh # start every service (mobile included by default)
113
+ ./dev.sh --no-mobile # skip Metro this run
114
+ ```
115
+
116
+ ### Update an existing project
117
+
118
+ When the umpordez template ships fixes / new files / refactors, pull
119
+ them into your project:
120
+
121
+ ```bash
122
+ umpordez update <path> # walk template + project, prompt per file
123
+ umpordez update <path> --yes # accept all auto-updates without prompting
77
124
  ```
78
125
 
126
+ How it knows what's safe to auto-update: at scaffold time, the CLI
127
+ writes `.umpordez/manifest.json` with sha256 of every file as it was
128
+ generated. On update:
129
+
130
+ - File untouched since scaffold → auto-updates if template changed
131
+ - File user-edited → shows a diff and asks (y / N / d=full diff /
132
+ a=accept all)
133
+ - Generated assets (icons, splash, favicon) regenerate from the
134
+ manifest's stored primary color when missing
135
+
136
+ Random secrets (JWT, content HMAC) are persisted in the manifest
137
+ so updates don't mint fresh ones and break sessions / signed URLs
138
+ already issued by the running app.
139
+
140
+ ### Generate icons
141
+
142
+ The mobile preset auto-generates a branded icon set at scaffold
143
+ time from your primary color + first letter of the project name. To
144
+ re-run later (e.g. after dropping in a real logo PNG):
145
+
146
+ ```bash
147
+ umpordez icons --primary '#02e027' --letter U --out ./assets
148
+ umpordez icons --from logo.png --primary '#02e027' --out ./assets
149
+ ```
150
+
151
+ Outputs: `icon.png`, `adaptive-icon.png`, `splash-icon.png`,
152
+ `favicon.png`, `appstore.png`, `playstore.png`.
153
+
79
154
  ### Build for production
80
155
 
81
- The build system is split in two steps on purpose; compilation is
82
- expensive and should run on your machine, dependency installation
83
- needs to happen on the target machine (native bindings, OS-specific
84
- stuff).
156
+ The build system is split in two on purpose: compilation is
157
+ expensive and should run locally; dependency installation needs to
158
+ happen on the target machine (native bindings, OS-specific stuff).
85
159
 
86
160
  ```bash
87
161
  # Step 1: compile locally, push artifacts to builds repo
@@ -91,27 +165,61 @@ umpordez build ../app ../builds
91
165
  umpordez build-deps ../builds
92
166
  ```
93
167
 
94
- After `build-deps`, the builds repo is ready to clone and run. Add
95
- your `.env`, start the services, done.
96
-
97
- ### Other commands
168
+ ### Mobile release
98
169
 
99
170
  ```bash
100
- umpordez --help # see all commands
101
- umpordez --version # check version
171
+ ./scripts/bump-version.sh patch # bumps every version-holding file atomically
172
+ ./scripts/release-android.sh # EAS local build → submit to Play (internal track)
173
+ # iOS: open ios/<slug>.xcworkspace and Archive, or use eas build
174
+ ```
175
+
176
+ ### All commands
177
+
178
+ ```
179
+ umpordez create a new project
180
+ umpordez create same
181
+ umpordez build <app> <builds> compile for production
182
+ umpordez build-deps <builds> install prod deps on target machine
183
+ umpordez icons [--from logo.png] generate the mobile icon set
184
+ umpordez update <path> [--yes] pull template changes into a project
185
+ umpordez --help show this help
186
+ umpordez --version check version
102
187
  ```
103
188
 
104
189
  ## Tech stack
105
190
 
106
- | Layer | Tech |
107
- |------------|------|
191
+ | Layer | Tech |
192
+ |---|---|
108
193
  | Backend | TypeScript, Express, PostgreSQL, Knex.js, Zod |
109
- | Admin UI | React 18, Vite, Tailwind, shadcn/ui (Radix), React Query v5 |
110
- | Public site| Express + EJS + Tailwind |
111
- | Auth | httpOnly JWT cookies (bcrypt + 30-day expiry) |
112
- | Uploads | AWS S3 via multer-s3, signed URLs |
194
+ | Admin UI | React 18, Vite, Tailwind, shadcn/ui (Radix), React Query v5, React Hook Form |
195
+ | Public site| Express + EJS + Tailwind + i18next (with hreflang/canonical/sitemap) |
196
+ | Mobile | Expo, React Native, TypeScript, expo-sqlite, react-native-iap, react-native-health, custom-native push |
197
+ | Auth | httpOnly JWT cookies (web) + Bearer + refresh rotation (mobile), Apple + Google OAuth |
198
+ | Uploads | AWS S3 with signed URLs |
113
199
  | Email | Nodemailer + HTML templates |
114
200
 
201
+ ## What changed in 1.1
202
+
203
+ The big one. v1.0.x was server + web only. v1.1 adds:
204
+
205
+ - **Mobile preset** — full Expo + RN app with auth, offline DB,
206
+ cloud backup, content sync, IAP, push, health.
207
+ - **CLI presets** — pick what you want generated up front.
208
+ - **`umpordez update`** — pull template changes into existing
209
+ projects without losing your custom code.
210
+ - **`umpordez icons`** — branded icon generator.
211
+ - **i18n** — site (URL-prefixed, SEO-best-practices) and mobile
212
+ (Profile picker with country flags).
213
+ - **Custom-native push** with cold-start tap dispatch (Swift +
214
+ Kotlin), Apple Sign-In + Google server-side OAuth.
215
+ - **Per-app `CLAUDE.md`** files + `.claude/settings.json` with a
216
+ permission allowlist for routine commands.
217
+ - **Tests scaffold** in `server/tests/` (KISS — node:test + tsx).
218
+ - **Production hardening** — TZ=UTC, fatal handlers,
219
+ trust-proxy lifted from agendalize.
220
+
221
+ Existing projects pick up everything via `umpordez update <path>`.
222
+
115
223
  ## License
116
224
 
117
225
  MIT; do whatever you want with it.
package/cli.mjs CHANGED
@@ -6,6 +6,8 @@ import { createRequire } from 'node:module';
6
6
 
7
7
  import { createProject } from './create.mjs';
8
8
  import { buildProject, buildDeps } from './build.mjs';
9
+ import { generateIcons } from './icons.mjs';
10
+ import { updateProject } from './update.mjs';
9
11
 
10
12
  const require = createRequire(import.meta.url);
11
13
  const { version } = require('./package.json');
@@ -33,6 +35,8 @@ function showHelp() {
33
35
  console.log(` umpordez create ${dim('Create a new project')}`);
34
36
  console.log(` umpordez build ${dim('<app> <builds>')} ${dim('Build for production')}`);
35
37
  console.log(` umpordez build-deps ${dim('<builds>')} ${dim('Install deps in build')}`);
38
+ console.log(` umpordez icons ${dim('[--from logo.png]')} ${dim('Generate mobile icons')}`);
39
+ console.log(` umpordez update ${dim('<path> [--yes]')} ${dim('Pull template updates into a project')}`);
36
40
  console.log(` umpordez --help ${dim('Show this help')}`);
37
41
  console.log(` umpordez --version ${dim('Show version')}`);
38
42
  console.log('');
@@ -71,6 +75,62 @@ async function runBuild(args) {
71
75
  await buildProject(appDir, buildDir);
72
76
  }
73
77
 
78
+ function parseFlags(args) {
79
+ const flags = {};
80
+ for (let i = 0; i < args.length; i++) {
81
+ const a = args[i];
82
+ if (!a.startsWith('--')) {
83
+ continue;
84
+ }
85
+ const key = a.slice(2);
86
+ const next = args[i + 1];
87
+ if (next === undefined || next.startsWith('--')) {
88
+ flags[key] = true;
89
+ } else {
90
+ flags[key] = next;
91
+ i++;
92
+ }
93
+ }
94
+ return flags;
95
+ }
96
+
97
+ async function runIcons(args) {
98
+ const flags = parseFlags(args.slice(1));
99
+ const out = flags.out ?? './assets';
100
+ const primary = flags.primary ?? '#2563eb';
101
+ const letter = flags.letter ?? 'A';
102
+ const sourcePath = typeof flags.from === 'string' ? flags.from : null;
103
+
104
+ if (!flags.primary) {
105
+ console.log(dim(`No --primary given, using default ${primary}`));
106
+ }
107
+ if (!flags.letter && !sourcePath) {
108
+ console.log(dim(`No --letter given, using default ${letter}`));
109
+ }
110
+
111
+ console.log('');
112
+ console.log(green(' Generating icons'));
113
+ console.log(dim(` Output: ${out}`));
114
+ console.log(dim(` Primary: ${primary}`));
115
+ if (sourcePath) {
116
+ console.log(dim(` Source: ${sourcePath}`));
117
+ } else {
118
+ console.log(dim(` Letter: ${letter}`));
119
+ }
120
+ console.log('');
121
+
122
+ const written = await generateIcons({
123
+ outDir: out,
124
+ primaryColor: primary,
125
+ letter: letter.charAt(0).toUpperCase(),
126
+ sourcePath
127
+ });
128
+ for (const p of written) {
129
+ console.log(` ${green('✓')} ${p}`);
130
+ }
131
+ console.log('');
132
+ }
133
+
74
134
  async function runBuildDeps(args) {
75
135
  const buildDir = args[1];
76
136
 
@@ -97,6 +157,21 @@ if (!command || command === 'create') {
97
157
  await runBuild(args);
98
158
  } else if (command === 'build-deps') {
99
159
  await runBuildDeps(args);
160
+ } else if (command === 'icons') {
161
+ await runIcons(args);
162
+ } else if (command === 'update') {
163
+ const flags = parseFlags(args.slice(1));
164
+ const projectPath = args[1] && !args[1].startsWith('--')
165
+ ? args[1]
166
+ : '.';
167
+ if (!projectPath) {
168
+ console.log(red('Usage: umpordez update <path> [--yes]'));
169
+ console.log(dim(
170
+ ' --yes Accept all auto-updates without prompting.'
171
+ ));
172
+ process.exit(1);
173
+ }
174
+ await updateProject(projectPath, { yes: !!flags.yes });
100
175
  } else if (command === '--help' || command === '-h' || command === 'help') {
101
176
  showHelp();
102
177
  } else if (command === '--version' || command === '-v') {