umpordez 1.5.0 → 1.6.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 (100) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/create.mjs +104 -0
  3. package/package.json +5 -5
  4. package/template/.github/copilot-instructions.md +1 -1
  5. package/template/AGENTS.md +1 -1
  6. package/template/gitignore +2 -0
  7. package/template/install.sh +4 -0
  8. package/template/mobile/{{PROJECT_SLUG}}/README.md +17 -2
  9. package/template/mobile/{{PROJECT_SLUG}}/package.json +23 -25
  10. package/template/scripts/clean-appledouble.sh +42 -0
  11. package/template/scripts/prebuild.sh +8 -1
  12. package/template/scripts/run-ios.sh +5 -0
  13. package/template/server/apps/api/app.ts +1 -1
  14. package/template/server/apps/api/routes/account.ts +32 -10
  15. package/template/server/apps/api/routes/admin.ts +13 -6
  16. package/template/server/apps/api/routes/auth.ts +109 -29
  17. package/template/server/apps/api/routes/files.ts +14 -5
  18. package/template/server/apps/api/routes/health.ts +2 -2
  19. package/template/server/apps/api/routes/user.ts +2 -1
  20. package/template/server/apps/shared/middlewares/request-logger.ts +3 -1
  21. package/template/server/apps/shared/utils.ts +2 -2
  22. package/template/server/apps/site-api/app.ts +1 -1
  23. package/template/server/console/index.ts +1 -1
  24. package/template/server/console/tasks/seed-admin.ts +8 -2
  25. package/template/server/core/context.ts +2 -2
  26. package/template/server/core/email.ts +9 -2
  27. package/template/server/core/knexfile.ts +1 -1
  28. package/template/server/core/models/account.ts +43 -13
  29. package/template/server/core/models/auth.ts +10 -3
  30. package/template/server/core/models/user.ts +61 -27
  31. package/template/server/core/s3.ts +29 -14
  32. package/template/server/eslint.config.js +9 -1
  33. package/template/server/migrations/20260208000000_initial-schema.ts +4 -2
  34. package/template/server/package.json +27 -28
  35. package/template/server/types/api.ts +3 -0
  36. package/template/ui/admin/CLAUDE.md +5 -4
  37. package/template/ui/admin/eslint.config.js +29 -1
  38. package/template/ui/admin/package.json +31 -39
  39. package/template/ui/admin/src/components/nav-user.tsx +4 -2
  40. package/template/ui/admin/src/components/theme-provider.tsx +7 -1
  41. package/template/ui/admin/src/components/ui/badge.tsx +1 -1
  42. package/template/ui/admin/src/components/ui/button.tsx +1 -1
  43. package/template/ui/admin/src/components/ui/card.tsx +1 -1
  44. package/template/ui/admin/src/components/ui/checkbox.tsx +1 -1
  45. package/template/ui/admin/src/components/ui/date-picker.tsx +3 -3
  46. package/template/ui/admin/src/components/ui/dialog.tsx +1 -1
  47. package/template/ui/admin/src/components/ui/dropdown-menu.tsx +6 -6
  48. package/template/ui/admin/src/components/ui/input.tsx +1 -1
  49. package/template/ui/admin/src/components/ui/popover.tsx +1 -1
  50. package/template/ui/admin/src/components/ui/select.tsx +4 -4
  51. package/template/ui/admin/src/components/ui/separator.tsx +1 -1
  52. package/template/ui/admin/src/components/ui/sheet.tsx +1 -1
  53. package/template/ui/admin/src/components/ui/sidebar.tsx +24 -24
  54. package/template/ui/admin/src/components/ui/sonner.tsx +4 -4
  55. package/template/ui/admin/src/components/ui/switch.tsx +1 -1
  56. package/template/ui/admin/src/components/ui/table.tsx +3 -3
  57. package/template/ui/admin/src/components/ui/tabs.tsx +2 -2
  58. package/template/ui/admin/src/components/ui/textarea.tsx +1 -1
  59. package/template/ui/admin/src/hooks/queries/use-accounts.ts +29 -9
  60. package/template/ui/admin/src/hooks/queries/use-auth.ts +20 -6
  61. package/template/ui/admin/src/hooks/queries/use-dashboard.ts +13 -5
  62. package/template/ui/admin/src/hooks/queries/use-members.ts +32 -10
  63. package/template/ui/admin/src/hooks/queries/use-users.ts +32 -10
  64. package/template/ui/admin/src/hooks/use-confirm.tsx +1 -1
  65. package/template/ui/admin/src/hooks/use-user.tsx +4 -1
  66. package/template/ui/admin/src/index.css +68 -6
  67. package/template/ui/admin/src/layouts/account.tsx +19 -6
  68. package/template/ui/admin/src/layouts/user.tsx +3 -1
  69. package/template/ui/admin/src/lib/fetch-api.ts +38 -14
  70. package/template/ui/admin/src/lib/query-keys.ts +2 -1
  71. package/template/ui/admin/src/pages/account/dashboard.tsx +10 -3
  72. package/template/ui/admin/src/pages/account/members.tsx +22 -12
  73. package/template/ui/admin/src/pages/account/settings.tsx +22 -11
  74. package/template/ui/admin/src/pages/admin/accounts.tsx +2 -3
  75. package/template/ui/admin/src/pages/admin/dashboard.tsx +10 -3
  76. package/template/ui/admin/src/pages/admin/users.tsx +19 -10
  77. package/template/ui/admin/src/pages/public/error.tsx +4 -1
  78. package/template/ui/admin/src/pages/public/forgot-password.tsx +2 -2
  79. package/template/ui/admin/src/pages/public/login.tsx +13 -3
  80. package/template/ui/admin/src/pages/public/reset-password.tsx +5 -3
  81. package/template/ui/admin/src/pages/public/signup.tsx +11 -3
  82. package/template/ui/admin/src/pages/user/profile.tsx +16 -11
  83. package/template/ui/admin/src/pages/user/select-account.tsx +3 -1
  84. package/template/ui/admin/src/vite-env.d.ts +1 -0
  85. package/template/ui/admin/tsconfig.json +0 -1
  86. package/template/ui/admin/vite.config.ts +2 -1
  87. package/template/ui/site/CLAUDE.md +1 -2
  88. package/template/ui/site/_variants/app/home.ejs +6 -6
  89. package/template/ui/site/app.ts +1 -1
  90. package/template/ui/site/eslint.config.js +9 -1
  91. package/template/ui/site/i18n/index.ts +0 -1
  92. package/template/ui/site/package.json +17 -17
  93. package/template/ui/site/styles/input.css +11 -3
  94. package/template/ui/site/views/pages/home.ejs +4 -4
  95. package/template/ui/site/views/partials/header.ejs +1 -1
  96. package/template-variables.mjs +1 -1
  97. package/template/ui/admin/postcss.config.js +0 -6
  98. package/template/ui/admin/tailwind.config.ts +0 -71
  99. package/template/ui/site/tailwind.config.js +0 -16
  100. /package/template/mobile/{{PROJECT_SLUG}}/{eslint.config.js → eslint.config.mjs} +0 -0
@@ -57,7 +57,26 @@
57
57
  "Bash(git *)",
58
58
  "Bash(npm init *)",
59
59
  "Bash(awk 'NR>=383 && NR<=386 {print NR\": \"substr\\($0,1,120\\)}' '/Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx')",
60
- "Bash(awk 'NR>=394 && NR<=410 {print NR\": \"substr\\($0,1,100\\)}' '/Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx')"
60
+ "Bash(awk 'NR>=394 && NR<=410 {print NR\": \"substr\\($0,1,100\\)}' '/Users/ligeiro/dev/umpordez-cli/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx')",
61
+ "Bash(diskutil info *)",
62
+ "Bash(mount)",
63
+ "Bash(bash -n template/scripts/clean-appledouble.sh)",
64
+ "Bash(bash -n template/scripts/prebuild.sh)",
65
+ "Bash(bash -n template/scripts/run-ios.sh)",
66
+ "Bash(bash -n template/install.sh)",
67
+ "WebSearch",
68
+ "Bash(tar -xzf expo-57.0.7.tgz package/bundledNativeModules.json)",
69
+ "Bash(npm -v)",
70
+ "Read(//dev/**)",
71
+ "Bash(npm ls *)",
72
+ "Bash(LC_ALL=C sed -i '' -e 's/dotenv\\\\.config\\(\\)/dotenv.config\\({ quiet: true }\\)/' apps/api/app.ts apps/site-api/app.ts console/index.ts)",
73
+ "Bash(LC_ALL=C sed -i '' -e \"s/dotenv\\\\.config\\({ path: path.resolve\\(dirname, '.env'\\) }\\)/dotenv.config\\({ path: path.resolve\\(dirname, '.env'\\), quiet: true }\\)/\" core/knexfile.ts)",
74
+ "Bash(npx eslint *)",
75
+ "Bash(ln -sfn /private/tmp/claude-501/-Users-ligeiro-dev-umpordez-cli/12177a90-5ebe-47b5-977f-9bb132141080/scratchpad/acme/server/node_modules /Users/ligeiro/dev/umpordez-cli/template/server/node_modules)",
76
+ "Bash(rm -f tailwind.config.ts postcss.config.js)",
77
+ "Bash(cp ../../server/eslint.config.js eslint.config.js)",
78
+ "Bash(ln -sfn /private/tmp/claude-501/-Users-ligeiro-dev-umpordez-cli/12177a90-5ebe-47b5-977f-9bb132141080/scratchpad/acme/ui/admin/node_modules /Users/ligeiro/dev/umpordez-cli/template/ui/admin/node_modules)",
79
+ "Bash(awk '{print $2}')"
61
80
  ]
62
81
  }
63
82
  }
package/create.mjs CHANGED
@@ -394,6 +394,104 @@ function validatePort(v) {
394
394
  : 'Must be a valid port number';
395
395
  }
396
396
 
397
+ // Filesystems that can't store extended attributes or symlinks. On
398
+ // macOS these make the OS scatter AppleDouble `._*` sidecar files next
399
+ // to everything npm installs — CocoaPods then fails parsing a
400
+ // `._Foo.podspec` during `expo prebuild`, and node_modules symlinks
401
+ // break. Generating an Expo project on one of these is a trap, so we
402
+ // warn before doing it.
403
+ const NON_NATIVE_FS = new Set([
404
+ 'exfat', 'msdos', 'fat32', 'vfat',
405
+ 'ntfs', 'ntfs-3g', 'fuseblk',
406
+ 'smbfs', 'cifs', 'nfs', 'afpfs', 'webdav',
407
+ ]);
408
+
409
+ // Resolve the filesystem type backing `targetPath`. The output dir
410
+ // usually doesn't exist yet, so we walk up to the nearest existing
411
+ // ancestor, then match it against the longest mount point in `mount`.
412
+ // Returns { mountPoint, fsType } or null (non-darwin, or undetectable).
413
+ function volumeInfo(targetPath) {
414
+ if (process.platform !== 'darwin') {
415
+ return null;
416
+ }
417
+
418
+ let dir = path.resolve(targetPath);
419
+ while (dir !== path.dirname(dir) && !fs.existsSync(dir)) {
420
+ dir = path.dirname(dir);
421
+ }
422
+
423
+ let mounts;
424
+ try {
425
+ mounts = execSync('mount', { encoding: 'utf-8' });
426
+ } catch {
427
+ return null;
428
+ }
429
+
430
+ // Lines look like: `/dev/disk10s1 on /Volumes/big (exfat, local, …)`
431
+ let best = null;
432
+ for (const line of mounts.split('\n')) {
433
+ const m = line.match(/ on (.+?) \(([^,)]+)/);
434
+ if (!m) {
435
+ continue;
436
+ }
437
+ const mountPoint = m[1];
438
+ const fsType = m[2].trim().toLowerCase();
439
+ const prefix = mountPoint.endsWith('/') ? mountPoint : mountPoint + '/';
440
+ if (dir === mountPoint || dir.startsWith(prefix)) {
441
+ if (!best || mountPoint.length > best.mountPoint.length) {
442
+ best = { mountPoint, fsType };
443
+ }
444
+ }
445
+ }
446
+ return best;
447
+ }
448
+
449
+ // Warn (and confirm) before generating a mobile project onto a
450
+ // filesystem macOS can't build native code on. Returns true to
451
+ // proceed, false to abort. Only meaningful when a mobile app is part
452
+ // of the project — the acute failure is CocoaPods + `expo prebuild`.
453
+ async function confirmFilesystem(outputDir, components) {
454
+ if (!components.mobile) {
455
+ return true;
456
+ }
457
+ const vol = volumeInfo(outputDir);
458
+ if (!vol || !NON_NATIVE_FS.has(vol.fsType)) {
459
+ return true;
460
+ }
461
+
462
+ console.log('');
463
+ console.log(red.bold(
464
+ ` ⚠ ${vol.mountPoint} is a ${vol.fsType} volume.`
465
+ ));
466
+ console.log(dim(
467
+ ' macOS can\'t store extended attributes or symlinks there,'
468
+ ));
469
+ console.log(dim(
470
+ ' so it scatters AppleDouble `._*` files next to everything'
471
+ ));
472
+ console.log(dim(
473
+ ' npm installs. `expo prebuild` then fails when CocoaPods'
474
+ ));
475
+ console.log(dim(
476
+ ' tries to parse a `._Foo.podspec`, and node_modules'
477
+ ));
478
+ console.log(dim(' symlinks break.'));
479
+ console.log('');
480
+ console.log(dim(
481
+ ' Recommended: generate on your internal drive (e.g. ~/dev),'
482
+ ));
483
+ console.log(dim(' an APFS / Mac OS Extended volume.'));
484
+ console.log('');
485
+
486
+ const { proceed } = await inquirer.prompt([{
487
+ type: 'confirm',
488
+ name: 'proceed',
489
+ message: `Generate on the ${vol.fsType} volume anyway?`,
490
+ default: false,
491
+ }]);
492
+ return proceed;
493
+ }
494
+
397
495
  function componentsLabel(c) {
398
496
  const parts = [];
399
497
  if (c.server) parts.push('server API');
@@ -604,6 +702,11 @@ export async function createProject() {
604
702
 
605
703
  const outputDir = path.resolve(details.outputDir);
606
704
 
705
+ if (!await confirmFilesystem(outputDir, details.components)) {
706
+ console.log(chalk.yellow('Aborted. No files were created.'));
707
+ return;
708
+ }
709
+
607
710
  if (fs.existsSync(outputDir)) {
608
711
  const { overwrite } = await inquirer.prompt([{
609
712
  type: 'confirm',
@@ -710,6 +813,7 @@ export async function createProject() {
710
813
  if (details.components.mobile) {
711
814
  scripts.push(
712
815
  'scripts/bump-version.sh',
816
+ 'scripts/clean-appledouble.sh',
713
817
  'scripts/prebuild.sh',
714
818
  'scripts/run-ios.sh',
715
819
  'scripts/release-android.sh',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "umpordez",
3
3
  "type": "module",
4
- "version": "1.5.0",
4
+ "version": "1.6.1",
5
5
  "description": "SaaS starter kit generator — server, admin SPA, public site (i18n), mobile app (offline-first, IAP, native push)",
6
6
  "main": "cli.mjs",
7
7
  "scripts": {
@@ -24,9 +24,9 @@
24
24
  },
25
25
  "homepage": "https://github.com/umpordez/cli#readme",
26
26
  "dependencies": {
27
- "chalk": "^5.4.1",
28
- "figlet": "^1.8.2",
29
- "inquirer": "^12.7.0",
30
- "sharp": "^0.33.5"
27
+ "chalk": "^5.6.2",
28
+ "figlet": "^1.11.2",
29
+ "inquirer": "^14.0.2",
30
+ "sharp": "^0.35.3"
31
31
  }
32
32
  }
@@ -9,7 +9,7 @@ Multi-tenant SaaS application built with the **umpordez** architecture pattern f
9
9
  ## Tech Stack
10
10
 
11
11
  - **Backend:** TypeScript, Express.js, PostgreSQL, Knex.js, Zod
12
- - **Admin UI:** React 18, Vite, TailwindCSS, shadcn/ui (Radix), React Router DOM 6, TanStack Query v5, React Hook Form + Zod
12
+ - **Admin UI:** React 19, Vite 8, TailwindCSS v4, shadcn/ui (Radix), React Router DOM 7, TanStack Query v5
13
13
  - **Site UI:** Express + EJS + TailwindCSS
14
14
  - **Auth:** httpOnly JWT cookies (30-day expiry)
15
15
  - **Uploads:** AWS S3 via multer-s3 with signed URLs
@@ -59,7 +59,7 @@ ui/site/ Public marketing site (Express + EJS, port 3000)
59
59
  ## Tech Stack
60
60
 
61
61
  - **Backend:** TypeScript, Express.js, PostgreSQL, Knex.js, Zod
62
- - **Admin UI:** React 18, Vite, TailwindCSS, shadcn/ui (Radix), React Router DOM 6, React Query (TanStack Query v5), React Hook Form + Zod, Recharts
62
+ - **Admin UI:** React 19, Vite 8, TailwindCSS v4, shadcn/ui (Radix), React Router DOM 7, React Query (TanStack Query v5)
63
63
  - **Site UI:** Express + EJS + TailwindCSS
64
64
  - **Auth:** httpOnly JWT cookies (30-day expiry)
65
65
  - **Uploads:** AWS S3 via multer-s3 with signed URLs
@@ -5,6 +5,8 @@ build/
5
5
  dist/
6
6
  *.log
7
7
  .DS_Store
8
+ ._*
9
+ .AppleDouble
8
10
  *.swp
9
11
  *.swo
10
12
  *~
@@ -55,6 +55,10 @@ if [ -d "mobile/{{PROJECT_SLUG}}" ]; then
55
55
 
56
56
  if [[ "$(uname)" == "Darwin" ]]; then
57
57
  if [ -d "mobile/{{PROJECT_SLUG}}/ios" ]; then
58
+ # Strip AppleDouble `._*` files (exFAT/NTFS/network
59
+ # volumes) so pod install doesn't choke on a
60
+ # `._Foo.podspec`. No-op on APFS.
61
+ ./scripts/clean-appledouble.sh "mobile/{{PROJECT_SLUG}}"
58
62
  echo "Installing iOS pods..."
59
63
  (cd "mobile/{{PROJECT_SLUG}}/ios" && pod install)
60
64
  fi
@@ -6,11 +6,26 @@ typed fetch client pointed at the project's admin API.
6
6
 
7
7
  ## Quick start
8
8
 
9
+ **This is a development-build app, not an Expo Go app.** It ships
10
+ native modules (Apple Sign-In, `expo-build-properties`, and optionally
11
+ `expo-iap`) and rides the newest Expo SDK, so the public Expo Go from
12
+ the App Store either won't run it (`"requires a newer version of Expo
13
+ Go"` right after an SDK release) or crashes on a native import. Use a
14
+ development build instead — `expo-dev-client` is already a dependency:
15
+
9
16
  ```bash
10
17
  npm install
11
- npm run start # Metro + QR open with Expo Go for the JS-only flow
12
- npm run ios # build + run on iOS (requires Xcode)
18
+ npm run ios # build + run a dev client on iOS (requires Xcode)
13
19
  npm run android # build + run on Android (requires Android Studio)
20
+ npm run start # Metro — connect from an installed dev build
21
+ ```
22
+
23
+ Need a device build without Xcode/Android Studio, or want the Expo Go
24
+ experience on the current SDK? Use EAS-hosted Expo Go, which always
25
+ matches the SDK you target (unlike the store build):
26
+
27
+ ```bash
28
+ npx eas go # installs a matching Expo Go for this SDK
14
29
  ```
15
30
 
16
31
  For a clean iOS run that picks a connected device automatically:
@@ -9,7 +9,7 @@
9
9
  "ios": "expo run:ios",
10
10
  "web": "expo start --web",
11
11
  "prebuild": "bash ../../scripts/prebuild.sh",
12
- "lint": "eslint src --ext .ts,.tsx",
12
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
13
13
  "typecheck": "tsc --noEmit"
14
14
  },
15
15
  "expo": {
@@ -20,41 +20,39 @@
20
20
  "dependencies": {
21
21
  "@noble/hashes": "^2.2.0",
22
22
  "@react-native-async-storage/async-storage": "2.2.0",
23
- "@react-navigation/bottom-tabs": "^7.7.3",
24
- "@react-navigation/native": "^7.1.19",
25
- "@react-navigation/native-stack": "^7.6.2",
26
- "expo": "~57.0.4",
27
- "expo-apple-authentication": "~57.0.0",
28
- "expo-auth-session": "~57.0.2",
29
- "expo-build-properties": "~57.0.3",
30
- "expo-constants": "~57.0.3",
31
- "expo-file-system": "~57.0.0",{{IAP_DEP_JSON}}
32
- "expo-linking": "~57.0.2",
33
- "expo-localization": "~57.0.0",
34
- "expo-splash-screen": "~57.0.2",
35
- "expo-sqlite": "~57.0.0",
36
- "expo-status-bar": "~57.0.0",
37
- "expo-system-ui": "~57.0.0",
38
- "expo-web-browser": "~57.0.0",
39
- "i18next": "^23.16.5",
40
- "lucide-react-native": "^1.24.0",
23
+ "@react-navigation/bottom-tabs": "^7.18.11",
24
+ "@react-navigation/native": "^7.3.11",
25
+ "@react-navigation/native-stack": "^7.18.3",
26
+ "expo": "~57.0.7",
27
+ "expo-apple-authentication": "~57.0.1",
28
+ "expo-build-properties": "~57.0.6",
29
+ "expo-constants": "~57.0.6",
30
+ "expo-dev-client": "~57.0.7",
31
+ "expo-file-system": "~57.0.1",{{IAP_DEP_JSON}}
32
+ "expo-localization": "~57.0.1",
33
+ "expo-splash-screen": "~57.0.4",
34
+ "expo-sqlite": "~57.0.1",
35
+ "expo-system-ui": "~57.0.1",
36
+ "expo-web-browser": "~57.0.1",
37
+ "i18next": "^23.16.8",
38
+ "lucide-react-native": "^1.25.0",
41
39
  "react": "19.2.3",
42
40
  "react-native": "0.86.0",
43
- "react-i18next": "^15.1.1",
41
+ "react-i18next": "^15.7.4",
44
42
  "react-native-gesture-handler": "~2.32.0",
45
43
  "react-native-reanimated": "4.5.0",
46
44
  "react-native-safe-area-context": "~5.7.0",
47
45
  "react-native-screens": "4.25.2",
48
46
  "react-native-svg": "15.15.4",
49
- "react-native-toast-message": "^2.3.3",
47
+ "react-native-toast-message": "^2.4.0",
50
48
  "react-native-worklets": "0.10.0"
51
49
  },
52
50
  "devDependencies": {
53
- "@stylistic/eslint-plugin": "^2.13.0",
51
+ "@stylistic/eslint-plugin": "^5.10.0",
54
52
  "@types/react": "~19.2.2",
55
- "babel-preset-expo": "~57.0.2",
56
- "eslint": "^9.20.1",
53
+ "babel-preset-expo": "~57.0.3",
54
+ "eslint": "^10.7.0",
57
55
  "typescript": "~5.9.3",
58
- "typescript-eslint": "^8.24.0"
56
+ "typescript-eslint": "^8.64.0"
59
57
  }
60
58
  }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # clean-appledouble — remove macOS AppleDouble `._*` sidecar files.
4
+ #
5
+ # macOS drops a `._<name>` metadata file next to every file that carries
6
+ # extended attributes when it lives on a filesystem that can't store
7
+ # xattrs natively (exFAT, MS-DOS/FAT, NTFS, SMB/NFS network mounts).
8
+ # CocoaPods then scans for `*.podspec`, finds `._Foo.podspec`, tries to
9
+ # parse the binary metadata as Ruby, and `pod install` — and therefore
10
+ # `expo prebuild` / `expo run:ios` — dies with:
11
+ #
12
+ # Invalid `Podfile` file: Invalid podspec file at path
13
+ # .../node_modules/.../ios/._Foo.podspec
14
+ #
15
+ # On APFS / Mac OS Extended there are none, so this is a fast no-op. Run
16
+ # it before any pod install / prebuild / native build.
17
+ #
18
+ # The real fix is to keep the project on an APFS / Mac OS Extended disk
19
+ # (e.g. ~/dev) — exFAT/NTFS regenerate these files on every npm install.
20
+ #
21
+ # Usage: ./scripts/clean-appledouble.sh [dir] (default: repo root)
22
+
23
+ set -euo pipefail
24
+
25
+ # AppleDouble files are a macOS-only artifact.
26
+ [[ "$(uname)" == "Darwin" ]] || exit 0
27
+
28
+ TARGET="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
29
+ [[ -d "$TARGET" ]] || exit 0
30
+
31
+ n=$(
32
+ find "$TARGET" -name '._*' -type f -print -delete 2>/dev/null \
33
+ | wc -l | tr -d ' '
34
+ )
35
+
36
+ if [[ "${n:-0}" -gt 0 ]]; then
37
+ echo "==> Removed $n AppleDouble ._* file(s) under $TARGET"
38
+ echo " This disk can't store macOS extended attributes (likely"
39
+ echo " exFAT/NTFS/network), so they'll return on the next npm"
40
+ echo " install. For clean native builds, move the project to an"
41
+ echo " APFS / Mac OS Extended disk (e.g. ~/dev)."
42
+ fi
@@ -24,5 +24,12 @@ for arg in "$@"; do
24
24
  esac
25
25
  done
26
26
 
27
- cd "$(dirname "$0")/../mobile/{{PROJECT_SLUG}}"
27
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
28
+ cd "$SCRIPT_DIR/../mobile/{{PROJECT_SLUG}}"
29
+
30
+ # Strip macOS AppleDouble `._*` files before prebuild — CocoaPods
31
+ # chokes on `._Foo.podspec` when the project lives on exFAT/NTFS/network
32
+ # volumes. No-op on APFS. See scripts/clean-appledouble.sh.
33
+ "$SCRIPT_DIR/clean-appledouble.sh" "$PWD"
34
+
28
35
  exec npx expo prebuild "$@"
@@ -53,6 +53,11 @@ done
53
53
 
54
54
  cd "$APP_DIR"
55
55
 
56
+ # Strip macOS AppleDouble `._*` files so the pod install triggered by
57
+ # `expo run:ios` doesn't choke on `._Foo.podspec` (exFAT/NTFS/network
58
+ # volumes). No-op on APFS. See scripts/clean-appledouble.sh.
59
+ "$SCRIPT_DIR/clean-appledouble.sh" "$APP_DIR"
60
+
56
61
  if [[ $CLEAN -eq 1 ]]; then
57
62
  echo "==> Cleaning iOS build artifacts..."
58
63
  rm -rf ios/build
@@ -1,5 +1,5 @@
1
1
  import dotenv from 'dotenv';
2
- dotenv.config();
2
+ dotenv.config({ quiet: true });
3
3
 
4
4
  import express from 'express';
5
5
  import cors from 'cors';
@@ -1,20 +1,22 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
3
  import { z } from 'zod';
4
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
4
+ import trySetUserByTokenMiddleware
5
+ from '#shared/middlewares/try-set-user-by-token.js';
5
6
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
6
- import demandAccountAccessMiddleware from '#shared/middlewares/demand-account-access.js';
7
+ import demandAccountAccessMiddleware
8
+ from '#shared/middlewares/demand-account-access.js';
7
9
  import { buildHandler } from '#shared/utils.js';
8
10
 
9
11
  const router = express.Router();
10
12
 
11
13
  const updateSettingsSchema = z.object({
12
14
  name: z.string().min(1, 'Nome é obrigatório'),
13
- settings: z.record(z.unknown()).optional()
15
+ settings: z.record(z.string(), z.unknown()).optional()
14
16
  });
15
17
 
16
18
  const addMemberSchema = z.object({
17
- email: z.string().email('Email inválido'),
19
+ email: z.email('Email inválido'),
18
20
  role: z.enum(['owner', 'manager', 'user']).default('user'),
19
21
  name: z.string().min(1).optional(),
20
22
  password: z.string().min(6).optional()
@@ -41,14 +43,21 @@ async function handleGetSettings(req: Request, res: Response): Promise<void> {
41
43
  res.json({ ok: true, account });
42
44
  }
43
45
 
44
- async function handleUpdateSettings(req: Request, res: Response): Promise<void> {
46
+ async function handleUpdateSettings(
47
+ req: Request,
48
+ res: Response
49
+ ): Promise<void> {
45
50
  const data = updateSettingsSchema.parse(req.body);
46
51
  const account = await req.context.account.update(req.accountId!, data);
47
52
  res.json({ ok: true, account });
48
53
  }
49
54
 
50
55
  async function handleDeleteAccount(req: Request, res: Response): Promise<void> {
51
- await req.context.account.delete(req.accountId!, req.user!.id, req.accountRole!);
56
+ await req.context.account.delete(
57
+ req.accountId!,
58
+ req.user!.id,
59
+ req.accountRole!
60
+ );
52
61
  res.json({ ok: true });
53
62
  }
54
63
 
@@ -57,7 +66,10 @@ async function handleListMembers(req: Request, res: Response): Promise<void> {
57
66
  const limit = parseInt(req.query.limit as string) || 20;
58
67
  const search = (req.query.search as string) || '';
59
68
 
60
- const result = await req.context.account.listMembers(req.accountId!, { page, limit, search });
69
+ const result = await req.context.account.listMembers(
70
+ req.accountId!,
71
+ { page, limit, search }
72
+ );
61
73
  res.json({ ok: true, ...result });
62
74
  }
63
75
 
@@ -67,14 +79,24 @@ async function handleAddMember(req: Request, res: Response): Promise<void> {
67
79
  res.json({ ok: true, member });
68
80
  }
69
81
 
70
- async function handleUpdateMemberRole(req: Request, res: Response): Promise<void> {
82
+ async function handleUpdateMemberRole(
83
+ req: Request,
84
+ res: Response
85
+ ): Promise<void> {
71
86
  const { role } = updateMemberRoleSchema.parse(req.body);
72
- await req.context.account.updateMemberRole(req.accountId!, (req.params.userId as string), role);
87
+ await req.context.account.updateMemberRole(
88
+ req.accountId!,
89
+ req.params.userId as string,
90
+ role
91
+ );
73
92
  res.json({ ok: true });
74
93
  }
75
94
 
76
95
  async function handleRemoveMember(req: Request, res: Response): Promise<void> {
77
- await req.context.account.removeMember(req.accountId!, (req.params.userId as string));
96
+ await req.context.account.removeMember(
97
+ req.accountId!,
98
+ req.params.userId as string
99
+ );
78
100
  res.json({ ok: true });
79
101
  }
80
102
 
@@ -1,25 +1,29 @@
1
1
  import type { Express, Request, Response } from 'express';
2
2
  import express from 'express';
3
3
  import { z } from 'zod';
4
- import trySetUserByTokenMiddleware from '#shared/middlewares/try-set-user-by-token.js';
4
+ import trySetUserByTokenMiddleware
5
+ from '#shared/middlewares/try-set-user-by-token.js';
5
6
  import demandUserMiddleware from '#shared/middlewares/demand-user.js';
6
- import demandAdminUserMiddleware from '#shared/middlewares/demand-admin-user.js';
7
+ import demandAdminUserMiddleware
8
+ from '#shared/middlewares/demand-admin-user.js';
7
9
  import { buildHandler } from '#shared/utils.js';
8
10
 
9
11
  const router = express.Router();
10
12
 
11
13
  const createUserSchema = z.object({
12
14
  name: z.string().min(1, 'Nome é obrigatório'),
13
- email: z.string().email('Email inválido'),
15
+ email: z.email('Email inválido'),
14
16
  password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
15
17
  role: z.enum(['admin', 'user']).default('user')
16
18
  });
17
19
 
18
20
  const updateUserSchema = z.object({
19
21
  name: z.string().min(1, 'Nome é obrigatório'),
20
- email: z.string().email('Email inválido'),
22
+ email: z.email('Email inválido'),
21
23
  role: z.enum(['admin', 'user']).optional(),
22
- password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres').optional()
24
+ password: z.string()
25
+ .min(6, 'Senha deve ter no mínimo 6 caracteres')
26
+ .optional()
23
27
  });
24
28
 
25
29
  async function handleGetDashboard(req: Request, res: Response): Promise<void> {
@@ -53,7 +57,10 @@ async function handleCreateUser(req: Request, res: Response): Promise<void> {
53
57
 
54
58
  async function handleUpdateUser(req: Request, res: Response): Promise<void> {
55
59
  const data = updateUserSchema.parse(req.body);
56
- const user = await req.context.user.adminUpdate((req.params.id as string), data);
60
+ const user = await req.context.user.adminUpdate(
61
+ req.params.id as string,
62
+ data
63
+ );
57
64
  res.json({ ok: true, user });
58
65
  }
59
66