@mostajs/orm-cli 0.4.7 → 0.5.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 (2) hide show
  1. package/bin/mostajs.sh +667 -2
  2. package/package.json +1 -1
package/bin/mostajs.sh CHANGED
@@ -27,7 +27,7 @@ CLI_NAME="mostajs"
27
27
  # PATHS — relative to the CALLER's CWD, not the script
28
28
  # ============================================================
29
29
 
30
- PROJECT_ROOT="$(pwd)"
30
+ PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
31
31
  CONFIG_DIR="$PROJECT_ROOT/.mostajs"
32
32
  CONFIG_FILE="$CONFIG_DIR/config.env"
33
33
  LOG_DIR="$CONFIG_DIR/logs"
@@ -214,7 +214,16 @@ header() {
214
214
  # ============================================================
215
215
 
216
216
  load_env() {
217
- [[ -f "$CONFIG_FILE" ]] && { set -a; source "$CONFIG_FILE"; set +a; }
217
+ # Source .mostajs/config.env WITHOUT overriding env vars that are already
218
+ # set (CLI invocation with DB_DIALECT=... mostajs ... takes precedence).
219
+ [[ -f "$CONFIG_FILE" ]] || return
220
+ while IFS='=' read -r key value; do
221
+ [[ -z "$key" || "$key" =~ ^# ]] && continue
222
+ # Trim whitespace from key and strip surrounding quotes from value
223
+ key="${key// /}"
224
+ value="${value%\"}"; value="${value#\"}"
225
+ [[ -z "${!key+x}" ]] && export "$key=$value"
226
+ done < "$CONFIG_FILE"
218
227
  }
219
228
 
220
229
  save_var() {
@@ -458,6 +467,7 @@ menu_main() {
458
467
  echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
459
468
  echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
460
469
  echo -e " ${CYAN}e${RESET}) ${BOLD}Export entities${RESET} → Prisma / JSON Schema / OpenAPI / Native"
470
+ echo -e " ${CYAN}r${RESET}) ${BOLD}Replicator${RESET} — CQRS master/slave, CDC rules, failover"
461
471
  echo -e " ${GREEN}b${RESET}) ${BOLD}Bootstrap${RESET} — one-shot migration of a Prisma project"
462
472
  echo -e " ${GREEN}i${RESET}) ${BOLD}Install bridge${RESET} — codemod PrismaClient → bridge (dry-run / apply / restore)"
463
473
  echo -e " ${CYAN}0${RESET}) About / Help"
@@ -478,6 +488,7 @@ menu_main() {
478
488
  9) action_generate_boilerplate ;;
479
489
  s|S) menu_seeding ;;
480
490
  e|E) action_export_entities ;;
491
+ r|R) menu_replicator ;;
481
492
  b|B) menu_bootstrap ;;
482
493
  i|I) menu_install_bridge ;;
483
494
  0) action_about ;;
@@ -1702,6 +1713,282 @@ menu_seeding() {
1702
1713
  menu_seeding
1703
1714
  }
1704
1715
 
1716
+ # ------------------------------------------------------------
1717
+ # Replicator menu — CQRS master/slave + cross-dialect CDC
1718
+ # ------------------------------------------------------------
1719
+ #
1720
+ # Thin wrapper around @mostajs/replicator. Every action is routed to
1721
+ # ReplicationManager methods via a Node --input-type=module shell.
1722
+ # State is persisted to $PROJECT_ROOT/.mostajs/replicator-tree.json
1723
+ # (same file format as saveToFile / loadFromFile of the lib).
1724
+
1725
+ _replicator_tree_file() {
1726
+ echo "$PROJECT_ROOT/.mostajs/replicator-tree.json"
1727
+ }
1728
+
1729
+ _replicator_has_lib() {
1730
+ [[ -d "$PROJECT_ROOT/node_modules/@mostajs/replicator" ]]
1731
+ }
1732
+
1733
+ _replicator_run() {
1734
+ # Execute a Node snippet with the replicator loaded. The snippet reads
1735
+ # from stdin, gets `rm` (ReplicationManager), `pm` (ProjectManager) in
1736
+ # scope and is expected to mutate them then call `await save()`.
1737
+ # $1 = inline snippet string.
1738
+ local tree_file
1739
+ tree_file=$(_replicator_tree_file)
1740
+ mkdir -p "$(dirname "$tree_file")"
1741
+ TREE_FILE="$tree_file" RUNTIME_ROOT="$PROJECT_ROOT" \
1742
+ node --input-type=module -e "
1743
+ const { existsSync } = await import('fs');
1744
+ const { ReplicationManager } = await import(process.env.RUNTIME_ROOT + '/node_modules/@mostajs/replicator/dist/index.js');
1745
+ const { ProjectManager } = await import(process.env.RUNTIME_ROOT + '/node_modules/@mostajs/mproject/dist/index.js');
1746
+ const pm = new ProjectManager();
1747
+ const rm = new ReplicationManager(pm);
1748
+ const save = async () => { await rm.saveToFile(process.env.TREE_FILE); };
1749
+ if (existsSync(process.env.TREE_FILE)) {
1750
+ try { await rm.loadFromFile(process.env.TREE_FILE); } catch (e) { console.error(' ⚠ load failed : ' + e.message); }
1751
+ }
1752
+ try {
1753
+ $1
1754
+ } catch (e) {
1755
+ console.error(' ✗ ' + e.message);
1756
+ process.exit(1);
1757
+ } finally {
1758
+ try { await rm.disconnectAll(); } catch {}
1759
+ }
1760
+ "
1761
+ }
1762
+
1763
+ menu_replicator() {
1764
+ header
1765
+ echo -e "${BOLD}${MAGENTA}▶ Replicator — CQRS, CDC, failover${RESET}"
1766
+ echo
1767
+ if ! _replicator_has_lib; then
1768
+ warn "@mostajs/replicator not installed in this project."
1769
+ echo
1770
+ if confirm "Install @mostajs/replicator + @mostajs/mproject now?"; then
1771
+ ensure_pkg "@mostajs/replicator" "@mostajs/mproject" || { pause; return; }
1772
+ else
1773
+ dim " Cannot proceed without the replicator lib."
1774
+ pause; return
1775
+ fi
1776
+ fi
1777
+
1778
+ local tree_file
1779
+ tree_file=$(_replicator_tree_file)
1780
+ echo -e " Tree file : ${DIM}${tree_file}${RESET}"
1781
+ if [[ -f "$tree_file" ]]; then
1782
+ local projects
1783
+ projects=$(node -e "try{const t=JSON.parse(require('fs').readFileSync('$tree_file','utf8'));console.log(Object.keys(t.replicas||{}).length+' projects, '+Object.keys(t.rules||{}).length+' CDC rules')}catch{console.log('?')}" 2>/dev/null)
1784
+ echo -e " State : ${DIM}${projects}${RESET}"
1785
+ else
1786
+ echo -e " State : ${DIM}(empty — will be created on first save)${RESET}"
1787
+ fi
1788
+
1789
+ echo
1790
+ echo -e "${BOLD}━━━ REPLICATOR MENU ━━━${RESET}"
1791
+ echo
1792
+ echo -e " ${CYAN}1${RESET}) Add replica (master / slave) to a project"
1793
+ echo -e " ${CYAN}2${RESET}) List replicas + status / lag"
1794
+ echo -e " ${CYAN}3${RESET}) Promote a slave to master (failover)"
1795
+ echo -e " ${CYAN}4${RESET}) Remove a replica"
1796
+ echo -e " ${CYAN}5${RESET}) Set read-routing strategy (round-robin / least-lag / random)"
1797
+ echo -e " ${CYAN}6${RESET}) Add a CDC rule (pg → mongo, mysql → analytics, …)"
1798
+ echo -e " ${CYAN}7${RESET}) List CDC rules"
1799
+ echo -e " ${CYAN}8${RESET}) Run a CDC sync + show stats"
1800
+ echo -e " ${CYAN}9${RESET}) Remove a CDC rule"
1801
+ echo -e " ${CYAN}v${RESET}) View the raw tree file"
1802
+ echo -e " ${CYAN}c${RESET}) Clear (delete the tree file — DESTRUCTIVE)"
1803
+ echo
1804
+ echo -e " ${CYAN}b${RESET}) Back"
1805
+ echo
1806
+ local choice
1807
+ choice=$(ask "Choice" "2")
1808
+ case "$choice" in
1809
+ 1) action_rep_add_replica ;;
1810
+ 2) action_rep_list_replicas ;;
1811
+ 3) action_rep_promote ;;
1812
+ 4) action_rep_remove_replica ;;
1813
+ 5) action_rep_set_routing ;;
1814
+ 6) action_rep_add_rule ;;
1815
+ 7) action_rep_list_rules ;;
1816
+ 8) action_rep_sync ;;
1817
+ 9) action_rep_remove_rule ;;
1818
+ v|V) action_rep_view_tree ;;
1819
+ c|C) action_rep_clear ;;
1820
+ b|B) return ;;
1821
+ *) warn "Unknown"; pause ;;
1822
+ esac
1823
+ menu_replicator
1824
+ }
1825
+
1826
+ action_rep_add_replica() {
1827
+ local project name role dialect uri lag
1828
+ project=$(ask "Project name" "default")
1829
+ name=$(ask "Replica name" "master")
1830
+ role=$(ask "Role (master|slave)" "master")
1831
+ dialect=$(ask "Dialect" "${DB_DIALECT:-postgres}")
1832
+ uri=$(ask "URI" "${SGBD_URI:-postgres://user:pass@localhost:5432/db}")
1833
+ if [[ "$role" == "slave" ]]; then
1834
+ lag=$(ask "Lag tolerance (ms)" "5000")
1835
+ else
1836
+ lag="0"
1837
+ fi
1838
+ _replicator_run "
1839
+ await rm.addReplica('$project', {
1840
+ name: '$name', role: '$role', dialect: '$dialect', uri: '$uri',
1841
+ lagTolerance: $lag,
1842
+ });
1843
+ await save();
1844
+ console.log(' ✓ replica added : $name (' + '$role' + ') on project $project');
1845
+ "
1846
+ pause
1847
+ }
1848
+
1849
+ action_rep_list_replicas() {
1850
+ local project
1851
+ project=$(ask "Project name" "default")
1852
+ _replicator_run "
1853
+ const status = rm.getReplicaStatus('$project');
1854
+ if (!status || status.length === 0) {
1855
+ console.log(' (no replicas registered for project $project)');
1856
+ } else {
1857
+ for (const r of status) {
1858
+ console.log(' ' + (r.role === 'master' ? '★' : '•') + ' ' + r.name + ' [' + r.role + '] lag=' + (r.lag ?? 'n/a') + 'ms');
1859
+ }
1860
+ }
1861
+ "
1862
+ pause
1863
+ }
1864
+
1865
+ action_rep_promote() {
1866
+ local project name
1867
+ project=$(ask "Project name" "default")
1868
+ name=$(ask "Slave to promote" "slave-1")
1869
+ if ! confirm "Promote '$name' to master on project '$project'?"; then return; fi
1870
+ _replicator_run "
1871
+ await rm.promoteToMaster('$project', '$name');
1872
+ await save();
1873
+ console.log(' ✓ $name is now the master of $project');
1874
+ "
1875
+ pause
1876
+ }
1877
+
1878
+ action_rep_remove_replica() {
1879
+ local project name
1880
+ project=$(ask "Project name" "default")
1881
+ name=$(ask "Replica name" "slave-1")
1882
+ if ! confirm "Remove replica '$name' from project '$project'?"; then return; fi
1883
+ _replicator_run "
1884
+ await rm.removeReplica('$project', '$name');
1885
+ await save();
1886
+ console.log(' ✓ removed : $name');
1887
+ "
1888
+ pause
1889
+ }
1890
+
1891
+ action_rep_set_routing() {
1892
+ local project strategy
1893
+ project=$(ask "Project name" "default")
1894
+ strategy=$(ask "Strategy (round-robin | least-lag | random)" "least-lag")
1895
+ _replicator_run "
1896
+ rm.setReadRouting('$project', '$strategy');
1897
+ await save();
1898
+ console.log(' ✓ read routing on $project = $strategy');
1899
+ "
1900
+ pause
1901
+ }
1902
+
1903
+ action_rep_add_rule() {
1904
+ local name source target mode colls conflict
1905
+ name=$(ask "Rule name" "pg-to-mongo")
1906
+ source=$(ask "Source project" "secuaccess")
1907
+ target=$(ask "Target project" "analytics")
1908
+ mode=$(ask "Mode (snapshot | cdc | bidirectional)" "cdc")
1909
+ colls=$(ask "Collections (comma-separated)" "users,clients")
1910
+ conflict=$(ask "Conflict resolution (source-wins | target-wins | timestamp)" "source-wins")
1911
+ _replicator_run "
1912
+ rm.addReplicationRule({
1913
+ name: '$name', source: '$source', target: '$target', mode: '$mode',
1914
+ collections: '$colls'.split(',').map(s => s.trim()).filter(Boolean),
1915
+ conflictResolution: '$conflict',
1916
+ enabled: true,
1917
+ });
1918
+ await save();
1919
+ console.log(' ✓ rule added : $name ($source → $target, mode=$mode)');
1920
+ "
1921
+ pause
1922
+ }
1923
+
1924
+ action_rep_list_rules() {
1925
+ _replicator_run "
1926
+ const rules = rm.listRules();
1927
+ if (!rules || rules.length === 0) {
1928
+ console.log(' (no CDC rules registered)');
1929
+ } else {
1930
+ for (const r of rules) {
1931
+ const flag = r.enabled ? '✓' : '✗';
1932
+ console.log(' ' + flag + ' ' + r.name + ' ' + r.source + ' → ' + r.target + ' [' + r.mode + '] ' + r.collections.join(','));
1933
+ }
1934
+ }
1935
+ "
1936
+ pause
1937
+ }
1938
+
1939
+ action_rep_sync() {
1940
+ local rule
1941
+ rule=$(ask "Rule name" "pg-to-mongo")
1942
+ _replicator_run "
1943
+ const stats = await rm.sync('$rule');
1944
+ console.log(' ✓ sync complete');
1945
+ console.log(' inserted: ' + (stats.inserted ?? 0));
1946
+ console.log(' updated : ' + (stats.updated ?? 0));
1947
+ console.log(' deleted : ' + (stats.deleted ?? 0));
1948
+ console.log(' failed : ' + (stats.failed ?? 0));
1949
+ await save();
1950
+ "
1951
+ pause
1952
+ }
1953
+
1954
+ action_rep_remove_rule() {
1955
+ local name
1956
+ name=$(ask "Rule name" "pg-to-mongo")
1957
+ if ! confirm "Remove CDC rule '$name'?"; then return; fi
1958
+ _replicator_run "
1959
+ rm.removeReplicationRule('$name');
1960
+ await save();
1961
+ console.log(' ✓ removed : $name');
1962
+ "
1963
+ pause
1964
+ }
1965
+
1966
+ action_rep_view_tree() {
1967
+ local tree_file
1968
+ tree_file=$(_replicator_tree_file)
1969
+ if [[ -f "$tree_file" ]]; then
1970
+ echo -e "${DIM}${tree_file}${RESET}"
1971
+ echo
1972
+ if command -v jq >/dev/null 2>&1; then
1973
+ jq . "$tree_file"
1974
+ else
1975
+ cat "$tree_file"
1976
+ fi
1977
+ else
1978
+ warn "No tree file yet at $tree_file"
1979
+ fi
1980
+ pause
1981
+ }
1982
+
1983
+ action_rep_clear() {
1984
+ local tree_file
1985
+ tree_file=$(_replicator_tree_file)
1986
+ if ! confirm "DELETE the replicator tree file ($tree_file)?"; then return; fi
1987
+ rm -f "$tree_file"
1988
+ ok " tree file deleted"
1989
+ pause
1990
+ }
1991
+
1705
1992
  # ------------------------------------------------------------
1706
1993
  # Export entities → Prisma / JSON Schema / OpenAPI / Native TS
1707
1994
  # ------------------------------------------------------------
@@ -2636,6 +2923,364 @@ EOF
2636
2923
  pause
2637
2924
  }
2638
2925
 
2926
+ # ============================================================
2927
+ # `mostajs init` — scaffold a new project
2928
+ # ============================================================
2929
+ #
2930
+ # Creates every file a fresh project needs to run on the bridge :
2931
+ # - .env with PORT / DB_DIALECT / SGBD_URI / AUTH_SECRET
2932
+ # - prisma/schema.prisma (minimal User model — starting point)
2933
+ # - src/lib/db.ts (createPrismaLikeDb)
2934
+ # - .mostajs/config.env (mirrors .env for the seed-runner)
2935
+ # - .mostajs/generated/entities.json (empty array, filled by menu 1)
2936
+ #
2937
+ # Dialect defaults to sqlite ./data.sqlite. Pass --dialect=postgres etc.
2938
+ # Refuses to overwrite existing files unless --force.
2939
+
2940
+ action_cli_init() {
2941
+ local dialect="sqlite"
2942
+ local uri=""
2943
+ local force=0
2944
+ while [[ $# -gt 0 ]]; do
2945
+ case "$1" in
2946
+ --dialect) dialect="$2"; shift 2 ;;
2947
+ --dialect=*) dialect="${1#*=}"; shift ;;
2948
+ --uri) uri="$2"; shift 2 ;;
2949
+ --uri=*) uri="${1#*=}"; shift ;;
2950
+ --force|-f) force=1; shift ;;
2951
+ *) warn "Unknown flag: $1"; shift ;;
2952
+ esac
2953
+ done
2954
+
2955
+ # Default URIs per dialect
2956
+ if [[ -z "$uri" ]]; then
2957
+ case "$dialect" in
2958
+ sqlite) uri="./data.sqlite" ;;
2959
+ postgres) uri="postgres://user:pass@localhost:5432/mydb" ;;
2960
+ mysql) uri="mysql://user:pass@localhost:3306/mydb" ;;
2961
+ mariadb) uri="mariadb://user:pass@localhost:3306/mydb" ;;
2962
+ mongodb) uri="mongodb://user:pass@localhost:27017/mydb" ;;
2963
+ oracle) uri="oracle://user:pass@localhost:1521/XE" ;;
2964
+ mssql) uri="mssql://user:pass@localhost:1433/mydb" ;;
2965
+ cockroachdb) uri="postgresql://user:pass@localhost:26257/mydb?sslmode=disable" ;;
2966
+ *) uri="./data.sqlite"; dialect="sqlite" ;;
2967
+ esac
2968
+ fi
2969
+
2970
+ header
2971
+ echo -e "${BOLD}${MAGENTA}▶ mostajs init — scaffold a bridge-ready project${RESET}"
2972
+ echo
2973
+ echo -e " Dialect : ${CYAN}${dialect}${RESET}"
2974
+ echo -e " URI : ${DIM}${uri}${RESET}"
2975
+ echo -e " Root : ${DIM}${PROJECT_ROOT}${RESET}"
2976
+ echo
2977
+
2978
+ local created=0 skipped=0
2979
+
2980
+ write_if_missing() {
2981
+ local path="$1"; local content="$2"
2982
+ if [[ -f "$PROJECT_ROOT/$path" && $force -eq 0 ]]; then
2983
+ dim " - skip $path (exists — use --force to overwrite)"
2984
+ ((skipped++))
2985
+ return
2986
+ fi
2987
+ mkdir -p "$(dirname "$PROJECT_ROOT/$path")"
2988
+ printf '%s' "$content" > "$PROJECT_ROOT/$path"
2989
+ ok "created $path"
2990
+ ((created++))
2991
+ }
2992
+
2993
+ # --- .env ---
2994
+ local secret
2995
+ secret=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" 2>/dev/null || echo 'CHANGE-ME-IN-PROD')
2996
+ write_if_missing ".env" "\
2997
+ # Port — used by next dev / start (reads PORT from here)
2998
+ PORT=3000
2999
+
3000
+ # Database — consumed by @mostajs/orm-bridge (createPrismaLikeDb)
3001
+ DB_DIALECT=${dialect}
3002
+ SGBD_URI=${uri}
3003
+ DB_SCHEMA_STRATEGY=update
3004
+
3005
+ # NextAuth (if you use it)
3006
+ NEXTAUTH_URL=http://localhost:3000
3007
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
3008
+ AUTH_SECRET=${secret}
3009
+ "
3010
+
3011
+ # --- .mostajs/config.env (mirror for the seed-runner) ---
3012
+ write_if_missing ".mostajs/config.env" "\
3013
+ DB_DIALECT=${dialect}
3014
+ SGBD_URI=${uri}
3015
+ DB_SCHEMA_STRATEGY=update
3016
+ APP_PORT=3000
3017
+ "
3018
+
3019
+ # --- .mostajs/generated/entities.json (empty — filled by menu 1) ---
3020
+ write_if_missing ".mostajs/generated/entities.json" "[]
3021
+ "
3022
+
3023
+ # --- prisma/schema.prisma (minimal starter) ---
3024
+ # Prisma's valid providers : sqlite, postgresql, mysql, mongodb, sqlserver, cockroachdb
3025
+ local provider="$dialect"
3026
+ case "$dialect" in
3027
+ postgres|postgresql) provider="postgresql" ;;
3028
+ mssql) provider="sqlserver" ;;
3029
+ mariadb) provider="mysql" ;;
3030
+ oracle|db2|hana|hsqldb|spanner|sybase) provider="sqlite" ;; # Prisma has no native provider — keep sqlite placeholder
3031
+ esac
3032
+ write_if_missing "prisma/schema.prisma" "\
3033
+ // Minimal starter — edit freely. Run 'mostajs' menu 1 to convert to EntitySchema.
3034
+ generator client {
3035
+ provider = \"prisma-client-js\"
3036
+ }
3037
+
3038
+ datasource db {
3039
+ provider = \"${provider}\"
3040
+ url = env(\"DATABASE_URL\")
3041
+ }
3042
+
3043
+ model User {
3044
+ id String @id @default(uuid())
3045
+ email String @unique
3046
+ password String
3047
+ name String?
3048
+ createdAt DateTime @default(now())
3049
+ updatedAt DateTime @updatedAt
3050
+ }
3051
+ "
3052
+
3053
+ # --- src/lib/db.ts (createPrismaLikeDb) ---
3054
+ write_if_missing "src/lib/db.ts" "\
3055
+ // Generated by 'mostajs init' — @mostajs/orm-bridge entry point.
3056
+ // Every Prisma-style db.User.findUnique(...) call below is routed to
3057
+ // @mostajs/orm (13 dialects). Edit DB_DIALECT / SGBD_URI in .env to switch.
3058
+ import { createPrismaLikeDb } from '@mostajs/orm-bridge/prisma-client'
3059
+
3060
+ export const db = createPrismaLikeDb()
3061
+ "
3062
+
3063
+ echo
3064
+ echo -e " ${BOLD}${created}${RESET} file(s) created, ${DIM}${skipped}${RESET} skipped"
3065
+ echo
3066
+ echo -e " ${BOLD}Next steps${RESET} :"
3067
+ echo -e " ${CYAN}1.${RESET} npm install @mostajs/orm @mostajs/orm-bridge @mostajs/orm-cli --legacy-peer-deps"
3068
+ echo -e " ${CYAN}2.${RESET} Edit ${DIM}prisma/schema.prisma${RESET} — add your models"
3069
+ echo -e " ${CYAN}3.${RESET} ${CYAN}mostajs${RESET} → menu 1 (Convert) → menu 3 (init DDL)"
3070
+ echo -e " ${CYAN}4.${RESET} ${CYAN}mostajs${RESET} → menu S (Seeds) — populate, hash, apply"
3071
+ echo -e " ${CYAN}5.${RESET} ${CYAN}npm run dev${RESET}"
3072
+ }
3073
+
3074
+ # ============================================================
3075
+ # `mostajs migrate` — incremental DDL diff / apply / status
3076
+ # ============================================================
3077
+ #
3078
+ # Subcommands :
3079
+ # diff — list ALTERs needed to make the live DB match entities.json
3080
+ # apply — execute those ALTERs (prompts for confirmation, --yes to skip)
3081
+ # status — show entities.json count + live tables count + missing columns
3082
+
3083
+ action_cli_migrate() {
3084
+ local sub="${1:-}"
3085
+ [[ -z "$sub" ]] && { action_migrate_help; return; }
3086
+ shift
3087
+ case "$sub" in
3088
+ diff|d) action_migrate_diff "$@" ;;
3089
+ apply|a) action_migrate_apply "$@" ;;
3090
+ status|s) action_migrate_status "$@" ;;
3091
+ help|h|--help) action_migrate_help ;;
3092
+ *) err "Unknown migrate subcommand: $sub"; action_migrate_help; return 1 ;;
3093
+ esac
3094
+ }
3095
+
3096
+ action_migrate_help() {
3097
+ cat <<EOF
3098
+
3099
+ ${BOLD}mostajs migrate${RESET} — incremental schema migration
3100
+
3101
+ ${CYAN}diff${RESET} show ALTER statements the DB needs to match entities.json
3102
+ ${CYAN}apply${RESET} execute those ALTERs (prompts for confirmation)
3103
+ flags : --yes (skip confirmation)
3104
+ ${CYAN}status${RESET} show live-vs-schema summary per entity
3105
+
3106
+ Every subcommand honors DB_DIALECT + SGBD_URI from ${DIM}.mostajs/config.env${RESET}.
3107
+
3108
+ EOF
3109
+ }
3110
+
3111
+ # Node helper : compare live columns vs schema.fields and emit ALTER plan as JSON.
3112
+ # Outputs to stdout : { changes: [{ table, column, sql }], ok: bool }
3113
+ _migrate_compute_plan() {
3114
+ load_env
3115
+ local entities_json="$GENERATED_DIR/entities.json"
3116
+ if [[ ! -f "$entities_json" ]]; then
3117
+ err "No entities.json — run menu 1 (Convert) first."
3118
+ return 1
3119
+ fi
3120
+ ENT_PATH="$entities_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
3121
+ node --input-type=module -e "
3122
+ import { readFileSync } from 'node:fs';
3123
+ import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
3124
+ const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
3125
+ const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
3126
+
3127
+ // Use the dialect's own introspection — protected method, exposed via cast
3128
+ const changes = [];
3129
+ for (const e of entities) {
3130
+ let live;
3131
+ try {
3132
+ live = await (d).getExistingColumns(e.collection);
3133
+ } catch {
3134
+ changes.push({ table: e.collection, column: '*', sql: '-- (cannot introspect — run menu 3 first)' });
3135
+ continue;
3136
+ }
3137
+ const hasCol = (name) => {
3138
+ const lc = name.toLowerCase();
3139
+ for (const c of live) if (c.toLowerCase() === lc) return true;
3140
+ return false;
3141
+ };
3142
+ // Field columns
3143
+ for (const [name, f] of Object.entries(e.fields || {})) {
3144
+ if (name === '_id') continue;
3145
+ if (hasCol(name)) continue;
3146
+ // Reconstruct the ALTER — d has fieldToSqlType + getIdColumnType + quoteIdentifier
3147
+ const q = (n) => (d).quoteIdentifier(n);
3148
+ let sql;
3149
+ if (name === 'id') {
3150
+ sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q('id') + ' ' + (d).getIdColumnType();
3151
+ } else {
3152
+ sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(name) + ' ' + (d).fieldToSqlType(f);
3153
+ }
3154
+ changes.push({ table: e.collection, column: name, sql });
3155
+ }
3156
+ // Relation FK columns
3157
+ for (const [rname, rel] of Object.entries(e.relations || {})) {
3158
+ if (rel.type !== 'many-to-one' && rel.type !== 'one-to-one') continue;
3159
+ const colName = rel.joinColumn || (rname + 'Id');
3160
+ if (hasCol(colName)) continue;
3161
+ const q = (n) => (d).quoteIdentifier(n);
3162
+ changes.push({
3163
+ table: e.collection, column: colName,
3164
+ sql: 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(colName) + ' ' + (d).getIdColumnType(),
3165
+ });
3166
+ }
3167
+ }
3168
+ await d.disconnect();
3169
+ console.log(JSON.stringify({ ok: true, changes }));
3170
+ "
3171
+ }
3172
+
3173
+ action_migrate_diff() {
3174
+ header
3175
+ echo -e "${BOLD}${MAGENTA}▶ mostajs migrate diff${RESET}"
3176
+ echo
3177
+ local plan_json
3178
+ plan_json=$(_migrate_compute_plan) || { pause; return 1; }
3179
+ local count
3180
+ count=$(echo "$plan_json" | node -e "process.stdin.on('data',d=>{console.log(JSON.parse(d).changes.length)})" 2>/dev/null || echo '?')
3181
+ if [[ "$count" == "0" ]]; then
3182
+ ok "Schema is up to date — nothing to ALTER."
3183
+ return 0
3184
+ fi
3185
+ echo -e " ${BOLD}${count}${RESET} pending change(s) :"
3186
+ echo
3187
+ echo "$plan_json" | node -e "
3188
+ let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
3189
+ const p = JSON.parse(d);
3190
+ for (const ch of p.changes) console.log(' ' + ch.sql + ';');
3191
+ });
3192
+ "
3193
+ echo
3194
+ echo -e " Run ${CYAN}mostajs migrate apply${RESET} to execute these statements."
3195
+ }
3196
+
3197
+ action_migrate_apply() {
3198
+ local auto_yes=0
3199
+ [[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && auto_yes=1
3200
+ header
3201
+ echo -e "${BOLD}${MAGENTA}▶ mostajs migrate apply${RESET}"
3202
+ echo
3203
+ local plan_json
3204
+ plan_json=$(_migrate_compute_plan) || return 1
3205
+ local count
3206
+ count=$(echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).changes.length))" 2>/dev/null || echo 0)
3207
+ if [[ "$count" == "0" ]]; then
3208
+ ok "Schema is up to date — nothing to ALTER."
3209
+ return 0
3210
+ fi
3211
+ echo " Pending : ${BOLD}${count}${RESET} statement(s)"
3212
+ echo
3213
+ echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const ch of JSON.parse(d).changes) console.log(' ' + ch.sql + ';')})"
3214
+ echo
3215
+ if [[ $auto_yes -eq 0 ]]; then
3216
+ if ! confirm "Execute these ALTER statements?"; then
3217
+ dim " Aborted."
3218
+ return
3219
+ fi
3220
+ fi
3221
+
3222
+ # Execute
3223
+ load_env
3224
+ PLAN="$plan_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
3225
+ node --input-type=module -e "
3226
+ import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
3227
+ const plan = JSON.parse(process.env.PLAN);
3228
+ const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
3229
+ let ok = 0, fail = 0;
3230
+ for (const ch of plan.changes) {
3231
+ try {
3232
+ await d.executeRun(ch.sql, []);
3233
+ console.log(' ✓ ' + ch.table + '.' + ch.column);
3234
+ ok++;
3235
+ } catch (e) {
3236
+ console.error(' ✗ ' + ch.table + '.' + ch.column + ' : ' + e.message);
3237
+ fail++;
3238
+ }
3239
+ }
3240
+ await d.disconnect();
3241
+ console.log('\nApplied : ' + ok + ' ok, ' + fail + ' failed');
3242
+ process.exit(fail > 0 ? 1 : 0);
3243
+ "
3244
+ }
3245
+
3246
+ action_migrate_status() {
3247
+ header
3248
+ echo -e "${BOLD}${MAGENTA}▶ mostajs migrate status${RESET}"
3249
+ echo
3250
+ load_env
3251
+ local ent_json="$GENERATED_DIR/entities.json"
3252
+ if [[ ! -f "$ent_json" ]]; then
3253
+ err "No entities.json — run menu 1 (Convert) first."
3254
+ return 1
3255
+ fi
3256
+ ENT_PATH="$ent_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
3257
+ node --input-type=module -e "
3258
+ import { readFileSync } from 'node:fs';
3259
+ import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
3260
+ const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
3261
+ const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
3262
+ let existing = 0, missing = 0, lagging = 0;
3263
+ for (const e of entities) {
3264
+ let live;
3265
+ try { live = await (d).getExistingColumns(e.collection); }
3266
+ catch { live = new Set(); }
3267
+ if (!live || live.size === 0) { console.log(' ✗ ' + e.collection + ' — table not found'); missing++; continue; }
3268
+ const hasCol = (n) => { const lc = n.toLowerCase(); for (const c of live) if (c.toLowerCase() === lc) return true; return false; };
3269
+ const schemaCols = Object.keys(e.fields || {});
3270
+ const need = schemaCols.filter(c => !hasCol(c));
3271
+ if (need.length) {
3272
+ console.log(' ⚠ ' + e.collection + ' — missing ' + need.length + ' column(s) : ' + need.join(', '));
3273
+ lagging++;
3274
+ } else {
3275
+ console.log(' ✓ ' + e.collection + ' (' + live.size + ' cols live, ' + schemaCols.length + ' in schema)');
3276
+ existing++;
3277
+ }
3278
+ }
3279
+ await d.disconnect();
3280
+ console.log('\n ' + existing + ' up-to-date · ' + lagging + ' need migrate · ' + missing + ' missing');
3281
+ "
3282
+ }
3283
+
2639
3284
  # ============================================================
2640
3285
  # CLI SUBCOMMANDS (non-interactive)
2641
3286
  # ============================================================
@@ -2721,6 +3366,26 @@ run_subcommand() {
2721
3366
  health|h)
2722
3367
  action_healthcheck
2723
3368
  ;;
3369
+ init)
3370
+ # mostajs init [--dialect sqlite|postgres|mongodb|...] [--force]
3371
+ # Scaffold a fresh project with bridge-ready layout :
3372
+ # .env (PORT, DB_DIALECT, SGBD_URI, AUTH_SECRET)
3373
+ # prisma/schema.prisma (minimal — User model only)
3374
+ # src/lib/db.ts (createPrismaLikeDb)
3375
+ # .mostajs/config.env (mirrors .env for the runner)
3376
+ # .mostajs/generated/entities.json (empty array)
3377
+ shift
3378
+ action_cli_init "$@"
3379
+ ;;
3380
+ migrate|mig|m)
3381
+ # mostajs migrate <subcommand> [options]
3382
+ # Subcommands :
3383
+ # diff — show ALTER statements the target DB needs to match entities.json
3384
+ # apply — execute those ALTERs (with confirmation)
3385
+ # status — show what's in entities.json vs what's live in the DB
3386
+ shift
3387
+ action_cli_migrate "$@"
3388
+ ;;
2724
3389
  install-bridge|ib)
2725
3390
  # mostajs install-bridge [--apply] [--file X] [--project P] [--restore]
2726
3391
  # Codemod : scans the project for `new PrismaClient(...)` sites and rewrites
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.4.7",
3
+ "version": "0.5.1",
4
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",