umpordez 0.0.3 → 1.0.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.
Files changed (152) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/README.md +123 -0
  3. package/build.mjs +409 -0
  4. package/cli.mjs +85 -107
  5. package/create.mjs +334 -0
  6. package/package.json +2 -7
  7. package/template/.claude/settings.local.json +20 -0
  8. package/template/.editorconfig +11 -0
  9. package/template/.github/copilot-instructions.md +186 -0
  10. package/template/.nvimrc +7 -0
  11. package/template/.vscode/extensions.json +9 -0
  12. package/template/.vscode/settings.json +28 -0
  13. package/template/AGENTS.md +305 -0
  14. package/template/CLAUDE.md +140 -0
  15. package/template/architecture.md +518 -0
  16. package/template/code.md +211 -0
  17. package/template/dev.sh +45 -0
  18. package/template/install.sh +47 -0
  19. package/template/misc/nginx.conf +151 -0
  20. package/template/misc/systemd/{{PROJECT_SLUG}}-api.service +31 -0
  21. package/template/misc/systemd/{{PROJECT_SLUG}}-site-api.service +31 -0
  22. package/template/misc/systemd/{{PROJECT_SLUG}}-site.service +30 -0
  23. package/template/seed.sh +23 -0
  24. package/template/server/.env.sample +40 -0
  25. package/template/server/.prettierrc +4 -0
  26. package/template/server/apps/api/app.ts +42 -0
  27. package/template/server/apps/api/routes/account.ts +98 -0
  28. package/template/server/apps/api/routes/admin.ts +80 -0
  29. package/template/server/apps/api/routes/auth.ts +222 -0
  30. package/template/server/apps/api/routes/files.ts +89 -0
  31. package/template/server/apps/api/routes/index.ts +19 -0
  32. package/template/server/apps/api/routes/user.ts +17 -0
  33. package/template/server/apps/shared/middlewares/demand-account-access.ts +52 -0
  34. package/template/server/apps/shared/middlewares/demand-admin-user.ts +25 -0
  35. package/template/server/apps/shared/middlewares/demand-user.ts +17 -0
  36. package/template/server/apps/shared/middlewares/error-handler.ts +28 -0
  37. package/template/server/apps/shared/middlewares/request-logger.ts +40 -0
  38. package/template/server/apps/shared/middlewares/try-set-user-by-token.ts +26 -0
  39. package/template/server/apps/shared/utils.ts +71 -0
  40. package/template/server/apps/site-api/app.ts +42 -0
  41. package/template/server/apps/site-api/routes/index.ts +10 -0
  42. package/template/server/apps/site-api/routes/public.ts +25 -0
  43. package/template/server/console/index.ts +26 -0
  44. package/template/server/console/runner.ts +67 -0
  45. package/template/server/console/tasks/example.ts +29 -0
  46. package/template/server/console/tasks/seed-admin.ts +71 -0
  47. package/template/server/core/context.ts +49 -0
  48. package/template/server/core/db.ts +12 -0
  49. package/template/server/core/email.ts +87 -0
  50. package/template/server/core/errors.ts +44 -0
  51. package/template/server/core/knexfile.ts +28 -0
  52. package/template/server/core/logger.ts +60 -0
  53. package/template/server/core/models/account.ts +319 -0
  54. package/template/server/core/models/auth.ts +317 -0
  55. package/template/server/core/models/base.ts +19 -0
  56. package/template/server/core/models/user.ts +343 -0
  57. package/template/server/core/s3.ts +183 -0
  58. package/template/server/emails/_styles.css +5 -0
  59. package/template/server/emails/_template.html +28 -0
  60. package/template/server/emails/accountWelcome.html +4 -0
  61. package/template/server/emails/forgetPassword.html +5 -0
  62. package/template/server/eslint.config.js +16 -0
  63. package/template/server/knex.sh +4 -0
  64. package/template/server/migrations/20260208000000_initial-schema.ts +56 -0
  65. package/template/server/migrations/20260208000001_seed-admin.ts +42 -0
  66. package/template/server/migrations/20260208000002_add-user-avatar.ts +13 -0
  67. package/template/server/package.json +58 -0
  68. package/template/server/tsconfig.json +27 -0
  69. package/template/server/types/api.ts +20 -0
  70. package/template/server/types/express.d.ts +35 -0
  71. package/template/ui/admin/.prettierrc +4 -0
  72. package/template/ui/admin/components.json +20 -0
  73. package/template/ui/admin/eslint.config.js +16 -0
  74. package/template/ui/admin/index.html +12 -0
  75. package/template/ui/admin/package.json +57 -0
  76. package/template/ui/admin/postcss.config.js +6 -0
  77. package/template/ui/admin/src/app.tsx +13 -0
  78. package/template/ui/admin/src/components/nav-user.tsx +95 -0
  79. package/template/ui/admin/src/components/theme-provider.tsx +65 -0
  80. package/template/ui/admin/src/components/theme-toggle.tsx +17 -0
  81. package/template/ui/admin/src/components/ui/badge.tsx +36 -0
  82. package/template/ui/admin/src/components/ui/button.tsx +56 -0
  83. package/template/ui/admin/src/components/ui/calendar.tsx +68 -0
  84. package/template/ui/admin/src/components/ui/card.tsx +79 -0
  85. package/template/ui/admin/src/components/ui/checkbox.tsx +28 -0
  86. package/template/ui/admin/src/components/ui/date-picker.tsx +78 -0
  87. package/template/ui/admin/src/components/ui/date-range-picker.tsx +208 -0
  88. package/template/ui/admin/src/components/ui/dialog.tsx +121 -0
  89. package/template/ui/admin/src/components/ui/dropdown-menu.tsx +200 -0
  90. package/template/ui/admin/src/components/ui/input.tsx +22 -0
  91. package/template/ui/admin/src/components/ui/label.tsx +24 -0
  92. package/template/ui/admin/src/components/ui/popover.tsx +29 -0
  93. package/template/ui/admin/src/components/ui/select.tsx +158 -0
  94. package/template/ui/admin/src/components/ui/separator.tsx +29 -0
  95. package/template/ui/admin/src/components/ui/sheet.tsx +136 -0
  96. package/template/ui/admin/src/components/ui/sidebar.tsx +759 -0
  97. package/template/ui/admin/src/components/ui/skeleton.tsx +15 -0
  98. package/template/ui/admin/src/components/ui/sonner.tsx +26 -0
  99. package/template/ui/admin/src/components/ui/switch.tsx +27 -0
  100. package/template/ui/admin/src/components/ui/table.tsx +117 -0
  101. package/template/ui/admin/src/components/ui/tabs.tsx +53 -0
  102. package/template/ui/admin/src/components/ui/textarea.tsx +22 -0
  103. package/template/ui/admin/src/components/ui/tooltip.tsx +28 -0
  104. package/template/ui/admin/src/hooks/queries/use-accounts.ts +68 -0
  105. package/template/ui/admin/src/hooks/queries/use-auth.ts +61 -0
  106. package/template/ui/admin/src/hooks/queries/use-dashboard.ts +70 -0
  107. package/template/ui/admin/src/hooks/queries/use-members.ts +90 -0
  108. package/template/ui/admin/src/hooks/queries/use-users.ts +99 -0
  109. package/template/ui/admin/src/hooks/use-confirm.tsx +92 -0
  110. package/template/ui/admin/src/hooks/use-mobile.tsx +22 -0
  111. package/template/ui/admin/src/hooks/use-sidebar.tsx +4 -0
  112. package/template/ui/admin/src/hooks/use-user.tsx +86 -0
  113. package/template/ui/admin/src/index.css +22 -0
  114. package/template/ui/admin/src/layouts/account.tsx +278 -0
  115. package/template/ui/admin/src/layouts/admin.tsx +182 -0
  116. package/template/ui/admin/src/layouts/public.tsx +9 -0
  117. package/template/ui/admin/src/layouts/user.tsx +107 -0
  118. package/template/ui/admin/src/lib/config.ts +9 -0
  119. package/template/ui/admin/src/lib/fetch-api.ts +101 -0
  120. package/template/ui/admin/src/lib/query-client.ts +12 -0
  121. package/template/ui/admin/src/lib/query-keys.ts +44 -0
  122. package/template/ui/admin/src/lib/utils.ts +24 -0
  123. package/template/ui/admin/src/main.tsx +17 -0
  124. package/template/ui/admin/src/pages/account/dashboard.tsx +128 -0
  125. package/template/ui/admin/src/pages/account/members.tsx +414 -0
  126. package/template/ui/admin/src/pages/account/settings.tsx +210 -0
  127. package/template/ui/admin/src/pages/admin/accounts.tsx +164 -0
  128. package/template/ui/admin/src/pages/admin/dashboard.tsx +243 -0
  129. package/template/ui/admin/src/pages/admin/users.tsx +395 -0
  130. package/template/ui/admin/src/pages/public/error.tsx +24 -0
  131. package/template/ui/admin/src/pages/public/forgot-password.tsx +74 -0
  132. package/template/ui/admin/src/pages/public/login.tsx +102 -0
  133. package/template/ui/admin/src/pages/public/reset-password.tsx +106 -0
  134. package/template/ui/admin/src/pages/public/signup.tsx +118 -0
  135. package/template/ui/admin/src/pages/user/profile.tsx +313 -0
  136. package/template/ui/admin/src/pages/user/select-account.tsx +114 -0
  137. package/template/ui/admin/src/router.tsx +71 -0
  138. package/template/ui/admin/tailwind.config.ts +71 -0
  139. package/template/ui/admin/tsconfig.json +24 -0
  140. package/template/ui/admin/vite.config.ts +15 -0
  141. package/template/ui/site/.env.sample +3 -0
  142. package/template/ui/site/.prettierrc +4 -0
  143. package/template/ui/site/app.ts +50 -0
  144. package/template/ui/site/eslint.config.js +16 -0
  145. package/template/ui/site/package.json +33 -0
  146. package/template/ui/site/styles/input.css +3 -0
  147. package/template/ui/site/tailwind.config.js +8 -0
  148. package/template/ui/site/tsconfig.json +15 -0
  149. package/template/ui/site/views/pages/home.ejs +305 -0
  150. package/template/ui/site/views/partials/footer.ejs +44 -0
  151. package/template/ui/site/views/partials/header.ejs +83 -0
  152. package/template-variables.mjs +186 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(chmod:*)",
5
+ "Bash(node -e:*)",
6
+ "Bash(grep:*)",
7
+ "Bash(find:*)",
8
+ "Bash(xargs cat:*)",
9
+ "Bash(node:*)",
10
+ "Bash(echo:*)",
11
+ "WebFetch(domain:umpordez.com)",
12
+ "Bash(curl:*)",
13
+ "Bash(python3:*)",
14
+ "WebFetch(domain:github.com)",
15
+ "WebFetch(domain:raw.githubusercontent.com)",
16
+ "Bash(gh api:*)",
17
+ "WebFetch(domain:api.github.com)"
18
+ ]
19
+ }
20
+ }
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # umpordez 🚀
2
+
3
+ SaaS starter kit generator.
4
+
5
+ ---
6
+
7
+ ## Why?
8
+
9
+ We all know the drill; you want to build a SaaS, you spend two weeks
10
+ setting up auth, multi-tenancy, file uploads, admin panels, role-based
11
+ access... and then you still haven't written a single line of your
12
+ actual product.
13
+
14
+ 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.
18
+
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.
22
+
23
+ **No magic, no hidden abstractions.** The generated code is yours;
24
+ plain TypeScript, plain React, plain SQL. Read it, change it, own it.
25
+
26
+ ## What you get
27
+
28
+ ```
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)
39
+ ```
40
+
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
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ npm install -g umpordez
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Create a project
64
+
65
+ ```bash
66
+ umpordez
67
+ ```
68
+
69
+ That's it. It asks you a few questions (name, domain, ports, colors)
70
+ and generates everything. Then:
71
+
72
+ ```bash
73
+ cd my-project
74
+ ./install.sh # install deps
75
+ ./seed.sh # create db + migrate + seed admin
76
+ ./dev.sh # start all services
77
+ ```
78
+
79
+ ### Build for production
80
+
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).
85
+
86
+ ```bash
87
+ # Step 1: compile locally, push artifacts to builds repo
88
+ umpordez build ../app ../builds
89
+
90
+ # Step 2: on prod/staging, install production deps
91
+ umpordez build-deps ../builds
92
+ ```
93
+
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
98
+
99
+ ```bash
100
+ umpordez --help # see all commands
101
+ umpordez --version # check version
102
+ ```
103
+
104
+ ## Tech stack
105
+
106
+ | Layer | Tech |
107
+ |------------|------|
108
+ | 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 |
113
+ | Email | Nodemailer + HTML templates |
114
+
115
+ ## License
116
+
117
+ MIT; do whatever you want with it.
118
+
119
+ ---
120
+
121
+ *May the speedy force be with you.* ✌️
122
+
123
+ [youtube.com/ligeiro](https://youtube.com/ligeiro)
package/build.mjs ADDED
@@ -0,0 +1,409 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+
6
+ // umpordez.com design tokens
7
+ const green = chalk.hex('#02e027');
8
+ const cyan = chalk.hex('#00fff9');
9
+ const red = chalk.hex('#ff3c00');
10
+ const dim = chalk.hex('#a0a0a0');
11
+
12
+ function run(cmd, cwd) {
13
+ execSync(cmd, { cwd, stdio: 'inherit' });
14
+ }
15
+
16
+ function runSilent(cmd, cwd) {
17
+ execSync(cmd, { cwd, stdio: 'ignore' });
18
+ }
19
+
20
+ // ─── build ───────────────────────────────────────────────────────────────────
21
+ // Run locally. Compiles TypeScript + Vite, moves artifacts to builds repo.
22
+ // Does NOT install node_modules — that's build-deps on the target machine.
23
+
24
+ export async function buildProject(appDir, buildDir) {
25
+ appDir = path.resolve(appDir);
26
+ buildDir = path.resolve(buildDir);
27
+
28
+ if (!fs.existsSync(appDir)) {
29
+ console.log(red(`App directory not found: ${appDir}`));
30
+ process.exit(1);
31
+ }
32
+
33
+ if (!fs.existsSync(buildDir)) {
34
+ console.log(red(`Build directory not found: ${buildDir}`));
35
+ console.log(dim('Create it first: mkdir -p ' + buildDir));
36
+ process.exit(1);
37
+ }
38
+
39
+ console.log('');
40
+ console.log(green.bold('=== umpordez build ==='));
41
+ console.log('');
42
+ console.log(` ${chalk.bold('Source:')} ${cyan(appDir)}`);
43
+ console.log(` ${chalk.bold('Output:')} ${cyan(buildDir)}`);
44
+ console.log('');
45
+
46
+ // Pull builds repo if git
47
+ if (fs.existsSync(path.join(buildDir, '.git'))) {
48
+ console.log(dim(' Pulling builds repo...'));
49
+ runSilent('git pull', buildDir);
50
+ }
51
+
52
+ // Bump build number
53
+ const buildNumberFile = path.join(buildDir, 'build-number');
54
+ let number = 0;
55
+ if (fs.existsSync(buildNumberFile)) {
56
+ number = parseInt(
57
+ fs.readFileSync(buildNumberFile, 'utf-8').trim(),
58
+ 10,
59
+ ) || 0;
60
+ }
61
+ const nextNumber = number + 1;
62
+
63
+ // ─── Admin UI ────────────────────────────────────────────────
64
+ // Static files (HTML/CSS/JS) — no node_modules needed at runtime
65
+
66
+ const adminDir = path.join(appDir, 'ui', 'admin');
67
+ if (fs.existsSync(adminDir)) {
68
+ console.log(green('[1/3]') + ' Building Admin UI...');
69
+
70
+ const distDir = path.join(adminDir, 'dist');
71
+ if (fs.existsSync(distDir)) {
72
+ fs.rmSync(distDir, { recursive: true });
73
+ }
74
+
75
+ run('npm run build', adminDir);
76
+
77
+ const buildAdminOut = path.join(buildDir, 'ui', 'admin');
78
+ if (fs.existsSync(buildAdminOut)) {
79
+ fs.rmSync(buildAdminOut, { recursive: true });
80
+ }
81
+ fs.mkdirSync(buildAdminOut, { recursive: true });
82
+
83
+ run(`rsync -av dist/* "${buildAdminOut}"`, adminDir);
84
+ }
85
+
86
+ // ─── Site UI ─────────────────────────────────────────────────
87
+ // Express + EJS — needs node_modules at runtime (installed by build-deps)
88
+
89
+ const siteDir = path.join(appDir, 'ui', 'site');
90
+ if (fs.existsSync(siteDir)) {
91
+ console.log(green('[2/3]') + ' Building Site UI...');
92
+
93
+ const siteBuildDir = path.join(siteDir, 'build');
94
+ if (fs.existsSync(siteBuildDir)) {
95
+ fs.rmSync(siteBuildDir, { recursive: true });
96
+ }
97
+
98
+ run('npm run build', siteDir);
99
+
100
+ const buildSiteOut = path.join(buildDir, 'ui', 'site');
101
+
102
+ // Preserve node_modules + .env from previous build-deps
103
+ const tmpModules = `/tmp/_umpordez_site_modules_${nextNumber}`;
104
+ const siteModules = path.join(buildSiteOut, 'node_modules');
105
+ if (fs.existsSync(siteModules)) {
106
+ fs.renameSync(siteModules, tmpModules);
107
+ }
108
+
109
+ const tmpEnv = `/tmp/_umpordez_site_env_${nextNumber}`;
110
+ const siteEnv = path.join(buildSiteOut, '.env');
111
+ if (fs.existsSync(siteEnv)) {
112
+ fs.renameSync(siteEnv, tmpEnv);
113
+ }
114
+
115
+ if (fs.existsSync(buildSiteOut)) {
116
+ fs.rmSync(buildSiteOut, { recursive: true });
117
+ }
118
+ fs.mkdirSync(buildSiteOut, { recursive: true });
119
+
120
+ run(`rsync -av build/ "${buildSiteOut}/"`, siteDir);
121
+
122
+ const viewsDir = path.join(siteDir, 'views');
123
+ if (fs.existsSync(viewsDir)) {
124
+ run(`rsync -av views "${buildSiteOut}/"`, siteDir);
125
+ }
126
+
127
+ const publicDir = path.join(siteDir, 'public');
128
+ if (fs.existsSync(publicDir)) {
129
+ run(`rsync -av public "${buildSiteOut}/"`, siteDir);
130
+ }
131
+
132
+ fs.copyFileSync(
133
+ path.join(siteDir, 'package.json'),
134
+ path.join(buildSiteOut, 'package.json'),
135
+ );
136
+
137
+ const lockFile = path.join(siteDir, 'package-lock.json');
138
+ if (fs.existsSync(lockFile)) {
139
+ fs.copyFileSync(
140
+ lockFile,
141
+ path.join(buildSiteOut, 'package-lock.json'),
142
+ );
143
+ }
144
+
145
+ // Restore .env and node_modules from previous build-deps
146
+ if (fs.existsSync(tmpEnv)) {
147
+ fs.renameSync(tmpEnv, path.join(buildSiteOut, '.env'));
148
+ }
149
+ if (fs.existsSync(tmpModules)) {
150
+ fs.renameSync(
151
+ tmpModules,
152
+ path.join(buildSiteOut, 'node_modules'),
153
+ );
154
+ }
155
+ }
156
+
157
+ // ─── Server ──────────────────────────────────────────────────
158
+ // Node.js — needs node_modules at runtime (installed by build-deps)
159
+
160
+ const serverDir = path.join(appDir, 'server');
161
+ if (fs.existsSync(serverDir)) {
162
+ console.log(green('[3/3]') + ' Building Server...');
163
+
164
+ const serverBuildDir = path.join(serverDir, 'build');
165
+ if (fs.existsSync(serverBuildDir)) {
166
+ fs.rmSync(serverBuildDir, { recursive: true });
167
+ }
168
+
169
+ run('npm run build', serverDir);
170
+
171
+ fs.copyFileSync(
172
+ path.join(serverDir, 'package.json'),
173
+ path.join(serverBuildDir, 'package.json'),
174
+ );
175
+
176
+ const serverLock = path.join(serverDir, 'package-lock.json');
177
+ if (fs.existsSync(serverLock)) {
178
+ fs.copyFileSync(
179
+ serverLock,
180
+ path.join(serverBuildDir, 'package-lock.json'),
181
+ );
182
+ }
183
+
184
+ // Copy email templates
185
+ const emailsDir = path.join(serverDir, 'emails');
186
+ if (fs.existsSync(emailsDir)) {
187
+ const buildEmails = path.join(serverBuildDir, 'emails');
188
+ fs.mkdirSync(buildEmails, { recursive: true });
189
+ for (const f of fs.readdirSync(emailsDir)) {
190
+ fs.copyFileSync(
191
+ path.join(emailsDir, f),
192
+ path.join(buildEmails, f),
193
+ );
194
+ }
195
+ }
196
+
197
+ // Remove .ts and .map from compiled migrations
198
+ const migrationsDir = path.join(serverBuildDir, 'migrations');
199
+ if (fs.existsSync(migrationsDir)) {
200
+ for (const f of fs.readdirSync(migrationsDir)) {
201
+ if (f.endsWith('.ts') || f.endsWith('.map')) {
202
+ fs.unlinkSync(path.join(migrationsDir, f));
203
+ }
204
+ }
205
+ }
206
+
207
+ const buildServerOut = path.join(buildDir, 'server');
208
+
209
+ // Preserve node_modules and .env from previous build-deps
210
+ const tmpServerModules = `/tmp/_umpordez_server_modules_${nextNumber}`;
211
+ const serverModules = path.join(buildServerOut, 'node_modules');
212
+ if (fs.existsSync(serverModules)) {
213
+ fs.renameSync(serverModules, tmpServerModules);
214
+ }
215
+
216
+ const tmpServerEnv = `/tmp/_umpordez_server_env_${nextNumber}`;
217
+ const serverEnv = path.join(buildServerOut, '.env');
218
+ if (fs.existsSync(serverEnv)) {
219
+ fs.renameSync(serverEnv, tmpServerEnv);
220
+ }
221
+
222
+ if (fs.existsSync(buildServerOut)) {
223
+ fs.rmSync(buildServerOut, { recursive: true });
224
+ }
225
+ fs.mkdirSync(buildServerOut, { recursive: true });
226
+
227
+ run(`rsync -av build/* "${buildServerOut}"`, serverDir);
228
+
229
+ // Write knex.sh for the build repo (local knex + .js knexfile)
230
+ const knexSh = '#!/bin/bash\n'
231
+ + 'node ./node_modules/knex/bin/cli.js \\\n'
232
+ + ' --knexfile core/knexfile.js \\\n'
233
+ + ' --migrations-directory ../migrations $@\n';
234
+ fs.writeFileSync(
235
+ path.join(buildServerOut, 'knex.sh'),
236
+ knexSh,
237
+ { mode: 0o755 }
238
+ );
239
+
240
+ // Restore node_modules and .env
241
+ if (fs.existsSync(tmpServerEnv)) {
242
+ fs.renameSync(tmpServerEnv, path.join(buildServerOut, '.env'));
243
+ }
244
+ if (fs.existsSync(tmpServerModules)) {
245
+ fs.renameSync(
246
+ tmpServerModules,
247
+ path.join(buildServerOut, 'node_modules')
248
+ );
249
+ }
250
+ }
251
+
252
+ // ─── Version & Git ───────────────────────────────────────────
253
+
254
+ fs.writeFileSync(buildNumberFile, String(nextNumber), 'utf-8');
255
+
256
+ if (fs.existsSync(path.join(buildDir, '.git'))) {
257
+ console.log(dim(' Committing build...'));
258
+
259
+ try {
260
+ run('git add .', buildDir);
261
+ run(
262
+ `git commit -m "build: #${nextNumber}"`,
263
+ buildDir,
264
+ );
265
+ } catch {
266
+ // nothing to commit
267
+ }
268
+
269
+ // Tag if doesn't exist
270
+ try {
271
+ execSync(
272
+ `git rev-parse "${nextNumber}"`,
273
+ { cwd: buildDir, stdio: 'ignore' },
274
+ );
275
+ } catch {
276
+ run(
277
+ `git tag -a "${nextNumber}" -m "#${nextNumber}"`,
278
+ buildDir,
279
+ );
280
+ run('git push origin --tags', buildDir);
281
+ }
282
+
283
+ run('git push', buildDir);
284
+ }
285
+
286
+ console.log('');
287
+ console.log(green.bold(`=== Build #${nextNumber} complete ===`));
288
+ console.log('');
289
+ console.log(` ${chalk.bold('Admin UI:')} ${cyan(path.join(buildDir, 'ui', 'admin'))}`);
290
+ console.log(` ${chalk.bold('Site UI:')} ${cyan(path.join(buildDir, 'ui', 'site'))}`);
291
+ console.log(` ${chalk.bold('Server:')} ${cyan(path.join(buildDir, 'server'))}`);
292
+ console.log('');
293
+ console.log(dim(' Next: run umpordez build-deps on the target machine'));
294
+ console.log('');
295
+ }
296
+
297
+ // ─── build-deps ──────────────────────────────────────────────────────────────
298
+ // Run on prod/staging. Installs production node_modules in the builds repo
299
+ // so native bindings match the target OS. After this, the repo is ready to run.
300
+
301
+ export async function buildDeps(buildDir) {
302
+ buildDir = path.resolve(buildDir);
303
+
304
+ if (!fs.existsSync(buildDir)) {
305
+ console.log(red(`Build directory not found: ${buildDir}`));
306
+ console.log(dim('Run umpordez build first.'));
307
+ process.exit(1);
308
+ }
309
+
310
+ console.log('');
311
+ console.log(green.bold('=== umpordez build-deps ==='));
312
+ console.log('');
313
+ console.log(` ${chalk.bold('Output:')} ${cyan(buildDir)}`);
314
+ console.log('');
315
+
316
+ // Pull builds repo if git
317
+ if (fs.existsSync(path.join(buildDir, '.git'))) {
318
+ console.log(dim(' Pulling builds repo...'));
319
+ runSilent('git pull', buildDir);
320
+ }
321
+
322
+ let step = 1;
323
+ const parts = [];
324
+
325
+ const buildServerOut = path.join(buildDir, 'server');
326
+ const buildSiteOut = path.join(buildDir, 'ui', 'site');
327
+
328
+ if (fs.existsSync(buildServerOut)) {
329
+ parts.push('server');
330
+ }
331
+ if (fs.existsSync(buildSiteOut)) {
332
+ parts.push('site');
333
+ }
334
+
335
+ if (parts.length === 0) {
336
+ console.log(red('No build output found.'));
337
+ console.log(dim('Run umpordez build first.'));
338
+ process.exit(1);
339
+ }
340
+
341
+ const total = parts.length;
342
+
343
+ // ─── Server deps ─────────────────────────────────────────────
344
+
345
+ if (parts.includes('server')) {
346
+ console.log(
347
+ green(`[${step}/${total}]`)
348
+ + ' Installing server dependencies...',
349
+ );
350
+
351
+ const serverModules = path.join(buildServerOut, 'node_modules');
352
+ if (fs.existsSync(serverModules)) {
353
+ fs.rmSync(serverModules, { recursive: true });
354
+ }
355
+
356
+ try {
357
+ run('npm ci --omit=dev', buildServerOut);
358
+ } catch {
359
+ run('npm install --omit=dev', buildServerOut);
360
+ }
361
+
362
+ step++;
363
+ }
364
+
365
+ // ─── Site UI deps ────────────────────────────────────────────
366
+
367
+ if (parts.includes('site')) {
368
+ console.log(
369
+ green(`[${step}/${total}]`)
370
+ + ' Installing site dependencies...',
371
+ );
372
+
373
+ const siteModules = path.join(buildSiteOut, 'node_modules');
374
+ if (fs.existsSync(siteModules)) {
375
+ fs.rmSync(siteModules, { recursive: true });
376
+ }
377
+
378
+ try {
379
+ run('npm ci --omit=dev', buildSiteOut);
380
+ } catch {
381
+ run('npm install --omit=dev', buildSiteOut);
382
+ }
383
+
384
+ step++;
385
+ }
386
+
387
+ // ─── Git commit ──────────────────────────────────────────────
388
+
389
+ if (fs.existsSync(path.join(buildDir, '.git'))) {
390
+ console.log(dim(' Committing dependencies...'));
391
+
392
+ try {
393
+ run('git add .', buildDir);
394
+ run(
395
+ 'git commit -m "deps: update node_modules"',
396
+ buildDir,
397
+ );
398
+ run('git push', buildDir);
399
+ } catch {
400
+ console.log(dim(' No dependency changes to commit.'));
401
+ }
402
+ }
403
+
404
+ console.log('');
405
+ console.log(green.bold('=== Dependencies ready ==='));
406
+ console.log('');
407
+ console.log(dim(' Add .env and start the services.'));
408
+ console.log('');
409
+ }