@mostajs/orm-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mostajs.sh ADDED
@@ -0,0 +1,994 @@
1
+ #!/usr/bin/env bash
2
+ # mostajs.sh — Universal interactive CLI for @mostajs/orm integration
3
+ # Works in any project directory : auto-detects Prisma, OpenAPI, JSON Schema
4
+ # Author: Dr Hamid MADANI drmdh@msn.com
5
+ # License: AGPL-3.0-or-later
6
+ #
7
+ # Install globally :
8
+ # curl -fsSL https://raw.githubusercontent.com/apolocine/mosta-orm-cli/main/bin/mostajs.sh -o /usr/local/bin/mostajs
9
+ # chmod +x /usr/local/bin/mostajs
10
+ #
11
+ # Or run directly :
12
+ # bash <(curl -fsSL https://raw.githubusercontent.com/apolocine/mosta-orm-cli/main/bin/mostajs.sh)
13
+ #
14
+ # Or via npx :
15
+ # npx @mostajs/orm-cli
16
+
17
+ set -uo pipefail
18
+
19
+ # ============================================================
20
+ # META
21
+ # ============================================================
22
+
23
+ VERSION="0.1.0"
24
+ CLI_NAME="mostajs"
25
+
26
+ # ============================================================
27
+ # PATHS — relative to the CALLER's CWD, not the script
28
+ # ============================================================
29
+
30
+ PROJECT_ROOT="$(pwd)"
31
+ CONFIG_DIR="$PROJECT_ROOT/.mostajs"
32
+ CONFIG_FILE="$CONFIG_DIR/config.env"
33
+ LOG_DIR="$CONFIG_DIR/logs"
34
+ GENERATED_DIR="$CONFIG_DIR/generated"
35
+
36
+ mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$GENERATED_DIR" 2>/dev/null
37
+
38
+ # ============================================================
39
+ # COLORS
40
+ # ============================================================
41
+
42
+ if [[ -t 1 ]]; then
43
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
44
+ BLUE='\033[0;34m'; MAGENTA='\033[0;35m'; CYAN='\033[0;36m'
45
+ BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
46
+ else
47
+ RED=''; GREEN=''; YELLOW=''; BLUE=''; MAGENTA=''; CYAN=''; BOLD=''; DIM=''; RESET=''
48
+ fi
49
+
50
+ ok() { echo -e " ${GREEN}✓${RESET} $*"; }
51
+ info() { echo -e " ${CYAN}ℹ${RESET} $*"; }
52
+ warn() { echo -e " ${YELLOW}⚠${RESET} $*"; }
53
+ err() { echo -e " ${RED}✗${RESET} $*"; }
54
+ dim() { echo -e " ${DIM}$*${RESET}"; }
55
+
56
+ pause() {
57
+ echo
58
+ read -n 1 -r -s -p "$(echo -e "${DIM}Press any key...${RESET}")" || true
59
+ echo
60
+ }
61
+
62
+ ask() {
63
+ local prompt="$1" default="${2:-}" var
64
+ if [[ -n "$default" ]]; then
65
+ read -r -p "$(echo -e "${YELLOW}?${RESET} $prompt ${DIM}[$default]${RESET}: ")" var
66
+ echo "${var:-$default}"
67
+ else
68
+ read -r -p "$(echo -e "${YELLOW}?${RESET} $prompt: ")" var
69
+ echo "$var"
70
+ fi
71
+ }
72
+
73
+ confirm() {
74
+ local response
75
+ read -r -p "$(echo -e "${YELLOW}?${RESET} $1 ${DIM}[y/N]${RESET}: ")" response
76
+ [[ "$response" =~ ^[Yy]$ ]]
77
+ }
78
+
79
+ header() {
80
+ clear
81
+ echo -e "${BOLD}${CYAN}"
82
+ echo "╔══════════════════════════════════════════════════════════════════╗"
83
+ echo "║ @mostajs/orm-cli v$VERSION — Universal Schema Adapter Tool ║"
84
+ echo "║ 13 databases · 4 input formats · one command ║"
85
+ echo "╚══════════════════════════════════════════════════════════════════╝"
86
+ echo -e "${RESET}"
87
+ }
88
+
89
+ # ============================================================
90
+ # CONFIG MANAGEMENT
91
+ # ============================================================
92
+
93
+ load_env() {
94
+ [[ -f "$CONFIG_FILE" ]] && { set -a; source "$CONFIG_FILE"; set +a; }
95
+ }
96
+
97
+ save_var() {
98
+ local key="$1" value="$2"
99
+ touch "$CONFIG_FILE"
100
+ grep -v "^${key}=" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" 2>/dev/null || true
101
+ echo "${key}=${value}" >> "$CONFIG_FILE.tmp"
102
+ mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
103
+ chmod 600 "$CONFIG_FILE" 2>/dev/null || true
104
+ }
105
+
106
+ # ============================================================
107
+ # PROJECT DETECTION
108
+ # ============================================================
109
+
110
+ detect_project() {
111
+ DETECTED_TYPES=()
112
+ PRISMA_SCHEMA=""
113
+ OPENAPI_FILE=""
114
+ JSON_SCHEMAS=()
115
+ PKG_MANAGER=""
116
+
117
+ # Prisma
118
+ if [[ -f "$PROJECT_ROOT/prisma/schema.prisma" ]]; then
119
+ PRISMA_SCHEMA="$PROJECT_ROOT/prisma/schema.prisma"
120
+ DETECTED_TYPES+=("prisma")
121
+ fi
122
+
123
+ # OpenAPI (common names)
124
+ for candidate in openapi.yaml openapi.yml openapi.json api.yaml api.yml api.json spec/openapi.yaml docs/openapi.yaml; do
125
+ if [[ -f "$PROJECT_ROOT/$candidate" ]]; then
126
+ OPENAPI_FILE="$PROJECT_ROOT/$candidate"
127
+ DETECTED_TYPES+=("openapi")
128
+ break
129
+ fi
130
+ done
131
+
132
+ # JSON Schema files
133
+ while IFS= read -r -d '' f; do
134
+ JSON_SCHEMAS+=("$f")
135
+ done < <(find "$PROJECT_ROOT/schemas" -name "*.json" -print0 2>/dev/null | head -c 10000)
136
+
137
+ [[ ${#JSON_SCHEMAS[@]} -gt 0 ]] && DETECTED_TYPES+=("jsonschema")
138
+
139
+ # Package manager
140
+ if [[ -f "$PROJECT_ROOT/pnpm-lock.yaml" ]]; then PKG_MANAGER="pnpm"
141
+ elif [[ -f "$PROJECT_ROOT/yarn.lock" ]]; then PKG_MANAGER="yarn"
142
+ elif [[ -f "$PROJECT_ROOT/bun.lockb" ]]; then PKG_MANAGER="bun"
143
+ elif [[ -f "$PROJECT_ROOT/package.json" ]]; then PKG_MANAGER="npm"
144
+ else PKG_MANAGER="npm"
145
+ fi
146
+ }
147
+
148
+ # ============================================================
149
+ # npm / npx wrapper — finds the installed adapter or uses npx
150
+ # ============================================================
151
+
152
+ run_adapter_convert() {
153
+ local input_type="$1" # prisma | jsonschema | openapi
154
+ local input_file="$2"
155
+ local output_file="$3"
156
+
157
+ # Check if @mostajs/orm-adapter is installed locally
158
+ local adapter_path=""
159
+ if [[ -f "$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js" ]]; then
160
+ adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
161
+ info "Using local @mostajs/orm-adapter"
162
+ else
163
+ # Try a neighbor path (dev setup — mosta-orm-adapter may be next to this CLI)
164
+ local cli_dir
165
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
166
+ if [[ -f "$cli_dir/../mosta-orm-adapter/dist/index.js" ]]; then
167
+ adapter_path="$cli_dir/../mosta-orm-adapter/dist/index.js"
168
+ info "Using sibling @mostajs/orm-adapter from $adapter_path"
169
+ else
170
+ warn "@mostajs/orm-adapter not found locally."
171
+ if confirm "Install it now ($PKG_MANAGER install --save-dev @mostajs/orm-adapter @mostajs/orm)?"; then
172
+ cd "$PROJECT_ROOT"
173
+ case "$PKG_MANAGER" in
174
+ pnpm) pnpm add -D @mostajs/orm-adapter @mostajs/orm ;;
175
+ yarn) yarn add -D @mostajs/orm-adapter @mostajs/orm ;;
176
+ bun) bun add -D @mostajs/orm-adapter @mostajs/orm ;;
177
+ *) npm install --save-dev @mostajs/orm-adapter @mostajs/orm --legacy-peer-deps ;;
178
+ esac
179
+ adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
180
+ else
181
+ err "Cannot proceed without the adapter."
182
+ return 1
183
+ fi
184
+ fi
185
+ fi
186
+
187
+ local adapter_class
188
+ case "$input_type" in
189
+ prisma) adapter_class="PrismaAdapter" ;;
190
+ openapi) adapter_class="OpenApiAdapter" ;;
191
+ jsonschema) adapter_class="JsonSchemaAdapter" ;;
192
+ *) err "Unknown input type: $input_type"; return 1 ;;
193
+ esac
194
+
195
+ cat > "$CONFIG_DIR/convert.mjs" << EOF
196
+ import { readFileSync, writeFileSync } from 'fs';
197
+ import { $adapter_class } from '$adapter_path';
198
+
199
+ const source = readFileSync('$input_file', 'utf8');
200
+ const adapter = new $adapter_class();
201
+ const warnings = [];
202
+ const input = '$input_type' === 'jsonschema' ? JSON.parse(source) : source;
203
+ const entities = await adapter.toEntitySchema(input, { onWarning: w => warnings.push(w) });
204
+
205
+ console.log('entities : ' + entities.length);
206
+ console.log('warnings : ' + warnings.length);
207
+ for (const w of warnings) console.log(' [' + w.code + '] ' + (w.entity ?? '-') + ' : ' + w.message);
208
+
209
+ const header = '// Auto-generated by @mostajs/orm-cli v$VERSION at ' + new Date().toISOString() + '\n';
210
+ const code = header +
211
+ '// Source : $input_file\n' +
212
+ '// Adapter : $adapter_class\n' +
213
+ '// DO NOT EDIT BY HAND — regenerate with: mostajs convert\n\n' +
214
+ 'import type { EntitySchema } from \"@mostajs/orm\";\n\n' +
215
+ 'export const entities: EntitySchema[] = ' + JSON.stringify(entities, null, 2) + ';\n\n' +
216
+ 'export const entityByName: Record<string, EntitySchema> = Object.fromEntries(\n' +
217
+ ' entities.map(e => [e.name, e])\n' +
218
+ ');\n';
219
+
220
+ writeFileSync('$output_file', code);
221
+ console.log('\u2713 Saved : $output_file');
222
+ EOF
223
+
224
+ node "$CONFIG_DIR/convert.mjs" 2>&1 | tee "$LOG_DIR/convert.log"
225
+ return "${PIPESTATUS[0]}"
226
+ }
227
+
228
+ # ============================================================
229
+ # MAIN MENU
230
+ # ============================================================
231
+
232
+ menu_main() {
233
+ load_env
234
+ detect_project
235
+ header
236
+ echo -e "${BOLD}Project :${RESET} ${DIM}$PROJECT_ROOT${RESET}"
237
+ echo -e "${BOLD}Manager :${RESET} ${DIM}$PKG_MANAGER${RESET}"
238
+ echo -e "${BOLD}Detected :${RESET} "
239
+ if [[ -n "$PRISMA_SCHEMA" ]]; then
240
+ local count
241
+ count=$(grep -c '^model ' "$PRISMA_SCHEMA" 2>/dev/null || echo 0)
242
+ ok "Prisma schema ($count models) at ${DIM}${PRISMA_SCHEMA#$PROJECT_ROOT/}${RESET}"
243
+ fi
244
+ [[ -n "$OPENAPI_FILE" ]] && ok "OpenAPI spec at ${DIM}${OPENAPI_FILE#$PROJECT_ROOT/}${RESET}"
245
+ [[ ${#JSON_SCHEMAS[@]} -gt 0 ]] && ok "JSON Schema files: ${#JSON_SCHEMAS[@]}"
246
+ [[ ${#DETECTED_TYPES[@]} -eq 0 ]] && warn "No schema detected. Menu 1 to create one, or cd into a project first."
247
+ echo
248
+
249
+ if [[ -f "$GENERATED_DIR/entities.ts" ]]; then
250
+ local count
251
+ count=$(grep -c '"name":' "$GENERATED_DIR/entities.ts" 2>/dev/null || echo 0)
252
+ ok "entities.ts generated ($count entities, $(du -h "$GENERATED_DIR/entities.ts" | cut -f1))"
253
+ else
254
+ warn "entities.ts not generated"
255
+ fi
256
+ echo
257
+
258
+ echo -e "${BOLD}${MAGENTA}━━━ MAIN MENU ━━━${RESET}"
259
+ echo
260
+ echo -e " ${CYAN}1${RESET}) Convert schema → EntitySchema[]"
261
+ echo -e " ${CYAN}2${RESET}) Configure database URIs"
262
+ echo -e " ${CYAN}3${RESET}) Initialize dialects (connect + create tables)"
263
+ echo -e " ${CYAN}4${RESET}) Tests menu (human / mobile / AI / curl / playwright)"
264
+ echo -e " ${CYAN}5${RESET}) Start services"
265
+ echo -e " ${CYAN}6${RESET}) Metrics & status"
266
+ echo -e " ${CYAN}7${RESET}) View logs"
267
+ echo -e " ${CYAN}8${RESET}) Health checks"
268
+ echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
269
+ echo -e " ${CYAN}0${RESET}) About / Help"
270
+ echo
271
+ echo -e " ${RED}q${RESET}) Quit"
272
+ echo
273
+ local choice
274
+ choice=$(ask "Choice" "1")
275
+ case "$choice" in
276
+ 1) action_convert ;;
277
+ 2) menu_databases ;;
278
+ 3) action_init_dialects ;;
279
+ 4) menu_tests ;;
280
+ 5) menu_services ;;
281
+ 6) action_metrics ;;
282
+ 7) action_logs ;;
283
+ 8) action_healthcheck ;;
284
+ 9) action_generate_boilerplate ;;
285
+ 0) action_about ;;
286
+ q|Q) exit 0 ;;
287
+ *) warn "Unknown choice"; pause ;;
288
+ esac
289
+ }
290
+
291
+ # ============================================================
292
+ # ACTION 1 : CONVERT
293
+ # ============================================================
294
+
295
+ action_convert() {
296
+ header
297
+ echo -e "${BOLD}${MAGENTA}▶ Convert schema → EntitySchema[]${RESET}"
298
+ echo
299
+ detect_project
300
+
301
+ if [[ ${#DETECTED_TYPES[@]} -eq 0 ]]; then
302
+ err "No schema file found."
303
+ info "Expected files (any one) :"
304
+ dim " - prisma/schema.prisma"
305
+ dim " - openapi.yaml / openapi.json / api.yaml / spec/openapi.yaml"
306
+ dim " - schemas/*.json"
307
+ echo
308
+ if confirm "Pick a file manually?"; then
309
+ local f; f=$(ask "Path to schema file (absolute or relative)")
310
+ [[ -z "$f" ]] && { pause; return; }
311
+ [[ ! -f "$f" ]] && { err "Not found: $f"; pause; return; }
312
+ # Infer type by extension / content
313
+ if [[ "$f" =~ \.prisma$ ]]; then
314
+ PRISMA_SCHEMA="$f"; DETECTED_TYPES=("prisma")
315
+ elif [[ "$f" =~ \.ya?ml$ ]] || grep -q "^openapi:" "$f" 2>/dev/null; then
316
+ OPENAPI_FILE="$f"; DETECTED_TYPES=("openapi")
317
+ else
318
+ JSON_SCHEMAS=("$f"); DETECTED_TYPES=("jsonschema")
319
+ fi
320
+ else
321
+ pause; return
322
+ fi
323
+ fi
324
+
325
+ # If multiple types, ask which one
326
+ local type input
327
+ if [[ ${#DETECTED_TYPES[@]} -eq 1 ]]; then
328
+ type="${DETECTED_TYPES[0]}"
329
+ else
330
+ echo "Multiple schema types detected. Choose:"
331
+ local i=1
332
+ for t in "${DETECTED_TYPES[@]}"; do echo " $i) $t"; i=$((i+1)); done
333
+ local choice; choice=$(ask "Number" "1")
334
+ type="${DETECTED_TYPES[$((choice-1))]}"
335
+ fi
336
+
337
+ case "$type" in
338
+ prisma) input="$PRISMA_SCHEMA" ;;
339
+ openapi) input="$OPENAPI_FILE" ;;
340
+ jsonschema) input="${JSON_SCHEMAS[0]}" ;;
341
+ esac
342
+
343
+ info "Input : $input"
344
+ info "Output: $GENERATED_DIR/entities.ts"
345
+ echo
346
+ run_adapter_convert "$type" "$input" "$GENERATED_DIR/entities.ts"
347
+ pause
348
+ }
349
+
350
+ # ============================================================
351
+ # MENU 2 : DATABASES
352
+ # ============================================================
353
+
354
+ menu_databases() {
355
+ load_env
356
+ header
357
+ echo -e "${BOLD}${MAGENTA}▶ Database configuration${RESET}"
358
+ echo
359
+ echo -e " ${CYAN}1${RESET}) MongoDB : ${DIM}${MONGODB_URI:-<not set>}${RESET}"
360
+ echo -e " ${CYAN}2${RESET}) PostgreSQL : ${DIM}${POSTGRES_URI:-<not set>}${RESET}"
361
+ echo -e " ${CYAN}3${RESET}) MySQL / MariaDB : ${DIM}${MYSQL_URI:-<not set>}${RESET}"
362
+ echo -e " ${CYAN}4${RESET}) SQLite : ${DIM}${SQLITE_URI:-<not set>}${RESET}"
363
+ echo -e " ${CYAN}5${RESET}) Oracle : ${DIM}${ORACLE_URI:-<not set>}${RESET}"
364
+ echo -e " ${CYAN}6${RESET}) MSSQL : ${DIM}${MSSQL_URI:-<not set>}${RESET}"
365
+ echo -e " ${CYAN}7${RESET}) DB2 : ${DIM}${DB2_URI:-<not set>}${RESET}"
366
+ echo -e " ${CYAN}8${RESET}) HANA : ${DIM}${HANA_URI:-<not set>}${RESET}"
367
+ echo -e " ${CYAN}9${RESET}) CockroachDB : ${DIM}${COCKROACH_URI:-<not set>}${RESET}"
368
+ echo
369
+ echo -e " ${CYAN}p${RESET}) App port (Next.js/Express) : ${DIM}${APP_PORT:-3000}${RESET}"
370
+ echo -e " ${CYAN}n${RESET}) mosta-net port : ${DIM}${MOSTA_NET_PORT:-4447}${RESET}"
371
+ echo
372
+ echo -e " ${CYAN}t${RESET}) Test all connections"
373
+ echo -e " ${CYAN}r${RESET}) Reset config"
374
+ echo -e " ${CYAN}b${RESET}) Back"
375
+ echo
376
+ local choice; choice=$(ask "Choice" "1")
377
+ case "$choice" in
378
+ 1) save_var MONGODB_URI "$(ask 'MongoDB URI' "${MONGODB_URI:-mongodb://localhost:27017/app}")";;
379
+ 2) save_var POSTGRES_URI "$(ask 'PostgreSQL URI' "${POSTGRES_URI:-postgres://user:pw@localhost:5432/app}")";;
380
+ 3) save_var MYSQL_URI "$(ask 'MySQL/MariaDB URI' "${MYSQL_URI:-mysql://user:pw@localhost:3306/app}")";;
381
+ 4) save_var SQLITE_URI "$(ask 'SQLite path or :memory:' "${SQLITE_URI:-./data.sqlite}")";;
382
+ 5) save_var ORACLE_URI "$(ask 'Oracle URI' "${ORACLE_URI:-oracle://user:pw@localhost:1521/ORCLPDB}")";;
383
+ 6) save_var MSSQL_URI "$(ask 'MSSQL URI' "${MSSQL_URI:-mssql://user:pw@localhost:1433/app}")";;
384
+ 7) save_var DB2_URI "$(ask 'DB2 URI' "${DB2_URI:-db2://user:pw@localhost:50000/app}")";;
385
+ 8) save_var HANA_URI "$(ask 'HANA URI' "${HANA_URI:-hana://user:pw@localhost:39041}")";;
386
+ 9) save_var COCKROACH_URI "$(ask 'CockroachDB URI' "${COCKROACH_URI:-postgres://user@localhost:26257/app}")";;
387
+ p|P) save_var APP_PORT "$(ask 'App port' "${APP_PORT:-3000}")";;
388
+ n|N) save_var MOSTA_NET_PORT "$(ask 'mosta-net port' "${MOSTA_NET_PORT:-4447}")";;
389
+ t|T) action_test_connections; return;;
390
+ r|R) confirm "Really reset config?" && rm -f "$CONFIG_FILE" && ok "Reset";;
391
+ b|B) return;;
392
+ *) warn "Unknown";;
393
+ esac
394
+ pause
395
+ menu_databases
396
+ }
397
+
398
+ action_test_connections() {
399
+ header
400
+ echo -e "${BOLD}${MAGENTA}▶ Testing connections${RESET}"
401
+ echo
402
+ load_env
403
+ local tested=0
404
+
405
+ if [[ -n "${MONGODB_URI:-}" ]]; then
406
+ info "MongoDB : $MONGODB_URI"
407
+ if command -v mongosh >/dev/null 2>&1; then
408
+ echo "db.stats()" | mongosh "$MONGODB_URI" --quiet >/dev/null 2>&1 && ok "reachable" || err "failed"
409
+ else warn "mongosh not installed"; fi
410
+ tested=$((tested+1))
411
+ fi
412
+
413
+ for pair in "POSTGRES_URI:psql" "MYSQL_URI:mysql" "MSSQL_URI:sqlcmd" "ORACLE_URI:sqlplus"; do
414
+ local var="${pair%%:*}"
415
+ local tool="${pair##*:}"
416
+ local uri="${!var:-}"
417
+ [[ -z "$uri" ]] && continue
418
+ info "$var : $uri"
419
+ if command -v "$tool" >/dev/null 2>&1; then
420
+ case "$tool" in
421
+ psql) psql "$uri" -c "SELECT 1" >/dev/null 2>&1 && ok reachable || err failed;;
422
+ mysql) mysql --defaults-file="/dev/null" -e "SELECT 1" >/dev/null 2>&1 && ok reachable || warn "manual test needed";;
423
+ *) warn "tested via tool $tool — manual check";;
424
+ esac
425
+ else warn "$tool not installed"; fi
426
+ tested=$((tested+1))
427
+ done
428
+
429
+ [[ $tested -eq 0 ]] && warn "No URIs configured"
430
+ pause
431
+ }
432
+
433
+ # ============================================================
434
+ # ACTION 3 : INIT DIALECTS
435
+ # ============================================================
436
+
437
+ action_init_dialects() {
438
+ header
439
+ echo -e "${BOLD}${MAGENTA}▶ Initialize dialects${RESET}"
440
+ echo
441
+ load_env
442
+
443
+ if [[ ! -f "$GENERATED_DIR/entities.ts" ]]; then
444
+ err "No entities.ts. Run menu 1 (Convert) first."
445
+ pause; return
446
+ fi
447
+
448
+ info "Will attempt to initialize dialects for these URIs:"
449
+ local any=0
450
+ for var in MONGODB_URI POSTGRES_URI MYSQL_URI SQLITE_URI ORACLE_URI MSSQL_URI DB2_URI; do
451
+ local val="${!var:-}"
452
+ [[ -n "$val" ]] && { dim " $var = $val"; any=1; }
453
+ done
454
+ [[ $any -eq 0 ]] && { err "No URIs set. Menu 2 first."; pause; return; }
455
+
456
+ confirm "Proceed?" || return
457
+
458
+ cat > "$CONFIG_DIR/init-all.mjs" << 'EOF'
459
+ import { readFileSync } from 'fs';
460
+ import { getDialect } from '@mostajs/orm';
461
+ import { entities } from './generated/entities.js';
462
+
463
+ const dialects = [
464
+ ['mongodb', process.env.MONGODB_URI],
465
+ ['postgres', process.env.POSTGRES_URI],
466
+ ['mysql', process.env.MYSQL_URI],
467
+ ['sqlite', process.env.SQLITE_URI],
468
+ ['oracle', process.env.ORACLE_URI],
469
+ ['mssql', process.env.MSSQL_URI],
470
+ ['db2', process.env.DB2_URI],
471
+ ];
472
+
473
+ for (const [dialect, uri] of dialects) {
474
+ if (!uri) continue;
475
+ console.log(`\n\u2192 ${dialect} : ${uri}`);
476
+ try {
477
+ const d = await getDialect({ dialect, uri, schemaStrategy: 'update' });
478
+ await d.initSchema(entities);
479
+ console.log(` \u2713 ${dialect} ready (${entities.length} entities)`);
480
+ await d.disconnect();
481
+ } catch (e) {
482
+ console.error(` \u2717 ${dialect} failed : ${e.message}`);
483
+ }
484
+ }
485
+ EOF
486
+ cd "$PROJECT_ROOT"
487
+ node "$CONFIG_DIR/init-all.mjs" 2>&1 | tee "$LOG_DIR/init.log"
488
+ pause
489
+ }
490
+
491
+ # ============================================================
492
+ # MENU 4 : TESTS
493
+ # ============================================================
494
+
495
+ menu_tests() {
496
+ load_env
497
+ header
498
+ echo -e "${BOLD}${MAGENTA}▶ Tests menu${RESET}"
499
+ echo
500
+ echo -e " ${CYAN}1${RESET}) Human : open app in browser"
501
+ echo -e " ${CYAN}2${RESET}) Human : open mosta-net dashboard"
502
+ echo -e " ${CYAN}3${RESET}) Mobile : QR code for LAN access"
503
+ echo -e " ${CYAN}4${RESET}) AI : MCP endpoint config (Claude/GPT)"
504
+ echo -e " ${CYAN}5${RESET}) curl : smoke test REST endpoints"
505
+ echo -e " ${CYAN}6${RESET}) Playwright"
506
+ echo -e " ${CYAN}7${RESET}) Jest / Vitest"
507
+ echo
508
+ echo -e " ${CYAN}b${RESET}) Back"
509
+ echo
510
+ local choice; choice=$(ask "Choice" "1")
511
+ case "$choice" in
512
+ 1) open_url "http://localhost:${APP_PORT:-3000}";;
513
+ 2) open_url "http://localhost:${MOSTA_NET_PORT:-4447}";;
514
+ 3) action_qr_mobile;;
515
+ 4) action_mcp_info;;
516
+ 5) action_curl_test;;
517
+ 6) run_in_project "npx playwright test";;
518
+ 7) run_in_project "$PKG_MANAGER test";;
519
+ b|B) return;;
520
+ *) warn Unknown;;
521
+ esac
522
+ pause
523
+ menu_tests
524
+ }
525
+
526
+ open_url() {
527
+ local url="$1"
528
+ info "Opening $url"
529
+ if command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 &
530
+ elif command -v open >/dev/null 2>&1; then open "$url" &
531
+ elif command -v start >/dev/null 2>&1; then start "$url"
532
+ else warn "Cannot auto-open. Visit: $url"
533
+ fi
534
+ }
535
+
536
+ action_qr_mobile() {
537
+ load_env
538
+ header
539
+ echo -e "${BOLD}${MAGENTA}▶ Mobile QR${RESET}"
540
+ echo
541
+ local port="${APP_PORT:-3000}"
542
+ local ip
543
+ ip=$(hostname -I 2>/dev/null | awk '{print $1}' || \
544
+ ifconfig 2>/dev/null | grep -oE 'inet (addr:)?([0-9]+\.){3}[0-9]+' | grep -v '127.0' | head -1 | awk '{print $2}' | sed 's/addr://')
545
+ [[ -z "$ip" ]] && ip="localhost"
546
+ local url="http://${ip}:${port}"
547
+ info "$url"
548
+ echo
549
+ if command -v qrencode >/dev/null 2>&1; then
550
+ qrencode -t ANSIUTF8 "$url"
551
+ else
552
+ warn "qrencode missing. Install: sudo apt install qrencode"
553
+ echo -e "${BOLD}URL for phone : ${CYAN}$url${RESET}"
554
+ fi
555
+ }
556
+
557
+ action_mcp_info() {
558
+ load_env
559
+ header
560
+ echo -e "${BOLD}${MAGENTA}▶ AI / MCP${RESET}"
561
+ echo
562
+ local url="http://localhost:${MOSTA_NET_PORT:-4447}/mcp"
563
+ info "MCP endpoint : $url"
564
+ echo
565
+ info "Claude Desktop (~/.config/Claude/claude_desktop_config.json or %APPDATA%\\Claude):"
566
+ cat <<EOF
567
+ ${DIM}{
568
+ "mcpServers": {
569
+ "$(basename "$PROJECT_ROOT")": {
570
+ "url": "$url"
571
+ }
572
+ }
573
+ }${RESET}
574
+ EOF
575
+ echo
576
+ info "For any MCP-compatible client (Cursor, Continue, GPT clients), point to $url"
577
+ echo
578
+ if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then
579
+ ok "Endpoint responds"
580
+ else
581
+ warn "Endpoint not reachable — start mosta-net first (menu 5)"
582
+ fi
583
+ }
584
+
585
+ action_curl_test() {
586
+ load_env
587
+ header
588
+ echo -e "${BOLD}${MAGENTA}▶ curl smoke test${RESET}"
589
+ echo
590
+ for url in \
591
+ "http://localhost:${APP_PORT:-3000}/" \
592
+ "http://localhost:${APP_PORT:-3000}/api/health" \
593
+ "http://localhost:${MOSTA_NET_PORT:-4447}/" \
594
+ "http://localhost:${MOSTA_NET_PORT:-4447}/mcp"; do
595
+ info "GET $url"
596
+ curl -s -o /dev/null -w " status=%{http_code} time=%{time_total}s\n" --max-time 5 "$url" 2>/dev/null || err "failed"
597
+ done
598
+ }
599
+
600
+ run_in_project() {
601
+ cd "$PROJECT_ROOT"
602
+ info "Running: $1"
603
+ eval "$1"
604
+ }
605
+
606
+ # ============================================================
607
+ # MENU 5 : SERVICES
608
+ # ============================================================
609
+
610
+ menu_services() {
611
+ load_env
612
+ header
613
+ echo -e "${BOLD}${MAGENTA}▶ Services${RESET}"
614
+ echo
615
+ echo -e " ${CYAN}1${RESET}) Start project dev server ($PKG_MANAGER run dev)"
616
+ echo -e " ${CYAN}2${RESET}) Start mosta-net server (requires separate install)"
617
+ echo -e " ${CYAN}3${RESET}) Stop all tracked services"
618
+ echo -e " ${CYAN}4${RESET}) Status"
619
+ echo -e " ${CYAN}5${RESET}) Show access URLs"
620
+ echo
621
+ echo -e " ${CYAN}b${RESET}) Back"
622
+ echo
623
+ local choice; choice=$(ask "Choice" "1")
624
+ case "$choice" in
625
+ 1) svc_start_dev;;
626
+ 2) svc_start_mostanet;;
627
+ 3) svc_stop_all;;
628
+ 4) svc_status;;
629
+ 5) show_urls;;
630
+ b|B) return;;
631
+ *) warn Unknown;;
632
+ esac
633
+ pause
634
+ menu_services
635
+ }
636
+
637
+ svc_start_dev() {
638
+ cd "$PROJECT_ROOT"
639
+ if [[ ! -f package.json ]]; then err "No package.json"; return; fi
640
+ info "Starting dev server (logs → $LOG_DIR/dev.log)"
641
+ nohup "$PKG_MANAGER" run dev > "$LOG_DIR/dev.log" 2>&1 &
642
+ echo "$!" > "$LOG_DIR/dev.pid"
643
+ ok "Started PID $!"
644
+ show_urls
645
+ }
646
+
647
+ svc_start_mostanet() {
648
+ warn "mosta-net server not yet installed. Provision with :"
649
+ dim " $PKG_MANAGER install @mostajs/net"
650
+ dim " then run : npx mosta-net --entities .mostajs/generated/entities.ts"
651
+ }
652
+
653
+ svc_stop_all() {
654
+ for pf in "$LOG_DIR"/*.pid; do
655
+ [[ -f "$pf" ]] || continue
656
+ local pid; pid=$(cat "$pf")
657
+ kill "$pid" 2>/dev/null && ok "Stopped $(basename "$pf" .pid) (PID $pid)"
658
+ rm -f "$pf"
659
+ done
660
+ }
661
+
662
+ svc_status() {
663
+ header
664
+ echo -e "${BOLD}${MAGENTA}▶ Status${RESET}"
665
+ echo
666
+ for pf in "$LOG_DIR"/*.pid; do
667
+ [[ -f "$pf" ]] || continue
668
+ local pid; pid=$(cat "$pf")
669
+ local name; name=$(basename "$pf" .pid)
670
+ kill -0 "$pid" 2>/dev/null && ok "$name (PID $pid)" || warn "$name dead (stale)"
671
+ done
672
+ [[ -z "$(ls "$LOG_DIR"/*.pid 2>/dev/null)" ]] && dim "No services tracked"
673
+ }
674
+
675
+ show_urls() {
676
+ load_env
677
+ local ip
678
+ ip=$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost)
679
+ echo
680
+ echo -e "${BOLD}Access URLs${RESET}"
681
+ echo -e " Dev server (local) : ${CYAN}http://localhost:${APP_PORT:-3000}${RESET}"
682
+ echo -e " Dev server (mobile) : ${CYAN}http://${ip}:${APP_PORT:-3000}${RESET}"
683
+ echo -e " mosta-net : ${CYAN}http://localhost:${MOSTA_NET_PORT:-4447}${RESET}"
684
+ echo -e " MCP endpoint (AI) : ${CYAN}http://localhost:${MOSTA_NET_PORT:-4447}/mcp${RESET}"
685
+ }
686
+
687
+ # ============================================================
688
+ # ACTION 6 : METRICS
689
+ # ============================================================
690
+
691
+ action_metrics() {
692
+ header
693
+ echo -e "${BOLD}${MAGENTA}▶ Metrics${RESET}"
694
+ echo
695
+ detect_project
696
+
697
+ echo -e "${BOLD}Source${RESET}"
698
+ if [[ -n "$PRISMA_SCHEMA" ]]; then
699
+ echo " Prisma models : $(grep -c '^model ' "$PRISMA_SCHEMA")"
700
+ echo " Prisma lines : $(wc -l < "$PRISMA_SCHEMA")"
701
+ fi
702
+ [[ -n "$OPENAPI_FILE" ]] && echo " OpenAPI file : $OPENAPI_FILE"
703
+ [[ ${#JSON_SCHEMAS[@]} -gt 0 ]] && echo " JSON schemas : ${#JSON_SCHEMAS[@]}"
704
+
705
+ echo
706
+ echo -e "${BOLD}Conversion${RESET}"
707
+ if [[ -f "$GENERATED_DIR/entities.ts" ]]; then
708
+ echo " entities.ts : $(grep -c '"name":' "$GENERATED_DIR/entities.ts") entities, $(du -h "$GENERATED_DIR/entities.ts" | cut -f1)"
709
+ echo " last generated : $(stat -c '%y' "$GENERATED_DIR/entities.ts" 2>/dev/null | cut -d. -f1 || stat -f '%Sm' "$GENERATED_DIR/entities.ts" 2>/dev/null)"
710
+ else
711
+ echo " (not generated yet)"
712
+ fi
713
+
714
+ echo
715
+ echo -e "${BOLD}Services${RESET}"
716
+ local n=0
717
+ for pf in "$LOG_DIR"/*.pid; do [[ -f "$pf" ]] && n=$((n+1)); done
718
+ echo " running : $n"
719
+
720
+ echo
721
+ echo -e "${BOLD}Logs${RESET}"
722
+ for f in "$LOG_DIR"/*.log; do
723
+ [[ -f "$f" ]] || continue
724
+ echo " $(basename "$f") : $(wc -l < "$f") lines"
725
+ done
726
+
727
+ pause
728
+ }
729
+
730
+ # ============================================================
731
+ # ACTION 7 : LOGS
732
+ # ============================================================
733
+
734
+ action_logs() {
735
+ header
736
+ echo -e "${BOLD}${MAGENTA}▶ Logs${RESET}"
737
+ echo
738
+ local logs=()
739
+ for f in "$LOG_DIR"/*.log; do [[ -f "$f" ]] && logs+=("$f"); done
740
+ if [[ ${#logs[@]} -eq 0 ]]; then warn "No logs yet"; pause; return; fi
741
+
742
+ local i=1
743
+ for f in "${logs[@]}"; do
744
+ echo -e " ${CYAN}$i${RESET}) $(basename "$f") ${DIM}($(wc -l < "$f") lines)${RESET}"
745
+ i=$((i+1))
746
+ done
747
+ echo
748
+ local choice; choice=$(ask "File #" 1)
749
+ local idx=$((choice-1))
750
+ [[ $idx -ge 0 && $idx -lt ${#logs[@]} ]] && ${PAGER:-less} "${logs[$idx]}"
751
+ }
752
+
753
+ # ============================================================
754
+ # ACTION 8 : HEALTH
755
+ # ============================================================
756
+
757
+ action_healthcheck() {
758
+ header
759
+ echo -e "${BOLD}${MAGENTA}▶ Health checks${RESET}"
760
+ echo
761
+ detect_project
762
+ command -v node >/dev/null 2>&1 && ok "node $(node -v)" || err "node missing"
763
+ command -v "$PKG_MANAGER" >/dev/null 2>&1 && ok "$PKG_MANAGER $("$PKG_MANAGER" --version 2>&1 | head -1)" || err "$PKG_MANAGER missing"
764
+ [[ -f "$PROJECT_ROOT/package.json" ]] && ok "package.json" || warn "no package.json"
765
+ [[ -d "$PROJECT_ROOT/node_modules" ]] && ok "node_modules present" || warn "node_modules missing"
766
+ [[ ${#DETECTED_TYPES[@]} -gt 0 ]] && ok "schemas detected: ${DETECTED_TYPES[*]}" || warn "no schema found"
767
+ [[ -f "$GENERATED_DIR/entities.ts" ]] && ok "entities.ts generated" || warn "not generated"
768
+ [[ -f "$CONFIG_FILE" ]] && ok "config present" || warn "config not set"
769
+
770
+ command -v curl >/dev/null 2>&1 && ok "curl" || warn "curl missing"
771
+ command -v qrencode >/dev/null 2>&1 && ok "qrencode" || warn "qrencode missing (optional, for mobile QR)"
772
+ command -v mongosh >/dev/null 2>&1 && ok "mongosh" || warn "mongosh missing (optional, for mongo tests)"
773
+ command -v psql >/dev/null 2>&1 && ok "psql" || warn "psql missing (optional, for PG tests)"
774
+ command -v xdg-open >/dev/null 2>&1 || command -v open >/dev/null 2>&1 && ok "browser-opener available" || warn "cannot auto-open URLs"
775
+ pause
776
+ }
777
+
778
+ # ============================================================
779
+ # ACTION 9 : GENERATE BOILERPLATE
780
+ # ============================================================
781
+
782
+ action_generate_boilerplate() {
783
+ header
784
+ echo -e "${BOLD}${MAGENTA}▶ Generate boilerplate${RESET}"
785
+ echo
786
+ echo "Available templates :"
787
+ echo " 1) src/db.ts — Prisma bridge wrapper (choose which models move)"
788
+ echo " 2) src/mosta-orm.ts — direct mosta-orm usage (no Prisma)"
789
+ echo " 3) .env.example with all URIs"
790
+ echo
791
+ local choice; choice=$(ask "Choice" 1)
792
+ case "$choice" in
793
+ 1) gen_prisma_bridge_boilerplate;;
794
+ 2) gen_direct_boilerplate;;
795
+ 3) gen_env_example;;
796
+ *) warn Unknown;;
797
+ esac
798
+ pause
799
+ }
800
+
801
+ gen_prisma_bridge_boilerplate() {
802
+ local target="$PROJECT_ROOT/src/db.ts"
803
+ mkdir -p "$(dirname "$target")"
804
+ cat > "$target" <<'EOF'
805
+ // Auto-generated by @mostajs/orm-cli
806
+ // Prisma bridge : intercepts specific models and routes them to @mostajs/orm dialects
807
+
808
+ import { PrismaClient } from '@prisma/client';
809
+ import { mostaExtension } from '@mostajs/orm-bridge/prisma';
810
+ import { entityByName } from '../.mostajs/generated/entities.js';
811
+
812
+ const g = globalThis as unknown as { prisma?: ReturnType<typeof build> };
813
+
814
+ function build() {
815
+ return new PrismaClient().$extends(mostaExtension({
816
+ models: {
817
+ // Example : move AuditLog to MongoDB (uncomment and adapt)
818
+ // AuditLog: {
819
+ // dialect: 'mongodb',
820
+ // url: process.env.MONGODB_URI!,
821
+ // schema: entityByName.AuditLog,
822
+ // },
823
+
824
+ // Example : move analytics-heavy models to PostgreSQL
825
+ // CheckIn: {
826
+ // dialect: 'postgres',
827
+ // url: process.env.ANALYTICS_PG!,
828
+ // schema: entityByName.CheckIn,
829
+ // },
830
+ },
831
+ fallback: 'source', // unmapped models → Prisma default engine
832
+ onIntercept: (e) => {
833
+ if (process.env.NODE_ENV !== 'production') {
834
+ console.log(`[bridge] ${e.model}.${e.operation} → ${e.dialect} (${e.duration}ms)`);
835
+ }
836
+ },
837
+ }));
838
+ }
839
+
840
+ export const prisma = g.prisma ?? build();
841
+ if (process.env.NODE_ENV !== 'production') g.prisma = prisma;
842
+ EOF
843
+ ok "Written : $target"
844
+ info "Next steps :"
845
+ dim " - Review + pick which models to move"
846
+ dim " - Replace 'new PrismaClient()' with this 'prisma' import throughout your app"
847
+ }
848
+
849
+ gen_direct_boilerplate() {
850
+ local target="$PROJECT_ROOT/src/mosta-orm.ts"
851
+ mkdir -p "$(dirname "$target")"
852
+ cat > "$target" <<'EOF'
853
+ // Auto-generated by @mostajs/orm-cli
854
+ // Direct @mostajs/orm usage — no Prisma dependency
855
+
856
+ import { getDialect } from '@mostajs/orm';
857
+ import { entities, entityByName } from '../.mostajs/generated/entities.js';
858
+
859
+ export async function createOrm() {
860
+ const dialect = await getDialect({
861
+ dialect: (process.env.MOSTA_DIALECT ?? 'postgres') as any,
862
+ uri: process.env.DATABASE_URL ?? '',
863
+ schemaStrategy: 'update',
864
+ });
865
+ await dialect.initSchema(entities);
866
+ return { dialect, entities, entityByName };
867
+ }
868
+ EOF
869
+ ok "Written : $target"
870
+ }
871
+
872
+ gen_env_example() {
873
+ local target="$PROJECT_ROOT/.env.example"
874
+ cat > "$target" <<'EOF'
875
+ # ===== @mostajs/orm — 13 databases =====
876
+ MONGODB_URI=mongodb://localhost:27017/app
877
+ POSTGRES_URI=postgres://user:pw@localhost:5432/app
878
+ MYSQL_URI=mysql://user:pw@localhost:3306/app
879
+ SQLITE_URI=./data.sqlite
880
+ ORACLE_URI=oracle://user:pw@localhost:1521/ORCLPDB
881
+ MSSQL_URI=mssql://user:pw@localhost:1433/app
882
+ DB2_URI=db2://user:pw@localhost:50000/app
883
+ HANA_URI=hana://user:pw@localhost:39041
884
+ COCKROACH_URI=postgres://user@localhost:26257/app
885
+
886
+ APP_PORT=3000
887
+ MOSTA_NET_PORT=4447
888
+ EOF
889
+ ok "Written : $target"
890
+ }
891
+
892
+ # ============================================================
893
+ # ACTION 0 : ABOUT
894
+ # ============================================================
895
+
896
+ action_about() {
897
+ header
898
+ cat <<EOF
899
+ ${BOLD}mostajs-cli v$VERSION${RESET}
900
+
901
+ Convert schemas from multiple formats to @mostajs/orm EntitySchema[]
902
+ and gain access to 13 databases without rewriting your code.
903
+
904
+ ${BOLD}Supported inputs${RESET}
905
+ - Prisma (.prisma files)
906
+ - OpenAPI (3.0, 3.1, YAML/JSON)
907
+ - JSON Schema (Draft-07, 2019-09, 2020-12)
908
+
909
+ ${BOLD}Supported databases${RESET}
910
+ PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server, Oracle, DB2,
911
+ CockroachDB, HANA, HSQLDB, Spanner, Sybase, MongoDB
912
+
913
+ ${BOLD}Links${RESET}
914
+ Packages : @mostajs/orm, @mostajs/orm-adapter, @mostajs/orm-bridge
915
+ GitHub : https://github.com/apolocine
916
+ Author : Dr Hamid MADANI <drmdh@msn.com>
917
+ License : AGPL-3.0-or-later (+ commercial)
918
+
919
+ ${BOLD}Workflow${RESET}
920
+ 1. cd your/project
921
+ 2. mostajs (this tool)
922
+ 3. Menu 1 → convert your schema
923
+ 4. Menu 2 → set your DB URIs
924
+ 5. Menu 3 → init tables
925
+ 6. Menu 9 → generate boilerplate
926
+ 7. Menu 5 → start services
927
+ 8. Menu 4 → test (human / mobile / AI)
928
+ EOF
929
+ pause
930
+ }
931
+
932
+ # ============================================================
933
+ # CLI SUBCOMMANDS (non-interactive)
934
+ # ============================================================
935
+
936
+ run_subcommand() {
937
+ case "$1" in
938
+ convert|c)
939
+ detect_project
940
+ [[ ${#DETECTED_TYPES[@]} -eq 0 ]] && { err "No schema found"; exit 1; }
941
+ local type="${DETECTED_TYPES[0]}" input
942
+ case "$type" in
943
+ prisma) input="$PRISMA_SCHEMA" ;;
944
+ openapi) input="$OPENAPI_FILE" ;;
945
+ jsonschema) input="${JSON_SCHEMAS[0]}" ;;
946
+ esac
947
+ run_adapter_convert "$type" "$input" "$GENERATED_DIR/entities.ts"
948
+ ;;
949
+ detect|d)
950
+ detect_project
951
+ echo "project: $PROJECT_ROOT"
952
+ echo "package manager: $PKG_MANAGER"
953
+ echo "detected: ${DETECTED_TYPES[*]:-none}"
954
+ [[ -n "$PRISMA_SCHEMA" ]] && echo "prisma: $PRISMA_SCHEMA"
955
+ [[ -n "$OPENAPI_FILE" ]] && echo "openapi: $OPENAPI_FILE"
956
+ ;;
957
+ health|h)
958
+ action_healthcheck
959
+ ;;
960
+ version|-v|--version)
961
+ echo "$CLI_NAME $VERSION"
962
+ ;;
963
+ help|-h|--help)
964
+ cat <<EOF
965
+ Usage :
966
+ $CLI_NAME Interactive menu
967
+ $CLI_NAME convert Run conversion (auto-detect schema type)
968
+ $CLI_NAME detect Print detected schemas
969
+ $CLI_NAME health Run health checks
970
+ $CLI_NAME version Print version
971
+ EOF
972
+ ;;
973
+ *)
974
+ err "Unknown command: $1"
975
+ echo "Run '$CLI_NAME help' for usage"
976
+ exit 1
977
+ ;;
978
+ esac
979
+ }
980
+
981
+ # ============================================================
982
+ # MAIN
983
+ # ============================================================
984
+
985
+ # Non-interactive mode if args provided
986
+ if [[ $# -gt 0 ]]; then
987
+ run_subcommand "$@"
988
+ exit 0
989
+ fi
990
+
991
+ # Interactive menu
992
+ while true; do
993
+ menu_main
994
+ done