@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/LICENSE +29 -0
- package/README.md +138 -0
- package/bin/mostajs-launcher.cjs +35 -0
- package/bin/mostajs.bat +490 -0
- package/bin/mostajs.sh +994 -0
- package/package.json +44 -0
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
|