tina4-nodejs 3.13.141 → 3.13.143
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/CLAUDE.md +28 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +817 -538
- package/packages/cli/src/bin.ts +4 -14
- package/packages/cli/src/commands/build.ts +2 -18
- package/packages/cli/src/commands/generate.ts +9 -24
- package/packages/cli/src/commands/init.ts +18 -8
- package/packages/cli/src/commands/lint.ts +3 -13
- package/packages/cli/src/commands/migrate.ts +2 -6
- package/packages/cli/src/commands/migrateRollback.ts +2 -6
- package/packages/cli/src/commands/migrateStatus.ts +2 -6
- package/packages/cli/src/commands/queue.ts +2 -30
- package/packages/cli/src/util.ts +90 -0
- package/packages/core/dist/index.js +592 -262
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
- package/packages/core/gallery/queue/src/lib/queueDb.ts +11 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +2 -5
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +2 -5
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
- package/packages/core/src/csp.ts +138 -0
- package/packages/core/src/devAdmin.ts +3 -12
- package/packages/core/src/dispatchPipeline.ts +2 -18
- package/packages/core/src/errorOverlay.ts +80 -37
- package/packages/core/src/etag.ts +24 -0
- package/packages/core/src/feedback.ts +2 -11
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/middleware.ts +20 -6
- package/packages/core/src/response.ts +14 -3
- package/packages/core/src/server.ts +52 -15
- package/packages/core/src/session.ts +174 -71
- package/packages/core/src/sessionHandlers/respSessionHandler.ts +94 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +22 -73
- package/packages/core/src/sso.ts +1 -1
- package/packages/core/src/static.ts +1 -13
- package/packages/core/src/types.ts +8 -0
- package/packages/core/src/upstreamResponse.ts +31 -0
- package/packages/core/src/websocket.ts +48 -7
- package/packages/core/templates/errors/302.twig +1 -1
- package/packages/core/templates/errors/401.twig +2 -2
- package/packages/core/templates/errors/403.twig +1 -1
- package/packages/core/templates/errors/404.twig +1 -1
- package/packages/core/templates/errors/500.twig +1 -1
- package/packages/core/templates/errors/502.twig +2 -2
- package/packages/core/templates/errors/503.twig +2 -2
- package/packages/core/templates/errors/base.twig +4 -1
- package/packages/frond/dist/index.js +2 -0
- package/packages/frond/src/engine.ts +23 -0
- package/packages/orm/dist/index.js +593 -271
- package/packages/orm/src/graph/adapters/bolt.ts +8 -29
- package/packages/orm/src/graph/adapters/rowMapping.ts +57 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -30
- package/packages/swagger/dist/index.js +4 -4
- package/packages/swagger/src/ui.ts +7 -4
- package/types/cli/src/util.d.ts +28 -0
- package/types/core/src/csp.d.ts +42 -0
- package/types/core/src/etag.d.ts +9 -0
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/middleware.d.ts +6 -0
- package/types/core/src/session.d.ts +64 -21
- package/types/core/src/sessionHandlers/respSessionHandler.d.ts +52 -0
- package/types/core/src/sessionHandlers/valkeyHandler.d.ts +2 -26
- package/types/core/src/types.d.ts +8 -0
- package/types/core/src/upstreamResponse.d.ts +10 -0
- package/types/core/src/websocket.d.ts +21 -0
- package/types/frond/src/engine.d.ts +7 -0
- package/types/orm/src/graph/adapters/rowMapping.d.ts +26 -0
package/packages/cli/src/bin.ts
CHANGED
|
@@ -20,8 +20,9 @@ import { runSeeds } from "./commands/seed.js";
|
|
|
20
20
|
import { queueCommand, QUEUE_SUBCOMMAND_NAMES } from "./commands/queue.js";
|
|
21
21
|
import { buildImage } from "./commands/build.js";
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
|
-
import { existsSync, readFileSync
|
|
24
|
-
import {
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { findOnPath } from "./util.js";
|
|
25
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
27
|
|
|
27
28
|
// ── Version (cheap, side-effect-free) ───────────────────────────────
|
|
@@ -496,18 +497,7 @@ function findClient(): string | null {
|
|
|
496
497
|
const names = windows
|
|
497
498
|
? [`${CLIENT_BINARY}.exe`, `${CLIENT_BINARY}.cmd`, `${CLIENT_BINARY}.bat`]
|
|
498
499
|
: [CLIENT_BINARY];
|
|
499
|
-
|
|
500
|
-
if (!dir) continue;
|
|
501
|
-
for (const name of names) {
|
|
502
|
-
const candidate = join(dir, name);
|
|
503
|
-
try {
|
|
504
|
-
if (statSync(candidate).isFile()) return candidate;
|
|
505
|
-
} catch {
|
|
506
|
-
// not there — keep looking
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return null;
|
|
500
|
+
return findOnPath(names);
|
|
511
501
|
}
|
|
512
502
|
|
|
513
503
|
/**
|
|
@@ -24,27 +24,11 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
24
24
|
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
25
25
|
import { basename, delimiter, join } from "node:path";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { parseFlags as parseFlagsShared } from "../util.js";
|
|
27
28
|
|
|
28
29
|
/** Parse --key value / bare --flag (build only needs --tag and --file values). */
|
|
29
30
|
function parseFlags(args: string[]): Record<string, string | boolean> {
|
|
30
|
-
|
|
31
|
-
let i = 0;
|
|
32
|
-
while (i < args.length) {
|
|
33
|
-
const arg = args[i];
|
|
34
|
-
if (arg.startsWith("--")) {
|
|
35
|
-
const key = arg.slice(2);
|
|
36
|
-
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
37
|
-
flags[key] = args[i + 1];
|
|
38
|
-
i += 2;
|
|
39
|
-
} else {
|
|
40
|
-
flags[key] = true;
|
|
41
|
-
i += 1;
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
i += 1;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return flags;
|
|
31
|
+
return parseFlagsShared(args).flags;
|
|
48
32
|
}
|
|
49
33
|
|
|
50
34
|
/**
|
|
@@ -42,6 +42,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
42
42
|
*/
|
|
43
43
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
44
44
|
import { join, relative, resolve, sep } from "node:path";
|
|
45
|
+
import { parseFlags } from "../util.js";
|
|
45
46
|
|
|
46
47
|
// ── Field type mapping ──────────────────────────────────────────────
|
|
47
48
|
const FIELD_TYPE_MAP: Record<string, { orm: string; sql: string; defaultVal: string }> = {
|
|
@@ -558,28 +559,7 @@ export function parseCliArgs(args: string[]): { flags: Record<string, string | b
|
|
|
558
559
|
"json", "dry-run",
|
|
559
560
|
]);
|
|
560
561
|
|
|
561
|
-
|
|
562
|
-
const positional: string[] = [];
|
|
563
|
-
let i = 0;
|
|
564
|
-
while (i < args.length) {
|
|
565
|
-
if (args[i].startsWith("--")) {
|
|
566
|
-
const key = args[i].slice(2);
|
|
567
|
-
if (booleanFlags.has(key)) {
|
|
568
|
-
flags[key] = true;
|
|
569
|
-
i += 1;
|
|
570
|
-
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
571
|
-
flags[key] = args[i + 1];
|
|
572
|
-
i += 2;
|
|
573
|
-
} else {
|
|
574
|
-
flags[key] = true;
|
|
575
|
-
i += 1;
|
|
576
|
-
}
|
|
577
|
-
} else {
|
|
578
|
-
positional.push(args[i]);
|
|
579
|
-
i += 1;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return { flags, positional };
|
|
562
|
+
return parseFlags(args, booleanFlags);
|
|
583
563
|
}
|
|
584
564
|
|
|
585
565
|
/**
|
|
@@ -1870,7 +1850,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
1870
1850
|
`{% extends "base.twig" %}
|
|
1871
1851
|
{% block title %}Login{% endblock %}
|
|
1872
1852
|
{% block content %}
|
|
1873
|
-
|
|
1853
|
+
{# No inline style attribute: a nonce lets this <style> block run under the
|
|
1854
|
+
strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
|
|
1855
|
+
that returns this response's nonce (ADR-0088). #}
|
|
1856
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
1857
|
+
<div class="container mt-4 auth-form">
|
|
1874
1858
|
<h1>Login</h1>
|
|
1875
1859
|
<form method="post" action="/api/auth/login">
|
|
1876
1860
|
{{ form_token() }}
|
|
@@ -1896,7 +1880,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
1896
1880
|
`{% extends "base.twig" %}
|
|
1897
1881
|
{% block title %}Register{% endblock %}
|
|
1898
1882
|
{% block content %}
|
|
1899
|
-
<
|
|
1883
|
+
<style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
|
|
1884
|
+
<div class="container mt-4 auth-form">
|
|
1900
1885
|
<h1>Register</h1>
|
|
1901
1886
|
<form method="post" action="/api/auth/register">
|
|
1902
1887
|
{{ form_token() }}
|
|
@@ -211,20 +211,30 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
|
|
|
211
211
|
`
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
// Static index
|
|
214
|
+
// Static index stylesheet. A static file cannot carry a per-response CSP
|
|
215
|
+
// nonce (ADR-0088), so its styles live in an external same-origin stylesheet
|
|
216
|
+
// the strict default policy ('self') already allows — not an inline <style>,
|
|
217
|
+
// which the browser would refuse, leaving a fresh project unstyled.
|
|
218
|
+
writeFileSync(
|
|
219
|
+
join(targetDir, "public/style.css"),
|
|
220
|
+
`body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
|
|
221
|
+
h1 { font-size: 2.5em; margin-bottom: 0.2em; }
|
|
222
|
+
p { color: #666; font-size: 1.1em; }
|
|
223
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
224
|
+
a { color: #2563eb; }
|
|
225
|
+
`
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Static index page. CSP-clean: an external stylesheet, no inline <style>.
|
|
229
|
+
// For a page that needs an inline block, render it through a Frond template
|
|
230
|
+
// and stamp the nonce: <style nonce="{{ csp_nonce() }}">...</style>.
|
|
215
231
|
writeFileSync(
|
|
216
232
|
join(targetDir, "public/index.html"),
|
|
217
233
|
`<!DOCTYPE html>
|
|
218
234
|
<html>
|
|
219
235
|
<head>
|
|
220
236
|
<title>Tina4</title>
|
|
221
|
-
<style>
|
|
222
|
-
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
|
|
223
|
-
h1 { font-size: 2.5em; margin-bottom: 0.2em; }
|
|
224
|
-
p { color: #666; font-size: 1.1em; }
|
|
225
|
-
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
226
|
-
a { color: #2563eb; }
|
|
227
|
-
</style>
|
|
237
|
+
<link rel="stylesheet" href="/style.css">
|
|
228
238
|
</head>
|
|
229
239
|
<body>
|
|
230
240
|
<h1>tina4</h1>
|
|
@@ -43,7 +43,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
43
43
|
*/
|
|
44
44
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
45
45
|
import { createRequire } from "node:module";
|
|
46
|
-
import {
|
|
46
|
+
import { dirname, join, relative } from "node:path";
|
|
47
|
+
import { findOnPath } from "../util.js";
|
|
47
48
|
import { spawnSync } from "node:child_process";
|
|
48
49
|
|
|
49
50
|
// Source extensions we count as lintable app code. `.d.ts` declaration files are
|
|
@@ -217,18 +218,7 @@ function hasPackage(cwd: string, pkg: string): boolean {
|
|
|
217
218
|
function resolveNpm(): string | null {
|
|
218
219
|
const windows = process.platform === "win32";
|
|
219
220
|
const names = windows ? ["npm.cmd", "npm.exe", "npm"] : ["npm"];
|
|
220
|
-
|
|
221
|
-
if (!dir) continue;
|
|
222
|
-
for (const name of names) {
|
|
223
|
-
const candidate = join(dir, name);
|
|
224
|
-
try {
|
|
225
|
-
if (statSync(candidate).isFile()) return candidate;
|
|
226
|
-
} catch {
|
|
227
|
-
// not there — keep looking
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
221
|
+
return findOnPath(names);
|
|
232
222
|
}
|
|
233
223
|
|
|
234
224
|
interface EslintSetup {
|
|
@@ -29,6 +29,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
29
29
|
import { existsSync } from "node:fs";
|
|
30
30
|
import { resolve } from "node:path";
|
|
31
31
|
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
32
|
+
import { initDatabaseOrExit } from "../util.js";
|
|
32
33
|
|
|
33
34
|
export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
34
35
|
// Load .env before initialising the DB so DATABASE_URL/TINA4_DATABASE_URL
|
|
@@ -59,12 +60,7 @@ export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
|
59
60
|
// async — MUST be awaited, otherwise setAdapter() has not run by the time
|
|
60
61
|
// migrate() asks for the adapter and the whole CLI crashes with
|
|
61
62
|
// "No database adapter configured."
|
|
62
|
-
|
|
63
|
-
await initDatabase();
|
|
64
|
-
} catch (err) {
|
|
65
|
-
console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
63
|
+
await initDatabaseOrExit(initDatabase);
|
|
68
64
|
|
|
69
65
|
const result = await migrate(undefined, { migrationsDir: dir });
|
|
70
66
|
|
|
@@ -18,6 +18,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
18
18
|
*/
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
21
|
+
import { initDatabaseOrExit } from "../util.js";
|
|
21
22
|
|
|
22
23
|
export async function migrateRollback(migrationDir?: string): Promise<void> {
|
|
23
24
|
// .env must load before initDatabase() — otherwise DATABASE_URL from the
|
|
@@ -45,12 +46,7 @@ export async function migrateRollback(migrationDir?: string): Promise<void> {
|
|
|
45
46
|
// Ensure database is initialised — MUST await; initDatabase() is async
|
|
46
47
|
// and calls setAdapter() inside the promise. Without await, the next call
|
|
47
48
|
// to getAdapter() throws "No database adapter configured."
|
|
48
|
-
|
|
49
|
-
await initDatabase();
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
49
|
+
await initDatabaseOrExit(initDatabase);
|
|
54
50
|
|
|
55
51
|
await ensureMigrationTable();
|
|
56
52
|
|
|
@@ -15,6 +15,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
15
15
|
*/
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
17
|
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
18
|
+
import { initDatabaseOrExit } from "../util.js";
|
|
18
19
|
|
|
19
20
|
export async function migrateStatus(migrationDir?: string): Promise<void> {
|
|
20
21
|
// .env must load before initDatabase() so the project's DATABASE_URL is seen.
|
|
@@ -38,12 +39,7 @@ export async function migrateStatus(migrationDir?: string): Promise<void> {
|
|
|
38
39
|
|
|
39
40
|
// Ensure database is initialised — MUST await; initDatabase() is async
|
|
40
41
|
// and calls setAdapter() inside. Without await, getAdapter() throws.
|
|
41
|
-
|
|
42
|
-
await initDatabase();
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
42
|
+
await initDatabaseOrExit(initDatabase);
|
|
47
43
|
|
|
48
44
|
ensureMigrationTable();
|
|
49
45
|
|
|
@@ -28,6 +28,7 @@ import { pathToFileURL } from "node:url";
|
|
|
28
28
|
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
29
29
|
import { Queue } from "../../../core/src/queue.js";
|
|
30
30
|
import type { QueueJob } from "../../../core/src/job.js";
|
|
31
|
+
import { parseFlags as parseFlagsShared } from "../util.js";
|
|
31
32
|
|
|
32
33
|
// ── Flag parsing ─────────────────────────────────────────────────────
|
|
33
34
|
//
|
|
@@ -36,36 +37,7 @@ import type { QueueJob } from "../../../core/src/job.js";
|
|
|
36
37
|
|
|
37
38
|
const BOOLEAN_FLAGS = new Set(["once", "json"]);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
flags: Record<string, string | boolean>;
|
|
41
|
-
positional: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseFlags(args: string[]): ParsedFlags {
|
|
45
|
-
const flags: Record<string, string | boolean> = {};
|
|
46
|
-
const positional: string[] = [];
|
|
47
|
-
let i = 0;
|
|
48
|
-
while (i < args.length) {
|
|
49
|
-
const arg = args[i];
|
|
50
|
-
if (arg.startsWith("--")) {
|
|
51
|
-
const key = arg.slice(2);
|
|
52
|
-
if (BOOLEAN_FLAGS.has(key)) {
|
|
53
|
-
flags[key] = true;
|
|
54
|
-
i += 1;
|
|
55
|
-
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
56
|
-
flags[key] = args[i + 1];
|
|
57
|
-
i += 2;
|
|
58
|
-
} else {
|
|
59
|
-
flags[key] = true;
|
|
60
|
-
i += 1;
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
positional.push(arg);
|
|
64
|
-
i += 1;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return { flags, positional };
|
|
68
|
-
}
|
|
40
|
+
const parseFlags = (args: string[]) => parseFlagsShared(args, BOOLEAN_FLAGS);
|
|
69
41
|
|
|
70
42
|
/** A per-job handler declared by a consumer module (receives the job payload). */
|
|
71
43
|
export type QueueHandler = (payload: unknown) => unknown | Promise<unknown>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shared CLI helpers — argument parsing, PATH lookup, and database bootstrap.
|
|
11
|
+
* Kept zero-dependency (node stdlib only) so the CLI entry path stays light.
|
|
12
|
+
*/
|
|
13
|
+
import { delimiter, join } from "node:path";
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
/** Flags and positional arguments split out of a command's argv slice. */
|
|
17
|
+
export interface ParsedFlags {
|
|
18
|
+
flags: Record<string, string | boolean>;
|
|
19
|
+
positional: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse `--key value` and bare `--flag` arguments into flags plus positionals.
|
|
24
|
+
*
|
|
25
|
+
* A name listed in `booleanFlags` never consumes the next token; every other
|
|
26
|
+
* `--key` takes the following token as its value unless that token is itself a
|
|
27
|
+
* `--flag` (or the argv ends), in which case the key is a bare boolean.
|
|
28
|
+
*/
|
|
29
|
+
export function parseFlags(args: string[], booleanFlags?: ReadonlySet<string>): ParsedFlags {
|
|
30
|
+
const flags: Record<string, string | boolean> = {};
|
|
31
|
+
const positional: string[] = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < args.length) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg.startsWith("--")) {
|
|
36
|
+
const key = arg.slice(2);
|
|
37
|
+
if (booleanFlags?.has(key)) {
|
|
38
|
+
flags[key] = true;
|
|
39
|
+
i += 1;
|
|
40
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
41
|
+
flags[key] = args[i + 1];
|
|
42
|
+
i += 2;
|
|
43
|
+
} else {
|
|
44
|
+
flags[key] = true;
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
positional.push(arg);
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { flags, positional };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Absolute path of the first of `names` found on PATH, or null.
|
|
57
|
+
*
|
|
58
|
+
* Scans PATH directories directly (no `which` shell-out) so it behaves the same
|
|
59
|
+
* on every platform and honours a deliberately emptied PATH. Callers pass the
|
|
60
|
+
* platform-specific candidate names (e.g. `["npm.cmd", "npm.exe", "npm"]`).
|
|
61
|
+
*/
|
|
62
|
+
export function findOnPath(names: string[]): string | null {
|
|
63
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
64
|
+
if (!dir) continue;
|
|
65
|
+
for (const name of names) {
|
|
66
|
+
const candidate = join(dir, name);
|
|
67
|
+
try {
|
|
68
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
69
|
+
} catch {
|
|
70
|
+
// not there — keep looking
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Await the ORM's async `initDatabase()` and exit(1) with a clear message on
|
|
79
|
+
* failure. The await matters: initDatabase() calls setAdapter() inside the
|
|
80
|
+
* promise, so without it the next getAdapter() throws "No database adapter
|
|
81
|
+
* configured." Shared by the migrate, rollback, and status commands.
|
|
82
|
+
*/
|
|
83
|
+
export async function initDatabaseOrExit(initDatabase: () => Promise<unknown>): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
await initDatabase();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|