@mostajs/orm-cli 0.3.2 → 0.4.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.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ // install-bridge.mjs — one-shot codemod : scan a Prisma project, find every
3
+ // file that instantiates PrismaClient, and rewrite it to use
4
+ // createPrismaLikeDb() from @mostajs/orm-bridge.
5
+ //
6
+ // Author: Dr Hamid MADANI drmdh@msn.com
7
+ // License: AGPL-3.0-or-later
8
+ //
9
+ // Usage (invoked by bin/mostajs.sh as `mostajs install-bridge`) :
10
+ //
11
+ // node install-bridge.mjs # dry-run (default, safe)
12
+ // node install-bridge.mjs --apply # actually write files
13
+ // node install-bridge.mjs --file X # restrict to a single file
14
+ // node install-bridge.mjs --project P # root to scan (default: cwd)
15
+ // node install-bridge.mjs --restore # restore from .prisma.bak backups
16
+ //
17
+ // The rewriter is deliberately conservative :
18
+ // - It only touches files that CREATE a PrismaClient (`new PrismaClient(...)`).
19
+ // - It preserves the original export name (`prisma`, `db`, `client`, `default`)
20
+ // so none of the 10 to 10000 call-sites elsewhere in the codebase need to change.
21
+ // - Original file is saved as <path>.prisma.bak (never overwritten).
22
+ // - Re-runs are idempotent : if the file already uses createPrismaLikeDb,
23
+ // the codemod skips it.
24
+
25
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, renameSync, copyFileSync } from 'node:fs';
26
+ import { join, resolve, relative, extname } from 'node:path';
27
+
28
+ // ---------- CLI ----------
29
+ const argv = process.argv.slice(2);
30
+ const flag = (name) => argv.includes(`--${name}`);
31
+ const val = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : null; };
32
+
33
+ const APPLY = flag('apply');
34
+ const RESTORE = flag('restore');
35
+ const ONE_FILE = val('file');
36
+ const ROOT = resolve(val('project') ?? process.cwd());
37
+ const QUIET = flag('quiet');
38
+
39
+ const log = (...a) => { if (!QUIET) console.log(...a); };
40
+ const c = { cyan: s => `\x1b[36m${s}\x1b[0m`, yellow: s => `\x1b[33m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`, red: s => `\x1b[31m${s}\x1b[0m`, bold: s => `\x1b[1m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m` };
41
+
42
+ // ---------- Walk ----------
43
+ const SKIP_DIRS = new Set(['node_modules', '.next', '.svelte-kit', 'dist', 'build', '.turbo', '.vercel', '.cache', 'coverage', '.git', '.vscode', '.idea']);
44
+ const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.js', '.jsx', '.mjs']);
45
+
46
+ function* walk(dir) {
47
+ let entries;
48
+ try { entries = readdirSync(dir); } catch { return; }
49
+ for (const name of entries) {
50
+ if (SKIP_DIRS.has(name)) continue;
51
+ const p = join(dir, name);
52
+ let st;
53
+ try { st = statSync(p); } catch { continue; }
54
+ if (st.isDirectory()) yield* walk(p);
55
+ else if (EXTENSIONS.has(extname(name))) yield p;
56
+ }
57
+ }
58
+
59
+ // ---------- Detection ----------
60
+ // Patterns we consider "PrismaClient instantiation sites"
61
+ const RX_IMPORT = /import\s*(?:\{[^}]*PrismaClient[^}]*\}|[^;]*?)\s*from\s*['"]@prisma\/client['"]/;
62
+ const RX_NEW = /new\s+PrismaClient\s*\(/;
63
+ const RX_ALREADY = /@mostajs\/orm-bridge\/prisma-client/;
64
+
65
+ // Detect the export shape to preserve the name the codebase depends on.
66
+ // export const db = new PrismaClient(...) → named, "db"
67
+ // export const prisma = new PrismaClient(...) → named, "prisma"
68
+ // export default new PrismaClient(...) → default
69
+ // const prisma = ...; export { prisma } → named, "prisma"
70
+ function detectExportShape(source) {
71
+ // Singleton pattern commonly used in Next.js :
72
+ // const g = globalThis as ... { prisma: ... }
73
+ // export const db = g.prisma ?? new PrismaClient()
74
+ const mNamed = source.match(/export\s+const\s+(\w+)\s*=\s*(?:[^;]*?)\bnew\s+PrismaClient/);
75
+ if (mNamed) return { kind: 'const', name: mNamed[1] };
76
+
77
+ const mLet = source.match(/export\s+let\s+(\w+)\s*=\s*(?:[^;]*?)\bnew\s+PrismaClient/);
78
+ if (mLet) return { kind: 'let', name: mLet[1] };
79
+
80
+ const mDefault = source.match(/export\s+default\s+(?:[^;]*?)\bnew\s+PrismaClient/);
81
+ if (mDefault) return { kind: 'default', name: null };
82
+
83
+ // Bare global : const prisma = new PrismaClient(); followed somewhere by `export { prisma }` or not
84
+ const mBare = source.match(/(?:^|\n)\s*(?:const|let)\s+(\w+)\s*=\s*(?:[^;]*?)\bnew\s+PrismaClient/);
85
+ if (mBare) {
86
+ const name = mBare[1];
87
+ if (new RegExp(`export\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`).test(source)) {
88
+ return { kind: 'export-block', name };
89
+ }
90
+ return { kind: 'const', name }; // assume user wants to export it ; safer default
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ // ---------- Rewrite ----------
97
+ function buildReplacement(shape) {
98
+ const header = `// Auto-generated by \`mostajs install-bridge\` on ${new Date().toISOString()}\n// Original file backed up as <this-file>.prisma.bak\n// Every db/prisma/client call is now routed to @mostajs/orm (13 dialects).\nimport 'server-only'\nimport { createPrismaLikeDb } from '@mostajs/orm-bridge/prisma-client'\n`;
99
+ if (shape.kind === 'default') {
100
+ return `${header}\nexport default createPrismaLikeDb()\n`;
101
+ }
102
+ const kw = shape.kind === 'let' ? 'let' : 'const';
103
+ if (shape.kind === 'export-block') {
104
+ return `${header}\n${kw} ${shape.name} = createPrismaLikeDb()\nexport { ${shape.name} }\n`;
105
+ }
106
+ return `${header}\nexport ${kw} ${shape.name} = createPrismaLikeDb()\n`;
107
+ }
108
+
109
+ // ---------- Restore ----------
110
+ function restoreBackups() {
111
+ const restored = [];
112
+ for (const p of walk(ROOT)) {
113
+ if (!p.endsWith('.prisma.bak')) continue;
114
+ const original = p.slice(0, -'.prisma.bak'.length);
115
+ if (APPLY) {
116
+ renameSync(p, original);
117
+ log(` ${c.green('✓')} restored ${c.dim(relative(ROOT, original))}`);
118
+ } else {
119
+ log(` ${c.yellow('•')} would restore ${c.dim(relative(ROOT, original))}`);
120
+ }
121
+ restored.push(original);
122
+ }
123
+ return restored;
124
+ }
125
+
126
+ // ---------- Main ----------
127
+ if (RESTORE) {
128
+ log(c.bold('▶ Restoring .prisma.bak files' + (APPLY ? '' : c.yellow(' (dry-run, use --apply)'))));
129
+ const r = restoreBackups();
130
+ log(`\n${r.length} file(s) ${APPLY ? 'restored' : 'would be restored'}`);
131
+ process.exit(0);
132
+ }
133
+
134
+ log(c.bold(`▶ mostajs install-bridge — scanning ${c.cyan(ROOT)}`));
135
+ log('');
136
+
137
+ const candidates = [];
138
+ const iter = ONE_FILE ? [resolve(ROOT, ONE_FILE)] : walk(ROOT);
139
+ for (const p of iter) {
140
+ let src;
141
+ try { src = readFileSync(p, 'utf8'); } catch { continue; }
142
+ if (!RX_IMPORT.test(src) && !RX_NEW.test(src)) continue;
143
+ if (RX_ALREADY.test(src)) {
144
+ log(` ${c.dim('— skip (already bridged) ' + relative(ROOT, p))}`);
145
+ continue;
146
+ }
147
+ if (!RX_NEW.test(src)) continue; // imports only = not an instantiation site
148
+ const shape = detectExportShape(src);
149
+ if (!shape) {
150
+ log(` ${c.yellow('?')} ${relative(ROOT, p)} — PrismaClient detected but export shape unknown, skipping`);
151
+ continue;
152
+ }
153
+ candidates.push({ path: p, rel: relative(ROOT, p), shape, src });
154
+ }
155
+
156
+ if (candidates.length === 0) {
157
+ log(c.yellow(' No instantiation sites found.'));
158
+ log('');
159
+ log(` Did you remove \`new PrismaClient()\` already ? The bridge may already be in place.`);
160
+ log(` Re-scan with: ${c.cyan('mostajs install-bridge --restore --apply')} to undo a prior run.`);
161
+ process.exit(0);
162
+ }
163
+
164
+ log(c.bold(`Found ${candidates.length} PrismaClient instantiation site(s):`));
165
+ for (const ca of candidates) {
166
+ log(` ${c.green('→')} ${ca.rel} ${c.dim(`(${ca.shape.kind}${ca.shape.name ? ' ' + ca.shape.name : ''})`)}`);
167
+ }
168
+ log('');
169
+
170
+ if (!APPLY) {
171
+ log(c.yellow('Dry-run — no files written. Re-run with --apply to execute.'));
172
+ log('');
173
+ log(c.bold('Preview of rewrite for the first file :'));
174
+ log(c.dim('─────────────────────────────────────────'));
175
+ log(buildReplacement(candidates[0].shape));
176
+ log(c.dim('─────────────────────────────────────────'));
177
+ process.exit(0);
178
+ }
179
+
180
+ // ---------- Apply ----------
181
+ log(c.bold('Applying rewrites :'));
182
+ for (const ca of candidates) {
183
+ const bak = ca.path + '.prisma.bak';
184
+ if (!existsSync(bak)) copyFileSync(ca.path, bak);
185
+ writeFileSync(ca.path, buildReplacement(ca.shape));
186
+ log(` ${c.green('✓')} rewrote ${ca.rel} ${c.dim(`(backup: ${relative(ROOT, bak)})`)}`);
187
+ }
188
+
189
+ log('');
190
+ log(c.bold(c.green(`✓ Bridge installed in ${candidates.length} file(s).`)));
191
+ log('');
192
+ log('Next steps :');
193
+ log(` 1. ${c.cyan('npm i @mostajs/orm @mostajs/orm-bridge server-only --legacy-peer-deps')}`);
194
+ log(` 2. ${c.cyan('npx @mostajs/orm-cli')} → menu 1 (Convert Prisma → entities.json) → menu 3 (init DDL)`);
195
+ log(` 3. Set ${c.cyan('DB_DIALECT')} + ${c.cyan('SGBD_URI')} in .env (or: menu 2 → i to import)`);
196
+ log(` 4. ${c.cyan('npm run dev')} and test.`);
197
+ log('');
198
+ log(`To undo : ${c.cyan('mostajs install-bridge --restore --apply')}`);
package/bin/mostajs.sh CHANGED
@@ -438,6 +438,8 @@ menu_main() {
438
438
  echo -e " ${CYAN}8${RESET}) Health checks"
439
439
  echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
440
440
  echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
441
+ echo -e " ${GREEN}b${RESET}) ${BOLD}Bootstrap${RESET} — one-shot migration of a Prisma project"
442
+ echo -e " ${GREEN}i${RESET}) ${BOLD}Install bridge${RESET} — codemod PrismaClient → bridge (dry-run / apply / restore)"
441
443
  echo -e " ${CYAN}0${RESET}) About / Help"
442
444
  echo
443
445
  echo -e " ${RED}q${RESET}) Quit"
@@ -455,12 +457,63 @@ menu_main() {
455
457
  8) action_healthcheck ;;
456
458
  9) action_generate_boilerplate ;;
457
459
  s|S) menu_seeding ;;
460
+ b|B) menu_bootstrap ;;
461
+ i|I) menu_install_bridge ;;
458
462
  0) action_about ;;
459
463
  q|Q) exit 0 ;;
460
464
  *) warn "Unknown choice"; pause ;;
461
465
  esac
462
466
  }
463
467
 
468
+ # ------------------------------------------------------------
469
+ # Interactive wrapper for `install-bridge` codemod
470
+ # ------------------------------------------------------------
471
+ menu_install_bridge() {
472
+ header
473
+ echo -e "${BOLD}${MAGENTA}▶ Install bridge — PrismaClient codemod${RESET}"
474
+ echo
475
+ echo " ${CYAN}1${RESET}) Dry-run : list files that would be rewritten (default)"
476
+ echo " ${CYAN}2${RESET}) Apply : rewrite PrismaClient sites to createPrismaLikeDb()"
477
+ echo " ${CYAN}3${RESET}) Restore : revert .prisma.bak files (dry-run)"
478
+ echo " ${CYAN}4${RESET}) Restore : revert .prisma.bak files (apply)"
479
+ echo " ${RED}0${RESET}) Back"
480
+ echo
481
+ local cli_dir
482
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
483
+ local choice; choice=$(ask "Choice" "1")
484
+ case "$choice" in
485
+ 1) node "$cli_dir/bin/install-bridge.mjs" ;;
486
+ 2) node "$cli_dir/bin/install-bridge.mjs" --apply ;;
487
+ 3) node "$cli_dir/bin/install-bridge.mjs" --restore ;;
488
+ 4) node "$cli_dir/bin/install-bridge.mjs" --restore --apply ;;
489
+ 0) return ;;
490
+ *) warn "Unknown" ;;
491
+ esac
492
+ pause
493
+ }
494
+
495
+ # ------------------------------------------------------------
496
+ # Interactive wrapper for `bootstrap` — one-shot full migration
497
+ # ------------------------------------------------------------
498
+ menu_bootstrap() {
499
+ header
500
+ echo -e "${BOLD}${GREEN}▶ Bootstrap — full Prisma → @mostajs/orm migration${RESET}"
501
+ echo
502
+ echo "This will, in ${BOLD}this project${RESET} :"
503
+ echo " 1. Rewrite every ${CYAN}new PrismaClient()${RESET} site to use ${CYAN}createPrismaLikeDb()${RESET}"
504
+ echo " (originals backed up as ${DIM}*.prisma.bak${RESET})"
505
+ echo " 2. Install ${CYAN}@mostajs/orm${RESET} + ${CYAN}@mostajs/orm-bridge${RESET} + ${CYAN}server-only${RESET}"
506
+ echo " 3. Convert ${CYAN}prisma/schema.prisma${RESET} → ${DIM}.mostajs/generated/entities.json${RESET}"
507
+ echo " 4. Write ${DIM}.mostajs/config.env${RESET} (default : sqlite ./data.sqlite) and init DDL"
508
+ echo
509
+ warn "Existing code changes will be backed up but NOT committed — review the diff before pushing."
510
+ echo
511
+ local go; go=$(ask "Proceed? (y/N)" "N")
512
+ [[ "$go" =~ ^[yY]$ ]] || { info "Cancelled"; return; }
513
+ run_subcommand bootstrap
514
+ pause
515
+ }
516
+
464
517
  # ============================================================
465
518
  # ACTION 1 : CONVERT
466
519
  # ============================================================
@@ -2316,6 +2369,14 @@ EOF
2316
2369
 
2317
2370
  run_subcommand() {
2318
2371
  case "$1" in
2372
+ diagnose|diag|d)
2373
+ # mostajs diagnose [email] [password]
2374
+ # Walks through: config vs project datasource mismatch, DB connection,
2375
+ # user lookup, isActive check, bcrypt verification.
2376
+ local email="${2:-}"
2377
+ local password="${3:-}"
2378
+ action_diagnose_login "$email" "$password"
2379
+ ;;
2319
2380
  hash|h)
2320
2381
  # mostajs hash <plaintext> [cost]
2321
2382
  local pw="${2:-}"
@@ -2387,6 +2448,71 @@ run_subcommand() {
2387
2448
  health|h)
2388
2449
  action_healthcheck
2389
2450
  ;;
2451
+ install-bridge|ib)
2452
+ # mostajs install-bridge [--apply] [--file X] [--project P] [--restore]
2453
+ # Codemod : scans the project for `new PrismaClient(...)` sites and rewrites
2454
+ # them in place to use createPrismaLikeDb() from @mostajs/orm-bridge.
2455
+ # Dry-run by default ; pass --apply to write the changes.
2456
+ local cli_dir
2457
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2458
+ shift
2459
+ node "$cli_dir/bin/install-bridge.mjs" "$@"
2460
+ ;;
2461
+ bootstrap|b)
2462
+ # mostajs bootstrap : the full zero-touch migration for a Prisma project.
2463
+ # 1. Run the codemod on the whole tree (install-bridge --apply)
2464
+ # 2. npm install @mostajs/orm @mostajs/orm-bridge server-only
2465
+ # 3. Convert prisma/schema.prisma → entities.json
2466
+ # 4. Write .mostajs/config.env + init SQLite DDL
2467
+ # 5. Tell the user what remains (.env, seeds, run dev).
2468
+ local cli_dir
2469
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2470
+ detect_project
2471
+ [[ ${#DETECTED_TYPES[@]} -eq 0 ]] && { err "No schema found. Bootstrap needs a prisma/schema.prisma (or OpenAPI/JSONSchema)."; exit 1; }
2472
+
2473
+ echo -e "\n\e[1m▶ Step 1/4 : rewrite PrismaClient sites\e[0m"
2474
+ node "$cli_dir/bin/install-bridge.mjs" --apply || { err "Codemod failed"; exit 1; }
2475
+
2476
+ echo -e "\n\e[1m▶ Step 2/4 : install runtime deps\e[0m"
2477
+ ( cd "$PROJECT_ROOT" && $PKG_MANAGER install @mostajs/orm @mostajs/orm-bridge server-only --legacy-peer-deps 2>&1 | tail -3 )
2478
+
2479
+ echo -e "\n\e[1m▶ Step 3/4 : convert schema + init DDL\e[0m"
2480
+ local type="${DETECTED_TYPES[0]}" input
2481
+ case "$type" in
2482
+ prisma) input="$PRISMA_SCHEMA" ;;
2483
+ openapi) input="$OPENAPI_FILE" ;;
2484
+ jsonschema) input="${JSON_SCHEMAS[0]}" ;;
2485
+ esac
2486
+ run_adapter_convert "$type" "$input" "$GENERATED_DIR/entities.ts"
2487
+
2488
+ mkdir -p "$CONFIG_DIR"
2489
+ if [[ ! -f "$CONFIG_DIR/config.env" ]]; then
2490
+ cat > "$CONFIG_DIR/config.env" <<CFG
2491
+ DB_DIALECT=sqlite
2492
+ SGBD_URI=./data.sqlite
2493
+ DB_SCHEMA_STRATEGY=update
2494
+ CFG
2495
+ ok " wrote $CONFIG_DIR/config.env (defaults: sqlite ./data.sqlite)"
2496
+ fi
2497
+ action_init_dialects || warn "DDL init returned non-zero — inspect and retry with menu 3"
2498
+
2499
+ echo -e "\n\e[1m▶ Step 4/4 : done\e[0m"
2500
+ cat <<DONE
2501
+
2502
+ ✓ Bridge installed in-place. Original files backed up as *.prisma.bak
2503
+ ✓ Schema converted : $GENERATED_DIR/entities.json
2504
+ ✓ SQLite DDL applied : $PROJECT_ROOT/data.sqlite
2505
+
2506
+ Next :
2507
+ - Add seeds to $CONFIG_DIR/seeds/*.json (one file per entity)
2508
+ - $CLI_NAME # menu S → h (hash) → 4 (apply)
2509
+ - npm run dev
2510
+
2511
+ To undo the codemod :
2512
+ $CLI_NAME install-bridge --restore --apply
2513
+
2514
+ DONE
2515
+ ;;
2390
2516
  version|-v|--version)
2391
2517
  echo "$CLI_NAME $VERSION"
2392
2518
  ;;
@@ -2394,14 +2520,21 @@ run_subcommand() {
2394
2520
  cat <<EOF
2395
2521
  Usage :
2396
2522
  $CLI_NAME Interactive menu
2523
+ $CLI_NAME bootstrap One-shot migration : codemod + deps + convert + DDL
2524
+ $CLI_NAME install-bridge Codemod only (dry-run ; add --apply to write)
2525
+ $CLI_NAME install-bridge --apply Rewrite PrismaClient sites to use @mostajs/orm-bridge
2526
+ $CLI_NAME install-bridge --restore --apply Undo a prior install-bridge
2397
2527
  $CLI_NAME convert Run conversion (auto-detect schema type)
2398
2528
  $CLI_NAME detect Print detected schemas
2399
2529
  $CLI_NAME health Run health checks
2400
2530
  $CLI_NAME hash <password> [cost] Hash a password with bcrypt (cost default 10)
2401
2531
  $CLI_NAME verify <password> <hash> Check if a plain password matches a bcrypt hash
2532
+ $CLI_NAME diagnose [email] [pw] Walk through login diagnostics
2402
2533
  $CLI_NAME version Print version
2403
2534
 
2404
2535
  Examples:
2536
+ $CLI_NAME bootstrap → zero-touch migrate a Prisma project to @mostajs/orm
2537
+ $CLI_NAME install-bridge → preview rewrites without touching files
2405
2538
  $CLI_NAME hash 'Admin@123456' → \$2b\$10\$N9qo8uLOickgx2ZMRZoMyeIjZA...
2406
2539
  $CLI_NAME verify 'Admin@123456' '\$2b\$10\$N9qo...'
2407
2540
  EOF
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.3.2",
4
- "description": "Universal CLI to integrate @mostajs/orm into any project — auto-detects Prisma, OpenAPI, JSON Schema. Interactive menu + subcommands. 13 databases.",
3
+ "version": "0.4.0",
4
+ "description": "Universal CLI to integrate @mostajs/orm into any project — one-shot `mostajs bootstrap` migrates a Prisma project (codemod + deps + schema convert + DDL) to 13 databases with zero code change.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "type": "module",