bunigniter 0.2.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 +21 -0
- package/README.md +229 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +229 -0
- package/dist/base/controller.ts +324 -0
- package/dist/base/index.ts +5 -0
- package/dist/base/service.ts +21 -0
- package/dist/cli/index.ts +318 -0
- package/dist/cli/list-routes.ts +72 -0
- package/dist/cli/repl.ts +461 -0
- package/dist/cli/templates.ts +283 -0
- package/dist/client/index.ts +159 -0
- package/dist/db/drizzle.ts +550 -0
- package/dist/db/validators.ts +229 -0
- package/dist/edge-builder.ts +120 -0
- package/dist/edge.ts +69 -0
- package/dist/helpers/cache.ts +173 -0
- package/dist/helpers/cors.ts +103 -0
- package/dist/helpers/csrf.ts +155 -0
- package/dist/helpers/debug.ts +158 -0
- package/dist/helpers/env.ts +147 -0
- package/dist/helpers/handler.ts +158 -0
- package/dist/helpers/http.ts +194 -0
- package/dist/helpers/image.ts +217 -0
- package/dist/helpers/jwt.ts +147 -0
- package/dist/helpers/logger.ts +96 -0
- package/dist/helpers/mail.ts +272 -0
- package/dist/helpers/middleware-loader.ts +116 -0
- package/dist/helpers/middleware.ts +57 -0
- package/dist/helpers/modules.ts +115 -0
- package/dist/helpers/openapi.ts +140 -0
- package/dist/helpers/pagination.ts +159 -0
- package/dist/helpers/queue.ts +186 -0
- package/dist/helpers/request-context.ts +13 -0
- package/dist/helpers/request.ts +376 -0
- package/dist/helpers/schedule.ts +173 -0
- package/dist/helpers/session-middleware.ts +89 -0
- package/dist/helpers/session.ts +286 -0
- package/dist/helpers/sse.ts +90 -0
- package/dist/helpers/throttle.ts +156 -0
- package/dist/helpers/upload.ts +417 -0
- package/dist/helpers/validator.ts +287 -0
- package/dist/helpers/ws.ts +123 -0
- package/dist/index.ts +221 -0
- package/dist/package.json +70 -0
- package/dist/router/file-router.ts +541 -0
- package/dist/router/server-router.ts +103 -0
- package/dist/view/page.ts +96 -0
- package/dist/view/renderer.tsx +390 -0
- package/dist/view/view-response.ts +10 -0
- package/package.json +70 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared route listing — used by both `nx list` CLI and `.routes` REPL command.
|
|
3
|
+
*/
|
|
4
|
+
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const CWD = process.cwd()
|
|
8
|
+
|
|
9
|
+
export async function listRoutes(): Promise<void> {
|
|
10
|
+
const routesDir = join(CWD, 'routes')
|
|
11
|
+
if (!existsSync(routesDir)) {
|
|
12
|
+
console.log(' No routes directory found.')
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const files = readdirSync(routesDir)
|
|
17
|
+
const prefix = process.env.ROUTER_PREFIX ?? '/api'
|
|
18
|
+
|
|
19
|
+
console.log('\n Bunigniter Routes')
|
|
20
|
+
console.log(' ─────────────────────────────────')
|
|
21
|
+
|
|
22
|
+
let count = 0
|
|
23
|
+
for (const file of files.sort()) {
|
|
24
|
+
if (!file.endsWith('.ts') || file.endsWith('.server.ts')) continue
|
|
25
|
+
|
|
26
|
+
const fullPath = join(routesDir, file)
|
|
27
|
+
if (!statSync(fullPath).isFile()) continue
|
|
28
|
+
|
|
29
|
+
const content = readFileSync(fullPath, 'utf-8')
|
|
30
|
+
const name = file.replace(/\.ts$/, '')
|
|
31
|
+
const isIndex = name === 'index'
|
|
32
|
+
|
|
33
|
+
const classMatch = content.match(/export (default )?class (\w+) extends Controller/)
|
|
34
|
+
const className = classMatch ? classMatch[2] : name
|
|
35
|
+
|
|
36
|
+
// Check for defineHandler pattern: export const GET = ...
|
|
37
|
+
const handlerMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
|
38
|
+
let hasHandler = false
|
|
39
|
+
|
|
40
|
+
for (const verb of handlerMethods) {
|
|
41
|
+
if (content.includes(`export const ${verb} =`)) {
|
|
42
|
+
hasHandler = true
|
|
43
|
+
const path = isIndex ? prefix : `${prefix}/${name}`
|
|
44
|
+
console.log(` ${verb.padEnd(6)} ${path.padEnd(25)} ${className}.${verb}()`)
|
|
45
|
+
count++
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (hasHandler) continue
|
|
50
|
+
|
|
51
|
+
// Controller class methods
|
|
52
|
+
const methods = ['index', 'show', 'create', 'update', 'destroy']
|
|
53
|
+
const methodVerbs: Record<string, string> = {
|
|
54
|
+
index: 'GET', show: 'GET', create: 'POST',
|
|
55
|
+
update: 'PUT', destroy: 'DELETE',
|
|
56
|
+
}
|
|
57
|
+
const idMethods = new Set(['show', 'update', 'destroy'])
|
|
58
|
+
|
|
59
|
+
for (const method of methods) {
|
|
60
|
+
if (!content.includes(`async ${method}`)) continue
|
|
61
|
+
const verb = methodVerbs[method]
|
|
62
|
+
const path = idMethods.has(method)
|
|
63
|
+
? (isIndex ? `${prefix}/:id` : `${prefix}/${name}/:id`)
|
|
64
|
+
: (isIndex ? prefix : `${prefix}/${name}`)
|
|
65
|
+
console.log(` ${verb.padEnd(6)} ${path.padEnd(25)} ${className}.${method}()`)
|
|
66
|
+
count++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (count === 0) console.log(' (no routes found)')
|
|
71
|
+
console.log()
|
|
72
|
+
}
|
package/dist/cli/repl.ts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL — Interactive console for Bunigniter.
|
|
3
|
+
*
|
|
4
|
+
* Usage: `bun run bi repl`
|
|
5
|
+
*
|
|
6
|
+
* Provides access to db, cache, session, and app services.
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* .help Show help
|
|
10
|
+
* .exit Exit REPL
|
|
11
|
+
* .routes List registered routes
|
|
12
|
+
* .services List available services
|
|
13
|
+
* .env Show environment variables
|
|
14
|
+
* .db Show database status
|
|
15
|
+
* .clear Clear screen
|
|
16
|
+
* .version Show version info
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, existsSync, appendFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { createInterface, clearLine, cursorTo } from "node:readline";
|
|
21
|
+
import { inspect } from "node:util";
|
|
22
|
+
|
|
23
|
+
const HISTORY_FILE = join(process.cwd(), ".nexus_repl_history");
|
|
24
|
+
const HISTORY_MAX = 100;
|
|
25
|
+
const PING_SQL = "SELECT 1 as ok";
|
|
26
|
+
|
|
27
|
+
/** Build a pretty ASCII box banner. */
|
|
28
|
+
function banner(version: string, runtime: string): string {
|
|
29
|
+
const lines = [
|
|
30
|
+
"",
|
|
31
|
+
" ╔══════════════════════════════════════════════════════╗",
|
|
32
|
+
" ║ ║",
|
|
33
|
+
" ║ ◈ Bunigniter ◈ ║",
|
|
34
|
+
" ║ Interactive Development Console ║",
|
|
35
|
+
" ║ ║",
|
|
36
|
+
` ║ version ${version.padEnd(25)} ║`,
|
|
37
|
+
` ║ runtime ${runtime.padEnd(25)} ║`,
|
|
38
|
+
" ║ ║",
|
|
39
|
+
" ║ Type .help for available commands ║",
|
|
40
|
+
" ║ ║",
|
|
41
|
+
" ╚══════════════════════════════════════════════════════╝",
|
|
42
|
+
];
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read version from package.json. */
|
|
47
|
+
function readVersion(): string {
|
|
48
|
+
try {
|
|
49
|
+
const pkgPath = join(process.cwd(), "package.json");
|
|
50
|
+
if (existsSync(pkgPath)) {
|
|
51
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
52
|
+
return pkg.version ?? "0.0.0";
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
return "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start the REPL.
|
|
62
|
+
*/
|
|
63
|
+
export async function startRepl() {
|
|
64
|
+
const version = readVersion();
|
|
65
|
+
const runtime = typeof Bun !== "undefined" ? `Bun ${Bun.version}` : "Node.js";
|
|
66
|
+
|
|
67
|
+
console.log(banner(version, runtime));
|
|
68
|
+
console.log("");
|
|
69
|
+
|
|
70
|
+
const context = await initializeContext(version, runtime);
|
|
71
|
+
|
|
72
|
+
const rl = createInterface({
|
|
73
|
+
input: process.stdin,
|
|
74
|
+
output: process.stdout,
|
|
75
|
+
prompt: "\x1b[36mnexus> \x1b[0m",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Load history
|
|
79
|
+
const history: string[] = [];
|
|
80
|
+
try {
|
|
81
|
+
if (existsSync(HISTORY_FILE)) {
|
|
82
|
+
const lines = readFileSync(HISTORY_FILE, "utf-8")
|
|
83
|
+
.split("\n")
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
history.push(...lines.slice(-HISTORY_MAX));
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// ignore
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let historyIndex = history.length;
|
|
92
|
+
|
|
93
|
+
rl.on("line", async (input: string) => {
|
|
94
|
+
const trimmed = input.trim();
|
|
95
|
+
|
|
96
|
+
if (!trimmed) {
|
|
97
|
+
rl.prompt();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle REPL commands
|
|
102
|
+
if (trimmed.startsWith(".")) {
|
|
103
|
+
await handleCommand(trimmed, context);
|
|
104
|
+
rl.prompt();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Save to history
|
|
109
|
+
history.push(trimmed);
|
|
110
|
+
historyIndex = history.length;
|
|
111
|
+
try {
|
|
112
|
+
appendFileSync(HISTORY_FILE, trimmed + "\n");
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Trim history
|
|
118
|
+
if (history.length > HISTORY_MAX) {
|
|
119
|
+
history.splice(0, history.length - HISTORY_MAX);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Evaluate the expression
|
|
123
|
+
try {
|
|
124
|
+
const result = await evaluateExpression(trimmed, context);
|
|
125
|
+
if (result !== undefined) {
|
|
126
|
+
const output =
|
|
127
|
+
typeof result === "object" && result !== null
|
|
128
|
+
? inspect(result, { depth: 3, colors: true, sorted: true })
|
|
129
|
+
: typeof result === "string"
|
|
130
|
+
? `\x1b[33m"${result}"\x1b[0m`
|
|
131
|
+
: String(result);
|
|
132
|
+
console.log(` \x1b[90m=>\x1b[0m ${output}`);
|
|
133
|
+
}
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
console.error(` \x1b[31m✗ ${err.message ?? err}\x1b[0m`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
rl.prompt();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
rl.on("SIGINT", () => {
|
|
142
|
+
console.log("\n \x1b[90mBye!\x1b[0m");
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Keypress handling for history navigation and tab completion
|
|
147
|
+
const stdin = process.stdin;
|
|
148
|
+
if (stdin.isTTY) {
|
|
149
|
+
stdin.on("data", (key: Buffer) => {
|
|
150
|
+
// Up arrow
|
|
151
|
+
if (key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x41) {
|
|
152
|
+
if (historyIndex > 0) {
|
|
153
|
+
historyIndex--;
|
|
154
|
+
clearLine(process.stdout, 0);
|
|
155
|
+
cursorTo(process.stdout, 0);
|
|
156
|
+
rl.write(history[historyIndex] ?? "");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Down arrow
|
|
160
|
+
if (key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x42) {
|
|
161
|
+
if (historyIndex < history.length - 1) {
|
|
162
|
+
historyIndex++;
|
|
163
|
+
clearLine(process.stdout, 0);
|
|
164
|
+
cursorTo(process.stdout, 0);
|
|
165
|
+
rl.write(history[historyIndex] ?? "");
|
|
166
|
+
} else {
|
|
167
|
+
historyIndex = history.length;
|
|
168
|
+
clearLine(process.stdout, 0);
|
|
169
|
+
cursorTo(process.stdout, 0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Tab completion
|
|
173
|
+
if (key[0] === 0x09) {
|
|
174
|
+
const line = rl.line;
|
|
175
|
+
const matches = Object.keys(context).filter(
|
|
176
|
+
(k) => k.startsWith(line) && !k.startsWith("__"),
|
|
177
|
+
);
|
|
178
|
+
if (matches.length === 1) {
|
|
179
|
+
clearLine(process.stdout, 0);
|
|
180
|
+
cursorTo(process.stdout, 0);
|
|
181
|
+
rl.write(matches[0]);
|
|
182
|
+
} else if (matches.length > 1) {
|
|
183
|
+
console.log("\n " + matches.join(", "));
|
|
184
|
+
rl.prompt();
|
|
185
|
+
rl.write(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
rl.prompt();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Initialize the REPL context with available services. */
|
|
195
|
+
async function initializeContext(
|
|
196
|
+
version: string,
|
|
197
|
+
runtime: string,
|
|
198
|
+
): Promise<Record<string, any>> {
|
|
199
|
+
const ctx: Record<string, any> = {
|
|
200
|
+
process,
|
|
201
|
+
console,
|
|
202
|
+
Date,
|
|
203
|
+
Math,
|
|
204
|
+
JSON,
|
|
205
|
+
setTimeout,
|
|
206
|
+
setInterval,
|
|
207
|
+
clearTimeout,
|
|
208
|
+
clearInterval,
|
|
209
|
+
crypto,
|
|
210
|
+
Buffer,
|
|
211
|
+
URL,
|
|
212
|
+
parseInt,
|
|
213
|
+
parseFloat,
|
|
214
|
+
__version: version,
|
|
215
|
+
__runtime: runtime,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Try to load Drizzle DB
|
|
219
|
+
try {
|
|
220
|
+
const { DbClient } = await import("../db/drizzle");
|
|
221
|
+
const dbConfig = loadDbConfig();
|
|
222
|
+
if (dbConfig) {
|
|
223
|
+
const db = new DbClient(dbConfig);
|
|
224
|
+
await db.open();
|
|
225
|
+
ctx.db = db;
|
|
226
|
+
ctx.query = (sql: string, params?: any[]) => db.query(sql, params);
|
|
227
|
+
ctx.first = (sql: string, params?: any[]) => db.first(sql, params);
|
|
228
|
+
console.log(" \x1b[32m◈\x1b[0m \x1b[90mdb\x1b[0m connected");
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
console.log(" \x1b[33m◈\x1b[0m \x1b[90mdb\x1b[0m not loaded");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Try to load Cache
|
|
235
|
+
try {
|
|
236
|
+
const { createCache } = await import("../helpers/cache");
|
|
237
|
+
ctx.cache = createCache();
|
|
238
|
+
console.log(" \x1b[32m◈\x1b[0m \x1b[90mcache\x1b[0m ready");
|
|
239
|
+
} catch {
|
|
240
|
+
console.log(" \x1b[33m◈\x1b[0m \x1b[90mcache\x1b[0m not available");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Try to load HTTP client
|
|
244
|
+
try {
|
|
245
|
+
const { createHttp } = await import("../helpers/http");
|
|
246
|
+
ctx.http = createHttp();
|
|
247
|
+
console.log(" \x1b[32m◈\x1b[0m \x1b[90mhttp\x1b[0m ready");
|
|
248
|
+
} catch {
|
|
249
|
+
console.log(" \x1b[33m◈\x1b[0m \x1b[90mhttp\x1b[0m not available");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return ctx;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Try to load DB config from the app config. */
|
|
256
|
+
function loadDbConfig(): any {
|
|
257
|
+
try {
|
|
258
|
+
const configPath = join(process.cwd(), "config/app.ts");
|
|
259
|
+
if (existsSync(configPath)) {
|
|
260
|
+
const content = readFileSync(configPath, "utf-8");
|
|
261
|
+
|
|
262
|
+
const dialectMatch = content.match(/dialect:\s*'([^']+)'/);
|
|
263
|
+
const fileMatch = content.match(/filename:\s*'([^']+)'/);
|
|
264
|
+
if (dialectMatch) {
|
|
265
|
+
return {
|
|
266
|
+
dialect: dialectMatch[1],
|
|
267
|
+
connection: { filename: fileMatch?.[1] ?? "app.db" },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// ignore
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Describe a service entry for the .services command. */
|
|
278
|
+
function describeService(val: unknown): string {
|
|
279
|
+
const type = typeof val;
|
|
280
|
+
if (type === "function") {
|
|
281
|
+
return `\x1b[33mfn\x1b[0m ${(val as Function).name || "(anonymous)"}()`;
|
|
282
|
+
}
|
|
283
|
+
if (type === "object" && val !== null) {
|
|
284
|
+
return `\x1b[35mobj\x1b[0m ${(val as object).constructor?.name || "Object"}`;
|
|
285
|
+
}
|
|
286
|
+
return `\x1b[34m${type}\x1b[0m`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Truncate an env value for display. */
|
|
290
|
+
function truncateEnvVal(val: string): string {
|
|
291
|
+
return val.length > 60 ? val.slice(0, 60) + "\x1b[90m...\x1b[0m" : val;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Handle REPL commands. */
|
|
295
|
+
async function handleCommand(
|
|
296
|
+
cmd: string,
|
|
297
|
+
ctx: Record<string, any>,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const args = cmd.split(/\s+/);
|
|
300
|
+
const command = args[0].toLowerCase();
|
|
301
|
+
|
|
302
|
+
switch (command) {
|
|
303
|
+
case ".help": {
|
|
304
|
+
console.log(`
|
|
305
|
+
\x1b[36m── Commands ──────────────────────────────────────\x1b[0m
|
|
306
|
+
\x1b[33m.help\x1b[0m Show this help
|
|
307
|
+
\x1b[33m.exit\x1b[0m Exit the REPL
|
|
308
|
+
\x1b[33m.quit\x1b[0m Exit the REPL
|
|
309
|
+
\x1b[33m.routes\x1b[0m List registered routes
|
|
310
|
+
\x1b[33m.services\x1b[0m List available services in context
|
|
311
|
+
\x1b[33m.env\x1b[0m Show environment variables
|
|
312
|
+
\x1b[33m.db\x1b[0m Show database status
|
|
313
|
+
\x1b[33m.clear\x1b[0m Clear screen
|
|
314
|
+
\x1b[33m.version\x1b[0m Show version info
|
|
315
|
+
|
|
316
|
+
\x1b[36m── Available Variables ────────────────────────────\x1b[0m
|
|
317
|
+
\x1b[33mdb\x1b[0m Database client (query, first, all)
|
|
318
|
+
\x1b[33mcache\x1b[0m Cache service (get, set, delete)
|
|
319
|
+
\x1b[33mhttp\x1b[0m HTTP client (get, post)
|
|
320
|
+
\x1b[33mquery()\x1b[0m Shortcut for db.query()
|
|
321
|
+
\x1b[33mfirst()\x1b[0m Shortcut for db.first()
|
|
322
|
+
|
|
323
|
+
\x1b[36m── Examples ──────────────────────────────────────\x1b[0m
|
|
324
|
+
\x1b[90mnexus>\x1b[0m await query('SELECT * FROM users')
|
|
325
|
+
\x1b[90mnexus>\x1b[0m await cache.get('my_key')
|
|
326
|
+
\x1b[90mnexus>\x1b[0m 1 + 2
|
|
327
|
+
\x1b[90mnexus>\x1b[0m const x = 42; x * 2
|
|
328
|
+
\x1b[90mnexus>\x1b[0m { hello: 'world' }
|
|
329
|
+
`);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case ".exit":
|
|
334
|
+
case ".quit": {
|
|
335
|
+
console.log(" \x1b[90mBye!\x1b[0m");
|
|
336
|
+
process.exit(0);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case ".routes":
|
|
341
|
+
case ".list": {
|
|
342
|
+
const { listRoutes } = await import("./list-routes");
|
|
343
|
+
await listRoutes();
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case ".services": {
|
|
348
|
+
console.log(`\n \x1b[36mAvailable services:\x1b[0m\n`);
|
|
349
|
+
for (const [key, val] of Object.entries(ctx)) {
|
|
350
|
+
if (key.startsWith("__")) continue;
|
|
351
|
+
console.log(` \x1b[32m${key}\x1b[0m ${describeService(val)}`);
|
|
352
|
+
}
|
|
353
|
+
console.log();
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case ".env": {
|
|
358
|
+
const envVars = Object.keys(process.env).sort();
|
|
359
|
+
console.log(
|
|
360
|
+
`\n \x1b[36mEnvironment (\x1b[33m${envVars.length}\x1b[36m vars):\x1b[0m\n`,
|
|
361
|
+
);
|
|
362
|
+
for (const key of envVars.slice(0, 30)) {
|
|
363
|
+
console.log(
|
|
364
|
+
` \x1b[32m${key.padEnd(25)}\x1b[0m ${truncateEnvVal(process.env[key] ?? "")}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (envVars.length > 30) {
|
|
368
|
+
console.log(` \x1b[90m... and ${envVars.length - 30} more\x1b[0m`);
|
|
369
|
+
}
|
|
370
|
+
console.log();
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case ".db": {
|
|
375
|
+
if (ctx.db) {
|
|
376
|
+
try {
|
|
377
|
+
await ctx.db.query(PING_SQL);
|
|
378
|
+
console.log(
|
|
379
|
+
`\n \x1b[32m◈\x1b[0m Database: \x1b[32mconnected\x1b[0m`,
|
|
380
|
+
);
|
|
381
|
+
console.log(
|
|
382
|
+
` \x1b[32m◈\x1b[0m Dialect: ${ctx.db.dialectName ?? "unknown"}\n`,
|
|
383
|
+
);
|
|
384
|
+
} catch (e: any) {
|
|
385
|
+
console.log(
|
|
386
|
+
`\n \x1b[31m◈\x1b[0m Database: \x1b[31merror\x1b[0m — ${e.message}\n`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
console.log("\n \x1b[33m◈\x1b[0m No database connection.\n");
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case ".clear":
|
|
396
|
+
case "clear": {
|
|
397
|
+
console.clear();
|
|
398
|
+
console.log(banner(ctx.__version, ctx.__runtime));
|
|
399
|
+
console.log("");
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case ".version": {
|
|
404
|
+
console.log(`
|
|
405
|
+
\x1b[36mBunigniter\x1b[0m \x1b[33mv${ctx.__version}\x1b[0m
|
|
406
|
+
\x1b[36mRuntime\x1b[0m ${ctx.__runtime}
|
|
407
|
+
`);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
default: {
|
|
412
|
+
console.log(
|
|
413
|
+
` \x1b[31mUnknown command:\x1b[0m ${command}. Type \x1b[33m.help\x1b[0m for available commands.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Evaluate a JavaScript expression in context. */
|
|
420
|
+
async function evaluateExpression(
|
|
421
|
+
input: string,
|
|
422
|
+
ctx: Record<string, any>,
|
|
423
|
+
): Promise<any> {
|
|
424
|
+
const hasAwait = input.includes("await ");
|
|
425
|
+
const keys = Object.keys(ctx);
|
|
426
|
+
const values = Object.values(ctx);
|
|
427
|
+
|
|
428
|
+
// Try as expression first (fast path)
|
|
429
|
+
try {
|
|
430
|
+
const fn = new Function(...keys, `"use strict"; return (${input})`);
|
|
431
|
+
return fn(...values);
|
|
432
|
+
} catch {
|
|
433
|
+
// try fallthrough
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Try as async expression (handles top-level await)
|
|
437
|
+
try {
|
|
438
|
+
const fn = new Function(
|
|
439
|
+
...keys,
|
|
440
|
+
`"use strict"; return (async () => { ${hasAwait ? "" : "return "}${input} })()`,
|
|
441
|
+
);
|
|
442
|
+
return fn(...values);
|
|
443
|
+
} catch {
|
|
444
|
+
// try fallthrough
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Try as statement
|
|
448
|
+
try {
|
|
449
|
+
const fn = new Function(...keys, `"use strict"; ${input}`);
|
|
450
|
+
return fn(...values);
|
|
451
|
+
} catch {
|
|
452
|
+
// try fallthrough
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Last resort — async statement
|
|
456
|
+
const fn = new Function(
|
|
457
|
+
...keys,
|
|
458
|
+
`"use strict"; return (async () => { ${input} })()`,
|
|
459
|
+
);
|
|
460
|
+
return fn(...values);
|
|
461
|
+
}
|