tina4-nodejs 3.13.120 → 3.13.122
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 +2 -2
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +10744 -10661
- package/packages/cli/src/bin.ts +5 -2
- package/packages/cli/src/commands/generate.ts +117 -19
- package/packages/cli/src/commands/migrateCreate.ts +32 -37
- package/packages/core/dist/index.js +2421 -339
- package/packages/core/src/mcp.ts +62 -11
- package/packages/core/src/middleware.ts +1156 -1120
- package/packages/orm/dist/index.js +2470 -388
- package/types/cli/src/commands/generate.d.ts +16 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -1
- package/types/core/src/middleware.d.ts +15 -0
|
@@ -3150,15 +3150,15 @@ function findOutsideQuotes(expr, needle) {
|
|
|
3150
3150
|
}
|
|
3151
3151
|
return -1;
|
|
3152
3152
|
}
|
|
3153
|
-
function splitOutsideQuotes(expr,
|
|
3154
|
-
if (!expr.includes(
|
|
3153
|
+
function splitOutsideQuotes(expr, sep7) {
|
|
3154
|
+
if (!expr.includes(sep7)) return [expr];
|
|
3155
3155
|
const parts = [];
|
|
3156
3156
|
let currentStart = 0;
|
|
3157
3157
|
let inQuote = null;
|
|
3158
3158
|
let depth = 0;
|
|
3159
3159
|
let bracketDepth = 0;
|
|
3160
3160
|
let i = 0;
|
|
3161
|
-
const sepLen =
|
|
3161
|
+
const sepLen = sep7.length;
|
|
3162
3162
|
const lastStart = expr.length - sepLen;
|
|
3163
3163
|
while (i <= lastStart) {
|
|
3164
3164
|
const ch = expr[i];
|
|
@@ -3179,7 +3179,7 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
3179
3179
|
else if (ch === ")") depth--;
|
|
3180
3180
|
else if (ch === "[") bracketDepth++;
|
|
3181
3181
|
else if (ch === "]") bracketDepth--;
|
|
3182
|
-
if (depth === 0 && bracketDepth === 0 && expr.startsWith(
|
|
3182
|
+
if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep7, i)) {
|
|
3183
3183
|
parts.push(expr.slice(currentStart, i));
|
|
3184
3184
|
i += sepLen;
|
|
3185
3185
|
currentStart = i;
|
|
@@ -3955,8 +3955,8 @@ var init_engine = __esm({
|
|
|
3955
3955
|
},
|
|
3956
3956
|
first: (v) => Array.isArray(v) ? v[0] ?? null : null,
|
|
3957
3957
|
last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
|
|
3958
|
-
join: (v,
|
|
3959
|
-
split: (v,
|
|
3958
|
+
join: (v, sep7) => Array.isArray(v) ? v.map(String).join(sep7 !== void 0 ? String(sep7) : ", ") : String(v),
|
|
3959
|
+
split: (v, sep7) => String(v).split(sep7 !== void 0 ? String(sep7) : " "),
|
|
3960
3960
|
replace: (v, from, to) => {
|
|
3961
3961
|
const s = String(v);
|
|
3962
3962
|
if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
|
|
@@ -6657,6 +6657,9 @@ var init_middleware = __esm({
|
|
|
6657
6657
|
`max-age=${hsts}; includeSubDomains`
|
|
6658
6658
|
);
|
|
6659
6659
|
}
|
|
6660
|
+
if (process.env.TINA4_CSP === void 0) {
|
|
6661
|
+
_SecurityHeadersMiddleware.warnCspDefaultOnce();
|
|
6662
|
+
}
|
|
6660
6663
|
res.header(
|
|
6661
6664
|
"Content-Security-Policy",
|
|
6662
6665
|
process.env.TINA4_CSP ?? "default-src 'self'"
|
|
@@ -6672,6 +6675,30 @@ var init_middleware = __esm({
|
|
|
6672
6675
|
);
|
|
6673
6676
|
return [req2, res];
|
|
6674
6677
|
}
|
|
6678
|
+
/** Warn-once ledger for the default-CSP heads-up (per process). */
|
|
6679
|
+
static cspDefaultWarned = false;
|
|
6680
|
+
/**
|
|
6681
|
+
* Warn once per process that the default CSP is in force (TINA4_CSP unset).
|
|
6682
|
+
*
|
|
6683
|
+
* Secure-by-default keeps `default-src 'self'` (SECHDR-DEC-01), but that
|
|
6684
|
+
* default is invisible: it blocks runtime-injected inline styles, cross-origin
|
|
6685
|
+
* fonts/scripts/CDNs, `data:` URIs, and cross-origin WebSocket/XHR (a separate
|
|
6686
|
+
* API or LiveKit host) — and the failure surfaces only in the browser at
|
|
6687
|
+
* runtime, long after a deploy has gone green. So the framework says so once,
|
|
6688
|
+
* naming the escape hatch. It NEVER fails the boot or a request — logging a
|
|
6689
|
+
* heads-up must not be the reason the server or a request dies. Fires only when
|
|
6690
|
+
* TINA4_CSP is ABSENT; setting it (even to empty) is an explicit opt-in.
|
|
6691
|
+
*/
|
|
6692
|
+
static warnCspDefaultOnce() {
|
|
6693
|
+
if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
|
|
6694
|
+
_SecurityHeadersMiddleware.cspDefaultWarned = true;
|
|
6695
|
+
const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. That default blocks runtime-injected inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
|
|
6696
|
+
try {
|
|
6697
|
+
Log.warning(message);
|
|
6698
|
+
} catch {
|
|
6699
|
+
console.warn(message);
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6675
6702
|
/**
|
|
6676
6703
|
* True when the client request is HTTPS. Proxy-aware and byte-parity with
|
|
6677
6704
|
* Python (request.is_secure_scheme), PHP (Request::isSecureScheme) and Ruby
|
|
@@ -7161,7 +7188,7 @@ ${s}\r
|
|
|
7161
7188
|
connect() {
|
|
7162
7189
|
if (this.connected) return Promise.resolve();
|
|
7163
7190
|
if (this.connecting) return this.connecting;
|
|
7164
|
-
this.connecting = new Promise((
|
|
7191
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
7165
7192
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
7166
7193
|
sock.setNoDelay(true);
|
|
7167
7194
|
const onError = (err) => {
|
|
@@ -7198,7 +7225,7 @@ ${s}\r
|
|
|
7198
7225
|
sock.on("error", (e) => {
|
|
7199
7226
|
this.brokenError = e;
|
|
7200
7227
|
});
|
|
7201
|
-
|
|
7228
|
+
resolve21();
|
|
7202
7229
|
} catch (e) {
|
|
7203
7230
|
onError(e);
|
|
7204
7231
|
}
|
|
@@ -7261,12 +7288,12 @@ ${s}\r
|
|
|
7261
7288
|
}
|
|
7262
7289
|
/** Send one command and await its reply (assumes socket is up). */
|
|
7263
7290
|
raw(args) {
|
|
7264
|
-
return new Promise((
|
|
7291
|
+
return new Promise((resolve21, reject) => {
|
|
7265
7292
|
if (!this.sock || this.sock.destroyed) {
|
|
7266
7293
|
reject(this.brokenError ?? new Error("redis socket not connected"));
|
|
7267
7294
|
return;
|
|
7268
7295
|
}
|
|
7269
|
-
this.waiters.push({ resolve:
|
|
7296
|
+
this.waiters.push({ resolve: resolve21, reject });
|
|
7270
7297
|
this.sock.write(_RespClient.encode(args));
|
|
7271
7298
|
});
|
|
7272
7299
|
}
|
|
@@ -7608,7 +7635,7 @@ ${s}\r
|
|
|
7608
7635
|
connect() {
|
|
7609
7636
|
if (this.connected) return Promise.resolve();
|
|
7610
7637
|
if (this.connecting) return this.connecting;
|
|
7611
|
-
this.connecting = new Promise((
|
|
7638
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
7612
7639
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
7613
7640
|
sock.setNoDelay(true);
|
|
7614
7641
|
sock.once("error", (err) => {
|
|
@@ -7629,7 +7656,7 @@ ${s}\r
|
|
|
7629
7656
|
p.resolve(this.buffer.toString("utf-8"));
|
|
7630
7657
|
}
|
|
7631
7658
|
});
|
|
7632
|
-
|
|
7659
|
+
resolve21();
|
|
7633
7660
|
});
|
|
7634
7661
|
});
|
|
7635
7662
|
return this.connecting;
|
|
@@ -7662,13 +7689,13 @@ ${s}\r
|
|
|
7662
7689
|
async send(payload, terminator) {
|
|
7663
7690
|
await this.connect();
|
|
7664
7691
|
if (!this.sock || this.sock.destroyed) return "";
|
|
7665
|
-
return new Promise((
|
|
7692
|
+
return new Promise((resolve21) => {
|
|
7666
7693
|
this.buffer = Buffer.alloc(0);
|
|
7667
|
-
this.pending = { terminator, resolve:
|
|
7694
|
+
this.pending = { terminator, resolve: resolve21 };
|
|
7668
7695
|
const timer = setTimeout(() => {
|
|
7669
|
-
if (this.pending && this.pending.resolve ===
|
|
7696
|
+
if (this.pending && this.pending.resolve === resolve21) {
|
|
7670
7697
|
this.pending = null;
|
|
7671
|
-
|
|
7698
|
+
resolve21(this.buffer.toString("utf-8"));
|
|
7672
7699
|
}
|
|
7673
7700
|
}, 4e3);
|
|
7674
7701
|
if (timer.unref) timer.unref();
|
|
@@ -9434,7 +9461,7 @@ async function parseBody(req2) {
|
|
|
9434
9461
|
}
|
|
9435
9462
|
const contentType = req2.headers["content-type"] ?? "";
|
|
9436
9463
|
const chunks = [];
|
|
9437
|
-
await new Promise((
|
|
9464
|
+
await new Promise((resolve21, reject) => {
|
|
9438
9465
|
let received = 0;
|
|
9439
9466
|
let refused = false;
|
|
9440
9467
|
req2.on("data", (chunk) => {
|
|
@@ -9449,7 +9476,7 @@ async function parseBody(req2) {
|
|
|
9449
9476
|
chunks.push(chunk);
|
|
9450
9477
|
});
|
|
9451
9478
|
req2.on("end", () => {
|
|
9452
|
-
if (!refused)
|
|
9479
|
+
if (!refused) resolve21();
|
|
9453
9480
|
});
|
|
9454
9481
|
req2.on("error", reject);
|
|
9455
9482
|
});
|
|
@@ -12682,6 +12709,2038 @@ var init_version = __esm({
|
|
|
12682
12709
|
}
|
|
12683
12710
|
});
|
|
12684
12711
|
|
|
12712
|
+
// ../cli/src/commands/generate.ts
|
|
12713
|
+
var generate_exports = {};
|
|
12714
|
+
__export(generate_exports, {
|
|
12715
|
+
DEFAULT_FIELDS: () => DEFAULT_FIELDS,
|
|
12716
|
+
GENERATORS: () => GENERATORS,
|
|
12717
|
+
RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
|
|
12718
|
+
SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
|
|
12719
|
+
aiFill: () => aiFill,
|
|
12720
|
+
currentResolution: () => currentResolution,
|
|
12721
|
+
extend: () => extend,
|
|
12722
|
+
fieldsOrDefault: () => fieldsOrDefault,
|
|
12723
|
+
generate: () => generate,
|
|
12724
|
+
generateMigration: () => generateMigration,
|
|
12725
|
+
generateProgrammatic: () => generateProgrammatic,
|
|
12726
|
+
parseCliArgs: () => parseCliArgs,
|
|
12727
|
+
parseEvery: () => parseEvery,
|
|
12728
|
+
parseFields: () => parseFields,
|
|
12729
|
+
pluralizeReserved: () => pluralizeReserved,
|
|
12730
|
+
toPascal: () => toPascal,
|
|
12731
|
+
toSnake: () => toSnake,
|
|
12732
|
+
toTableName: () => toTableName
|
|
12733
|
+
});
|
|
12734
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
|
|
12735
|
+
import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
|
|
12736
|
+
function ensureDir(dir) {
|
|
12737
|
+
if (__resolution.dryRun) return;
|
|
12738
|
+
if (!existsSync13(dir)) {
|
|
12739
|
+
mkdirSync8(dir, { recursive: true });
|
|
12740
|
+
}
|
|
12741
|
+
}
|
|
12742
|
+
function writeFileSafe(path8, content) {
|
|
12743
|
+
captureEditHints(path8, content);
|
|
12744
|
+
if (__resolution.dryRun) {
|
|
12745
|
+
return;
|
|
12746
|
+
}
|
|
12747
|
+
if (existsSync13(path8)) {
|
|
12748
|
+
if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
|
|
12749
|
+
return;
|
|
12750
|
+
}
|
|
12751
|
+
writeFileSync7(path8, content, "utf-8");
|
|
12752
|
+
__resolution.actionsTaken.push(`wrote ${path8}`);
|
|
12753
|
+
if (!__resolution.jsonMode) console.log(` Created ${path8}`);
|
|
12754
|
+
}
|
|
12755
|
+
function toSnake(name) {
|
|
12756
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
12757
|
+
}
|
|
12758
|
+
function pluralizeReserved(name) {
|
|
12759
|
+
if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
|
|
12760
|
+
if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
|
|
12761
|
+
return name + "s";
|
|
12762
|
+
}
|
|
12763
|
+
function toTableName(name) {
|
|
12764
|
+
const raw = toSnake(name);
|
|
12765
|
+
if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
|
|
12766
|
+
const safe = pluralizeReserved(raw);
|
|
12767
|
+
recordTransformation({
|
|
12768
|
+
kind: "reserved_word_pluralize",
|
|
12769
|
+
from: raw,
|
|
12770
|
+
to: safe,
|
|
12771
|
+
reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
|
|
12772
|
+
override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
|
|
12773
|
+
});
|
|
12774
|
+
return safe;
|
|
12775
|
+
}
|
|
12776
|
+
return raw;
|
|
12777
|
+
}
|
|
12778
|
+
function resetResolution(target, input, opts) {
|
|
12779
|
+
__resolution.target = target;
|
|
12780
|
+
__resolution.input = input;
|
|
12781
|
+
__resolution.body = { transformations: [] };
|
|
12782
|
+
__resolution.actionsTaken = [];
|
|
12783
|
+
__resolution.dryRun = opts.dryRun;
|
|
12784
|
+
__resolution.jsonMode = opts.jsonMode;
|
|
12785
|
+
}
|
|
12786
|
+
function recordTransformation(t) {
|
|
12787
|
+
__resolution.body.transformations.push(t);
|
|
12788
|
+
}
|
|
12789
|
+
function currentResolution() {
|
|
12790
|
+
const body = {
|
|
12791
|
+
...__resolution.body,
|
|
12792
|
+
transformations: [...__resolution.body.transformations]
|
|
12793
|
+
};
|
|
12794
|
+
if (__resolution.body.edit_hints) {
|
|
12795
|
+
body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
|
|
12796
|
+
}
|
|
12797
|
+
if (__resolution.body.next) {
|
|
12798
|
+
body.next = [...__resolution.body.next];
|
|
12799
|
+
}
|
|
12800
|
+
if (__resolution.body.test_paths) {
|
|
12801
|
+
body.test_paths = [...__resolution.body.test_paths];
|
|
12802
|
+
}
|
|
12803
|
+
if (__resolution.body.routes) {
|
|
12804
|
+
body.routes = [...__resolution.body.routes];
|
|
12805
|
+
}
|
|
12806
|
+
return {
|
|
12807
|
+
command: "generate",
|
|
12808
|
+
target: __resolution.target,
|
|
12809
|
+
input: { ...__resolution.input },
|
|
12810
|
+
resolution: body,
|
|
12811
|
+
actions_taken: [...__resolution.actionsTaken],
|
|
12812
|
+
dry_run: __resolution.dryRun
|
|
12813
|
+
};
|
|
12814
|
+
}
|
|
12815
|
+
function setResolutionField(key, value) {
|
|
12816
|
+
__resolution.body[key] = value;
|
|
12817
|
+
}
|
|
12818
|
+
function pushRoute(routePattern) {
|
|
12819
|
+
if (!__resolution.body.routes) __resolution.body.routes = [];
|
|
12820
|
+
__resolution.body.routes.push(routePattern);
|
|
12821
|
+
}
|
|
12822
|
+
function pushTestPath(path8) {
|
|
12823
|
+
if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
|
|
12824
|
+
__resolution.body.test_paths.push(path8);
|
|
12825
|
+
}
|
|
12826
|
+
function pushEditHint(hint) {
|
|
12827
|
+
if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
|
|
12828
|
+
__resolution.body.edit_hints.push(hint);
|
|
12829
|
+
}
|
|
12830
|
+
function setNextSteps(steps) {
|
|
12831
|
+
if (steps.length === 0) return;
|
|
12832
|
+
__resolution.body.next = [...steps];
|
|
12833
|
+
}
|
|
12834
|
+
function toRelPath(absPath) {
|
|
12835
|
+
const cwd = process.cwd();
|
|
12836
|
+
const rel = relative2(cwd, absPath);
|
|
12837
|
+
if (!rel) return absPath;
|
|
12838
|
+
return sep3 === "/" ? rel : rel.split(sep3).join("/");
|
|
12839
|
+
}
|
|
12840
|
+
function captureEditHints(absPath, content) {
|
|
12841
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
12842
|
+
const relPath = toRelPath(absPath);
|
|
12843
|
+
const lines = content.split("\n");
|
|
12844
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12845
|
+
const match = TINA4_EDIT_MARKER.exec(lines[i]);
|
|
12846
|
+
if (match) {
|
|
12847
|
+
pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
|
|
12848
|
+
}
|
|
12849
|
+
}
|
|
12850
|
+
}
|
|
12851
|
+
function printResolution() {
|
|
12852
|
+
if (__resolution.jsonMode) {
|
|
12853
|
+
process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
|
|
12854
|
+
return;
|
|
12855
|
+
}
|
|
12856
|
+
const b = __resolution.body;
|
|
12857
|
+
const lines = [];
|
|
12858
|
+
lines.push("");
|
|
12859
|
+
lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
|
|
12860
|
+
if (b.class_name || b.file_path) {
|
|
12861
|
+
const where = b.file_path ? ` (in ${b.file_path})` : "";
|
|
12862
|
+
lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
|
|
12863
|
+
}
|
|
12864
|
+
if (b.table_name) {
|
|
12865
|
+
const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
|
|
12866
|
+
const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
|
|
12867
|
+
lines.push(` table ${b.table_name}${note}`);
|
|
12868
|
+
}
|
|
12869
|
+
if (b.routes && b.routes.length) {
|
|
12870
|
+
lines.push(` routes ${b.routes.join(", ")}`);
|
|
12871
|
+
}
|
|
12872
|
+
if (b.migration_path) {
|
|
12873
|
+
lines.push(` migration ${b.migration_path}`);
|
|
12874
|
+
}
|
|
12875
|
+
const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
|
|
12876
|
+
if (reserved && reserved.from && reserved.override) {
|
|
12877
|
+
lines.push("");
|
|
12878
|
+
lines.push(` To keep the raw name '${reserved.from}' as the table:`);
|
|
12879
|
+
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
|
|
12880
|
+
}
|
|
12881
|
+
if (b.test_paths && b.test_paths.length > 0) {
|
|
12882
|
+
lines.push("");
|
|
12883
|
+
lines.push(" Tests:");
|
|
12884
|
+
for (const testPath of b.test_paths) lines.push(` ${testPath}`);
|
|
12885
|
+
}
|
|
12886
|
+
if (b.edit_hints && b.edit_hints.length > 0) {
|
|
12887
|
+
lines.push("");
|
|
12888
|
+
lines.push(" Edit these lines:");
|
|
12889
|
+
for (const hint of b.edit_hints) {
|
|
12890
|
+
lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
|
|
12891
|
+
}
|
|
12892
|
+
}
|
|
12893
|
+
if (b.next && b.next.length > 0) {
|
|
12894
|
+
lines.push("");
|
|
12895
|
+
lines.push(" Next:");
|
|
12896
|
+
for (const step of b.next) lines.push(` ${step}`);
|
|
12897
|
+
}
|
|
12898
|
+
lines.push("");
|
|
12899
|
+
process.stderr.write(lines.join("\n"));
|
|
12900
|
+
}
|
|
12901
|
+
function toPlural(name) {
|
|
12902
|
+
const lower = name.toLowerCase();
|
|
12903
|
+
if (lower.endsWith("s")) return lower;
|
|
12904
|
+
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
|
|
12905
|
+
return lower + "s";
|
|
12906
|
+
}
|
|
12907
|
+
function toCamel(name) {
|
|
12908
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
12909
|
+
}
|
|
12910
|
+
function toPascal(name) {
|
|
12911
|
+
return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
12912
|
+
}
|
|
12913
|
+
function parseFields(fieldsStr) {
|
|
12914
|
+
if (!fieldsStr || !fieldsStr.trim()) return [];
|
|
12915
|
+
const result = [];
|
|
12916
|
+
for (const part of fieldsStr.split(",")) {
|
|
12917
|
+
const trimmed = part.trim();
|
|
12918
|
+
if (trimmed.includes(":")) {
|
|
12919
|
+
const [fname, ftype] = trimmed.split(":", 2);
|
|
12920
|
+
if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
|
|
12921
|
+
} else if (trimmed) {
|
|
12922
|
+
result.push([trimmed, "string"]);
|
|
12923
|
+
}
|
|
12924
|
+
}
|
|
12925
|
+
return result;
|
|
12926
|
+
}
|
|
12927
|
+
function fieldsOrDefault(fieldsStr) {
|
|
12928
|
+
const parsed = parseFields(fieldsStr);
|
|
12929
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
12930
|
+
}
|
|
12931
|
+
function parseCliArgs(args) {
|
|
12932
|
+
const booleanFlags = /* @__PURE__ */ new Set([
|
|
12933
|
+
"no-browser",
|
|
12934
|
+
"no-reload",
|
|
12935
|
+
"production",
|
|
12936
|
+
"managed",
|
|
12937
|
+
"all",
|
|
12938
|
+
"clear",
|
|
12939
|
+
"public",
|
|
12940
|
+
"no-migration",
|
|
12941
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
12942
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
12943
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
12944
|
+
"no-test",
|
|
12945
|
+
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
12946
|
+
"json",
|
|
12947
|
+
"dry-run"
|
|
12948
|
+
]);
|
|
12949
|
+
const flags = {};
|
|
12950
|
+
const positional = [];
|
|
12951
|
+
let i = 0;
|
|
12952
|
+
while (i < args.length) {
|
|
12953
|
+
if (args[i].startsWith("--")) {
|
|
12954
|
+
const key = args[i].slice(2);
|
|
12955
|
+
if (booleanFlags.has(key)) {
|
|
12956
|
+
flags[key] = true;
|
|
12957
|
+
i += 1;
|
|
12958
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
12959
|
+
flags[key] = args[i + 1];
|
|
12960
|
+
i += 2;
|
|
12961
|
+
} else {
|
|
12962
|
+
flags[key] = true;
|
|
12963
|
+
i += 1;
|
|
12964
|
+
}
|
|
12965
|
+
} else {
|
|
12966
|
+
positional.push(args[i]);
|
|
12967
|
+
i += 1;
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
return { flags, positional };
|
|
12971
|
+
}
|
|
12972
|
+
function parseEvery(every) {
|
|
12973
|
+
if (!every || every === true) return 60;
|
|
12974
|
+
const s = String(every).trim().toLowerCase();
|
|
12975
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
12976
|
+
const unit = s.slice(-1);
|
|
12977
|
+
if (unit in units) {
|
|
12978
|
+
const n2 = parseFloat(s.slice(0, -1));
|
|
12979
|
+
return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
|
|
12980
|
+
}
|
|
12981
|
+
const n = parseFloat(s);
|
|
12982
|
+
return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
|
|
12983
|
+
}
|
|
12984
|
+
function aiFill(fn, spec, indent = " ") {
|
|
12985
|
+
const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
|
|
12986
|
+
const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
|
|
12987
|
+
lines.push(`${indent}// Intent: ${spec.intent}`);
|
|
12988
|
+
if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
|
|
12989
|
+
lines.push(`${indent}// Use: ${spec.use}`);
|
|
12990
|
+
if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
|
|
12991
|
+
lines.push(`${indent}// Ground: ${spec.ground}`);
|
|
12992
|
+
lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
|
|
12993
|
+
lines.push(`${indent}// ${"\u2500".repeat(52)}`);
|
|
12994
|
+
return lines.join("\n") + "\n";
|
|
12995
|
+
}
|
|
12996
|
+
function extend(note, hint = "", indent = " ") {
|
|
12997
|
+
let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
|
|
12998
|
+
`;
|
|
12999
|
+
if (hint) out += `${indent}// ${hint}
|
|
13000
|
+
`;
|
|
13001
|
+
return out;
|
|
13002
|
+
}
|
|
13003
|
+
function timestamp() {
|
|
13004
|
+
const now = /* @__PURE__ */ new Date();
|
|
13005
|
+
return now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
|
|
13006
|
+
}
|
|
13007
|
+
function isoNow() {
|
|
13008
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
13009
|
+
}
|
|
13010
|
+
async function generate(what, name, extraArgs = []) {
|
|
13011
|
+
if (!what) {
|
|
13012
|
+
console.error(" Usage: tina4nodejs generate <what> <name> [options]");
|
|
13013
|
+
console.error(` Generators: ${GENERATOR_LIST}`);
|
|
13014
|
+
console.error(' Options: --fields "name:string,price:float" --model ModelName');
|
|
13015
|
+
console.error(" --public open a route's writes (default: secure)");
|
|
13016
|
+
console.error(' --every 5m | --cron "\u2026" service schedule');
|
|
13017
|
+
console.error(" --json emit machine-readable resolution envelope on stdout");
|
|
13018
|
+
console.error(" --dry-run report resolution without writing any files");
|
|
13019
|
+
process.exit(1);
|
|
13020
|
+
}
|
|
13021
|
+
const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
|
|
13022
|
+
if (noNameGenerators.has(what) && name.startsWith("--")) {
|
|
13023
|
+
extraArgs = [name, ...extraArgs];
|
|
13024
|
+
name = "";
|
|
13025
|
+
}
|
|
13026
|
+
if (!noNameGenerators.has(what) && !name) {
|
|
13027
|
+
console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
|
|
13028
|
+
process.exit(1);
|
|
13029
|
+
}
|
|
13030
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
13031
|
+
const jsonMode = Boolean(flags.json);
|
|
13032
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
13033
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
|
|
13034
|
+
const spec = GENERATORS[what];
|
|
13035
|
+
if (spec) {
|
|
13036
|
+
spec.handler(name, flags);
|
|
13037
|
+
} else {
|
|
13038
|
+
console.error(` Unknown generator: ${what}`);
|
|
13039
|
+
console.error(` Available: ${GENERATOR_LIST}`);
|
|
13040
|
+
process.exit(1);
|
|
13041
|
+
}
|
|
13042
|
+
const nextFn = NEXT_STEPS[what];
|
|
13043
|
+
if (nextFn) {
|
|
13044
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
13045
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
13046
|
+
}
|
|
13047
|
+
printResolution();
|
|
13048
|
+
}
|
|
13049
|
+
async function generateProgrammatic(what, name, extraArgs = []) {
|
|
13050
|
+
const spec = GENERATORS[what];
|
|
13051
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
13052
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
13053
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
13054
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
|
|
13055
|
+
spec.handler(name, flags);
|
|
13056
|
+
const nextFn = NEXT_STEPS[what];
|
|
13057
|
+
if (nextFn) {
|
|
13058
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
13059
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
13060
|
+
}
|
|
13061
|
+
return currentResolution();
|
|
13062
|
+
}
|
|
13063
|
+
function generateModel(name, flags, emitTest = true) {
|
|
13064
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13065
|
+
const table2 = toTableName(name);
|
|
13066
|
+
const dir = resolve6("src/models");
|
|
13067
|
+
ensureDir(dir);
|
|
13068
|
+
const path8 = join15(dir, `${name}.ts`);
|
|
13069
|
+
setResolutionField("class_name", name);
|
|
13070
|
+
setResolutionField("table_name", table2);
|
|
13071
|
+
setResolutionField("file_path", `src/models/${name}.ts`);
|
|
13072
|
+
pushTestPath(`tests/${table2}_model.test.ts`);
|
|
13073
|
+
const fieldLines = [
|
|
13074
|
+
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
13075
|
+
` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
|
|
13076
|
+
];
|
|
13077
|
+
for (const [fname, ftype] of fields) {
|
|
13078
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
13079
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
13080
|
+
}
|
|
13081
|
+
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
13082
|
+
const content = `import { BaseModel } from "tina4-nodejs/orm";
|
|
13083
|
+
|
|
13084
|
+
export default class ${name} extends BaseModel {
|
|
13085
|
+
static tableName = "${table2}";
|
|
13086
|
+
static fields = {
|
|
13087
|
+
${fieldLines.join("\n")}
|
|
13088
|
+
};
|
|
13089
|
+
}
|
|
13090
|
+
`;
|
|
13091
|
+
writeFileSafe(path8, content);
|
|
13092
|
+
if (!flags["no-migration"]) {
|
|
13093
|
+
generateMigration(`create_${table2}`, flags, fields, table2, false);
|
|
13094
|
+
}
|
|
13095
|
+
if (emitTest) emitModelTest(name, table2, fields);
|
|
13096
|
+
}
|
|
13097
|
+
function secureOptOut(isPublic) {
|
|
13098
|
+
return isPublic ? `export const secure = false;
|
|
13099
|
+
|
|
13100
|
+
` : "";
|
|
13101
|
+
}
|
|
13102
|
+
function generateRoute(name, flags, emitTest = true) {
|
|
13103
|
+
const routePath = name.replace(/^\//, "");
|
|
13104
|
+
const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
|
|
13105
|
+
const model = flags.model;
|
|
13106
|
+
const isPublic = Boolean(flags.public);
|
|
13107
|
+
const base = resolve6("src/routes/api", routePath);
|
|
13108
|
+
const idDir = join15(base, "[id]");
|
|
13109
|
+
ensureDir(base);
|
|
13110
|
+
ensureDir(idDir);
|
|
13111
|
+
pushRoute(`/api/${routePath}`);
|
|
13112
|
+
pushRoute(`/api/${routePath}/{id}`);
|
|
13113
|
+
if (__resolution.target === "route") {
|
|
13114
|
+
setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
|
|
13115
|
+
}
|
|
13116
|
+
const table2 = model ? toTableName(model) : "";
|
|
13117
|
+
const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
|
|
13118
|
+
` : "";
|
|
13119
|
+
const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
|
|
13120
|
+
` : "";
|
|
13121
|
+
const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
|
|
13122
|
+
if (model) {
|
|
13123
|
+
writeFileSafe(
|
|
13124
|
+
join15(base, "get.ts"),
|
|
13125
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13126
|
+
${modelImportBase}
|
|
13127
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
13128
|
+
|
|
13129
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13130
|
+
// tina4:edit tune pagination defaults or add filter/sort parsing here
|
|
13131
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
13132
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
13133
|
+
const offset = (page - 1) * limit;
|
|
13134
|
+
const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
|
|
13135
|
+
res.json({ data: rows.map((r) => r.toObject()), page, limit });
|
|
13136
|
+
}
|
|
13137
|
+
`
|
|
13138
|
+
);
|
|
13139
|
+
} else {
|
|
13140
|
+
writeFileSafe(
|
|
13141
|
+
join15(base, "get.ts"),
|
|
13142
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13143
|
+
|
|
13144
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
13145
|
+
|
|
13146
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13147
|
+
${aiFill(`list_${routePath}`, {
|
|
13148
|
+
intent: `return the ${routePath} collection (add pagination if it grows)`,
|
|
13149
|
+
given: "req.query -> filters/paging",
|
|
13150
|
+
use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
|
|
13151
|
+
ret: "res.json({ data: rows })",
|
|
13152
|
+
ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13153
|
+
raise: `${routePath} list not implemented`
|
|
13154
|
+
})}}
|
|
13155
|
+
`
|
|
13156
|
+
);
|
|
13157
|
+
}
|
|
13158
|
+
if (model) {
|
|
13159
|
+
writeFileSafe(
|
|
13160
|
+
join15(base, "post.ts"),
|
|
13161
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13162
|
+
${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
13163
|
+
|
|
13164
|
+
// ${writeDoc}
|
|
13165
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13166
|
+
// tina4:edit validate the body before persist (Validator or hand-checks)
|
|
13167
|
+
${extend(
|
|
13168
|
+
"validate / business rules before persist",
|
|
13169
|
+
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
|
|
13170
|
+
)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
13171
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
13172
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
13173
|
+
if ((await item.save()) === false) {
|
|
13174
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
13175
|
+
return;
|
|
13176
|
+
}
|
|
13177
|
+
res.json({ data: item.toObject() }, 201);
|
|
13178
|
+
}
|
|
13179
|
+
`
|
|
13180
|
+
);
|
|
13181
|
+
} else {
|
|
13182
|
+
writeFileSafe(
|
|
13183
|
+
join15(base, "post.ts"),
|
|
13184
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13185
|
+
|
|
13186
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
13187
|
+
|
|
13188
|
+
// ${writeDoc}
|
|
13189
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13190
|
+
// tina4:edit fill the create handler (see AI-FILL fill-spec below)
|
|
13191
|
+
${aiFill(`create_${singular}`, {
|
|
13192
|
+
intent: `validate the body and persist a new ${singular}`,
|
|
13193
|
+
given: "req.body -> the posted fields",
|
|
13194
|
+
use: "new Model(req.body).save() then item.toObject() (import your model)",
|
|
13195
|
+
ret: "res.json({ data: item }, 201)",
|
|
13196
|
+
ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13197
|
+
raise: `create ${singular} not implemented`
|
|
13198
|
+
})}}
|
|
13199
|
+
`
|
|
13200
|
+
);
|
|
13201
|
+
}
|
|
13202
|
+
if (model) {
|
|
13203
|
+
writeFileSafe(
|
|
13204
|
+
join15(idDir, "get.ts"),
|
|
13205
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13206
|
+
${modelImportId}
|
|
13207
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
13208
|
+
|
|
13209
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13210
|
+
const { id } = req.params;
|
|
13211
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13212
|
+
if (!item) {
|
|
13213
|
+
res.json({ error: "Not found" }, 404);
|
|
13214
|
+
return;
|
|
13215
|
+
}
|
|
13216
|
+
res.json({ data: item.toObject() });
|
|
13217
|
+
}
|
|
13218
|
+
`
|
|
13219
|
+
);
|
|
13220
|
+
} else {
|
|
13221
|
+
writeFileSafe(
|
|
13222
|
+
join15(idDir, "get.ts"),
|
|
13223
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13224
|
+
|
|
13225
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
13226
|
+
|
|
13227
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13228
|
+
${aiFill(`get_${singular}`, {
|
|
13229
|
+
intent: `fetch one ${singular} by id`,
|
|
13230
|
+
given: "req.params.id -> the record id",
|
|
13231
|
+
use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
|
|
13232
|
+
ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
|
|
13233
|
+
ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13234
|
+
raise: `get ${singular} not implemented`
|
|
13235
|
+
})}}
|
|
13236
|
+
`
|
|
13237
|
+
);
|
|
13238
|
+
}
|
|
13239
|
+
if (model) {
|
|
13240
|
+
writeFileSafe(
|
|
13241
|
+
join15(idDir, "put.ts"),
|
|
13242
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13243
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
13244
|
+
|
|
13245
|
+
// ${writeDoc}
|
|
13246
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13247
|
+
const { id } = req.params;
|
|
13248
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13249
|
+
if (!item) {
|
|
13250
|
+
res.json({ error: "Not found" }, 404);
|
|
13251
|
+
return;
|
|
13252
|
+
}
|
|
13253
|
+
// tina4:edit guard which fields may be updated and who may update this row
|
|
13254
|
+
${extend(
|
|
13255
|
+
"guard which fields / who may update",
|
|
13256
|
+
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
|
|
13257
|
+
)} Object.assign(item, req.body as Record<string, unknown>);
|
|
13258
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
13259
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
13260
|
+
if ((await item.save()) === false) {
|
|
13261
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
13262
|
+
return;
|
|
13263
|
+
}
|
|
13264
|
+
res.json({ data: item.toObject() });
|
|
13265
|
+
}
|
|
13266
|
+
`
|
|
13267
|
+
);
|
|
13268
|
+
} else {
|
|
13269
|
+
writeFileSafe(
|
|
13270
|
+
join15(idDir, "put.ts"),
|
|
13271
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13272
|
+
|
|
13273
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
13274
|
+
|
|
13275
|
+
// ${writeDoc}
|
|
13276
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13277
|
+
// tina4:edit fill the update handler (see AI-FILL fill-spec below)
|
|
13278
|
+
${aiFill(`update_${singular}`, {
|
|
13279
|
+
intent: `load, mutate and save an existing ${singular}`,
|
|
13280
|
+
given: "req.params.id -> id; req.body -> changed fields",
|
|
13281
|
+
use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
|
|
13282
|
+
ret: "res.json({ data: item }) or 404",
|
|
13283
|
+
ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13284
|
+
raise: `update ${singular} not implemented`
|
|
13285
|
+
})}}
|
|
13286
|
+
`
|
|
13287
|
+
);
|
|
13288
|
+
}
|
|
13289
|
+
if (model) {
|
|
13290
|
+
writeFileSafe(
|
|
13291
|
+
join15(idDir, "delete.ts"),
|
|
13292
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13293
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
13294
|
+
|
|
13295
|
+
// ${writeDoc}
|
|
13296
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13297
|
+
const { id } = req.params;
|
|
13298
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13299
|
+
if (!item) {
|
|
13300
|
+
res.json({ error: "Not found" }, 404);
|
|
13301
|
+
return;
|
|
13302
|
+
}
|
|
13303
|
+
await item.delete();
|
|
13304
|
+
res.json({ message: "deleted", id });
|
|
13305
|
+
}
|
|
13306
|
+
`
|
|
13307
|
+
);
|
|
13308
|
+
} else {
|
|
13309
|
+
writeFileSafe(
|
|
13310
|
+
join15(idDir, "delete.ts"),
|
|
13311
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13312
|
+
|
|
13313
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
13314
|
+
|
|
13315
|
+
// ${writeDoc}
|
|
13316
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13317
|
+
${aiFill(`delete_${singular}`, {
|
|
13318
|
+
intent: `delete a ${singular} by id`,
|
|
13319
|
+
given: "req.params.id -> id",
|
|
13320
|
+
use: "Model.selectOne(\u2026) then item.delete()",
|
|
13321
|
+
ret: "res.json({ message: 'deleted', id }) or 404",
|
|
13322
|
+
ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13323
|
+
raise: `delete ${singular} not implemented`
|
|
13324
|
+
})}}
|
|
13325
|
+
`
|
|
13326
|
+
);
|
|
13327
|
+
}
|
|
13328
|
+
if (emitTest) {
|
|
13329
|
+
if (model) {
|
|
13330
|
+
generateTest(routePath, { model, "secure-writes": true, public: isPublic });
|
|
13331
|
+
} else {
|
|
13332
|
+
emitRouteStubTest(routePath);
|
|
13333
|
+
}
|
|
13334
|
+
}
|
|
13335
|
+
}
|
|
13336
|
+
function generateCrud(name, flags) {
|
|
13337
|
+
const table2 = toTableName(name);
|
|
13338
|
+
const routeName = toPlural(table2);
|
|
13339
|
+
const isPublic = Boolean(flags.public);
|
|
13340
|
+
if (!__resolution.jsonMode) console.log(`
|
|
13341
|
+
Generating CRUD for ${name}...
|
|
13342
|
+
`);
|
|
13343
|
+
generateModel(name, flags, false);
|
|
13344
|
+
generateRoute(routeName, { ...flags, model: name }, false);
|
|
13345
|
+
generateForm(name, flags);
|
|
13346
|
+
generateView(name, flags);
|
|
13347
|
+
generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
|
|
13348
|
+
if (!__resolution.jsonMode) {
|
|
13349
|
+
console.log(`
|
|
13350
|
+
CRUD generation complete for ${name}.`);
|
|
13351
|
+
console.log(" Run: tina4nodejs migrate");
|
|
13352
|
+
console.log(" Visit: /swagger to see the API docs");
|
|
13353
|
+
}
|
|
13354
|
+
}
|
|
13355
|
+
function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
|
|
13356
|
+
const ts = timestamp();
|
|
13357
|
+
const dir = resolve6("migrations");
|
|
13358
|
+
ensureDir(dir);
|
|
13359
|
+
let table2;
|
|
13360
|
+
if (tableOverride) {
|
|
13361
|
+
table2 = tableOverride;
|
|
13362
|
+
} else {
|
|
13363
|
+
const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
|
|
13364
|
+
table2 = toTableName(raw);
|
|
13365
|
+
}
|
|
13366
|
+
if (__resolution.target === "migration") {
|
|
13367
|
+
setResolutionField("table_name", table2);
|
|
13368
|
+
}
|
|
13369
|
+
const fields = fieldsOverride || parseFields(flags.fields || "");
|
|
13370
|
+
const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
|
|
13371
|
+
const fileName = `${ts}_${name}.sql`;
|
|
13372
|
+
const path8 = join15(dir, fileName);
|
|
13373
|
+
setResolutionField("migration_path", `migrations/${fileName}`);
|
|
13374
|
+
if (__resolution.target === "migration") {
|
|
13375
|
+
setResolutionField("file_path", `migrations/${fileName}`);
|
|
13376
|
+
pushTestPath(`tests/${table2}_migration.test.ts`);
|
|
13377
|
+
}
|
|
13378
|
+
let upSql;
|
|
13379
|
+
let downSql;
|
|
13380
|
+
if (isCreate) {
|
|
13381
|
+
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
13382
|
+
for (const [fname, ftype] of fields) {
|
|
13383
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
13384
|
+
const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
|
|
13385
|
+
colLines.push(` ${fname} ${info.sql}${defaultClause}`);
|
|
13386
|
+
}
|
|
13387
|
+
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
13388
|
+
upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
|
|
13389
|
+
-- tina4:edit add columns beyond id + created_at
|
|
13390
|
+
${colLines.join(",\n")}
|
|
13391
|
+
);`;
|
|
13392
|
+
downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
|
|
13393
|
+
DROP TABLE IF EXISTS ${table2};`;
|
|
13394
|
+
} else {
|
|
13395
|
+
upSql = `-- tina4:edit write your UP migration SQL here
|
|
13396
|
+
-- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
13397
|
+
downSql = `-- tina4:edit write your DOWN rollback SQL here
|
|
13398
|
+
-- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
|
|
13399
|
+
}
|
|
13400
|
+
const now = isoNow();
|
|
13401
|
+
const content = `-- Migration: ${name}
|
|
13402
|
+
-- Created: ${now}
|
|
13403
|
+
|
|
13404
|
+
-- UP
|
|
13405
|
+
${upSql}
|
|
13406
|
+
|
|
13407
|
+
-- DOWN
|
|
13408
|
+
${downSql}
|
|
13409
|
+
`;
|
|
13410
|
+
writeFileSafe(path8, content);
|
|
13411
|
+
const downPath = join15(dir, `${ts}_${name}.down.sql`);
|
|
13412
|
+
const downContent = `-- Rollback: ${name}
|
|
13413
|
+
-- Created: ${now}
|
|
13414
|
+
|
|
13415
|
+
${downSql}
|
|
13416
|
+
`;
|
|
13417
|
+
writeFileSafe(downPath, downContent);
|
|
13418
|
+
if (emitTest && isCreate) emitMigrationTest(name, table2);
|
|
13419
|
+
}
|
|
13420
|
+
function generateMiddleware(name, _flags) {
|
|
13421
|
+
const snake = toSnake(name);
|
|
13422
|
+
const dir = resolve6("src/middleware");
|
|
13423
|
+
ensureDir(dir);
|
|
13424
|
+
const path8 = join15(dir, `${snake}.ts`);
|
|
13425
|
+
if (__resolution.target === "middleware") {
|
|
13426
|
+
setResolutionField("class_name", name);
|
|
13427
|
+
setResolutionField("file_path", `src/middleware/${snake}.ts`);
|
|
13428
|
+
pushTestPath(`tests/${snake}.test.ts`);
|
|
13429
|
+
}
|
|
13430
|
+
const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13431
|
+
|
|
13432
|
+
/**
|
|
13433
|
+
* ${name} middleware \u2014 runs before and after route handlers.
|
|
13434
|
+
*
|
|
13435
|
+
* Usage:
|
|
13436
|
+
* import { before${name}, after${name} } from "../middleware/${snake}.js";
|
|
13437
|
+
*/
|
|
13438
|
+
|
|
13439
|
+
export async function before${name}(
|
|
13440
|
+
req: Tina4Request,
|
|
13441
|
+
res: Tina4Response,
|
|
13442
|
+
next: () => Promise<void>,
|
|
13443
|
+
): Promise<void> {
|
|
13444
|
+
// tina4:edit replace the Authorization check with the real pre-request rule
|
|
13445
|
+
const auth = req.headers["authorization"];
|
|
13446
|
+
if (!auth) {
|
|
13447
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
13448
|
+
return;
|
|
13449
|
+
}
|
|
13450
|
+
await next();
|
|
13451
|
+
}
|
|
13452
|
+
|
|
13453
|
+
export async function after${name}(
|
|
13454
|
+
req: Tina4Request,
|
|
13455
|
+
res: Tina4Response,
|
|
13456
|
+
next: () => Promise<void>,
|
|
13457
|
+
): Promise<void> {
|
|
13458
|
+
// tina4:edit add post-processing (logging, header injection, telemetry)
|
|
13459
|
+
await next();
|
|
13460
|
+
}
|
|
13461
|
+
`;
|
|
13462
|
+
writeFileSafe(path8, content);
|
|
13463
|
+
emitMiddlewareTest(name, snake);
|
|
13464
|
+
}
|
|
13465
|
+
function generateTest(name, flags) {
|
|
13466
|
+
const snake = toSnake(name);
|
|
13467
|
+
const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
|
|
13468
|
+
const model = flags.model;
|
|
13469
|
+
const dir = resolve6("tests");
|
|
13470
|
+
ensureDir(dir);
|
|
13471
|
+
const path8 = join15(dir, `${snake}.test.ts`);
|
|
13472
|
+
if (model && flags["secure-writes"]) {
|
|
13473
|
+
const isPublic = Boolean(flags.public);
|
|
13474
|
+
const posture = isPublic ? "open (--public)" : "gated";
|
|
13475
|
+
const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
|
|
13476
|
+
assert("anonymous POST is public -> 201",
|
|
13477
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
|
|
13478
|
+
assert("anonymous POST is gated -> 401",
|
|
13479
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
|
|
13480
|
+
// A valid Bearer token passes the gate and creates -> 201.
|
|
13481
|
+
const token = getToken({ userId: 1 });
|
|
13482
|
+
assert("authenticated POST creates -> 201",
|
|
13483
|
+
(await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
|
|
13484
|
+
const content2 = `/**
|
|
13485
|
+
* ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
|
|
13486
|
+
*
|
|
13487
|
+
* Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
|
|
13488
|
+
* JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
|
|
13489
|
+
*/
|
|
13490
|
+
import { dirname, resolve } from "node:path";
|
|
13491
|
+
import { fileURLToPath } from "node:url";
|
|
13492
|
+
import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
13493
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
13494
|
+
import ${model} from "../src/models/${model}.js";
|
|
13495
|
+
|
|
13496
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
13497
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13498
|
+
|
|
13499
|
+
let pass = 0;
|
|
13500
|
+
let fail = 0;
|
|
13501
|
+
function assert(label: string, ok: boolean): void {
|
|
13502
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
13503
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
13504
|
+
}
|
|
13505
|
+
|
|
13506
|
+
await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
|
|
13507
|
+
await ${model}.createTable();
|
|
13508
|
+
|
|
13509
|
+
const router = new Router();
|
|
13510
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
13511
|
+
const client = new TestClient(router);
|
|
13512
|
+
|
|
13513
|
+
// Reads are public.
|
|
13514
|
+
assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
|
|
13515
|
+
${writeCase}
|
|
13516
|
+
|
|
13517
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
13518
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
13519
|
+
`;
|
|
13520
|
+
writeFileSafe(path8, content2);
|
|
13521
|
+
return;
|
|
13522
|
+
}
|
|
13523
|
+
let content;
|
|
13524
|
+
if (model) {
|
|
13525
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
13526
|
+
|
|
13527
|
+
/**
|
|
13528
|
+
* Tests for ${name} CRUD operations.
|
|
13529
|
+
*/
|
|
13530
|
+
|
|
13531
|
+
const list${model}s = tests(
|
|
13532
|
+
assertTrue([]),
|
|
13533
|
+
)(function list${model}s() {
|
|
13534
|
+
// tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
|
|
13535
|
+
return true;
|
|
13536
|
+
});
|
|
13537
|
+
|
|
13538
|
+
const get${model} = tests(
|
|
13539
|
+
assertTrue([]),
|
|
13540
|
+
)(function get${model}() {
|
|
13541
|
+
// tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
|
|
13542
|
+
return true;
|
|
13543
|
+
});
|
|
13544
|
+
|
|
13545
|
+
const create${model} = tests(
|
|
13546
|
+
assertTrue([]),
|
|
13547
|
+
)(function create${model}() {
|
|
13548
|
+
// tina4:edit POST a valid + an invalid body, assert 201 vs 400
|
|
13549
|
+
return true;
|
|
13550
|
+
});
|
|
13551
|
+
|
|
13552
|
+
const update${model} = tests(
|
|
13553
|
+
assertTrue([]),
|
|
13554
|
+
)(function update${model}() {
|
|
13555
|
+
// tina4:edit PUT changed fields, assert the row was persisted
|
|
13556
|
+
return true;
|
|
13557
|
+
});
|
|
13558
|
+
|
|
13559
|
+
const delete${model} = tests(
|
|
13560
|
+
assertTrue([]),
|
|
13561
|
+
)(function delete${model}() {
|
|
13562
|
+
// tina4:edit DELETE the id, assert 200 then GET returns 404
|
|
13563
|
+
return true;
|
|
13564
|
+
});
|
|
13565
|
+
|
|
13566
|
+
void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
|
|
13567
|
+
`;
|
|
13568
|
+
} else {
|
|
13569
|
+
const titleName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
13570
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
13571
|
+
|
|
13572
|
+
/**
|
|
13573
|
+
* Tests for ${name}.
|
|
13574
|
+
*/
|
|
13575
|
+
|
|
13576
|
+
const test${titleName} = tests(
|
|
13577
|
+
assertTrue([]),
|
|
13578
|
+
)(function test${titleName}() {
|
|
13579
|
+
// tina4:edit assert against the real behaviour under test (no mocks)
|
|
13580
|
+
return true;
|
|
13581
|
+
});
|
|
13582
|
+
|
|
13583
|
+
void test${titleName};
|
|
13584
|
+
`;
|
|
13585
|
+
}
|
|
13586
|
+
writeFileSafe(path8, content);
|
|
13587
|
+
}
|
|
13588
|
+
function generateForm(name, flags) {
|
|
13589
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13590
|
+
const table2 = toTableName(name);
|
|
13591
|
+
const routeName = toPlural(table2);
|
|
13592
|
+
const inputTypes = {
|
|
13593
|
+
string: "text",
|
|
13594
|
+
str: "text",
|
|
13595
|
+
text: "textarea",
|
|
13596
|
+
int: "number",
|
|
13597
|
+
integer: "number",
|
|
13598
|
+
float: "number",
|
|
13599
|
+
numeric: "number",
|
|
13600
|
+
decimal: "number",
|
|
13601
|
+
bool: "checkbox",
|
|
13602
|
+
boolean: "checkbox",
|
|
13603
|
+
datetime: "datetime-local",
|
|
13604
|
+
blob: "file"
|
|
13605
|
+
};
|
|
13606
|
+
const dir = resolve6("src/templates/forms");
|
|
13607
|
+
ensureDir(dir);
|
|
13608
|
+
const path8 = join15(dir, `${table2}.twig`);
|
|
13609
|
+
let fieldHtml = "";
|
|
13610
|
+
for (const [fname, ftype] of fields) {
|
|
13611
|
+
const itype = inputTypes[ftype] || "text";
|
|
13612
|
+
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
13613
|
+
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
13614
|
+
if (itype === "textarea") {
|
|
13615
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13616
|
+
<label for="${fname}">${label}</label>
|
|
13617
|
+
<textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
|
|
13618
|
+
</div>
|
|
13619
|
+
`;
|
|
13620
|
+
} else if (itype === "checkbox") {
|
|
13621
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13622
|
+
<label>
|
|
13623
|
+
<input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
|
|
13624
|
+
${label}
|
|
13625
|
+
</label>
|
|
13626
|
+
</div>
|
|
13627
|
+
`;
|
|
13628
|
+
} else {
|
|
13629
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13630
|
+
<label for="${fname}">${label}</label>
|
|
13631
|
+
<input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
|
|
13632
|
+
</div>
|
|
13633
|
+
`;
|
|
13634
|
+
}
|
|
13635
|
+
}
|
|
13636
|
+
const content = `{% extends "base.twig" %}
|
|
13637
|
+
{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
|
|
13638
|
+
{% block content %}
|
|
13639
|
+
<div class="container mt-4">
|
|
13640
|
+
<h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
|
|
13641
|
+
{# tina4:edit restyle the form beyond the scaffolded defaults #}
|
|
13642
|
+
<form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
|
|
13643
|
+
{{ form_token() }}
|
|
13644
|
+
` + fieldHtml + ` <button type="submit" class="btn btn-primary">
|
|
13645
|
+
{% if item.id %}Update{% else %}Create{% endif %}
|
|
13646
|
+
</button>
|
|
13647
|
+
<a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
|
|
13648
|
+
</form>
|
|
13649
|
+
</div>
|
|
13650
|
+
{% endblock %}
|
|
13651
|
+
`;
|
|
13652
|
+
writeFileSafe(path8, content);
|
|
13653
|
+
}
|
|
13654
|
+
function generateView(name, flags) {
|
|
13655
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13656
|
+
const table2 = toTableName(name);
|
|
13657
|
+
const routeName = toPlural(table2);
|
|
13658
|
+
const cols = fields.map(([f]) => f);
|
|
13659
|
+
const dir = resolve6("src/templates/pages");
|
|
13660
|
+
ensureDir(dir);
|
|
13661
|
+
const listPath = join15(dir, `${routeName}.twig`);
|
|
13662
|
+
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
13663
|
+
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
13664
|
+
const listContent = `{% extends "base.twig" %}
|
|
13665
|
+
{% block title %}${name}s{% endblock %}
|
|
13666
|
+
{% block content %}
|
|
13667
|
+
<div class="container mt-4">
|
|
13668
|
+
{# tina4:edit add sort / filter / pagination controls to the list #}
|
|
13669
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
13670
|
+
<h1>${name}s</h1>
|
|
13671
|
+
<a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
|
|
13672
|
+
</div>
|
|
13673
|
+
<table class="table">
|
|
13674
|
+
<thead>
|
|
13675
|
+
<tr>
|
|
13676
|
+
<th>ID</th>
|
|
13677
|
+
${th}
|
|
13678
|
+
<th>Actions</th>
|
|
13679
|
+
</tr>
|
|
13680
|
+
</thead>
|
|
13681
|
+
<tbody>
|
|
13682
|
+
{% for item in items %}
|
|
13683
|
+
<tr>
|
|
13684
|
+
<td>{{ item.id }}</td>
|
|
13685
|
+
${td}
|
|
13686
|
+
<td>
|
|
13687
|
+
<a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
|
|
13688
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
|
|
13689
|
+
</td>
|
|
13690
|
+
</tr>
|
|
13691
|
+
{% endfor %}
|
|
13692
|
+
</tbody>
|
|
13693
|
+
</table>
|
|
13694
|
+
</div>
|
|
13695
|
+
{% endblock %}
|
|
13696
|
+
`;
|
|
13697
|
+
writeFileSafe(listPath, listContent);
|
|
13698
|
+
const detailPath = join15(dir, `${table2}.twig`);
|
|
13699
|
+
const detailFields = cols.map((c) => ` <div class="mb-3"><strong>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}:</strong> {{ item.${c} }}</div>`).join("\n");
|
|
13700
|
+
const detailContent = `{% extends "base.twig" %}
|
|
13701
|
+
{% block title %}${name} Detail{% endblock %}
|
|
13702
|
+
{% block content %}
|
|
13703
|
+
<div class="container mt-4">
|
|
13704
|
+
{# tina4:edit extend the detail view with related records or actions #}
|
|
13705
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
13706
|
+
<h1>${name} #{{ item.id }}</h1>
|
|
13707
|
+
<div>
|
|
13708
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
|
|
13709
|
+
<a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
|
|
13710
|
+
</div>
|
|
13711
|
+
</div>
|
|
13712
|
+
${detailFields}
|
|
13713
|
+
</div>
|
|
13714
|
+
{% endblock %}
|
|
13715
|
+
`;
|
|
13716
|
+
writeFileSafe(detailPath, detailContent);
|
|
13717
|
+
}
|
|
13718
|
+
function generateAuth(_flags) {
|
|
13719
|
+
if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
|
|
13720
|
+
generateModel("User", { fields: "email:string,password:string,role:string" }, false);
|
|
13721
|
+
const registerDir = resolve6("src/routes/api/auth/register");
|
|
13722
|
+
const loginDir = resolve6("src/routes/api/auth/login");
|
|
13723
|
+
const meDir = resolve6("src/routes/api/auth/me");
|
|
13724
|
+
ensureDir(registerDir);
|
|
13725
|
+
ensureDir(loginDir);
|
|
13726
|
+
ensureDir(meDir);
|
|
13727
|
+
writeFileSafe(
|
|
13728
|
+
join15(registerDir, "post.ts"),
|
|
13729
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13730
|
+
import { hashPassword } from "tina4-nodejs";
|
|
13731
|
+
import User from "../../../../models/User.js";
|
|
13732
|
+
|
|
13733
|
+
// Public: registration mints an account for a user who has no token yet.
|
|
13734
|
+
export const secure = false;
|
|
13735
|
+
|
|
13736
|
+
export const meta = { summary: "Register a new user", tags: ["auth"] };
|
|
13737
|
+
|
|
13738
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13739
|
+
// tina4:edit add password-strength / email-format / captcha rules before mint
|
|
13740
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
13741
|
+
|
|
13742
|
+
if (!email || !password) {
|
|
13743
|
+
res.json({ error: "Email and password required" }, 400);
|
|
13744
|
+
return;
|
|
13745
|
+
}
|
|
13746
|
+
|
|
13747
|
+
const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
13748
|
+
if (existing) {
|
|
13749
|
+
res.json({ error: "Email already registered" }, 409);
|
|
13750
|
+
return;
|
|
13751
|
+
}
|
|
13752
|
+
|
|
13753
|
+
const user = new User({ email, password: hashPassword(password), role: "user" });
|
|
13754
|
+
await user.save();
|
|
13755
|
+
res.json({ message: "Registered", id: user.toObject().id }, 201);
|
|
13756
|
+
}
|
|
13757
|
+
`
|
|
13758
|
+
);
|
|
13759
|
+
writeFileSafe(
|
|
13760
|
+
join15(loginDir, "post.ts"),
|
|
13761
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13762
|
+
import { checkPassword, getToken } from "tina4-nodejs";
|
|
13763
|
+
import User from "../../../../models/User.js";
|
|
13764
|
+
|
|
13765
|
+
// Public: login authenticates by password and mints the token.
|
|
13766
|
+
export const secure = false;
|
|
13767
|
+
|
|
13768
|
+
export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
|
|
13769
|
+
|
|
13770
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13771
|
+
// tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
|
|
13772
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
13773
|
+
|
|
13774
|
+
if (!email || !password) {
|
|
13775
|
+
res.json({ error: "Email and password required" }, 400);
|
|
13776
|
+
return;
|
|
13777
|
+
}
|
|
13778
|
+
|
|
13779
|
+
const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
13780
|
+
if (!user || !checkPassword(password, user.toObject().password as string)) {
|
|
13781
|
+
res.json({ error: "Invalid credentials" }, 401);
|
|
13782
|
+
return;
|
|
13783
|
+
}
|
|
13784
|
+
|
|
13785
|
+
const data = user.toObject();
|
|
13786
|
+
// tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
|
|
13787
|
+
const token = getToken({ userId: data.id, email: data.email, role: data.role });
|
|
13788
|
+
res.json({ token });
|
|
13789
|
+
}
|
|
13790
|
+
`
|
|
13791
|
+
);
|
|
13792
|
+
writeFileSafe(
|
|
13793
|
+
join15(meDir, "get.ts"),
|
|
13794
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13795
|
+
import { authenticateRequest } from "tina4-nodejs";
|
|
13796
|
+
|
|
13797
|
+
export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
|
|
13798
|
+
|
|
13799
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13800
|
+
const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
|
|
13801
|
+
if (!payload) {
|
|
13802
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
13803
|
+
return;
|
|
13804
|
+
}
|
|
13805
|
+
res.json({ user: payload });
|
|
13806
|
+
}
|
|
13807
|
+
`
|
|
13808
|
+
);
|
|
13809
|
+
const formsDir = resolve6("src/templates/forms");
|
|
13810
|
+
ensureDir(formsDir);
|
|
13811
|
+
writeFileSafe(
|
|
13812
|
+
join15(formsDir, "login.twig"),
|
|
13813
|
+
`{% extends "base.twig" %}
|
|
13814
|
+
{% block title %}Login{% endblock %}
|
|
13815
|
+
{% block content %}
|
|
13816
|
+
<div class="container mt-4" style="max-width:400px">
|
|
13817
|
+
<h1>Login</h1>
|
|
13818
|
+
<form method="post" action="/api/auth/login">
|
|
13819
|
+
{{ form_token() }}
|
|
13820
|
+
<div class="form-group mb-3">
|
|
13821
|
+
<label for="email">Email</label>
|
|
13822
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
13823
|
+
</div>
|
|
13824
|
+
<div class="form-group mb-3">
|
|
13825
|
+
<label for="password">Password</label>
|
|
13826
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
|
|
13827
|
+
</div>
|
|
13828
|
+
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
13829
|
+
<p class="mt-3 text-center"><a href="/register">Create an account</a></p>
|
|
13830
|
+
</form>
|
|
13831
|
+
</div>
|
|
13832
|
+
{% endblock %}
|
|
13833
|
+
`
|
|
13834
|
+
);
|
|
13835
|
+
writeFileSafe(
|
|
13836
|
+
join15(formsDir, "register.twig"),
|
|
13837
|
+
`{% extends "base.twig" %}
|
|
13838
|
+
{% block title %}Register{% endblock %}
|
|
13839
|
+
{% block content %}
|
|
13840
|
+
<div class="container mt-4" style="max-width:400px">
|
|
13841
|
+
<h1>Register</h1>
|
|
13842
|
+
<form method="post" action="/api/auth/register">
|
|
13843
|
+
{{ form_token() }}
|
|
13844
|
+
<div class="form-group mb-3">
|
|
13845
|
+
<label for="email">Email</label>
|
|
13846
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
13847
|
+
</div>
|
|
13848
|
+
<div class="form-group mb-3">
|
|
13849
|
+
<label for="password">Password</label>
|
|
13850
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
|
|
13851
|
+
</div>
|
|
13852
|
+
<button type="submit" class="btn btn-primary w-100">Register</button>
|
|
13853
|
+
<p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
|
|
13854
|
+
</form>
|
|
13855
|
+
</div>
|
|
13856
|
+
{% endblock %}
|
|
13857
|
+
`
|
|
13858
|
+
);
|
|
13859
|
+
emitAuthTest();
|
|
13860
|
+
if (!__resolution.jsonMode) {
|
|
13861
|
+
console.log("\n Authentication scaffolding complete.");
|
|
13862
|
+
console.log(" Run: tina4nodejs migrate");
|
|
13863
|
+
console.log(" POST /api/auth/register \u2014 create account (public)");
|
|
13864
|
+
console.log(" POST /api/auth/login \u2014 get JWT token (public)");
|
|
13865
|
+
console.log(" GET /api/auth/me \u2014 get profile (requires token)");
|
|
13866
|
+
}
|
|
13867
|
+
}
|
|
13868
|
+
function generateService(name, flags) {
|
|
13869
|
+
const snake = toSnake(name);
|
|
13870
|
+
const camel = toCamel(toPascal(name)) || snake;
|
|
13871
|
+
const cron = flags.cron;
|
|
13872
|
+
const dir = resolve6("src/services");
|
|
13873
|
+
ensureDir(dir);
|
|
13874
|
+
const path8 = join15(dir, `${snake}.ts`);
|
|
13875
|
+
let scheduleField;
|
|
13876
|
+
let note;
|
|
13877
|
+
if (cron && cron !== true) {
|
|
13878
|
+
scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
|
|
13879
|
+
note = `cron '${cron}'`;
|
|
13880
|
+
} else {
|
|
13881
|
+
const seconds = parseEvery(flags.every);
|
|
13882
|
+
scheduleField = ` interval: ${seconds},`;
|
|
13883
|
+
note = `every ${seconds}s`;
|
|
13884
|
+
}
|
|
13885
|
+
const body = aiFill(`${camel}Task`, {
|
|
13886
|
+
intent: "do the scheduled work for this service",
|
|
13887
|
+
given: "context -> ServiceContext (.name, .running, .lastRun)",
|
|
13888
|
+
use: "your ORM / Api / Messenger code (re-run on schedule)",
|
|
13889
|
+
ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13890
|
+
raise: `service ${snake} not implemented`
|
|
13891
|
+
});
|
|
13892
|
+
const content = `import type { ServiceContext } from "tina4-nodejs";
|
|
13893
|
+
|
|
13894
|
+
/**
|
|
13895
|
+
* ${name} background service \u2014 runs ${note} via ServiceRunner.
|
|
13896
|
+
*
|
|
13897
|
+
* Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
|
|
13898
|
+
* does NOT auto-start services:
|
|
13899
|
+
*
|
|
13900
|
+
* import { ServiceRunner } from "tina4-nodejs";
|
|
13901
|
+
* await ServiceRunner.discover("src/services"); // registers this default export
|
|
13902
|
+
* ServiceRunner.start();
|
|
13903
|
+
*/
|
|
13904
|
+
|
|
13905
|
+
export async function ${camel}Task(context: ServiceContext): Promise<void> {
|
|
13906
|
+
// tina4:edit replace the AI-FILL stub below with the scheduled work
|
|
13907
|
+
${body}}
|
|
13908
|
+
|
|
13909
|
+
// Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
|
|
13910
|
+
// (+ interval or timing) off this default export.
|
|
13911
|
+
export default {
|
|
13912
|
+
name: "${snake}",
|
|
13913
|
+
handler: ${camel}Task,
|
|
13914
|
+
${scheduleField}
|
|
13915
|
+
};
|
|
13916
|
+
`;
|
|
13917
|
+
writeFileSafe(path8, content);
|
|
13918
|
+
emitServiceTest(name, snake, camel);
|
|
13919
|
+
}
|
|
13920
|
+
function generateQueue(name, _flags) {
|
|
13921
|
+
const topic = name.replace(/^\//, "");
|
|
13922
|
+
const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
13923
|
+
const pascal = toPascal(topic) || "Topic";
|
|
13924
|
+
const dir = resolve6("src/services");
|
|
13925
|
+
ensureDir(dir);
|
|
13926
|
+
const path8 = join15(dir, `${slug}_consumer.ts`);
|
|
13927
|
+
const body = aiFill(`handle${pascal}`, {
|
|
13928
|
+
intent: `process ONE ${topic} job payload`,
|
|
13929
|
+
given: "payload -> the produced job data (job.payload)",
|
|
13930
|
+
use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
|
|
13931
|
+
ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13932
|
+
raise: `queue ${topic} handler not implemented`
|
|
13933
|
+
});
|
|
13934
|
+
const content = `import { Queue } from "tina4-nodejs";
|
|
13935
|
+
import type { ServiceContext } from "tina4-nodejs";
|
|
13936
|
+
|
|
13937
|
+
/**
|
|
13938
|
+
* ${topic} queue \u2014 producer + consumer worker.
|
|
13939
|
+
*
|
|
13940
|
+
* Produce from anywhere: publish${pascal}({ ... })
|
|
13941
|
+
* The consumer is a long-running worker wired as a ServiceRunner daemon:
|
|
13942
|
+
* await ServiceRunner.discover("src/services"); ServiceRunner.start();
|
|
13943
|
+
*/
|
|
13944
|
+
|
|
13945
|
+
/** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
|
|
13946
|
+
export function publish${pascal}(payload: Record<string, unknown>): string {
|
|
13947
|
+
return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
|
|
13948
|
+
}
|
|
13949
|
+
|
|
13950
|
+
/** Process ONE ${topic} job payload. */
|
|
13951
|
+
export async function handle${pascal}(payload: unknown): Promise<void> {
|
|
13952
|
+
// tina4:edit implement the per-job handler; return to ack, throw to nack
|
|
13953
|
+
${body}}
|
|
13954
|
+
|
|
13955
|
+
/** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
|
|
13956
|
+
export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
|
|
13957
|
+
const queue = new Queue({ topic: "${topic}" });
|
|
13958
|
+
for await (const job of queue.consume("${topic}")) {
|
|
13959
|
+
const one = Array.isArray(job) ? job[0] : job;
|
|
13960
|
+
try {
|
|
13961
|
+
await handle${pascal}(one.payload);
|
|
13962
|
+
one.complete(); // ack \u2014 remove from the queue
|
|
13963
|
+
} catch (err) {
|
|
13964
|
+
one.fail(String(err)); // nack \u2014 retry / dead-letter
|
|
13965
|
+
}
|
|
13966
|
+
}
|
|
13967
|
+
}
|
|
13968
|
+
|
|
13969
|
+
// Discovered by ServiceRunner.discover("src/services"); daemon:true because
|
|
13970
|
+
// consume${pascal} owns its own loop. The topic + per-job handle keys let
|
|
13971
|
+
// \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
|
|
13972
|
+
// loop / bounded --once drain) without wiring a ServiceRunner.
|
|
13973
|
+
export default {
|
|
13974
|
+
name: "${topic}-consumer",
|
|
13975
|
+
topic: "${topic}",
|
|
13976
|
+
handler: consume${pascal},
|
|
13977
|
+
handle: handle${pascal},
|
|
13978
|
+
daemon: true,
|
|
13979
|
+
};
|
|
13980
|
+
`;
|
|
13981
|
+
writeFileSafe(path8, content);
|
|
13982
|
+
emitQueueTest(topic, slug, pascal);
|
|
13983
|
+
}
|
|
13984
|
+
function generateValidator(name, _flags) {
|
|
13985
|
+
const dir = resolve6("src/validators");
|
|
13986
|
+
ensureDir(dir);
|
|
13987
|
+
const path8 = join15(dir, `${toSnake(name)}.ts`);
|
|
13988
|
+
const rules = extend(
|
|
13989
|
+
"add / adjust the validation rules for this payload",
|
|
13990
|
+
`e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
|
|
13991
|
+
);
|
|
13992
|
+
const content = `import { Validator } from "tina4-nodejs";
|
|
13993
|
+
|
|
13994
|
+
/**
|
|
13995
|
+
* Validate a ${name} payload. Returns a Validator (chainable rules).
|
|
13996
|
+
*
|
|
13997
|
+
* Usage in a route:
|
|
13998
|
+
* const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
|
|
13999
|
+
* if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
|
|
14000
|
+
*/
|
|
14001
|
+
export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
|
|
14002
|
+
const validator = new Validator(data);
|
|
14003
|
+
// tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
|
|
14004
|
+
${rules} validator.required("name"); // starter rule (matches the model's default field)
|
|
14005
|
+
return validator;
|
|
14006
|
+
}
|
|
14007
|
+
`;
|
|
14008
|
+
writeFileSafe(path8, content);
|
|
14009
|
+
emitValidatorTest(name, toSnake(name), toPascal(name));
|
|
14010
|
+
}
|
|
14011
|
+
function generateSeeder(name, _flags) {
|
|
14012
|
+
const table2 = toTableName(name);
|
|
14013
|
+
const dir = resolve6("src/seeds");
|
|
14014
|
+
ensureDir(dir);
|
|
14015
|
+
const path8 = join15(dir, `${table2}_seeder.ts`);
|
|
14016
|
+
const overrides = extend(
|
|
14017
|
+
"override fields that need a specific shape (seedOrm auto-fills the rest)",
|
|
14018
|
+
`e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
|
|
14019
|
+
);
|
|
14020
|
+
const content = `import { pathToFileURL } from "node:url";
|
|
14021
|
+
import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
|
|
14022
|
+
import ${name} from "../models/${name}.js";
|
|
14023
|
+
|
|
14024
|
+
/**
|
|
14025
|
+
* Seeder for ${name} \u2014 run with: tina4nodejs seed
|
|
14026
|
+
*
|
|
14027
|
+
* seedOrm auto-fills every field by type/name; override the ones that need a
|
|
14028
|
+
* specific shape below. Each callable receives a FakeData instance.
|
|
14029
|
+
*/
|
|
14030
|
+
export function fieldOverrides(fake: FakeData): Record<string, unknown> {
|
|
14031
|
+
// tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
|
|
14032
|
+
${overrides} void fake; // available for overrides above
|
|
14033
|
+
return {};
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
/** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
|
|
14037
|
+
export async function run(): Promise<void> {
|
|
14038
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
|
|
14039
|
+
const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
|
|
14040
|
+
console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
|
|
14041
|
+
}
|
|
14042
|
+
|
|
14043
|
+
// Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
|
|
14044
|
+
// importing this module (e.g. in a test) must NOT trigger seeding.
|
|
14045
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
14046
|
+
await run();
|
|
14047
|
+
}
|
|
14048
|
+
`;
|
|
14049
|
+
writeFileSafe(path8, content);
|
|
14050
|
+
emitSeederTest(name, table2);
|
|
14051
|
+
}
|
|
14052
|
+
function generateWebsocket(name, _flags) {
|
|
14053
|
+
const raw = name.trim();
|
|
14054
|
+
const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
|
|
14055
|
+
let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
14056
|
+
const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
|
|
14057
|
+
const handlerName = `${toCamel(toPascal(base))}Ws`;
|
|
14058
|
+
const dir = resolve6("src/routes");
|
|
14059
|
+
ensureDir(dir);
|
|
14060
|
+
const path8 = join15(dir, `ws_${base}.ts`);
|
|
14061
|
+
const body = aiFill(handlerName, {
|
|
14062
|
+
intent: `handle an inbound "message" frame on ${wsPath}`,
|
|
14063
|
+
given: "data -> the message payload (string); connection -> WebSocketConnection",
|
|
14064
|
+
use: "connection.broadcast(data) or connection.sendJson({ ... })",
|
|
14065
|
+
ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
14066
|
+
raise: `websocket ${wsPath} not implemented`
|
|
14067
|
+
});
|
|
14068
|
+
const content = `import { websocket } from "tina4-nodejs";
|
|
14069
|
+
import type { WebSocketConnection } from "tina4-nodejs";
|
|
14070
|
+
|
|
14071
|
+
/**
|
|
14072
|
+
* ${wsPath} WebSocket route.
|
|
14073
|
+
*
|
|
14074
|
+
* Registered on import by websocket(). Node has NO file-based WS
|
|
14075
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it (add
|
|
14076
|
+
* \`.secure()\` to require a JWT on the upgrade):
|
|
14077
|
+
*
|
|
14078
|
+
* import "./src/routes/ws_${base}.js";
|
|
14079
|
+
*
|
|
14080
|
+
* The server invokes the handler as (connection, event, data) for each event:
|
|
14081
|
+
* "open" (connect), "message" (inbound frame), "close" (disconnect).
|
|
14082
|
+
*/
|
|
14083
|
+
export async function ${handlerName}(
|
|
14084
|
+
connection: WebSocketConnection,
|
|
14085
|
+
event: "open" | "message" | "close",
|
|
14086
|
+
data: string,
|
|
14087
|
+
): Promise<void> {
|
|
14088
|
+
if (event === "open") {
|
|
14089
|
+
// tina4:edit customize the welcome frame (or drop it)
|
|
14090
|
+
connection.sendJson({ type: "welcome" });
|
|
14091
|
+
return;
|
|
14092
|
+
}
|
|
14093
|
+
if (event === "close") {
|
|
14094
|
+
return;
|
|
14095
|
+
}
|
|
14096
|
+
// event === "message"
|
|
14097
|
+
// tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
|
|
14098
|
+
${body}}
|
|
14099
|
+
|
|
14100
|
+
websocket("${wsPath}", ${handlerName});
|
|
14101
|
+
`;
|
|
14102
|
+
writeFileSafe(path8, content);
|
|
14103
|
+
emitWebsocketTest(wsPath, base, handlerName);
|
|
14104
|
+
}
|
|
14105
|
+
function generateListener(name, _flags) {
|
|
14106
|
+
const event = name.trim();
|
|
14107
|
+
const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
14108
|
+
const handlerName = `on${toPascal(slug)}`;
|
|
14109
|
+
const dir = resolve6("src/listeners");
|
|
14110
|
+
ensureDir(dir);
|
|
14111
|
+
const path8 = join15(dir, `${slug}.ts`);
|
|
14112
|
+
const body = aiFill(handlerName, {
|
|
14113
|
+
intent: `react to the '${event}' event`,
|
|
14114
|
+
given: `args -> whatever Events.emit("${event}", ...args) passed`,
|
|
14115
|
+
use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
|
|
14116
|
+
ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
14117
|
+
raise: `listener ${event} not implemented`
|
|
14118
|
+
});
|
|
14119
|
+
const content = `import { Events } from "tina4-nodejs";
|
|
14120
|
+
|
|
14121
|
+
/**
|
|
14122
|
+
* Listener for the '${event}' event.
|
|
14123
|
+
*
|
|
14124
|
+
* Registered on import by Events.on(). Node has NO src/listeners/
|
|
14125
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it:
|
|
14126
|
+
*
|
|
14127
|
+
* import "./src/listeners/${slug}.js";
|
|
14128
|
+
*
|
|
14129
|
+
* Fires when something calls Events.emit("${event}", ...args).
|
|
14130
|
+
*/
|
|
14131
|
+
export function ${handlerName}(...args: unknown[]): void {
|
|
14132
|
+
// tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
|
|
14133
|
+
${body}}
|
|
14134
|
+
|
|
14135
|
+
Events.on("${event}", ${handlerName});
|
|
14136
|
+
`;
|
|
14137
|
+
writeFileSafe(path8, content);
|
|
14138
|
+
emitListenerTest(event, slug);
|
|
14139
|
+
}
|
|
14140
|
+
function writeTest(testName, content) {
|
|
14141
|
+
const dir = resolve6("tests");
|
|
14142
|
+
ensureDir(dir);
|
|
14143
|
+
writeFileSafe(join15(dir, `${testName}.test.ts`), content);
|
|
14144
|
+
}
|
|
14145
|
+
function standaloneTest(doc, body) {
|
|
14146
|
+
return `${doc}
|
|
14147
|
+
|
|
14148
|
+
let pass = 0;
|
|
14149
|
+
let fail = 0;
|
|
14150
|
+
function assert(label: string, ok: boolean): void {
|
|
14151
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
14152
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
14153
|
+
}
|
|
14154
|
+
async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
|
|
14155
|
+
try { await fn(); assert(label, false); }
|
|
14156
|
+
catch { assert(label, true); }
|
|
14157
|
+
}
|
|
14158
|
+
|
|
14159
|
+
${body}
|
|
14160
|
+
|
|
14161
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
14162
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
14163
|
+
`;
|
|
14164
|
+
}
|
|
14165
|
+
function sampleLiteral(fieldType) {
|
|
14166
|
+
switch ((fieldType || "string").toLowerCase()) {
|
|
14167
|
+
case "int":
|
|
14168
|
+
case "integer":
|
|
14169
|
+
return "1";
|
|
14170
|
+
case "float":
|
|
14171
|
+
case "number":
|
|
14172
|
+
case "numeric":
|
|
14173
|
+
case "decimal":
|
|
14174
|
+
return "1.5";
|
|
14175
|
+
case "bool":
|
|
14176
|
+
case "boolean":
|
|
14177
|
+
return "true";
|
|
14178
|
+
case "datetime":
|
|
14179
|
+
return '"2020-01-01 00:00:00"';
|
|
14180
|
+
case "blob":
|
|
14181
|
+
return '"x"';
|
|
14182
|
+
default:
|
|
14183
|
+
return '"sample"';
|
|
14184
|
+
}
|
|
14185
|
+
}
|
|
14186
|
+
function emitModelTest(model, table2, fields) {
|
|
14187
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
14188
|
+
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
14189
|
+
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
14190
|
+
const valueAssert = stringField ? `
|
|
14191
|
+
assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
|
|
14192
|
+
const doc = `/**
|
|
14193
|
+
* Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
|
|
14194
|
+
*
|
|
14195
|
+
* Generated with src/models/${model}.ts by \`tina4nodejs generate model
|
|
14196
|
+
* ${model}\`. The model scaffold is working code, so this passes on generation:
|
|
14197
|
+
* binds a real in-memory SQLite DB, creates the table, saves a row, reads it
|
|
14198
|
+
* back. Run with: npx tsx tests/${table2}_model.test.ts
|
|
14199
|
+
*/
|
|
14200
|
+
import ${model} from "../src/models/${model}.js";
|
|
14201
|
+
import { initDatabase } from "tina4-nodejs/orm";`;
|
|
14202
|
+
const body = `await initDatabase({ url: "sqlite:///:memory:" });
|
|
14203
|
+
await ${model}.createTable();
|
|
14204
|
+
|
|
14205
|
+
const row = new ${model}({ ${payload} });
|
|
14206
|
+
const saved = await row.save();
|
|
14207
|
+
assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
|
|
14208
|
+
|
|
14209
|
+
const id = row.toObject().id;
|
|
14210
|
+
const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
14211
|
+
assert("row reads back by id", fetched !== null);
|
|
14212
|
+
assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
|
|
14213
|
+
|
|
14214
|
+
const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
|
|
14215
|
+
assert("find missing returns null", missing === null);`;
|
|
14216
|
+
writeTest(`${table2}_model`, standaloneTest(doc, body));
|
|
14217
|
+
}
|
|
14218
|
+
function emitRouteStubTest(route) {
|
|
14219
|
+
const doc = `/**
|
|
14220
|
+
* Routing test for ${route} \u2014 no mocks, real Router + route discovery.
|
|
14221
|
+
*
|
|
14222
|
+
* Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
|
|
14223
|
+
* ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
|
|
14224
|
+
* implement them, so this tests what IS live on generation: all five routes
|
|
14225
|
+
* register on the REAL Router, and the list handler fails loud until filled.
|
|
14226
|
+
* Run with: npx tsx tests/${route}.test.ts
|
|
14227
|
+
*/
|
|
14228
|
+
import { dirname, resolve } from "node:path";
|
|
14229
|
+
import { fileURLToPath } from "node:url";
|
|
14230
|
+
import { Router, discoverRoutes } from "tina4-nodejs";
|
|
14231
|
+
import listHandler from "../src/routes/api/${route}/get.js";`;
|
|
14232
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
14233
|
+
const router = new Router();
|
|
14234
|
+
const defs = await discoverRoutes(resolve(here, "../src/routes"));
|
|
14235
|
+
for (const def of defs) router.addRoute(def);
|
|
14236
|
+
|
|
14237
|
+
const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
|
|
14238
|
+
for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
|
|
14239
|
+
assert(\`route registered: \${sig}\`, sigs.includes(sig));
|
|
14240
|
+
}
|
|
14241
|
+
|
|
14242
|
+
// The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
|
|
14243
|
+
await assertThrows("list handler is a live stub (throws until filled)",
|
|
14244
|
+
() => listHandler({} as never, {} as never));`;
|
|
14245
|
+
writeTest(route, standaloneTest(doc, body));
|
|
14246
|
+
}
|
|
14247
|
+
function emitMiddlewareTest(name, snake) {
|
|
14248
|
+
const doc = `/**
|
|
14249
|
+
* Real dispatch test for the ${name} middleware \u2014 no mocks.
|
|
14250
|
+
*
|
|
14251
|
+
* Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
|
|
14252
|
+
* ${name}\`. Drives the scaffolded before/after functions through the REAL
|
|
14253
|
+
* MiddlewareChain with a real Tina4Request/Response (built from real node http
|
|
14254
|
+
* objects) \u2014 the same continuation dispatch the live server runs.
|
|
14255
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14256
|
+
*/
|
|
14257
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
14258
|
+
import { Socket } from "node:net";
|
|
14259
|
+
import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
|
|
14260
|
+
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
14261
|
+
import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
|
|
14262
|
+
const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
|
|
14263
|
+
const socket = new Socket();
|
|
14264
|
+
const rawReq = new IncomingMessage(socket);
|
|
14265
|
+
rawReq.method = "GET";
|
|
14266
|
+
rawReq.url = "/";
|
|
14267
|
+
rawReq.headers = { ...headers, host: "localhost" };
|
|
14268
|
+
rawReq.push(null);
|
|
14269
|
+
const rawRes = new ServerResponse(rawReq);
|
|
14270
|
+
rawRes.write = (() => true) as typeof rawRes.write;
|
|
14271
|
+
rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
|
|
14272
|
+
return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
|
|
14273
|
+
}
|
|
14274
|
+
|
|
14275
|
+
// before(): blocks an unauthenticated request (401, does not call next()).
|
|
14276
|
+
{
|
|
14277
|
+
const chain = new MiddlewareChain();
|
|
14278
|
+
chain.use(before${name});
|
|
14279
|
+
let reached = false;
|
|
14280
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14281
|
+
const { req, res, raw } = realPair({});
|
|
14282
|
+
await chain.run(req, res);
|
|
14283
|
+
assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
|
|
14284
|
+
}
|
|
14285
|
+
|
|
14286
|
+
// before(): lets an authenticated request through to the next middleware.
|
|
14287
|
+
{
|
|
14288
|
+
const chain = new MiddlewareChain();
|
|
14289
|
+
chain.use(before${name});
|
|
14290
|
+
let reached = false;
|
|
14291
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14292
|
+
const { req, res } = realPair({ authorization: "Bearer test" });
|
|
14293
|
+
await chain.run(req, res);
|
|
14294
|
+
assert("before passes an authenticated request through", reached === true);
|
|
14295
|
+
}
|
|
14296
|
+
|
|
14297
|
+
// after(): always continues the chain.
|
|
14298
|
+
{
|
|
14299
|
+
const chain = new MiddlewareChain();
|
|
14300
|
+
chain.use(after${name});
|
|
14301
|
+
let reached = false;
|
|
14302
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14303
|
+
const { req, res } = realPair({});
|
|
14304
|
+
await chain.run(req, res);
|
|
14305
|
+
assert("after runs and continues the chain", reached === true);
|
|
14306
|
+
}`;
|
|
14307
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14308
|
+
}
|
|
14309
|
+
function emitServiceTest(name, snake, camel) {
|
|
14310
|
+
const doc = `/**
|
|
14311
|
+
* Real ServiceRunner test for the ${name} service \u2014 no mocks.
|
|
14312
|
+
*
|
|
14313
|
+
* Generated with src/services/${snake}.ts by \`tina4nodejs generate service
|
|
14314
|
+
* ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
|
|
14315
|
+
* descriptor; the task body is an AI-FILL stub that throws until filled.
|
|
14316
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14317
|
+
*/
|
|
14318
|
+
import { ServiceRunner } from "tina4-nodejs";
|
|
14319
|
+
import service, { ${camel}Task } from "../src/services/${snake}.js";`;
|
|
14320
|
+
const body = `assert("descriptor has a name + callable handler",
|
|
14321
|
+
service.name === "${snake}" && typeof service.handler === "function");
|
|
14322
|
+
|
|
14323
|
+
// Register on a REAL ServiceRunner and confirm it is listed.
|
|
14324
|
+
ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
|
|
14325
|
+
assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
|
|
14326
|
+
ServiceRunner.remove("${snake}");
|
|
14327
|
+
|
|
14328
|
+
// The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
|
|
14329
|
+
await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
|
|
14330
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14331
|
+
}
|
|
14332
|
+
function emitQueueTest(topic, slug, pascal) {
|
|
14333
|
+
const doc = `/**
|
|
14334
|
+
* Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
|
|
14335
|
+
*
|
|
14336
|
+
* Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
|
|
14337
|
+
* queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
|
|
14338
|
+
* asserts it is enqueued, and that the consumer is wired as a daemon. The
|
|
14339
|
+
* per-job handle is an AI-FILL stub that throws until filled.
|
|
14340
|
+
* Run with: npx tsx tests/${slug}.test.ts
|
|
14341
|
+
*/
|
|
14342
|
+
import { Queue } from "tina4-nodejs";
|
|
14343
|
+
import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
|
|
14344
|
+
const body = `const jobId = publish${pascal}({ hello: "world" });
|
|
14345
|
+
assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
|
|
14346
|
+
assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
|
|
14347
|
+
|
|
14348
|
+
assert("consumer default export is a daemon", worker.daemon === true);
|
|
14349
|
+
assert("consumer handler is wired", typeof worker.handler === "function");
|
|
14350
|
+
|
|
14351
|
+
// The per-job handler is an AI-FILL stub \u2014 it throws until filled.
|
|
14352
|
+
await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
|
|
14353
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
14354
|
+
}
|
|
14355
|
+
function emitValidatorTest(name, snake, pascal) {
|
|
14356
|
+
const doc = `/**
|
|
14357
|
+
* Real validation test for validate${pascal} \u2014 no mocks.
|
|
14358
|
+
*
|
|
14359
|
+
* Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
|
|
14360
|
+
* ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
|
|
14361
|
+
* on generation \u2014 adjust the rules for your payload and update these cases.
|
|
14362
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14363
|
+
*/
|
|
14364
|
+
import { validate${pascal} } from "../src/validators/${snake}.js";`;
|
|
14365
|
+
const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
|
|
14366
|
+
|
|
14367
|
+
const bad = validate${pascal}({});
|
|
14368
|
+
assert("invalid input fails", bad.isValid() === false);
|
|
14369
|
+
assert("invalid input reports errors", bad.errors().length > 0);`;
|
|
14370
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14371
|
+
}
|
|
14372
|
+
function emitSeederTest(model, table2) {
|
|
14373
|
+
const doc = `/**
|
|
14374
|
+
* Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
|
|
14375
|
+
*
|
|
14376
|
+
* Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
|
|
14377
|
+
* ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
|
|
14378
|
+
* seeder (auto-fills every field via FakeData) and asserts rows were created.
|
|
14379
|
+
* Run with: npx tsx tests/${table2}_seeder.test.ts
|
|
14380
|
+
*/
|
|
14381
|
+
import { initDatabase, FakeData } from "tina4-nodejs/orm";
|
|
14382
|
+
import ${model} from "../src/models/${model}.js";
|
|
14383
|
+
import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
|
|
14384
|
+
const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
|
|
14385
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL });
|
|
14386
|
+
await ${model}.createTable();
|
|
14387
|
+
|
|
14388
|
+
assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
|
|
14389
|
+
|
|
14390
|
+
await run(); // run() re-binds the same DB URL and seeds via seedOrm
|
|
14391
|
+
const rows = await ${model}.all();
|
|
14392
|
+
assert("run() seeds real rows", rows.length >= 1);`;
|
|
14393
|
+
writeTest(`${table2}_seeder`, standaloneTest(doc, body));
|
|
14394
|
+
}
|
|
14395
|
+
function emitWebsocketTest(wsPath, base, handler) {
|
|
14396
|
+
const doc = `/**
|
|
14397
|
+
* Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
|
|
14398
|
+
*
|
|
14399
|
+
* Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
|
|
14400
|
+
* ...\`. Confirms the handler registers on the REAL Router (importing runs the
|
|
14401
|
+
* module-level websocket() call) and drives the real handler for the "close"
|
|
14402
|
+
* event (no socket needed). The "message" branch is an AI-FILL stub that throws
|
|
14403
|
+
* until filled. Run with: npx tsx tests/ws_${base}.test.ts
|
|
14404
|
+
*/
|
|
14405
|
+
import { Router } from "tina4-nodejs";
|
|
14406
|
+
import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
|
|
14407
|
+
const body = `assert("handler registered on the real router",
|
|
14408
|
+
Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
|
|
14409
|
+
|
|
14410
|
+
// The "close" branch returns cleanly without a live connection.
|
|
14411
|
+
const closed = await ${handler}(null as never, "close", "");
|
|
14412
|
+
assert("close event handled cleanly", closed === undefined);
|
|
14413
|
+
|
|
14414
|
+
// The "message" branch is an AI-FILL stub \u2014 it throws until filled.
|
|
14415
|
+
await assertThrows("message branch is a live stub (throws until filled)",
|
|
14416
|
+
() => ${handler}(null as never, "message", "hi"));`;
|
|
14417
|
+
writeTest(`ws_${base}`, standaloneTest(doc, body));
|
|
14418
|
+
}
|
|
14419
|
+
function emitListenerTest(event, slug) {
|
|
14420
|
+
const doc = `/**
|
|
14421
|
+
* Real event-bus test for the '${event}' listener \u2014 no mocks.
|
|
14422
|
+
*
|
|
14423
|
+
* Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
|
|
14424
|
+
* ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
|
|
14425
|
+
* the module-level Events.on) and that emitting the event reaches it. The
|
|
14426
|
+
* reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
|
|
14427
|
+
* ran). Run with: npx tsx tests/${slug}.test.ts
|
|
14428
|
+
*/
|
|
14429
|
+
import { Events } from "tina4-nodejs";
|
|
14430
|
+
import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
|
|
14431
|
+
const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
|
|
14432
|
+
|
|
14433
|
+
// strict emit re-raises the stub error, proving the listener actually ran.
|
|
14434
|
+
await assertThrows("emitting the event reaches the (stub) listener",
|
|
14435
|
+
() => Events.emit("${event}", { strict: true }, { id: 1 }));`;
|
|
14436
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
14437
|
+
}
|
|
14438
|
+
function emitAuthTest() {
|
|
14439
|
+
const doc = `/**
|
|
14440
|
+
* Real auth test \u2014 register / login / me via the real TestClient.
|
|
14441
|
+
*
|
|
14442
|
+
* Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
|
|
14443
|
+
* real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
|
|
14444
|
+
* + login are public; the token from login authenticates GET /api/auth/me.
|
|
14445
|
+
* Run with: npx tsx tests/auth.test.ts
|
|
14446
|
+
*/
|
|
14447
|
+
import { dirname, resolve } from "node:path";
|
|
14448
|
+
import { fileURLToPath } from "node:url";
|
|
14449
|
+
import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
|
|
14450
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
14451
|
+
import User from "../src/models/User.js";
|
|
14452
|
+
|
|
14453
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
14454
|
+
delete process.env.TINA4_API_KEY;
|
|
14455
|
+
const here = dirname(fileURLToPath(import.meta.url));`;
|
|
14456
|
+
const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
|
|
14457
|
+
await User.createTable();
|
|
14458
|
+
for (const existing of await User.all()) await existing.delete(); // start from an empty table
|
|
14459
|
+
|
|
14460
|
+
const router = new Router();
|
|
14461
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
14462
|
+
const client = new TestClient(router);
|
|
14463
|
+
|
|
14464
|
+
const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
14465
|
+
assert("register a new user -> 201", registered.status === 201);
|
|
14466
|
+
|
|
14467
|
+
const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
14468
|
+
assert("duplicate register -> 409", duplicate.status === 409);
|
|
14469
|
+
|
|
14470
|
+
const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
|
|
14471
|
+
assert("login -> 200", login.status === 200);
|
|
14472
|
+
const token = (login.json() as { token?: string }).token;
|
|
14473
|
+
assert("login returns a token", typeof token === "string" && token.length > 0);
|
|
14474
|
+
|
|
14475
|
+
const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
|
|
14476
|
+
assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
|
|
14477
|
+
assert("me returns the authenticated user's email",
|
|
14478
|
+
((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
|
|
14479
|
+
|
|
14480
|
+
const anon = await client.get("/api/auth/me");
|
|
14481
|
+
assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
|
|
14482
|
+
|
|
14483
|
+
const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
|
|
14484
|
+
assert("wrong password -> 401", bad.status === 401);`;
|
|
14485
|
+
writeTest("auth", standaloneTest(doc, body));
|
|
14486
|
+
}
|
|
14487
|
+
function emitMigrationTest(migrationName, table2) {
|
|
14488
|
+
const doc = `/**
|
|
14489
|
+
* Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
|
|
14490
|
+
*
|
|
14491
|
+
* Generated with the migration by \`tina4nodejs generate migration
|
|
14492
|
+
* ${migrationName}\`. Applies the generated UP SQL against a fresh real
|
|
14493
|
+
* in-memory SQLite database and asserts the table exists, then applies the DOWN
|
|
14494
|
+
* SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
|
|
14495
|
+
* Run with: npx tsx tests/${table2}_migration.test.ts
|
|
14496
|
+
*/
|
|
14497
|
+
import { dirname, join, resolve } from "node:path";
|
|
14498
|
+
import { fileURLToPath } from "node:url";
|
|
14499
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
14500
|
+
import { SQLiteAdapter } from "tina4-nodejs/orm";`;
|
|
14501
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
14502
|
+
const migrationsDir = resolve(here, "../migrations");
|
|
14503
|
+
|
|
14504
|
+
const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
|
|
14505
|
+
assert("generated UP migration file exists", Boolean(upFile));
|
|
14506
|
+
const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
|
|
14507
|
+
|
|
14508
|
+
function statements(sql: string): string[] {
|
|
14509
|
+
const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
|
|
14510
|
+
return noComments.split(";").map((s) => s.trim()).filter(Boolean);
|
|
14511
|
+
}
|
|
14512
|
+
|
|
14513
|
+
const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
|
|
14514
|
+
const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
|
|
14515
|
+
const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
|
|
14516
|
+
|
|
14517
|
+
const db = new SQLiteAdapter(":memory:");
|
|
14518
|
+
for (const stmt of statements(upSql)) db.execute(stmt);
|
|
14519
|
+
assert("UP creates the ${table2} table", db.tableExists("${table2}"));
|
|
14520
|
+
|
|
14521
|
+
for (const stmt of statements(downSql)) db.execute(stmt);
|
|
14522
|
+
assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
|
|
14523
|
+
writeTest(`${table2}_migration`, standaloneTest(doc, body));
|
|
14524
|
+
}
|
|
14525
|
+
var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
|
|
14526
|
+
var init_generate = __esm({
|
|
14527
|
+
"../cli/src/commands/generate.ts"() {
|
|
14528
|
+
"use strict";
|
|
14529
|
+
FIELD_TYPE_MAP = {
|
|
14530
|
+
string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14531
|
+
str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14532
|
+
int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14533
|
+
integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14534
|
+
float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14535
|
+
number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14536
|
+
numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14537
|
+
decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14538
|
+
bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14539
|
+
boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14540
|
+
text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14541
|
+
datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
|
|
14542
|
+
blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
|
|
14543
|
+
};
|
|
14544
|
+
SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
14545
|
+
"order",
|
|
14546
|
+
"group",
|
|
14547
|
+
"user",
|
|
14548
|
+
"table",
|
|
14549
|
+
"select",
|
|
14550
|
+
"from",
|
|
14551
|
+
"where",
|
|
14552
|
+
"index",
|
|
14553
|
+
"key",
|
|
14554
|
+
"values",
|
|
14555
|
+
"column",
|
|
14556
|
+
"constraint",
|
|
14557
|
+
"check",
|
|
14558
|
+
"default",
|
|
14559
|
+
"primary",
|
|
14560
|
+
"foreign",
|
|
14561
|
+
"references",
|
|
14562
|
+
"unique",
|
|
14563
|
+
"join",
|
|
14564
|
+
"union",
|
|
14565
|
+
"having",
|
|
14566
|
+
"limit",
|
|
14567
|
+
"offset",
|
|
14568
|
+
"desc",
|
|
14569
|
+
"asc",
|
|
14570
|
+
"case",
|
|
14571
|
+
"when",
|
|
14572
|
+
"then",
|
|
14573
|
+
"else",
|
|
14574
|
+
"end",
|
|
14575
|
+
"and",
|
|
14576
|
+
"or",
|
|
14577
|
+
"not",
|
|
14578
|
+
"null",
|
|
14579
|
+
"insert",
|
|
14580
|
+
"update",
|
|
14581
|
+
"delete",
|
|
14582
|
+
"create",
|
|
14583
|
+
"drop",
|
|
14584
|
+
"alter",
|
|
14585
|
+
"grant",
|
|
14586
|
+
"revoke",
|
|
14587
|
+
"commit",
|
|
14588
|
+
"rollback",
|
|
14589
|
+
"view",
|
|
14590
|
+
"trigger",
|
|
14591
|
+
"procedure",
|
|
14592
|
+
"function",
|
|
14593
|
+
"database",
|
|
14594
|
+
"schema",
|
|
14595
|
+
"session",
|
|
14596
|
+
"set",
|
|
14597
|
+
"into",
|
|
14598
|
+
"as",
|
|
14599
|
+
"on",
|
|
14600
|
+
"by",
|
|
14601
|
+
"inner",
|
|
14602
|
+
"outer",
|
|
14603
|
+
"left",
|
|
14604
|
+
"right",
|
|
14605
|
+
"full",
|
|
14606
|
+
"natural",
|
|
14607
|
+
"using",
|
|
14608
|
+
"with",
|
|
14609
|
+
"distinct",
|
|
14610
|
+
"between",
|
|
14611
|
+
"exists",
|
|
14612
|
+
"like",
|
|
14613
|
+
"in",
|
|
14614
|
+
"is",
|
|
14615
|
+
"all",
|
|
14616
|
+
"any",
|
|
14617
|
+
"cross",
|
|
14618
|
+
"add",
|
|
14619
|
+
"row",
|
|
14620
|
+
"rows",
|
|
14621
|
+
"range",
|
|
14622
|
+
"current",
|
|
14623
|
+
"to"
|
|
14624
|
+
]);
|
|
14625
|
+
RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
|
|
14626
|
+
__resolution = {
|
|
14627
|
+
target: "",
|
|
14628
|
+
input: { name: "", fields: null },
|
|
14629
|
+
body: { transformations: [] },
|
|
14630
|
+
actionsTaken: [],
|
|
14631
|
+
dryRun: false,
|
|
14632
|
+
jsonMode: false
|
|
14633
|
+
};
|
|
14634
|
+
TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
14635
|
+
DEFAULT_FIELDS = [["name", "string"]];
|
|
14636
|
+
GENERATORS = {
|
|
14637
|
+
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
14638
|
+
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
14639
|
+
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
14640
|
+
migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
14641
|
+
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
14642
|
+
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
14643
|
+
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
14644
|
+
view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
|
|
14645
|
+
auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
|
|
14646
|
+
service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
14647
|
+
queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
|
|
14648
|
+
validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
14649
|
+
seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
|
|
14650
|
+
websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
|
|
14651
|
+
listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
|
|
14652
|
+
};
|
|
14653
|
+
GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
|
|
14654
|
+
NEXT_STEPS = {
|
|
14655
|
+
model: ({ name, table: table2 }) => [
|
|
14656
|
+
`Edit src/models/${name}.ts to add fields beyond the default 'name'`,
|
|
14657
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14658
|
+
`Run its test: npx tsx tests/${table2}_model.test.ts`,
|
|
14659
|
+
`Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
|
|
14660
|
+
],
|
|
14661
|
+
route: ({ name, table: table2 }) => [
|
|
14662
|
+
`Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
|
|
14663
|
+
`Run its test: npx tsx tests/${table2}.test.ts`,
|
|
14664
|
+
`Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
|
|
14665
|
+
],
|
|
14666
|
+
crud: ({ name, table: table2 }) => [
|
|
14667
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14668
|
+
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
14669
|
+
`Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
|
|
14670
|
+
`Change fields: edit src/models/${name}.ts then re-run generate crud`
|
|
14671
|
+
],
|
|
14672
|
+
migration: () => [
|
|
14673
|
+
`Apply pending migrations: npx tina4nodejs migrate`,
|
|
14674
|
+
`Check status: npx tina4nodejs migrate:status`,
|
|
14675
|
+
`Roll back the batch: npx tina4nodejs migrate:rollback`
|
|
14676
|
+
],
|
|
14677
|
+
middleware: ({ name }) => [
|
|
14678
|
+
`Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
|
|
14679
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14680
|
+
],
|
|
14681
|
+
test: ({ name }) => [
|
|
14682
|
+
`Fill the TODOs in tests/${toSnake(name)}.test.ts`,
|
|
14683
|
+
`Run it: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14684
|
+
],
|
|
14685
|
+
form: ({ name, table: table2 }) => [
|
|
14686
|
+
`Render from a route: res.render("forms/${table2}.twig", { item })`,
|
|
14687
|
+
`Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
|
|
14688
|
+
],
|
|
14689
|
+
view: ({ table: table2 }) => [
|
|
14690
|
+
`Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
|
|
14691
|
+
`Customize the templates in src/templates/pages/`
|
|
14692
|
+
],
|
|
14693
|
+
auth: () => [
|
|
14694
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14695
|
+
`Run the auth test: npx tsx tests/auth.test.ts`,
|
|
14696
|
+
`Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
14697
|
+
`Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
|
|
14698
|
+
],
|
|
14699
|
+
service: ({ name }) => [
|
|
14700
|
+
`Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
|
|
14701
|
+
`Fill the task body in src/services/${toSnake(name)}.ts`,
|
|
14702
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14703
|
+
],
|
|
14704
|
+
queue: ({ name }) => {
|
|
14705
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
14706
|
+
return [
|
|
14707
|
+
`Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
|
|
14708
|
+
`Produce a job: publish${toPascal(name)}({ ... })`,
|
|
14709
|
+
`Run the worker: npx tina4nodejs queue work ${name}`,
|
|
14710
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
14711
|
+
];
|
|
14712
|
+
},
|
|
14713
|
+
validator: ({ name }) => [
|
|
14714
|
+
`Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
|
|
14715
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14716
|
+
],
|
|
14717
|
+
seeder: ({ name, table: table2 }) => [
|
|
14718
|
+
`Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
|
|
14719
|
+
`Seed the table: npx tina4nodejs seed`,
|
|
14720
|
+
`Run its test: npx tsx tests/${table2}_seeder.test.ts`
|
|
14721
|
+
],
|
|
14722
|
+
websocket: ({ name }) => {
|
|
14723
|
+
const raw = name.trim();
|
|
14724
|
+
const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
14725
|
+
const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
|
|
14726
|
+
return [
|
|
14727
|
+
`Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
|
|
14728
|
+
`Fill the "message" branch in src/routes/ws_${base}.ts`,
|
|
14729
|
+
`Run its test: npx tsx tests/ws_${base}.test.ts`
|
|
14730
|
+
];
|
|
14731
|
+
},
|
|
14732
|
+
listener: ({ name }) => {
|
|
14733
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
14734
|
+
return [
|
|
14735
|
+
`Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
|
|
14736
|
+
`Fill the reaction in src/listeners/${slug}.ts`,
|
|
14737
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
14738
|
+
];
|
|
14739
|
+
}
|
|
14740
|
+
};
|
|
14741
|
+
}
|
|
14742
|
+
});
|
|
14743
|
+
|
|
12685
14744
|
// ../core/src/mcp.ts
|
|
12686
14745
|
var mcp_exports = {};
|
|
12687
14746
|
__export(mcp_exports, {
|
|
@@ -13182,10 +15241,10 @@ function registerDevTools(server) {
|
|
|
13182
15241
|
"swagger_spec",
|
|
13183
15242
|
(_args) => {
|
|
13184
15243
|
try {
|
|
13185
|
-
const { generate:
|
|
15244
|
+
const { generate: generate3 } = reqSibling("swagger");
|
|
13186
15245
|
const { defaultRouter: defaultRouter2 } = req("./router.js");
|
|
13187
15246
|
const routes = defaultRouter2?.getRoutes?.() ?? [];
|
|
13188
|
-
return
|
|
15247
|
+
return generate3?.(routes, []) ?? { info: "Swagger not available" };
|
|
13189
15248
|
} catch (e) {
|
|
13190
15249
|
return { error: e.message };
|
|
13191
15250
|
}
|
|
@@ -13344,20 +15403,43 @@ function registerDevTools(server) {
|
|
|
13344
15403
|
);
|
|
13345
15404
|
server.registerTool(
|
|
13346
15405
|
"migration_create",
|
|
13347
|
-
(args) => {
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
15406
|
+
async (args) => {
|
|
15407
|
+
try {
|
|
15408
|
+
const rawDesc = String(args.description ?? "").trim();
|
|
15409
|
+
if (!rawDesc) return { ok: false, error: "description is required" };
|
|
15410
|
+
const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
15411
|
+
if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
|
|
15412
|
+
const migrationsDir = path3.join(projectRoot3, "migrations");
|
|
15413
|
+
if (fs4.existsSync(migrationsDir)) {
|
|
15414
|
+
const upSuffix = `_${slug}.sql`;
|
|
15415
|
+
const downSuffix = `_${slug}.down.sql`;
|
|
15416
|
+
const existing = fs4.readdirSync(migrationsDir).filter(
|
|
15417
|
+
(f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
|
|
15418
|
+
);
|
|
15419
|
+
if (existing.length > 0) {
|
|
15420
|
+
return {
|
|
15421
|
+
ok: false,
|
|
15422
|
+
error: `A migration with slug "${slug}" already exists`,
|
|
15423
|
+
existing
|
|
15424
|
+
};
|
|
15425
|
+
}
|
|
15426
|
+
}
|
|
15427
|
+
const originalCwd = process.cwd();
|
|
15428
|
+
try {
|
|
15429
|
+
process.chdir(projectRoot3);
|
|
15430
|
+
const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
15431
|
+
const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
|
|
15432
|
+
const migrationPath = envelope.resolution?.migration_path;
|
|
15433
|
+
const created = migrationPath ? path3.basename(migrationPath) : "";
|
|
15434
|
+
return { ok: true, created, resolution: envelope };
|
|
15435
|
+
} finally {
|
|
15436
|
+
process.chdir(originalCwd);
|
|
15437
|
+
}
|
|
15438
|
+
} catch (e) {
|
|
15439
|
+
return { ok: false, error: e.message };
|
|
15440
|
+
}
|
|
13359
15441
|
},
|
|
13360
|
-
"Create a new migration file",
|
|
15442
|
+
"Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
|
|
13361
15443
|
schemaFromParams([{ name: "description", type: "string" }])
|
|
13362
15444
|
);
|
|
13363
15445
|
server.registerTool(
|
|
@@ -14125,14 +16207,14 @@ data: ${channel.buffer.shift()}
|
|
|
14125
16207
|
`;
|
|
14126
16208
|
continue;
|
|
14127
16209
|
}
|
|
14128
|
-
const gotMessage = await new Promise((
|
|
16210
|
+
const gotMessage = await new Promise((resolve21) => {
|
|
14129
16211
|
const timer = setTimeout(() => {
|
|
14130
16212
|
channel.wake = null;
|
|
14131
|
-
|
|
16213
|
+
resolve21(false);
|
|
14132
16214
|
}, keepaliveMs);
|
|
14133
16215
|
channel.wake = () => {
|
|
14134
16216
|
clearTimeout(timer);
|
|
14135
|
-
|
|
16217
|
+
resolve21(true);
|
|
14136
16218
|
};
|
|
14137
16219
|
});
|
|
14138
16220
|
if (!gotMessage) yield `: keep-alive
|
|
@@ -15451,8 +17533,8 @@ __export(context_exports, {
|
|
|
15451
17533
|
fts5Supported: () => fts5Supported
|
|
15452
17534
|
});
|
|
15453
17535
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
15454
|
-
import { existsSync as
|
|
15455
|
-
import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as
|
|
17536
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
|
|
17537
|
+
import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
|
|
15456
17538
|
function fts5Supported() {
|
|
15457
17539
|
try {
|
|
15458
17540
|
const conn = new DatabaseSync2(":memory:");
|
|
@@ -15475,13 +17557,13 @@ function realResolve(abs) {
|
|
|
15475
17557
|
} catch {
|
|
15476
17558
|
}
|
|
15477
17559
|
try {
|
|
15478
|
-
return
|
|
17560
|
+
return join17(realpathSync5(dirname8(abs)), basename4(abs));
|
|
15479
17561
|
} catch {
|
|
15480
17562
|
return abs;
|
|
15481
17563
|
}
|
|
15482
17564
|
}
|
|
15483
17565
|
function dbKey(db) {
|
|
15484
|
-
return
|
|
17566
|
+
return resolve8(db ? String(db) : join17(process.cwd(), ".tina4", "context.db"));
|
|
15485
17567
|
}
|
|
15486
17568
|
function defaultContext(root, db) {
|
|
15487
17569
|
const key = dbKey(db);
|
|
@@ -15565,7 +17647,7 @@ var init_context = __esm({
|
|
|
15565
17647
|
if (!this.available) return;
|
|
15566
17648
|
const parent = dirname8(this.path);
|
|
15567
17649
|
if (parent !== "" && parent !== ".") {
|
|
15568
|
-
|
|
17650
|
+
mkdirSync10(parent, { recursive: true });
|
|
15569
17651
|
}
|
|
15570
17652
|
this.conn = new DatabaseSync2(this.path);
|
|
15571
17653
|
this.ensureTable();
|
|
@@ -15637,7 +17719,7 @@ var init_context = __esm({
|
|
|
15637
17719
|
*/
|
|
15638
17720
|
indexRoot(root) {
|
|
15639
17721
|
if (!this.available) return 0;
|
|
15640
|
-
const rootAbs = realResolve(
|
|
17722
|
+
const rootAbs = realResolve(resolve8(String(root)));
|
|
15641
17723
|
this.root = rootAbs;
|
|
15642
17724
|
let total = 0;
|
|
15643
17725
|
const walk2 = (dir) => {
|
|
@@ -15654,11 +17736,11 @@ var init_context = __esm({
|
|
|
15654
17736
|
files.sort();
|
|
15655
17737
|
for (const fn of files) {
|
|
15656
17738
|
if (!_Context.eligible(fn)) continue;
|
|
15657
|
-
const full =
|
|
15658
|
-
const rel =
|
|
17739
|
+
const full = join17(dir, fn);
|
|
17740
|
+
const rel = relative4(rootAbs, full);
|
|
15659
17741
|
total += this.indexPath(full, rel);
|
|
15660
17742
|
}
|
|
15661
|
-
for (const d of subdirs) walk2(
|
|
17743
|
+
for (const d of subdirs) walk2(join17(dir, d));
|
|
15662
17744
|
};
|
|
15663
17745
|
walk2(rootAbs);
|
|
15664
17746
|
return total;
|
|
@@ -15674,9 +17756,9 @@ var init_context = __esm({
|
|
|
15674
17756
|
reindexFile(changedPath) {
|
|
15675
17757
|
if (!this.available || this.root === null) return -1;
|
|
15676
17758
|
const raw = String(changedPath);
|
|
15677
|
-
const abs = isAbsolute4(raw) ? raw :
|
|
15678
|
-
const resolved = realResolve(
|
|
15679
|
-
const rel =
|
|
17759
|
+
const abs = isAbsolute4(raw) ? raw : join17(process.cwd(), raw);
|
|
17760
|
+
const resolved = realResolve(resolve8(abs));
|
|
17761
|
+
const rel = relative4(this.root, resolved);
|
|
15680
17762
|
if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
15681
17763
|
return -1;
|
|
15682
17764
|
}
|
|
@@ -15687,7 +17769,7 @@ var init_context = __esm({
|
|
|
15687
17769
|
}
|
|
15688
17770
|
if (!_Context.eligible(basename4(rel))) return -1;
|
|
15689
17771
|
const stored = rel;
|
|
15690
|
-
if (!
|
|
17772
|
+
if (!existsSync15(abs)) {
|
|
15691
17773
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
15692
17774
|
return 0;
|
|
15693
17775
|
}
|
|
@@ -16433,7 +18515,7 @@ var init_websocket = __esm({
|
|
|
16433
18515
|
* Start the WebSocket server.
|
|
16434
18516
|
*/
|
|
16435
18517
|
async start() {
|
|
16436
|
-
return new Promise((
|
|
18518
|
+
return new Promise((resolve21, reject) => {
|
|
16437
18519
|
this.server = createServer((req2, res) => {
|
|
16438
18520
|
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
16439
18521
|
res.end("Upgrade Required");
|
|
@@ -16443,7 +18525,7 @@ var init_websocket = __esm({
|
|
|
16443
18525
|
});
|
|
16444
18526
|
this.server.listen(this.port, () => {
|
|
16445
18527
|
this.startIdleReaper();
|
|
16446
|
-
|
|
18528
|
+
resolve21();
|
|
16447
18529
|
});
|
|
16448
18530
|
this.server.on("error", (err) => {
|
|
16449
18531
|
this.emit("error", err);
|
|
@@ -17664,8 +19746,8 @@ var init_job = __esm({
|
|
|
17664
19746
|
});
|
|
17665
19747
|
|
|
17666
19748
|
// ../core/src/queueBackends/liteBackend.ts
|
|
17667
|
-
import { mkdirSync as
|
|
17668
|
-
import { join as
|
|
19749
|
+
import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
|
|
19750
|
+
import { join as join18 } from "node:path";
|
|
17669
19751
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17670
19752
|
var LiteBackend;
|
|
17671
19753
|
var init_liteBackend = __esm({
|
|
@@ -17689,22 +19771,22 @@ var init_liteBackend = __esm({
|
|
|
17689
19771
|
this.visibilityTimeout = visibilityTimeout;
|
|
17690
19772
|
}
|
|
17691
19773
|
ensureDir(queue) {
|
|
17692
|
-
const dir =
|
|
17693
|
-
|
|
19774
|
+
const dir = join18(this.basePath, queue);
|
|
19775
|
+
mkdirSync11(dir, { recursive: true });
|
|
17694
19776
|
return dir;
|
|
17695
19777
|
}
|
|
17696
19778
|
ensureFailedDir(queue) {
|
|
17697
|
-
const dir =
|
|
17698
|
-
|
|
19779
|
+
const dir = join18(this.basePath, queue, "failed");
|
|
19780
|
+
mkdirSync11(dir, { recursive: true });
|
|
17699
19781
|
return dir;
|
|
17700
19782
|
}
|
|
17701
19783
|
ensureReservedDir(queue) {
|
|
17702
|
-
const dir =
|
|
17703
|
-
|
|
19784
|
+
const dir = join18(this.basePath, queue, "reserved");
|
|
19785
|
+
mkdirSync11(dir, { recursive: true });
|
|
17704
19786
|
return dir;
|
|
17705
19787
|
}
|
|
17706
19788
|
reservedPath(queue, jobId) {
|
|
17707
|
-
return
|
|
19789
|
+
return join18(this.ensureReservedDir(queue), `${jobId}.queue-data`);
|
|
17708
19790
|
}
|
|
17709
19791
|
nowIso() {
|
|
17710
19792
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -17742,7 +19824,7 @@ var init_liteBackend = __esm({
|
|
|
17742
19824
|
error: void 0
|
|
17743
19825
|
};
|
|
17744
19826
|
const prefix = this.nextPrefix();
|
|
17745
|
-
|
|
19827
|
+
writeFileSync9(join18(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
|
|
17746
19828
|
return id;
|
|
17747
19829
|
}
|
|
17748
19830
|
/**
|
|
@@ -17761,7 +19843,7 @@ var init_liteBackend = __esm({
|
|
|
17761
19843
|
}
|
|
17762
19844
|
const candidates = [];
|
|
17763
19845
|
for (const filename of filenames) {
|
|
17764
|
-
const filePath =
|
|
19846
|
+
const filePath = join18(dir, filename);
|
|
17765
19847
|
let job;
|
|
17766
19848
|
try {
|
|
17767
19849
|
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -17804,7 +19886,7 @@ var init_liteBackend = __esm({
|
|
|
17804
19886
|
createdAt: job.createdAt ?? now,
|
|
17805
19887
|
topic: job.topic ?? queue
|
|
17806
19888
|
};
|
|
17807
|
-
|
|
19889
|
+
writeFileSync9(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
|
|
17808
19890
|
}
|
|
17809
19891
|
/**
|
|
17810
19892
|
* Return expired reservations to the queue (at-least-once delivery).
|
|
@@ -17825,7 +19907,7 @@ var init_liteBackend = __esm({
|
|
|
17825
19907
|
return;
|
|
17826
19908
|
}
|
|
17827
19909
|
for (const filename of filenames) {
|
|
17828
|
-
const filePath =
|
|
19910
|
+
const filePath = join18(reservedDir, filename);
|
|
17829
19911
|
let record;
|
|
17830
19912
|
try {
|
|
17831
19913
|
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -17863,7 +19945,7 @@ var init_liteBackend = __esm({
|
|
|
17863
19945
|
this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
|
|
17864
19946
|
const now = this.nowIso();
|
|
17865
19947
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
17866
|
-
const filePath =
|
|
19948
|
+
const filePath = join18(dir, filename);
|
|
17867
19949
|
job.topic = queue;
|
|
17868
19950
|
job.priority = job.priority ?? 0;
|
|
17869
19951
|
this.writeReserved(queue, job);
|
|
@@ -17888,7 +19970,7 @@ var init_liteBackend = __esm({
|
|
|
17888
19970
|
const results = [];
|
|
17889
19971
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
17890
19972
|
if (results.length >= count) break;
|
|
17891
|
-
const filePath =
|
|
19973
|
+
const filePath = join18(dir, filename);
|
|
17892
19974
|
job.topic = queue;
|
|
17893
19975
|
job.priority = job.priority ?? 0;
|
|
17894
19976
|
this.writeReserved(queue, job);
|
|
@@ -17941,7 +20023,7 @@ var init_liteBackend = __esm({
|
|
|
17941
20023
|
let count = 0;
|
|
17942
20024
|
for (const file of files) {
|
|
17943
20025
|
try {
|
|
17944
|
-
const job = JSON.parse(readFileSync14(
|
|
20026
|
+
const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
|
|
17945
20027
|
if (job.status === status2) count++;
|
|
17946
20028
|
} catch {
|
|
17947
20029
|
}
|
|
@@ -17954,28 +20036,28 @@ var init_liteBackend = __esm({
|
|
|
17954
20036
|
try {
|
|
17955
20037
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
17956
20038
|
for (const file of files) {
|
|
17957
|
-
unlinkSync6(
|
|
20039
|
+
unlinkSync6(join18(dir, file));
|
|
17958
20040
|
count++;
|
|
17959
20041
|
}
|
|
17960
20042
|
} catch {
|
|
17961
20043
|
}
|
|
17962
|
-
const failedDir =
|
|
20044
|
+
const failedDir = join18(dir, "failed");
|
|
17963
20045
|
try {
|
|
17964
|
-
if (
|
|
20046
|
+
if (existsSync16(failedDir)) {
|
|
17965
20047
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
17966
20048
|
for (const file of files) {
|
|
17967
|
-
unlinkSync6(
|
|
20049
|
+
unlinkSync6(join18(failedDir, file));
|
|
17968
20050
|
count++;
|
|
17969
20051
|
}
|
|
17970
20052
|
}
|
|
17971
20053
|
} catch {
|
|
17972
20054
|
}
|
|
17973
|
-
const reservedDir =
|
|
20055
|
+
const reservedDir = join18(dir, "reserved");
|
|
17974
20056
|
try {
|
|
17975
|
-
if (
|
|
20057
|
+
if (existsSync16(reservedDir)) {
|
|
17976
20058
|
const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
|
|
17977
20059
|
for (const file of files) {
|
|
17978
|
-
unlinkSync6(
|
|
20060
|
+
unlinkSync6(join18(reservedDir, file));
|
|
17979
20061
|
count++;
|
|
17980
20062
|
}
|
|
17981
20063
|
}
|
|
@@ -17998,7 +20080,7 @@ var init_liteBackend = __esm({
|
|
|
17998
20080
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
17999
20081
|
for (const file of files) {
|
|
18000
20082
|
try {
|
|
18001
|
-
const job = JSON.parse(readFileSync14(
|
|
20083
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
18002
20084
|
const attempts = job.attempts || 0;
|
|
18003
20085
|
if (attempts > 0 && attempts < maxRetries) {
|
|
18004
20086
|
results.push(job);
|
|
@@ -18021,9 +20103,9 @@ var init_liteBackend = __esm({
|
|
|
18021
20103
|
try {
|
|
18022
20104
|
const queues = readdirSync8(this.basePath);
|
|
18023
20105
|
for (const q of queues) {
|
|
18024
|
-
const failedDir =
|
|
18025
|
-
const filePath =
|
|
18026
|
-
if (
|
|
20106
|
+
const failedDir = join18(this.basePath, q, "failed");
|
|
20107
|
+
const filePath = join18(failedDir, `${jobId}.queue-data`);
|
|
20108
|
+
if (existsSync16(filePath)) {
|
|
18027
20109
|
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18028
20110
|
job.status = "pending";
|
|
18029
20111
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -18031,8 +20113,8 @@ var init_liteBackend = __esm({
|
|
|
18031
20113
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18032
20114
|
job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
|
|
18033
20115
|
const prefix = this.nextPrefix();
|
|
18034
|
-
const queueDir =
|
|
18035
|
-
|
|
20116
|
+
const queueDir = join18(this.basePath, q);
|
|
20117
|
+
writeFileSync9(join18(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
|
|
18036
20118
|
unlinkSync6(filePath);
|
|
18037
20119
|
return true;
|
|
18038
20120
|
}
|
|
@@ -18048,7 +20130,7 @@ var init_liteBackend = __esm({
|
|
|
18048
20130
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
18049
20131
|
for (const file of files) {
|
|
18050
20132
|
try {
|
|
18051
|
-
const job = JSON.parse(readFileSync14(
|
|
20133
|
+
const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
|
|
18052
20134
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18053
20135
|
job.status = "dead";
|
|
18054
20136
|
results.push(job);
|
|
@@ -18069,7 +20151,7 @@ var init_liteBackend = __esm({
|
|
|
18069
20151
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
18070
20152
|
for (const file of files) {
|
|
18071
20153
|
try {
|
|
18072
|
-
unlinkSync6(
|
|
20154
|
+
unlinkSync6(join18(failedDir, file));
|
|
18073
20155
|
count++;
|
|
18074
20156
|
} catch {
|
|
18075
20157
|
}
|
|
@@ -18082,9 +20164,9 @@ var init_liteBackend = __esm({
|
|
|
18082
20164
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
18083
20165
|
for (const file of files) {
|
|
18084
20166
|
try {
|
|
18085
|
-
const job = JSON.parse(readFileSync14(
|
|
20167
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
18086
20168
|
if (job.status === status2) {
|
|
18087
|
-
unlinkSync6(
|
|
20169
|
+
unlinkSync6(join18(dir, file));
|
|
18088
20170
|
count++;
|
|
18089
20171
|
}
|
|
18090
20172
|
} catch {
|
|
@@ -18108,7 +20190,7 @@ var init_liteBackend = __esm({
|
|
|
18108
20190
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
18109
20191
|
for (const file of files) {
|
|
18110
20192
|
try {
|
|
18111
|
-
const filePath =
|
|
20193
|
+
const filePath = join18(failedDir, file);
|
|
18112
20194
|
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18113
20195
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18114
20196
|
continue;
|
|
@@ -18118,7 +20200,7 @@ var init_liteBackend = __esm({
|
|
|
18118
20200
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18119
20201
|
job.delayUntil = null;
|
|
18120
20202
|
const prefix = this.nextPrefix();
|
|
18121
|
-
|
|
20203
|
+
writeFileSync9(join18(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
18122
20204
|
unlinkSync6(filePath);
|
|
18123
20205
|
count++;
|
|
18124
20206
|
} catch {
|
|
@@ -18138,7 +20220,7 @@ var init_liteBackend = __esm({
|
|
|
18138
20220
|
}
|
|
18139
20221
|
for (const file of files) {
|
|
18140
20222
|
if (!file.includes(id)) continue;
|
|
18141
|
-
const filePath =
|
|
20223
|
+
const filePath = join18(dir, file);
|
|
18142
20224
|
let job;
|
|
18143
20225
|
try {
|
|
18144
20226
|
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -18186,7 +20268,7 @@ var init_liteBackend = __esm({
|
|
|
18186
20268
|
error
|
|
18187
20269
|
};
|
|
18188
20270
|
const prefix = this.nextPrefix();
|
|
18189
|
-
|
|
20271
|
+
writeFileSync9(join18(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
18190
20272
|
}
|
|
18191
20273
|
/**
|
|
18192
20274
|
* Move the job to the dead-letter (failed/) directory. Terminal until a
|
|
@@ -18206,7 +20288,7 @@ var init_liteBackend = __esm({
|
|
|
18206
20288
|
error,
|
|
18207
20289
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18208
20290
|
};
|
|
18209
|
-
|
|
20291
|
+
writeFileSync9(join18(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
18210
20292
|
}
|
|
18211
20293
|
/**
|
|
18212
20294
|
* Record a failed attempt.
|
|
@@ -18242,7 +20324,7 @@ var init_liteBackend = __esm({
|
|
|
18242
20324
|
retryJob(queue, job, delaySeconds) {
|
|
18243
20325
|
this.clearReservation(queue, job.id);
|
|
18244
20326
|
try {
|
|
18245
|
-
unlinkSync6(
|
|
20327
|
+
unlinkSync6(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
18246
20328
|
} catch {
|
|
18247
20329
|
}
|
|
18248
20330
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -18661,7 +20743,7 @@ var init_queue = __esm({
|
|
|
18661
20743
|
const jobs = this.popBatch(resolvedBatchSize);
|
|
18662
20744
|
if (jobs.length === 0) {
|
|
18663
20745
|
if (resolvedPollInterval <= 0) break;
|
|
18664
|
-
await new Promise((
|
|
20746
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
18665
20747
|
continue;
|
|
18666
20748
|
}
|
|
18667
20749
|
yield jobs;
|
|
@@ -18671,7 +20753,7 @@ var init_queue = __esm({
|
|
|
18671
20753
|
const raw = this.pop();
|
|
18672
20754
|
if (raw === null) {
|
|
18673
20755
|
if (resolvedPollInterval <= 0) break;
|
|
18674
|
-
await new Promise((
|
|
20756
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
18675
20757
|
continue;
|
|
18676
20758
|
}
|
|
18677
20759
|
yield createJob(raw, this);
|
|
@@ -20746,8 +22828,8 @@ ${end}
|
|
|
20746
22828
|
|
|
20747
22829
|
// ../core/src/devAdmin.ts
|
|
20748
22830
|
import { cpus as osCpus } from "node:os";
|
|
20749
|
-
import { readFileSync as readFileSync18, writeFileSync as
|
|
20750
|
-
import { join as
|
|
22831
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync14 } from "node:fs";
|
|
22832
|
+
import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative8 } from "node:path";
|
|
20751
22833
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
20752
22834
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
20753
22835
|
function escapeHtml(value) {
|
|
@@ -20861,12 +22943,12 @@ function mapQueueJob(job, topic, status2) {
|
|
|
20861
22943
|
};
|
|
20862
22944
|
}
|
|
20863
22945
|
function readQueueDir(dir, topic, status2) {
|
|
20864
|
-
if (!
|
|
22946
|
+
if (!existsSync20(dir)) return [];
|
|
20865
22947
|
const jobs = [];
|
|
20866
22948
|
for (const filename of readdirSync12(dir).sort()) {
|
|
20867
22949
|
if (!filename.endsWith(".queue-data")) continue;
|
|
20868
22950
|
try {
|
|
20869
|
-
jobs.push(mapQueueJob(JSON.parse(readFileSync18(
|
|
22951
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
|
|
20870
22952
|
} catch {
|
|
20871
22953
|
}
|
|
20872
22954
|
}
|
|
@@ -20983,8 +23065,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
|
|
|
20983
23065
|
function resolveDevEnvVar(key) {
|
|
20984
23066
|
const live = process.env[key];
|
|
20985
23067
|
if (live !== void 0 && live !== "") return live;
|
|
20986
|
-
const envPath =
|
|
20987
|
-
if (!
|
|
23068
|
+
const envPath = join22(process.cwd(), ".env");
|
|
23069
|
+
if (!existsSync20(envPath)) return "";
|
|
20988
23070
|
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
20989
23071
|
const t = line.trim();
|
|
20990
23072
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
@@ -20994,8 +23076,8 @@ function resolveDevEnvVar(key) {
|
|
|
20994
23076
|
return "";
|
|
20995
23077
|
}
|
|
20996
23078
|
function upsertDevEnvVar(key, value) {
|
|
20997
|
-
const envPath =
|
|
20998
|
-
const lines =
|
|
23079
|
+
const envPath = join22(process.cwd(), ".env");
|
|
23080
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
20999
23081
|
let found = false;
|
|
21000
23082
|
const out = [];
|
|
21001
23083
|
for (const line of lines) {
|
|
@@ -21010,7 +23092,7 @@ function upsertDevEnvVar(key, value) {
|
|
|
21010
23092
|
} else out.push(line);
|
|
21011
23093
|
}
|
|
21012
23094
|
if (!found) out.push(`${key}=${value}`);
|
|
21013
|
-
|
|
23095
|
+
writeFileSync13(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
|
|
21014
23096
|
}
|
|
21015
23097
|
function formatUptime(seconds) {
|
|
21016
23098
|
const d = Math.floor(seconds / 86400);
|
|
@@ -21025,9 +23107,9 @@ function formatUptime(seconds) {
|
|
|
21025
23107
|
return parts.join(" ");
|
|
21026
23108
|
}
|
|
21027
23109
|
function parseEnvFile() {
|
|
21028
|
-
const envPath =
|
|
23110
|
+
const envPath = join22(process.cwd(), ".env");
|
|
21029
23111
|
const result = {};
|
|
21030
|
-
if (!
|
|
23112
|
+
if (!existsSync20(envPath)) return result;
|
|
21031
23113
|
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
21032
23114
|
for (const line of lines) {
|
|
21033
23115
|
const trimmed = line.trim();
|
|
@@ -21039,9 +23121,9 @@ function parseEnvFile() {
|
|
|
21039
23121
|
}
|
|
21040
23122
|
function walkDirRecursive(dir) {
|
|
21041
23123
|
const results = [];
|
|
21042
|
-
if (!
|
|
23124
|
+
if (!existsSync20(dir)) return results;
|
|
21043
23125
|
for (const entry of readdirSync12(dir)) {
|
|
21044
|
-
const full =
|
|
23126
|
+
const full = join22(dir, entry);
|
|
21045
23127
|
if (statSync14(full).isDirectory()) {
|
|
21046
23128
|
results.push(...walkDirRecursive(full));
|
|
21047
23129
|
} else {
|
|
@@ -21058,25 +23140,25 @@ function handleGalleryDeploy(router) {
|
|
|
21058
23140
|
res.json({ error: "No gallery item specified" }, 400);
|
|
21059
23141
|
return;
|
|
21060
23142
|
}
|
|
21061
|
-
const galleryDir =
|
|
21062
|
-
const gallerySrc =
|
|
21063
|
-
if (!
|
|
23143
|
+
const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
|
|
23144
|
+
const gallerySrc = join22(galleryDir, name, "src");
|
|
23145
|
+
if (!existsSync20(gallerySrc)) {
|
|
21064
23146
|
res.json({ error: `Gallery item '${name}' not found` }, 404);
|
|
21065
23147
|
return;
|
|
21066
23148
|
}
|
|
21067
|
-
const projectSrc =
|
|
23149
|
+
const projectSrc = resolve12(process.cwd(), "src");
|
|
21068
23150
|
const copied = [];
|
|
21069
23151
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
21070
23152
|
for (const srcFile of allFiles) {
|
|
21071
|
-
const rel =
|
|
21072
|
-
const dest =
|
|
21073
|
-
|
|
23153
|
+
const rel = relative8(gallerySrc, srcFile);
|
|
23154
|
+
const dest = join22(projectSrc, rel);
|
|
23155
|
+
mkdirSync14(dirname10(dest), { recursive: true });
|
|
21074
23156
|
copyFileSync(srcFile, dest);
|
|
21075
23157
|
copied.push(rel);
|
|
21076
23158
|
}
|
|
21077
23159
|
try {
|
|
21078
|
-
const routesDir =
|
|
21079
|
-
if (
|
|
23160
|
+
const routesDir = resolve12(process.cwd(), "src", "routes");
|
|
23161
|
+
if (existsSync20(routesDir)) {
|
|
21080
23162
|
const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
|
|
21081
23163
|
const routes = await discoverRoutes2(routesDir);
|
|
21082
23164
|
for (const route of routes) {
|
|
@@ -21092,7 +23174,7 @@ function handleGalleryDeploy(router) {
|
|
|
21092
23174
|
};
|
|
21093
23175
|
}
|
|
21094
23176
|
function safeJoin(projectRoot3, rel) {
|
|
21095
|
-
const resolved =
|
|
23177
|
+
const resolved = resolve12(projectRoot3, rel);
|
|
21096
23178
|
if (!resolved.startsWith(projectRoot3)) return null;
|
|
21097
23179
|
return resolved;
|
|
21098
23180
|
}
|
|
@@ -22055,16 +24137,16 @@ var init_devAdmin = __esm({
|
|
|
22055
24137
|
failed: queue.size("failed"),
|
|
22056
24138
|
reserved: queue.size("reserved")
|
|
22057
24139
|
};
|
|
22058
|
-
const topicDir =
|
|
24140
|
+
const topicDir = join22(queueBasePath2(), topic);
|
|
22059
24141
|
const jobs = [];
|
|
22060
24142
|
if (!statusFilter || statusFilter === "pending") {
|
|
22061
24143
|
jobs.push(...readQueueDir(topicDir, topic, "pending"));
|
|
22062
24144
|
}
|
|
22063
24145
|
if (!statusFilter || statusFilter === "reserved") {
|
|
22064
|
-
jobs.push(...readQueueDir(
|
|
24146
|
+
jobs.push(...readQueueDir(join22(topicDir, "reserved"), topic, "reserved"));
|
|
22065
24147
|
}
|
|
22066
24148
|
if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
|
|
22067
|
-
jobs.push(...readQueueDir(
|
|
24149
|
+
jobs.push(...readQueueDir(join22(topicDir, "failed"), topic, "dead_letter"));
|
|
22068
24150
|
}
|
|
22069
24151
|
res.json({ stats, jobs });
|
|
22070
24152
|
} catch (e) {
|
|
@@ -22080,10 +24162,10 @@ var init_devAdmin = __esm({
|
|
|
22080
24162
|
const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
|
|
22081
24163
|
const queueDir = queueBasePath2();
|
|
22082
24164
|
let topics = [];
|
|
22083
|
-
if (
|
|
24165
|
+
if (existsSync20(queueDir)) {
|
|
22084
24166
|
topics = readdirSync12(queueDir).filter((d) => {
|
|
22085
24167
|
try {
|
|
22086
|
-
return statSync14(
|
|
24168
|
+
return statSync14(join22(queueDir, d)).isDirectory();
|
|
22087
24169
|
} catch {
|
|
22088
24170
|
return false;
|
|
22089
24171
|
}
|
|
@@ -22398,7 +24480,7 @@ var init_devAdmin = __esm({
|
|
|
22398
24480
|
const count = parseInt(String(body.count ?? "10"), 10) || 10;
|
|
22399
24481
|
try {
|
|
22400
24482
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
22401
|
-
const dirs = ["src/orm", "src/models"].map((d) =>
|
|
24483
|
+
const dirs = ["src/orm", "src/models"].map((d) => resolve12(process.cwd(), d)).filter((d) => existsSync20(d));
|
|
22402
24484
|
const classes = [];
|
|
22403
24485
|
for (const dir of dirs) {
|
|
22404
24486
|
for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
|
|
@@ -22425,7 +24507,7 @@ var init_devAdmin = __esm({
|
|
|
22425
24507
|
const run = promisify(execFile);
|
|
22426
24508
|
try {
|
|
22427
24509
|
const { stdout, stderr } = await run("npm", ["test"], {
|
|
22428
|
-
cwd:
|
|
24510
|
+
cwd: resolve12(process.cwd()),
|
|
22429
24511
|
timeout: 18e4,
|
|
22430
24512
|
encoding: "utf-8",
|
|
22431
24513
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -22539,8 +24621,8 @@ var init_devAdmin = __esm({
|
|
|
22539
24621
|
return;
|
|
22540
24622
|
}
|
|
22541
24623
|
try {
|
|
22542
|
-
const envPath =
|
|
22543
|
-
const lines =
|
|
24624
|
+
const envPath = join22(process.cwd(), ".env");
|
|
24625
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
22544
24626
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
22545
24627
|
const newLines = [];
|
|
22546
24628
|
for (const line of lines) {
|
|
@@ -22567,7 +24649,7 @@ var init_devAdmin = __esm({
|
|
|
22567
24649
|
for (const [key, found] of Object.entries(keysFound)) {
|
|
22568
24650
|
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
22569
24651
|
}
|
|
22570
|
-
|
|
24652
|
+
writeFileSync13(envPath, newLines.join("\n") + "\n");
|
|
22571
24653
|
res.json({ success: true });
|
|
22572
24654
|
} catch (e) {
|
|
22573
24655
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -22577,26 +24659,26 @@ var init_devAdmin = __esm({
|
|
|
22577
24659
|
__devAdminFilename = fileURLToPath5(import.meta.url);
|
|
22578
24660
|
__devAdminDirname = dirname10(__devAdminFilename);
|
|
22579
24661
|
handleGalleryList = (_req, res) => {
|
|
22580
|
-
const galleryDir =
|
|
24662
|
+
const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
|
|
22581
24663
|
const items = [];
|
|
22582
|
-
if (
|
|
24664
|
+
if (existsSync20(galleryDir)) {
|
|
22583
24665
|
const entries = readdirSync12(galleryDir).sort();
|
|
22584
24666
|
for (const entry of entries) {
|
|
22585
|
-
const entryPath =
|
|
22586
|
-
const metaFile =
|
|
22587
|
-
if (statSync14(entryPath).isDirectory() &&
|
|
24667
|
+
const entryPath = join22(galleryDir, entry);
|
|
24668
|
+
const metaFile = join22(entryPath, "meta.json");
|
|
24669
|
+
if (statSync14(entryPath).isDirectory() && existsSync20(metaFile)) {
|
|
22588
24670
|
try {
|
|
22589
24671
|
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
22590
24672
|
meta.id = entry;
|
|
22591
|
-
const srcDir =
|
|
22592
|
-
if (
|
|
24673
|
+
const srcDir = join22(entryPath, "src");
|
|
24674
|
+
if (existsSync20(srcDir)) {
|
|
22593
24675
|
const allFiles = walkDirRecursive(srcDir);
|
|
22594
|
-
meta.files = allFiles.map((f) =>
|
|
24676
|
+
meta.files = allFiles.map((f) => relative8(srcDir, f));
|
|
22595
24677
|
}
|
|
22596
|
-
const projectSrc =
|
|
22597
|
-
if (
|
|
24678
|
+
const projectSrc = resolve12(process.cwd(), "src");
|
|
24679
|
+
if (existsSync20(srcDir) && meta.files) {
|
|
22598
24680
|
meta.deployed = meta.files.every(
|
|
22599
|
-
(f) =>
|
|
24681
|
+
(f) => existsSync20(join22(projectSrc, f))
|
|
22600
24682
|
);
|
|
22601
24683
|
} else {
|
|
22602
24684
|
meta.deployed = false;
|
|
@@ -22692,10 +24774,10 @@ var init_devAdmin = __esm({
|
|
|
22692
24774
|
handleFiles = async (req2, res) => {
|
|
22693
24775
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
22694
24776
|
const rel = url.searchParams.get("path") ?? ".";
|
|
22695
|
-
const root =
|
|
24777
|
+
const root = resolve12(process.cwd());
|
|
22696
24778
|
const target = safeJoin(root, rel);
|
|
22697
24779
|
const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
|
|
22698
|
-
if (!target || !
|
|
24780
|
+
if (!target || !existsSync20(target) || !statSync14(target).isDirectory()) {
|
|
22699
24781
|
res.json({ path: rel, branch, entries: [], error: "not a directory" });
|
|
22700
24782
|
return;
|
|
22701
24783
|
}
|
|
@@ -22708,8 +24790,8 @@ var init_devAdmin = __esm({
|
|
|
22708
24790
|
const entries = [];
|
|
22709
24791
|
for (const name of readdirSync12(target).sort()) {
|
|
22710
24792
|
if (devFilesHidden(name)) continue;
|
|
22711
|
-
const full =
|
|
22712
|
-
const entryRel =
|
|
24793
|
+
const full = join22(target, name);
|
|
24794
|
+
const entryRel = relative8(root, full).replace(/\\/g, "/");
|
|
22713
24795
|
if (isSecretPath(entryRel)) continue;
|
|
22714
24796
|
let isDir = false;
|
|
22715
24797
|
let size = null;
|
|
@@ -22754,7 +24836,7 @@ var init_devAdmin = __esm({
|
|
|
22754
24836
|
size
|
|
22755
24837
|
});
|
|
22756
24838
|
}
|
|
22757
|
-
res.json({ path:
|
|
24839
|
+
res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
22758
24840
|
};
|
|
22759
24841
|
DEV_ADMIN_LANG_MAP = {
|
|
22760
24842
|
".py": "python",
|
|
@@ -22799,15 +24881,15 @@ var init_devAdmin = __esm({
|
|
|
22799
24881
|
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
22800
24882
|
return;
|
|
22801
24883
|
}
|
|
22802
|
-
const root =
|
|
24884
|
+
const root = resolve12(process.cwd());
|
|
22803
24885
|
const target = safeJoin(root, rel);
|
|
22804
|
-
if (!target || !
|
|
24886
|
+
if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
|
|
22805
24887
|
res.json({ error: `File not found: ${rel}` }, 404);
|
|
22806
24888
|
return;
|
|
22807
24889
|
}
|
|
22808
24890
|
try {
|
|
22809
24891
|
const content = readFileSync18(target, "utf-8");
|
|
22810
|
-
const path8 =
|
|
24892
|
+
const path8 = relative8(root, target);
|
|
22811
24893
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22812
24894
|
} catch (e) {
|
|
22813
24895
|
res.json({ error: e.message }, 500);
|
|
@@ -22817,22 +24899,22 @@ var init_devAdmin = __esm({
|
|
|
22817
24899
|
const body = req2.body || {};
|
|
22818
24900
|
const rel = body.path || "";
|
|
22819
24901
|
const content = body.content ?? "";
|
|
22820
|
-
const root =
|
|
24902
|
+
const root = resolve12(process.cwd());
|
|
22821
24903
|
const target = safeJoin(root, rel);
|
|
22822
24904
|
if (!target) {
|
|
22823
24905
|
res.json({ error: `Path escapes project directory: ${rel}` }, 400);
|
|
22824
24906
|
return;
|
|
22825
24907
|
}
|
|
22826
24908
|
try {
|
|
22827
|
-
|
|
22828
|
-
const existed =
|
|
22829
|
-
|
|
24909
|
+
mkdirSync14(dirname10(target), { recursive: true });
|
|
24910
|
+
const existed = existsSync20(target);
|
|
24911
|
+
writeFileSync13(target, content, "utf-8");
|
|
22830
24912
|
try {
|
|
22831
24913
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
22832
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
24914
|
+
Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
|
|
22833
24915
|
} catch {
|
|
22834
24916
|
}
|
|
22835
|
-
res.json({ ok: true, path:
|
|
24917
|
+
res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22836
24918
|
} catch (e) {
|
|
22837
24919
|
res.json({ error: e.message }, 500);
|
|
22838
24920
|
}
|
|
@@ -22844,9 +24926,9 @@ var init_devAdmin = __esm({
|
|
|
22844
24926
|
res.json({ error: "Refused: secret file" }, 403);
|
|
22845
24927
|
return;
|
|
22846
24928
|
}
|
|
22847
|
-
const root =
|
|
24929
|
+
const root = resolve12(process.cwd());
|
|
22848
24930
|
const target = safeJoin(root, rel);
|
|
22849
|
-
if (!target || !
|
|
24931
|
+
if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
|
|
22850
24932
|
res.raw.writeHead(404);
|
|
22851
24933
|
res.raw.end("Not found");
|
|
22852
24934
|
return;
|
|
@@ -22879,22 +24961,22 @@ var init_devAdmin = __esm({
|
|
|
22879
24961
|
const body = req2.body || {};
|
|
22880
24962
|
const from = body.from || "";
|
|
22881
24963
|
const to = body.to || "";
|
|
22882
|
-
const root =
|
|
24964
|
+
const root = resolve12(process.cwd());
|
|
22883
24965
|
const src = safeJoin(root, from);
|
|
22884
24966
|
const dst = safeJoin(root, to);
|
|
22885
24967
|
if (!src || !dst) {
|
|
22886
24968
|
res.json({ error: "Invalid path" }, 400);
|
|
22887
24969
|
return;
|
|
22888
24970
|
}
|
|
22889
|
-
if (!
|
|
24971
|
+
if (!existsSync20(src)) {
|
|
22890
24972
|
res.json({ error: `Source not found: ${from}` }, 404);
|
|
22891
24973
|
return;
|
|
22892
24974
|
}
|
|
22893
24975
|
try {
|
|
22894
24976
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
22895
|
-
|
|
24977
|
+
mkdirSync14(dirname10(dst), { recursive: true });
|
|
22896
24978
|
renameSync3(src, dst);
|
|
22897
|
-
res.json({ ok: true, from:
|
|
24979
|
+
res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
|
|
22898
24980
|
} catch (e) {
|
|
22899
24981
|
res.json({ error: e.message }, 500);
|
|
22900
24982
|
}
|
|
@@ -22902,20 +24984,20 @@ var init_devAdmin = __esm({
|
|
|
22902
24984
|
handleFileDelete = async (req2, res) => {
|
|
22903
24985
|
const body = req2.body || {};
|
|
22904
24986
|
const rel = body.path || "";
|
|
22905
|
-
const root =
|
|
24987
|
+
const root = resolve12(process.cwd());
|
|
22906
24988
|
const target = safeJoin(root, rel);
|
|
22907
24989
|
if (!target) {
|
|
22908
24990
|
res.json({ error: "Invalid path" }, 400);
|
|
22909
24991
|
return;
|
|
22910
24992
|
}
|
|
22911
|
-
if (!
|
|
24993
|
+
if (!existsSync20(target)) {
|
|
22912
24994
|
res.json({ error: `Not found: ${rel}` }, 404);
|
|
22913
24995
|
return;
|
|
22914
24996
|
}
|
|
22915
24997
|
try {
|
|
22916
24998
|
const { rmSync } = await import("node:fs");
|
|
22917
24999
|
rmSync(target, { recursive: true, force: true });
|
|
22918
|
-
res.json({ ok: true, deleted:
|
|
25000
|
+
res.json({ ok: true, deleted: relative8(root, target) });
|
|
22919
25001
|
} catch (e) {
|
|
22920
25002
|
res.json({ error: e.message }, 500);
|
|
22921
25003
|
}
|
|
@@ -22953,7 +25035,7 @@ var init_devAdmin = __esm({
|
|
|
22953
25035
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
22954
25036
|
const args = ["install", dev ? "--save-dev" : "--save", pkg];
|
|
22955
25037
|
const output = execFileSync7("npm", args, {
|
|
22956
|
-
cwd:
|
|
25038
|
+
cwd: resolve12(process.cwd()),
|
|
22957
25039
|
timeout: 12e4,
|
|
22958
25040
|
encoding: "utf-8"
|
|
22959
25041
|
}).toString();
|
|
@@ -22965,7 +25047,7 @@ var init_devAdmin = __esm({
|
|
|
22965
25047
|
handleGitStatus = async (_req, res) => {
|
|
22966
25048
|
try {
|
|
22967
25049
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
22968
|
-
const cwd =
|
|
25050
|
+
const cwd = resolve12(process.cwd());
|
|
22969
25051
|
try {
|
|
22970
25052
|
execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
|
|
22971
25053
|
} catch {
|
|
@@ -23104,7 +25186,7 @@ var init_devAdmin = __esm({
|
|
|
23104
25186
|
try {
|
|
23105
25187
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
23106
25188
|
const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
|
|
23107
|
-
cwd:
|
|
25189
|
+
cwd: resolve12(process.cwd()),
|
|
23108
25190
|
timeout: 3e4,
|
|
23109
25191
|
encoding: "utf-8"
|
|
23110
25192
|
}).toString();
|
|
@@ -23249,21 +25331,21 @@ var init_devAdmin = __esm({
|
|
|
23249
25331
|
});
|
|
23250
25332
|
};
|
|
23251
25333
|
handleDevAdminJs = async (_req, res) => {
|
|
23252
|
-
const { readFileSync: readFileSync27, existsSync:
|
|
23253
|
-
const { dirname: dirname15, join:
|
|
25334
|
+
const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
|
|
25335
|
+
const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
|
|
23254
25336
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
23255
25337
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
23256
25338
|
const candidates = [
|
|
23257
|
-
|
|
25339
|
+
join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
23258
25340
|
// src/../public/js/
|
|
23259
|
-
|
|
25341
|
+
join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
23260
25342
|
// deeper nesting
|
|
23261
|
-
|
|
23262
|
-
|
|
25343
|
+
resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
|
|
25344
|
+
resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
|
|
23263
25345
|
// project public/
|
|
23264
25346
|
];
|
|
23265
25347
|
for (const jsPath of candidates) {
|
|
23266
|
-
if (
|
|
25348
|
+
if (existsSync28(jsPath)) {
|
|
23267
25349
|
try {
|
|
23268
25350
|
const content = readFileSync27(jsPath, "utf-8");
|
|
23269
25351
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -23288,8 +25370,8 @@ var init_devAdmin = __esm({
|
|
|
23288
25370
|
});
|
|
23289
25371
|
|
|
23290
25372
|
// ../core/src/i18n.ts
|
|
23291
|
-
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as
|
|
23292
|
-
import { join as
|
|
25373
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
|
|
25374
|
+
import { join as join23, resolve as resolve13 } from "node:path";
|
|
23293
25375
|
var I18n;
|
|
23294
25376
|
var init_i18n = __esm({
|
|
23295
25377
|
"../core/src/i18n.ts"() {
|
|
@@ -23309,7 +25391,7 @@ var init_i18n = __esm({
|
|
|
23309
25391
|
* (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
|
|
23310
25392
|
*/
|
|
23311
25393
|
constructor(locale, path8) {
|
|
23312
|
-
this._localeDir =
|
|
25394
|
+
this._localeDir = resolve13(
|
|
23313
25395
|
path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
|
|
23314
25396
|
);
|
|
23315
25397
|
this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
|
|
@@ -23366,7 +25448,7 @@ var init_i18n = __esm({
|
|
|
23366
25448
|
}
|
|
23367
25449
|
/** List available locale codes based on JSON files in the locale directory. */
|
|
23368
25450
|
availableLocales() {
|
|
23369
|
-
if (!
|
|
25451
|
+
if (!existsSync21(this._localeDir)) {
|
|
23370
25452
|
return [this._defaultLocale];
|
|
23371
25453
|
}
|
|
23372
25454
|
try {
|
|
@@ -23382,8 +25464,8 @@ var init_i18n = __esm({
|
|
|
23382
25464
|
if (this._translations.has(locale)) {
|
|
23383
25465
|
return;
|
|
23384
25466
|
}
|
|
23385
|
-
const filePath =
|
|
23386
|
-
if (
|
|
25467
|
+
const filePath = join23(this._localeDir, `${locale}.json`);
|
|
25468
|
+
if (existsSync21(filePath)) {
|
|
23387
25469
|
try {
|
|
23388
25470
|
const raw = readFileSync19(filePath, "utf-8");
|
|
23389
25471
|
const data = JSON.parse(raw);
|
|
@@ -23395,8 +25477,8 @@ var init_i18n = __esm({
|
|
|
23395
25477
|
}
|
|
23396
25478
|
}
|
|
23397
25479
|
for (const ext of [".yml", ".yaml"]) {
|
|
23398
|
-
const yamlPath =
|
|
23399
|
-
if (
|
|
25480
|
+
const yamlPath = join23(this._localeDir, `${locale}${ext}`);
|
|
25481
|
+
if (existsSync21(yamlPath)) {
|
|
23400
25482
|
try {
|
|
23401
25483
|
const raw = readFileSync19(yamlPath, "utf-8");
|
|
23402
25484
|
const data = _I18n._parseSimpleYaml(raw);
|
|
@@ -23741,7 +25823,7 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
23741
25823
|
return clean;
|
|
23742
25824
|
});
|
|
23743
25825
|
}
|
|
23744
|
-
function
|
|
25826
|
+
function generate2(routes, models = []) {
|
|
23745
25827
|
const info = {
|
|
23746
25828
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
23747
25829
|
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
@@ -24189,7 +26271,7 @@ __export(src_exports, {
|
|
|
24189
26271
|
addSchema: () => addSchema,
|
|
24190
26272
|
addSecurityScheme: () => addSecurityScheme,
|
|
24191
26273
|
createSwaggerRoutes: () => createSwaggerRoutes,
|
|
24192
|
-
generate: () =>
|
|
26274
|
+
generate: () => generate2,
|
|
24193
26275
|
resetRegistry: () => resetRegistry,
|
|
24194
26276
|
swaggerEnabled: () => swaggerEnabled
|
|
24195
26277
|
});
|
|
@@ -24543,8 +26625,8 @@ function writeMcpDiscovery(projectRoot3, port) {
|
|
|
24543
26625
|
const lines = contents.split(/\r?\n/);
|
|
24544
26626
|
const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
|
|
24545
26627
|
if (!already) {
|
|
24546
|
-
const
|
|
24547
|
-
fs8.writeFileSync(gitignorePath, `${contents}${
|
|
26628
|
+
const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
|
|
26629
|
+
fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
|
|
24548
26630
|
`, "utf-8");
|
|
24549
26631
|
}
|
|
24550
26632
|
}
|
|
@@ -24563,8 +26645,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
24563
26645
|
// ../core/src/server.ts
|
|
24564
26646
|
import { createServer as createServer2 } from "node:http";
|
|
24565
26647
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
24566
|
-
import { resolve as
|
|
24567
|
-
import { existsSync as
|
|
26648
|
+
import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative9 } from "node:path";
|
|
26649
|
+
import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
24568
26650
|
import { isatty } from "node:tty";
|
|
24569
26651
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
24570
26652
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -24602,8 +26684,8 @@ function swaggerAdvertised() {
|
|
|
24602
26684
|
return TRUTHY2.includes(raw);
|
|
24603
26685
|
}
|
|
24604
26686
|
async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
|
|
24605
|
-
const dir =
|
|
24606
|
-
if (!
|
|
26687
|
+
const dir = resolve15(base, migrationDir);
|
|
26688
|
+
if (!existsSync23(dir)) return;
|
|
24607
26689
|
let hasSql = false;
|
|
24608
26690
|
try {
|
|
24609
26691
|
hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
|
|
@@ -24744,12 +26826,12 @@ async function renderErrorPage(code, data, templatesDir) {
|
|
|
24744
26826
|
}
|
|
24745
26827
|
return instance;
|
|
24746
26828
|
};
|
|
24747
|
-
const userTemplatePath =
|
|
24748
|
-
if (
|
|
26829
|
+
const userTemplatePath = join25(templatesDir, templateFile);
|
|
26830
|
+
if (existsSync23(userTemplatePath)) {
|
|
24749
26831
|
return getCachedFrond(templatesDir).render(templateFile, data);
|
|
24750
26832
|
}
|
|
24751
|
-
const builtinTemplatePath =
|
|
24752
|
-
if (
|
|
26833
|
+
const builtinTemplatePath = join25(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
|
|
26834
|
+
if (existsSync23(builtinTemplatePath)) {
|
|
24753
26835
|
return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
|
|
24754
26836
|
}
|
|
24755
26837
|
return null;
|
|
@@ -24766,29 +26848,29 @@ function injectDevToolbar(html, ctx) {
|
|
|
24766
26848
|
}
|
|
24767
26849
|
function walkGalleryFiles(dir) {
|
|
24768
26850
|
const results = [];
|
|
24769
|
-
if (!
|
|
26851
|
+
if (!existsSync23(dir)) return results;
|
|
24770
26852
|
for (const f of readdirSync14(dir)) {
|
|
24771
|
-
const full =
|
|
26853
|
+
const full = join25(dir, f);
|
|
24772
26854
|
if (statSync15(full).isDirectory()) results.push(...walkGalleryFiles(full));
|
|
24773
26855
|
else results.push(full);
|
|
24774
26856
|
}
|
|
24775
26857
|
return results;
|
|
24776
26858
|
}
|
|
24777
26859
|
function getGalleryDeployedState() {
|
|
24778
|
-
const galleryDir =
|
|
26860
|
+
const galleryDir = resolve15(__dirname, "..", "gallery");
|
|
24779
26861
|
const state = {};
|
|
24780
|
-
if (!
|
|
26862
|
+
if (!existsSync23(galleryDir)) return state;
|
|
24781
26863
|
try {
|
|
24782
26864
|
const entries = readdirSync14(galleryDir).sort();
|
|
24783
26865
|
for (const entry of entries) {
|
|
24784
|
-
const entryPath =
|
|
24785
|
-
const metaFile =
|
|
24786
|
-
if (statSync15(entryPath).isDirectory() &&
|
|
24787
|
-
const srcDir =
|
|
24788
|
-
if (
|
|
26866
|
+
const entryPath = join25(galleryDir, entry);
|
|
26867
|
+
const metaFile = join25(entryPath, "meta.json");
|
|
26868
|
+
if (statSync15(entryPath).isDirectory() && existsSync23(metaFile)) {
|
|
26869
|
+
const srcDir = join25(entryPath, "src");
|
|
26870
|
+
if (existsSync23(srcDir)) {
|
|
24789
26871
|
const files = walkGalleryFiles(srcDir);
|
|
24790
|
-
const projectSrc =
|
|
24791
|
-
state[entry] = files.every((f) =>
|
|
26872
|
+
const projectSrc = resolve15(process.cwd(), "src");
|
|
26873
|
+
state[entry] = files.every((f) => existsSync23(join25(projectSrc, relative9(srcDir, f))));
|
|
24792
26874
|
} else {
|
|
24793
26875
|
state[entry] = false;
|
|
24794
26876
|
}
|
|
@@ -24816,9 +26898,9 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
24816
26898
|
const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
|
|
24817
26899
|
if (isDev2) {
|
|
24818
26900
|
if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
|
|
24819
|
-
const pagesDir =
|
|
26901
|
+
const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
|
|
24820
26902
|
for (const ext of [".twig", ".html"]) {
|
|
24821
|
-
if (
|
|
26903
|
+
if (existsSync23(resolve15(pagesDir, cleanPath + ext))) {
|
|
24822
26904
|
return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
|
|
24823
26905
|
}
|
|
24824
26906
|
}
|
|
@@ -24826,14 +26908,14 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
24826
26908
|
}
|
|
24827
26909
|
if (!templateCache) {
|
|
24828
26910
|
templateCache = /* @__PURE__ */ new Map();
|
|
24829
|
-
const pagesDir =
|
|
24830
|
-
if (
|
|
26911
|
+
const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
|
|
26912
|
+
if (existsSync23(pagesDir)) {
|
|
24831
26913
|
const scan = (dir, prefix) => {
|
|
24832
26914
|
for (const entry of readdirSync14(dir, { withFileTypes: true })) {
|
|
24833
26915
|
if (entry.name.startsWith("_")) continue;
|
|
24834
26916
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
24835
26917
|
if (entry.isDirectory()) {
|
|
24836
|
-
scan(
|
|
26918
|
+
scan(resolve15(dir, entry.name), rel);
|
|
24837
26919
|
} else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
|
|
24838
26920
|
const urlPath = rel.replace(/\.(twig|html)$/, "");
|
|
24839
26921
|
if (!templateCache.has(urlPath)) {
|
|
@@ -25238,7 +27320,7 @@ function serveTemplateFallback(ctx) {
|
|
|
25238
27320
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
25239
27321
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
25240
27322
|
if (!tplFile) return false;
|
|
25241
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(
|
|
27323
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve15(ctx.templatesDir, tplFile), "utf-8");
|
|
25242
27324
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
25243
27325
|
ctx.res.raw.end(html);
|
|
25244
27326
|
return true;
|
|
@@ -25277,9 +27359,9 @@ function serveMethodNotAllowed(ctx) {
|
|
|
25277
27359
|
}
|
|
25278
27360
|
function serveStaticAsset(ctx) {
|
|
25279
27361
|
const custom = process.env.TINA4_PUBLIC_DIR;
|
|
25280
|
-
if (custom &&
|
|
25281
|
-
if (
|
|
25282
|
-
if (
|
|
27362
|
+
if (custom && existsSync23(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
|
|
27363
|
+
if (existsSync23(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
|
|
27364
|
+
if (existsSync23(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
|
|
25283
27365
|
if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
|
|
25284
27366
|
if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
|
|
25285
27367
|
}
|
|
@@ -25303,10 +27385,10 @@ async function serveNotFound(ctx) {
|
|
|
25303
27385
|
return true;
|
|
25304
27386
|
}
|
|
25305
27387
|
async function buildDispatchContext(router, base) {
|
|
25306
|
-
const root = base ?
|
|
25307
|
-
const staticDir =
|
|
25308
|
-
const srcPublicDir =
|
|
25309
|
-
const templatesDir =
|
|
27388
|
+
const root = base ? resolve15(base) : process.cwd();
|
|
27389
|
+
const staticDir = resolve15(root, "public");
|
|
27390
|
+
const srcPublicDir = resolve15(root, "src/public");
|
|
27391
|
+
const templatesDir = resolve15(root, "src/templates");
|
|
25310
27392
|
let frondEngine = null;
|
|
25311
27393
|
try {
|
|
25312
27394
|
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
@@ -25470,13 +27552,13 @@ ${reset2}
|
|
|
25470
27552
|
};
|
|
25471
27553
|
}
|
|
25472
27554
|
}
|
|
25473
|
-
const base = config?.basePath ?
|
|
25474
|
-
const routesDir =
|
|
25475
|
-
const modelsDir =
|
|
25476
|
-
const ormDir =
|
|
25477
|
-
const staticDir =
|
|
25478
|
-
const srcPublicDir =
|
|
25479
|
-
const templatesDir =
|
|
27555
|
+
const base = config?.basePath ? resolve15(config.basePath) : process.cwd();
|
|
27556
|
+
const routesDir = resolve15(base, config?.routesDir ?? "src/routes");
|
|
27557
|
+
const modelsDir = resolve15(base, config?.modelsDir ?? "src/models");
|
|
27558
|
+
const ormDir = resolve15(base, "src/orm");
|
|
27559
|
+
const staticDir = resolve15(base, config?.staticDir ?? "public");
|
|
27560
|
+
const srcPublicDir = resolve15(base, "src/public");
|
|
27561
|
+
const templatesDir = resolve15(base, config?.templatesDir ?? "src/templates");
|
|
25480
27562
|
const router = new Router();
|
|
25481
27563
|
const middleware = new MiddlewareChain();
|
|
25482
27564
|
globalThis.__tina4_router = router;
|
|
@@ -25510,8 +27592,8 @@ ${reset2}
|
|
|
25510
27592
|
} catch {
|
|
25511
27593
|
}
|
|
25512
27594
|
if (frondEngine) {
|
|
25513
|
-
const localeDir =
|
|
25514
|
-
if (
|
|
27595
|
+
const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
27596
|
+
if (existsSync23(localeDir)) {
|
|
25515
27597
|
try {
|
|
25516
27598
|
const localeFiles = readdirSync14(localeDir).filter((f) => f.endsWith(".json"));
|
|
25517
27599
|
if (localeFiles.length > 0 && !frondEngine.globals?.t) {
|
|
@@ -25526,7 +27608,7 @@ ${reset2}
|
|
|
25526
27608
|
middleware.use(requestLogger());
|
|
25527
27609
|
middleware.use(rateLimiter());
|
|
25528
27610
|
MiddlewareRunner.use(SecurityHeadersMiddleware);
|
|
25529
|
-
if (
|
|
27611
|
+
if (existsSync23(routesDir)) {
|
|
25530
27612
|
const routes = await discoverRoutes(routesDir);
|
|
25531
27613
|
for (const route of routes) {
|
|
25532
27614
|
router.addRoute(route);
|
|
@@ -25546,8 +27628,8 @@ ${reset2}
|
|
|
25546
27628
|
console.log(`
|
|
25547
27629
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
25548
27630
|
}
|
|
25549
|
-
const hasOrmDir =
|
|
25550
|
-
const hasModelsDir =
|
|
27631
|
+
const hasOrmDir = existsSync23(ormDir);
|
|
27632
|
+
const hasModelsDir = existsSync23(modelsDir);
|
|
25551
27633
|
if (hasOrmDir || hasModelsDir) {
|
|
25552
27634
|
try {
|
|
25553
27635
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
@@ -25604,7 +27686,7 @@ ${reset2}
|
|
|
25604
27686
|
let modelDefs = [];
|
|
25605
27687
|
try {
|
|
25606
27688
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
25607
|
-
const allModelDirs = [ormDir, modelsDir].filter((d) =>
|
|
27689
|
+
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync23(d));
|
|
25608
27690
|
const seenTables = /* @__PURE__ */ new Set();
|
|
25609
27691
|
for (const dir of allModelDirs) {
|
|
25610
27692
|
const discovered = await orm.discoverModels(dir);
|
|
@@ -25839,8 +27921,8 @@ var init_server = __esm({
|
|
|
25839
27921
|
init_version();
|
|
25840
27922
|
__filename = fileURLToPath6(import.meta.url);
|
|
25841
27923
|
__dirname = dirname11(__filename);
|
|
25842
|
-
BUILTIN_ERROR_TEMPLATES_DIR =
|
|
25843
|
-
BUILTIN_PUBLIC_DIR =
|
|
27924
|
+
BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
|
|
27925
|
+
BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
|
|
25844
27926
|
swaggerAssetsEnabled = false;
|
|
25845
27927
|
DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
25846
27928
|
frondCache = /* @__PURE__ */ new Map();
|
|
@@ -25996,8 +28078,8 @@ var init_env = __esm({
|
|
|
25996
28078
|
|
|
25997
28079
|
// ../core/src/fakeData.ts
|
|
25998
28080
|
import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
|
|
25999
|
-
import { existsSync as
|
|
26000
|
-
import { resolve as
|
|
28081
|
+
import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
|
|
28082
|
+
import { resolve as resolve16, join as join26 } from "node:path";
|
|
26001
28083
|
function mulberry32(seed) {
|
|
26002
28084
|
let s = seed | 0;
|
|
26003
28085
|
return () => {
|
|
@@ -26431,12 +28513,12 @@ var init_fakeData = __esm({
|
|
|
26431
28513
|
* Returns an array of executed file paths.
|
|
26432
28514
|
*/
|
|
26433
28515
|
async seedDir(seedDir) {
|
|
26434
|
-
const dir =
|
|
26435
|
-
if (!
|
|
28516
|
+
const dir = resolve16(seedDir ?? "src/seeds");
|
|
28517
|
+
if (!existsSync24(dir)) return [];
|
|
26436
28518
|
const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
|
|
26437
28519
|
const executed = [];
|
|
26438
28520
|
for (const file of files) {
|
|
26439
|
-
const fullPath =
|
|
28521
|
+
const fullPath = join26(dir, file);
|
|
26440
28522
|
try {
|
|
26441
28523
|
const mod = await import(fullPath);
|
|
26442
28524
|
if (typeof mod.default === "function") {
|
|
@@ -26535,7 +28617,7 @@ var init_mqttMessage = __esm({
|
|
|
26535
28617
|
import net2 from "node:net";
|
|
26536
28618
|
import tls from "node:tls";
|
|
26537
28619
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
26538
|
-
import { existsSync as
|
|
28620
|
+
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
|
|
26539
28621
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
26540
28622
|
var init_mqtt = __esm({
|
|
26541
28623
|
"../core/src/mqtt.ts"() {
|
|
@@ -26738,7 +28820,7 @@ var init_mqtt = __esm({
|
|
|
26738
28820
|
*/
|
|
26739
28821
|
async connect() {
|
|
26740
28822
|
this.closeSocket();
|
|
26741
|
-
if (this.secure && this.tlsVerify && this.caFile && !
|
|
28823
|
+
if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
|
|
26742
28824
|
throw new MqttError(
|
|
26743
28825
|
`MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
|
|
26744
28826
|
);
|
|
@@ -26975,7 +29057,7 @@ var init_mqtt = __esm({
|
|
|
26975
29057
|
* a later client.
|
|
26976
29058
|
*/
|
|
26977
29059
|
openSocket() {
|
|
26978
|
-
return new Promise((
|
|
29060
|
+
return new Promise((resolve21, reject) => {
|
|
26979
29061
|
let settled = false;
|
|
26980
29062
|
const settle = (fn) => {
|
|
26981
29063
|
if (settled) return;
|
|
@@ -27001,9 +29083,9 @@ var init_mqtt = __esm({
|
|
|
27001
29083
|
rejectUnauthorized: this.tlsVerify
|
|
27002
29084
|
};
|
|
27003
29085
|
if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
|
|
27004
|
-
sock = tls.connect(opts, () => settle(() =>
|
|
29086
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
27005
29087
|
} else {
|
|
27006
|
-
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() =>
|
|
29088
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
27007
29089
|
}
|
|
27008
29090
|
sock.once("error", (err) => {
|
|
27009
29091
|
settle(() => {
|
|
@@ -27042,13 +29124,13 @@ var init_mqtt = __esm({
|
|
|
27042
29124
|
writePacket(header, body) {
|
|
27043
29125
|
if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
|
|
27044
29126
|
const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
|
|
27045
|
-
return new Promise((
|
|
29127
|
+
return new Promise((resolve21, reject) => {
|
|
27046
29128
|
this.socket.write(packet, (err) => {
|
|
27047
29129
|
if (err) {
|
|
27048
29130
|
reject(new MqttError(`MQTT write failed: ${err.message}`));
|
|
27049
29131
|
} else {
|
|
27050
29132
|
this.lastWriteAt = Date.now();
|
|
27051
|
-
|
|
29133
|
+
resolve21();
|
|
27052
29134
|
}
|
|
27053
29135
|
});
|
|
27054
29136
|
});
|
|
@@ -27081,7 +29163,7 @@ var init_mqtt = __esm({
|
|
|
27081
29163
|
if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
|
|
27082
29164
|
if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
|
|
27083
29165
|
if (this.socketError !== null) return Promise.reject(this.socketError);
|
|
27084
|
-
return new Promise((
|
|
29166
|
+
return new Promise((resolve21, reject) => {
|
|
27085
29167
|
let timer = null;
|
|
27086
29168
|
if (deadline !== null) {
|
|
27087
29169
|
const remaining = deadline - Date.now();
|
|
@@ -27096,7 +29178,7 @@ var init_mqtt = __esm({
|
|
|
27096
29178
|
}
|
|
27097
29179
|
}, remaining);
|
|
27098
29180
|
}
|
|
27099
|
-
this.waiter = { need, resolve:
|
|
29181
|
+
this.waiter = { need, resolve: resolve21, reject, timer };
|
|
27100
29182
|
this.serviceWaiter();
|
|
27101
29183
|
});
|
|
27102
29184
|
}
|
|
@@ -27221,7 +29303,7 @@ var init_mqtt = __esm({
|
|
|
27221
29303
|
|
|
27222
29304
|
// ../core/src/service.ts
|
|
27223
29305
|
import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
|
|
27224
|
-
import { join as
|
|
29306
|
+
import { join as join27, extname as extname6 } from "node:path";
|
|
27225
29307
|
import { pathToFileURL } from "node:url";
|
|
27226
29308
|
function matchCronField(field, value) {
|
|
27227
29309
|
if (field === "*") return true;
|
|
@@ -27395,7 +29477,7 @@ var init_service = __esm({
|
|
|
27395
29477
|
for (const entry of entries) {
|
|
27396
29478
|
const ext = extname6(entry);
|
|
27397
29479
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27398
|
-
const fullPath =
|
|
29480
|
+
const fullPath = join27(dir, entry);
|
|
27399
29481
|
const stat = statSync16(fullPath);
|
|
27400
29482
|
if (!stat.isFile()) continue;
|
|
27401
29483
|
try {
|
|
@@ -27522,7 +29604,7 @@ var init_service = __esm({
|
|
|
27522
29604
|
for (const entry of entries) {
|
|
27523
29605
|
const ext = extname6(entry);
|
|
27524
29606
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27525
|
-
const fullPath =
|
|
29607
|
+
const fullPath = join27(dir, entry);
|
|
27526
29608
|
if (watchedFiles.has(fullPath)) continue;
|
|
27527
29609
|
watchedFiles.add(fullPath);
|
|
27528
29610
|
watchFile(fullPath, { interval: 1e3 }, async () => {
|
|
@@ -28131,7 +30213,7 @@ var init_api = __esm({
|
|
|
28131
30213
|
* `res.destroy()`.
|
|
28132
30214
|
*/
|
|
28133
30215
|
openStreamRequest(method, url, headers, data, connectSec) {
|
|
28134
|
-
return new Promise((
|
|
30216
|
+
return new Promise((resolve21, reject) => {
|
|
28135
30217
|
let parsed;
|
|
28136
30218
|
try {
|
|
28137
30219
|
parsed = new URL2(url);
|
|
@@ -28153,7 +30235,7 @@ var init_api = __esm({
|
|
|
28153
30235
|
options.rejectUnauthorized = false;
|
|
28154
30236
|
}
|
|
28155
30237
|
const req2 = protocolModule.request(options, (res) => {
|
|
28156
|
-
|
|
30238
|
+
resolve21({ res });
|
|
28157
30239
|
});
|
|
28158
30240
|
req2.on("timeout", () => {
|
|
28159
30241
|
req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
|
|
@@ -28291,12 +30373,12 @@ var init_api = __esm({
|
|
|
28291
30373
|
* authenticate to.
|
|
28292
30374
|
*/
|
|
28293
30375
|
performRequest(method, url, headers, data, redirectsLeft) {
|
|
28294
|
-
return new Promise((
|
|
30376
|
+
return new Promise((resolve21) => {
|
|
28295
30377
|
let parsed;
|
|
28296
30378
|
try {
|
|
28297
30379
|
parsed = new URL2(url);
|
|
28298
30380
|
} catch (err) {
|
|
28299
|
-
|
|
30381
|
+
resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
|
|
28300
30382
|
return;
|
|
28301
30383
|
}
|
|
28302
30384
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -28321,7 +30403,7 @@ var init_api = __esm({
|
|
|
28321
30403
|
try {
|
|
28322
30404
|
nextUrl = new URL2(location, url).toString();
|
|
28323
30405
|
} catch {
|
|
28324
|
-
|
|
30406
|
+
resolve21({ kind: "response", res });
|
|
28325
30407
|
return;
|
|
28326
30408
|
}
|
|
28327
30409
|
const crossOrigin = !sameOrigin(url, nextUrl);
|
|
@@ -28339,17 +30421,17 @@ var init_api = __esm({
|
|
|
28339
30421
|
deleteHeaderCaseInsensitive(nextHeaders, name);
|
|
28340
30422
|
}
|
|
28341
30423
|
}
|
|
28342
|
-
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(
|
|
30424
|
+
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
|
|
28343
30425
|
return;
|
|
28344
30426
|
}
|
|
28345
|
-
|
|
30427
|
+
resolve21({ kind: "response", res });
|
|
28346
30428
|
});
|
|
28347
30429
|
req2.on("timeout", () => {
|
|
28348
30430
|
req2.destroy();
|
|
28349
|
-
|
|
30431
|
+
resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
|
|
28350
30432
|
});
|
|
28351
30433
|
req2.on("error", (err) => {
|
|
28352
|
-
|
|
30434
|
+
resolve21({ kind: "error", error: err.message });
|
|
28353
30435
|
});
|
|
28354
30436
|
if (data) {
|
|
28355
30437
|
req2.write(data);
|
|
@@ -28359,7 +30441,7 @@ var init_api = __esm({
|
|
|
28359
30441
|
}
|
|
28360
30442
|
/** Buffer a response body, parse JSON if possible, and store cookies. */
|
|
28361
30443
|
readResponse(res) {
|
|
28362
|
-
return new Promise((
|
|
30444
|
+
return new Promise((resolve21) => {
|
|
28363
30445
|
const chunks = [];
|
|
28364
30446
|
res.on("data", (chunk) => {
|
|
28365
30447
|
chunks.push(chunk);
|
|
@@ -28374,7 +30456,7 @@ var init_api = __esm({
|
|
|
28374
30456
|
} catch {
|
|
28375
30457
|
parsed = raw;
|
|
28376
30458
|
}
|
|
28377
|
-
|
|
30459
|
+
resolve21({
|
|
28378
30460
|
http_code: res.statusCode ?? null,
|
|
28379
30461
|
body: parsed,
|
|
28380
30462
|
headers: respHeaders,
|
|
@@ -28382,7 +30464,7 @@ var init_api = __esm({
|
|
|
28382
30464
|
});
|
|
28383
30465
|
});
|
|
28384
30466
|
res.on("error", (err) => {
|
|
28385
|
-
|
|
30467
|
+
resolve21({ http_code: null, body: null, headers: {}, error: err.message });
|
|
28386
30468
|
});
|
|
28387
30469
|
});
|
|
28388
30470
|
}
|
|
@@ -28446,7 +30528,7 @@ function parseMailRedirectList(raw) {
|
|
|
28446
30528
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
28447
30529
|
}
|
|
28448
30530
|
function readResponse(socket) {
|
|
28449
|
-
return new Promise((
|
|
30531
|
+
return new Promise((resolve21, reject) => {
|
|
28450
30532
|
let buffer = "";
|
|
28451
30533
|
const onData = (chunk) => {
|
|
28452
30534
|
buffer += chunk.toString("utf-8");
|
|
@@ -28458,7 +30540,7 @@ function readResponse(socket) {
|
|
|
28458
30540
|
if (line.length >= 4 && line[3] === " ") {
|
|
28459
30541
|
socket.removeListener("data", onData);
|
|
28460
30542
|
socket.removeListener("error", onError);
|
|
28461
|
-
|
|
30543
|
+
resolve21({ code, text: buffer.trim() });
|
|
28462
30544
|
return;
|
|
28463
30545
|
}
|
|
28464
30546
|
}
|
|
@@ -28472,10 +30554,10 @@ function readResponse(socket) {
|
|
|
28472
30554
|
});
|
|
28473
30555
|
}
|
|
28474
30556
|
function sendCommand(socket, command) {
|
|
28475
|
-
return new Promise((
|
|
30557
|
+
return new Promise((resolve21, reject) => {
|
|
28476
30558
|
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
28477
30559
|
if (err) return reject(err);
|
|
28478
|
-
readResponse(socket).then(
|
|
30560
|
+
readResponse(socket).then(resolve21, reject);
|
|
28479
30561
|
});
|
|
28480
30562
|
});
|
|
28481
30563
|
}
|
|
@@ -28575,7 +30657,7 @@ function imapQuote(s) {
|
|
|
28575
30657
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
28576
30658
|
}
|
|
28577
30659
|
function imapReadLine(socket) {
|
|
28578
|
-
return new Promise((
|
|
30660
|
+
return new Promise((resolve21, reject) => {
|
|
28579
30661
|
let buffer = "";
|
|
28580
30662
|
const onData = (chunk) => {
|
|
28581
30663
|
buffer += chunk.toString("utf-8");
|
|
@@ -28583,7 +30665,7 @@ function imapReadLine(socket) {
|
|
|
28583
30665
|
if (nlIndex !== -1) {
|
|
28584
30666
|
socket.removeListener("data", onData);
|
|
28585
30667
|
socket.removeListener("error", onError);
|
|
28586
|
-
|
|
30668
|
+
resolve21(buffer);
|
|
28587
30669
|
}
|
|
28588
30670
|
};
|
|
28589
30671
|
const onError = (err) => {
|
|
@@ -28595,7 +30677,7 @@ function imapReadLine(socket) {
|
|
|
28595
30677
|
});
|
|
28596
30678
|
}
|
|
28597
30679
|
function imapCommand(socket, command) {
|
|
28598
|
-
return new Promise((
|
|
30680
|
+
return new Promise((resolve21, reject) => {
|
|
28599
30681
|
imapTagCounter++;
|
|
28600
30682
|
const tag = `T${imapTagCounter}`;
|
|
28601
30683
|
const fullCommand = `${tag} ${command}\r
|
|
@@ -28606,7 +30688,7 @@ function imapCommand(socket, command) {
|
|
|
28606
30688
|
if (buffer.includes(`${tag} OK`)) {
|
|
28607
30689
|
socket.removeListener("data", onData);
|
|
28608
30690
|
socket.removeListener("error", onError);
|
|
28609
|
-
|
|
30691
|
+
resolve21(buffer);
|
|
28610
30692
|
return;
|
|
28611
30693
|
}
|
|
28612
30694
|
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
@@ -28911,14 +30993,14 @@ var init_messenger = __esm({
|
|
|
28911
30993
|
let socket;
|
|
28912
30994
|
if (this.port === 465) {
|
|
28913
30995
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
28914
|
-
await new Promise((
|
|
28915
|
-
socket.once("secureConnect",
|
|
30996
|
+
await new Promise((resolve21, reject) => {
|
|
30997
|
+
socket.once("secureConnect", resolve21);
|
|
28916
30998
|
socket.once("error", reject);
|
|
28917
30999
|
});
|
|
28918
31000
|
} else {
|
|
28919
31001
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
28920
|
-
await new Promise((
|
|
28921
|
-
socket.once("connect",
|
|
31002
|
+
await new Promise((resolve21, reject) => {
|
|
31003
|
+
socket.once("connect", resolve21);
|
|
28922
31004
|
socket.once("error", reject);
|
|
28923
31005
|
});
|
|
28924
31006
|
}
|
|
@@ -28942,8 +31024,8 @@ var init_messenger = __esm({
|
|
|
28942
31024
|
socket = tls2.connect(
|
|
28943
31025
|
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
28944
31026
|
);
|
|
28945
|
-
await new Promise((
|
|
28946
|
-
socket.once("secureConnect",
|
|
31027
|
+
await new Promise((resolve21, reject) => {
|
|
31028
|
+
socket.once("secureConnect", resolve21);
|
|
28947
31029
|
socket.once("error", reject);
|
|
28948
31030
|
});
|
|
28949
31031
|
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
@@ -29036,14 +31118,14 @@ var init_messenger = __esm({
|
|
|
29036
31118
|
let socket;
|
|
29037
31119
|
if (this.port === 465) {
|
|
29038
31120
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
29039
|
-
await new Promise((
|
|
29040
|
-
socket.once("secureConnect",
|
|
31121
|
+
await new Promise((resolve21, reject) => {
|
|
31122
|
+
socket.once("secureConnect", resolve21);
|
|
29041
31123
|
socket.once("error", reject);
|
|
29042
31124
|
});
|
|
29043
31125
|
} else {
|
|
29044
31126
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
29045
|
-
await new Promise((
|
|
29046
|
-
socket.once("connect",
|
|
31127
|
+
await new Promise((resolve21, reject) => {
|
|
31128
|
+
socket.once("connect", resolve21);
|
|
29047
31129
|
socket.once("error", reject);
|
|
29048
31130
|
});
|
|
29049
31131
|
}
|
|
@@ -29078,14 +31160,14 @@ var init_messenger = __esm({
|
|
|
29078
31160
|
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
29079
31161
|
if (useTls) {
|
|
29080
31162
|
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
29081
|
-
await new Promise((
|
|
29082
|
-
socket.once("secureConnect",
|
|
31163
|
+
await new Promise((resolve21, reject) => {
|
|
31164
|
+
socket.once("secureConnect", resolve21);
|
|
29083
31165
|
socket.once("error", reject);
|
|
29084
31166
|
});
|
|
29085
31167
|
} else {
|
|
29086
31168
|
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
29087
|
-
await new Promise((
|
|
29088
|
-
socket.once("connect",
|
|
31169
|
+
await new Promise((resolve21, reject) => {
|
|
31170
|
+
socket.once("connect", resolve21);
|
|
29089
31171
|
socket.once("error", reject);
|
|
29090
31172
|
});
|
|
29091
31173
|
}
|
|
@@ -29995,16 +32077,16 @@ var init_htmlElement = __esm({
|
|
|
29995
32077
|
});
|
|
29996
32078
|
|
|
29997
32079
|
// ../core/src/ai.ts
|
|
29998
|
-
import { existsSync as
|
|
32080
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
|
|
29999
32081
|
import { homedir } from "node:os";
|
|
30000
|
-
import { join as
|
|
32082
|
+
import { join as join28, resolve as resolve17, relative as relative10, dirname as dirname12 } from "node:path";
|
|
30001
32083
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
30002
32084
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
30003
32085
|
import { createInterface } from "node:readline";
|
|
30004
32086
|
function readVersion() {
|
|
30005
32087
|
try {
|
|
30006
32088
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
30007
|
-
const rootPkg =
|
|
32089
|
+
const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
|
|
30008
32090
|
const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
|
|
30009
32091
|
return pkg.version ?? "0.0.0";
|
|
30010
32092
|
} catch {
|
|
@@ -30068,8 +32150,8 @@ function downloadSkillsSync(jobs) {
|
|
|
30068
32150
|
function installSkills(root = ".", targets) {
|
|
30069
32151
|
const ref = skillsRef();
|
|
30070
32152
|
const dests = targets ?? [
|
|
30071
|
-
|
|
30072
|
-
|
|
32153
|
+
join28(resolve17(root), ".claude", "skills"),
|
|
32154
|
+
join28(homedir(), ".claude", "skills")
|
|
30073
32155
|
];
|
|
30074
32156
|
const jobs = [];
|
|
30075
32157
|
const index = /* @__PURE__ */ new Map();
|
|
@@ -30087,9 +32169,9 @@ function installSkills(root = ".", targets) {
|
|
|
30087
32169
|
const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
|
|
30088
32170
|
skillMdUrl[skill] = `${base}/SKILL.md`;
|
|
30089
32171
|
for (const dest of dests) {
|
|
30090
|
-
add(`${base}/SKILL.md`,
|
|
32172
|
+
add(`${base}/SKILL.md`, join28(dest, skill, "SKILL.md"));
|
|
30091
32173
|
for (const r of spec.references) {
|
|
30092
|
-
add(`${base}/references/${r}`,
|
|
32174
|
+
add(`${base}/references/${r}`, join28(dest, skill, "references", r));
|
|
30093
32175
|
}
|
|
30094
32176
|
}
|
|
30095
32177
|
}
|
|
@@ -30101,10 +32183,10 @@ function installSkills(root = ".", targets) {
|
|
|
30101
32183
|
return installed;
|
|
30102
32184
|
}
|
|
30103
32185
|
function isInstalled(root, tool) {
|
|
30104
|
-
return
|
|
32186
|
+
return existsSync26(join28(resolve17(root), tool.contextFile));
|
|
30105
32187
|
}
|
|
30106
32188
|
function showMenu(root = ".") {
|
|
30107
|
-
const r =
|
|
32189
|
+
const r = resolve17(root);
|
|
30108
32190
|
console.log("\n Tina4 AI Context Installer\n");
|
|
30109
32191
|
for (let i = 0; i < AI_TOOLS.length; i++) {
|
|
30110
32192
|
const tool = AI_TOOLS[i];
|
|
@@ -30122,16 +32204,16 @@ function showMenu(root = ".") {
|
|
|
30122
32204
|
const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
|
|
30123
32205
|
console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
|
|
30124
32206
|
console.log();
|
|
30125
|
-
return new Promise((
|
|
32207
|
+
return new Promise((resolve21) => {
|
|
30126
32208
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
30127
32209
|
rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
|
|
30128
32210
|
rl.close();
|
|
30129
|
-
|
|
32211
|
+
resolve21(answer.trim());
|
|
30130
32212
|
});
|
|
30131
32213
|
});
|
|
30132
32214
|
}
|
|
30133
32215
|
function installSelected(root, selection) {
|
|
30134
|
-
const rootPath =
|
|
32216
|
+
const rootPath = resolve17(root);
|
|
30135
32217
|
const created = [];
|
|
30136
32218
|
let indices;
|
|
30137
32219
|
let doInstallTina4Ai = false;
|
|
@@ -30224,35 +32306,35 @@ function looksLikeOldFrameworkInstall(existing) {
|
|
|
30224
32306
|
function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
30225
32307
|
const block = skillBlock(contextFile);
|
|
30226
32308
|
const [start2, end] = markersFor(contextFile);
|
|
30227
|
-
if (!
|
|
30228
|
-
|
|
32309
|
+
if (!existsSync26(contextPath)) {
|
|
32310
|
+
writeFileSync16(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
30229
32311
|
return "Installed";
|
|
30230
32312
|
}
|
|
30231
32313
|
const existing = readFileSync24(contextPath, "utf-8");
|
|
30232
32314
|
if (hasMarkers(existing, start2, end)) {
|
|
30233
|
-
|
|
32315
|
+
writeFileSync16(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
30234
32316
|
return "Refreshed skill block in";
|
|
30235
32317
|
}
|
|
30236
32318
|
if (looksLikeOldFrameworkInstall(existing)) {
|
|
30237
32319
|
const head = existing.replace(/^\s+/, "");
|
|
30238
32320
|
const preamble = existing.slice(0, existing.length - head.length);
|
|
30239
32321
|
const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
30240
|
-
|
|
32322
|
+
writeFileSync16(contextPath, newContent, "utf-8");
|
|
30241
32323
|
return "Migrated (replaced old framework dump in)";
|
|
30242
32324
|
}
|
|
30243
|
-
|
|
32325
|
+
writeFileSync16(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
30244
32326
|
return "Appended skill block to";
|
|
30245
32327
|
}
|
|
30246
32328
|
function installForTool(root, tool, context) {
|
|
30247
32329
|
const created = [];
|
|
30248
|
-
const contextPath =
|
|
32330
|
+
const contextPath = join28(root, tool.contextFile);
|
|
30249
32331
|
if (tool.configDir) {
|
|
30250
|
-
|
|
32332
|
+
mkdirSync17(join28(root, tool.configDir), { recursive: true });
|
|
30251
32333
|
}
|
|
30252
32334
|
const parentDir = dirname12(contextPath);
|
|
30253
|
-
|
|
32335
|
+
mkdirSync17(parentDir, { recursive: true });
|
|
30254
32336
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
30255
|
-
const rel =
|
|
32337
|
+
const rel = relative10(root, contextPath);
|
|
30256
32338
|
created.push(rel);
|
|
30257
32339
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
30258
32340
|
if (tool.name === "claude-code") {
|
|
@@ -30285,7 +32367,7 @@ function installTina4Ai() {
|
|
|
30285
32367
|
function installClaudeSkills(root) {
|
|
30286
32368
|
const created = [];
|
|
30287
32369
|
for (const skill of installSkills(root)) {
|
|
30288
|
-
created.push(
|
|
32370
|
+
created.push(join28(".claude", "skills", skill));
|
|
30289
32371
|
console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
|
|
30290
32372
|
}
|
|
30291
32373
|
return created;
|
|
@@ -30627,9 +32709,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
|
|
|
30627
32709
|
function generateClaudeCodeContext() {
|
|
30628
32710
|
try {
|
|
30629
32711
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
30630
|
-
const repoRoot =
|
|
30631
|
-
const claudeMdPath =
|
|
30632
|
-
if (
|
|
32712
|
+
const repoRoot = resolve17(thisDir, "..", "..", "..");
|
|
32713
|
+
const claudeMdPath = join28(repoRoot, "CLAUDE.md");
|
|
32714
|
+
if (existsSync26(claudeMdPath)) {
|
|
30633
32715
|
return readFileSync24(claudeMdPath, "utf-8");
|
|
30634
32716
|
}
|
|
30635
32717
|
} catch {
|
|
@@ -31176,11 +33258,11 @@ var init_aiClient = __esm({
|
|
|
31176
33258
|
const payload = JSON.stringify(body);
|
|
31177
33259
|
const controller = new AbortController();
|
|
31178
33260
|
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
31179
|
-
return new Promise((
|
|
33261
|
+
return new Promise((resolve21, reject) => {
|
|
31180
33262
|
const client = url.protocol === "https:" ? https2 : http2;
|
|
31181
33263
|
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
31182
33264
|
clearTimeout(connectTimer);
|
|
31183
|
-
|
|
33265
|
+
resolve21({ response, cleanup: () => {
|
|
31184
33266
|
clearTimeout(totalTimer);
|
|
31185
33267
|
clearTimeout(connectTimer);
|
|
31186
33268
|
} });
|
|
@@ -31209,7 +33291,7 @@ var init_aiClient = __esm({
|
|
|
31209
33291
|
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
31210
33292
|
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
31211
33293
|
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
31212
|
-
return new Promise((
|
|
33294
|
+
return new Promise((resolve21) => setTimeout(resolve21, delay));
|
|
31213
33295
|
}
|
|
31214
33296
|
static async requestJson(config, headers, body) {
|
|
31215
33297
|
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
@@ -34153,8 +36235,8 @@ __export(sqlite_exports, {
|
|
|
34153
36235
|
SQLiteAdapter: () => SQLiteAdapter
|
|
34154
36236
|
});
|
|
34155
36237
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
34156
|
-
import { mkdirSync as
|
|
34157
|
-
import { dirname as dirname13, isAbsolute as isAbsolute5, join as
|
|
36238
|
+
import { mkdirSync as mkdirSync18 } from "node:fs";
|
|
36239
|
+
import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18 } from "node:path";
|
|
34158
36240
|
function isIdentifier(str) {
|
|
34159
36241
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
|
|
34160
36242
|
}
|
|
@@ -34187,13 +36269,13 @@ function resolveSqlitePath(dbPath) {
|
|
|
34187
36269
|
if (dbPath === ":memory:") return dbPath;
|
|
34188
36270
|
let path8 = dbPath;
|
|
34189
36271
|
if (!isAbsolute5(path8)) {
|
|
34190
|
-
path8 =
|
|
34191
|
-
|
|
36272
|
+
path8 = join29(process.cwd(), path8);
|
|
36273
|
+
mkdirSync18(dirname13(path8), { recursive: true });
|
|
34192
36274
|
} else {
|
|
34193
|
-
const cwd =
|
|
34194
|
-
const abs =
|
|
36275
|
+
const cwd = resolve18(process.cwd());
|
|
36276
|
+
const abs = resolve18(path8);
|
|
34195
36277
|
if (abs.startsWith(cwd + "/") || abs === cwd) {
|
|
34196
|
-
|
|
36278
|
+
mkdirSync18(dirname13(abs), { recursive: true });
|
|
34197
36279
|
}
|
|
34198
36280
|
}
|
|
34199
36281
|
return path8;
|
|
@@ -34591,7 +36673,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
34591
36673
|
const elapsedMs = () => performance.now() - startedAt;
|
|
34592
36674
|
if (budgetMs === null) return attempt();
|
|
34593
36675
|
const started = attempt();
|
|
34594
|
-
return new Promise((
|
|
36676
|
+
return new Promise((resolve21, reject) => {
|
|
34595
36677
|
let expired = false;
|
|
34596
36678
|
const timer = setTimeout(() => {
|
|
34597
36679
|
expired = true;
|
|
@@ -34601,7 +36683,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
34601
36683
|
(arrived) => {
|
|
34602
36684
|
clearTimeout(timer);
|
|
34603
36685
|
if (expired) abandon?.(arrived);
|
|
34604
|
-
else
|
|
36686
|
+
else resolve21(arrived);
|
|
34605
36687
|
},
|
|
34606
36688
|
(failure) => {
|
|
34607
36689
|
clearTimeout(timer);
|
|
@@ -35190,10 +37272,10 @@ var init_mysql = __esm({
|
|
|
35190
37272
|
...timeoutOption
|
|
35191
37273
|
});
|
|
35192
37274
|
}
|
|
35193
|
-
return new Promise((
|
|
37275
|
+
return new Promise((resolve21, reject) => {
|
|
35194
37276
|
this.connection.connect((err) => {
|
|
35195
37277
|
if (err) reject(err);
|
|
35196
|
-
else
|
|
37278
|
+
else resolve21();
|
|
35197
37279
|
});
|
|
35198
37280
|
});
|
|
35199
37281
|
},
|
|
@@ -35215,10 +37297,10 @@ var init_mysql = __esm({
|
|
|
35215
37297
|
}
|
|
35216
37298
|
}
|
|
35217
37299
|
queryPromise(sql, params) {
|
|
35218
|
-
return new Promise((
|
|
37300
|
+
return new Promise((resolve21, reject) => {
|
|
35219
37301
|
this.connection.query(sql, params ?? [], (err, results) => {
|
|
35220
37302
|
if (err) reject(err);
|
|
35221
|
-
else
|
|
37303
|
+
else resolve21(results);
|
|
35222
37304
|
});
|
|
35223
37305
|
});
|
|
35224
37306
|
}
|
|
@@ -35604,11 +37686,11 @@ var init_mssql = __esm({
|
|
|
35604
37686
|
};
|
|
35605
37687
|
}
|
|
35606
37688
|
await withConnectTimeout(
|
|
35607
|
-
() => new Promise((
|
|
37689
|
+
() => new Promise((resolve21, reject) => {
|
|
35608
37690
|
this.connection = new Connection(tediousConfig);
|
|
35609
37691
|
this.connection.on("connect", (err) => {
|
|
35610
37692
|
if (err) reject(err);
|
|
35611
|
-
else
|
|
37693
|
+
else resolve21();
|
|
35612
37694
|
});
|
|
35613
37695
|
this.connection.connect();
|
|
35614
37696
|
}),
|
|
@@ -35653,11 +37735,11 @@ var init_mssql = __esm({
|
|
|
35653
37735
|
const tediousModule = requireTedious();
|
|
35654
37736
|
const Request = tediousModule.Request;
|
|
35655
37737
|
const TYPES = tediousModule.TYPES;
|
|
35656
|
-
return new Promise((
|
|
37738
|
+
return new Promise((resolve21, reject) => {
|
|
35657
37739
|
const rows = [];
|
|
35658
37740
|
const request = new Request(sql, (err, rowCount) => {
|
|
35659
37741
|
if (err) reject(err);
|
|
35660
|
-
else
|
|
37742
|
+
else resolve21({ rows, rowCount });
|
|
35661
37743
|
});
|
|
35662
37744
|
if (params) {
|
|
35663
37745
|
params.forEach((p, i) => {
|
|
@@ -35859,8 +37941,8 @@ var init_mssql = __esm({
|
|
|
35859
37941
|
throw new Error("Use startTransactionAsync() for MSSQL.");
|
|
35860
37942
|
}
|
|
35861
37943
|
async startTransactionAsync() {
|
|
35862
|
-
await new Promise((
|
|
35863
|
-
this.connection.beginTransaction((err) => err ? reject(err) :
|
|
37944
|
+
await new Promise((resolve21, reject) => {
|
|
37945
|
+
this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
|
|
35864
37946
|
});
|
|
35865
37947
|
this._inTransaction = true;
|
|
35866
37948
|
}
|
|
@@ -35868,8 +37950,8 @@ var init_mssql = __esm({
|
|
|
35868
37950
|
throw new Error("Use commitAsync() for MSSQL.");
|
|
35869
37951
|
}
|
|
35870
37952
|
async commitAsync() {
|
|
35871
|
-
await new Promise((
|
|
35872
|
-
this.connection.commitTransaction((err) => err ? reject(err) :
|
|
37953
|
+
await new Promise((resolve21, reject) => {
|
|
37954
|
+
this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
|
|
35873
37955
|
});
|
|
35874
37956
|
this._inTransaction = false;
|
|
35875
37957
|
}
|
|
@@ -35877,8 +37959,8 @@ var init_mssql = __esm({
|
|
|
35877
37959
|
throw new Error("Use rollbackAsync() for MSSQL.");
|
|
35878
37960
|
}
|
|
35879
37961
|
async rollbackAsync() {
|
|
35880
|
-
await new Promise((
|
|
35881
|
-
this.connection.rollbackTransaction((err) => err ? reject(err) :
|
|
37962
|
+
await new Promise((resolve21, reject) => {
|
|
37963
|
+
this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
|
|
35882
37964
|
});
|
|
35883
37965
|
this._inTransaction = false;
|
|
35884
37966
|
}
|
|
@@ -36203,8 +38285,8 @@ var init_firebird = __esm({
|
|
|
36203
38285
|
}
|
|
36204
38286
|
attachOnce(config) {
|
|
36205
38287
|
const fb = requireFirebird();
|
|
36206
|
-
return new Promise((
|
|
36207
|
-
fb.attach(config, (err, db) => err ? reject(err) :
|
|
38288
|
+
return new Promise((resolve21, reject) => {
|
|
38289
|
+
fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
|
|
36208
38290
|
});
|
|
36209
38291
|
}
|
|
36210
38292
|
/**
|
|
@@ -36223,7 +38305,7 @@ var init_firebird = __esm({
|
|
|
36223
38305
|
} catch (err) {
|
|
36224
38306
|
lastError = err;
|
|
36225
38307
|
if (attempt < attempts - 1) {
|
|
36226
|
-
await new Promise((
|
|
38308
|
+
await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
|
|
36227
38309
|
}
|
|
36228
38310
|
}
|
|
36229
38311
|
}
|
|
@@ -36306,19 +38388,19 @@ var init_firebird = __esm({
|
|
|
36306
38388
|
}
|
|
36307
38389
|
queryPromise(sql, params) {
|
|
36308
38390
|
const translated = this.translateSql(sql);
|
|
36309
|
-
return this.withReconnect(() => new Promise((
|
|
38391
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
36310
38392
|
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
36311
38393
|
if (err) reject(err);
|
|
36312
|
-
else
|
|
38394
|
+
else resolve21(result ?? []);
|
|
36313
38395
|
});
|
|
36314
38396
|
}));
|
|
36315
38397
|
}
|
|
36316
38398
|
executePromise(sql, params) {
|
|
36317
38399
|
const translated = this.translateSql(sql);
|
|
36318
|
-
return this.withReconnect(() => new Promise((
|
|
38400
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
36319
38401
|
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
36320
38402
|
if (err) reject(err);
|
|
36321
|
-
else
|
|
38403
|
+
else resolve21();
|
|
36322
38404
|
});
|
|
36323
38405
|
}));
|
|
36324
38406
|
}
|
|
@@ -36357,13 +38439,13 @@ var init_firebird = __esm({
|
|
|
36357
38439
|
* and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
|
|
36358
38440
|
*/
|
|
36359
38441
|
readBlob(blobFn) {
|
|
36360
|
-
return new Promise((
|
|
38442
|
+
return new Promise((resolve21, reject) => {
|
|
36361
38443
|
blobFn((err, _name, emitter) => {
|
|
36362
38444
|
if (err) return reject(err);
|
|
36363
|
-
if (!emitter) return
|
|
38445
|
+
if (!emitter) return resolve21(null);
|
|
36364
38446
|
const chunks = [];
|
|
36365
38447
|
emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
36366
|
-
emitter.on("end", () =>
|
|
38448
|
+
emitter.on("end", () => resolve21(Buffer.concat(chunks)));
|
|
36367
38449
|
emitter.on("error", (streamErr) => reject(streamErr));
|
|
36368
38450
|
});
|
|
36369
38451
|
});
|
|
@@ -36499,12 +38581,12 @@ var init_firebird = __esm({
|
|
|
36499
38581
|
}
|
|
36500
38582
|
async startTransactionAsync() {
|
|
36501
38583
|
this.ensureConnected();
|
|
36502
|
-
await new Promise((
|
|
38584
|
+
await new Promise((resolve21, reject) => {
|
|
36503
38585
|
this.db.transaction(0, (err, transaction) => {
|
|
36504
38586
|
if (err) reject(err);
|
|
36505
38587
|
else {
|
|
36506
38588
|
this.transaction = transaction;
|
|
36507
|
-
|
|
38589
|
+
resolve21();
|
|
36508
38590
|
}
|
|
36509
38591
|
});
|
|
36510
38592
|
});
|
|
@@ -36514,12 +38596,12 @@ var init_firebird = __esm({
|
|
|
36514
38596
|
}
|
|
36515
38597
|
async commitAsync() {
|
|
36516
38598
|
if (!this.transaction) throw new Error("No active transaction to commit.");
|
|
36517
|
-
await new Promise((
|
|
38599
|
+
await new Promise((resolve21, reject) => {
|
|
36518
38600
|
this.transaction.commit((err) => {
|
|
36519
38601
|
if (err) reject(err);
|
|
36520
38602
|
else {
|
|
36521
38603
|
this.transaction = null;
|
|
36522
|
-
|
|
38604
|
+
resolve21();
|
|
36523
38605
|
}
|
|
36524
38606
|
});
|
|
36525
38607
|
});
|
|
@@ -36529,12 +38611,12 @@ var init_firebird = __esm({
|
|
|
36529
38611
|
}
|
|
36530
38612
|
async rollbackAsync() {
|
|
36531
38613
|
if (!this.transaction) throw new Error("No active transaction to rollback.");
|
|
36532
|
-
await new Promise((
|
|
38614
|
+
await new Promise((resolve21, reject) => {
|
|
36533
38615
|
this.transaction.rollback((err) => {
|
|
36534
38616
|
if (err) reject(err);
|
|
36535
38617
|
else {
|
|
36536
38618
|
this.transaction = null;
|
|
36537
|
-
|
|
38619
|
+
resolve21();
|
|
36538
38620
|
}
|
|
36539
38621
|
});
|
|
36540
38622
|
});
|
|
@@ -39014,7 +41096,7 @@ var init_database = __esm({
|
|
|
39014
41096
|
|
|
39015
41097
|
// src/model.ts
|
|
39016
41098
|
import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
|
|
39017
|
-
import { join as
|
|
41099
|
+
import { join as join30, extname as extname7 } from "node:path";
|
|
39018
41100
|
async function discoverModels(modelsDir) {
|
|
39019
41101
|
const models = [];
|
|
39020
41102
|
let files;
|
|
@@ -39024,7 +41106,7 @@ async function discoverModels(modelsDir) {
|
|
|
39024
41106
|
return models;
|
|
39025
41107
|
}
|
|
39026
41108
|
for (const file of files) {
|
|
39027
|
-
const filePath =
|
|
41109
|
+
const filePath = join30(modelsDir, file);
|
|
39028
41110
|
const stat = statSync17(filePath);
|
|
39029
41111
|
if (!stat.isFile()) continue;
|
|
39030
41112
|
const ext = extname7(file);
|
|
@@ -39066,8 +41148,8 @@ var init_model = __esm({
|
|
|
39066
41148
|
});
|
|
39067
41149
|
|
|
39068
41150
|
// src/migration.ts
|
|
39069
|
-
import { existsSync as
|
|
39070
|
-
import { join as
|
|
41151
|
+
import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
|
|
41152
|
+
import { join as join31, resolve as resolve19 } from "node:path";
|
|
39071
41153
|
function unwrapAdapter(db) {
|
|
39072
41154
|
let cur = db;
|
|
39073
41155
|
while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
|
|
@@ -39383,15 +41465,15 @@ async function rollback(migrationsDir, delimiter2) {
|
|
|
39383
41465
|
}
|
|
39384
41466
|
return rolledBack2;
|
|
39385
41467
|
}
|
|
39386
|
-
const dir =
|
|
41468
|
+
const dir = resolve19(migrationsDir ?? "migrations");
|
|
39387
41469
|
const delim = delimiter2 ?? ";";
|
|
39388
41470
|
const db = getAdapter();
|
|
39389
41471
|
const migrations = await getLastBatchMigrations();
|
|
39390
41472
|
const rolledBack = [];
|
|
39391
41473
|
for (const migration of migrations) {
|
|
39392
41474
|
const downFile = `${migration.migration_name}.down.sql`;
|
|
39393
|
-
const downPath =
|
|
39394
|
-
if (!
|
|
41475
|
+
const downPath = join31(dir, downFile);
|
|
41476
|
+
if (!existsSync27(downPath)) {
|
|
39395
41477
|
throw new Error(
|
|
39396
41478
|
`Cannot rollback ${migration.migration_name}: no .down.sql file found`
|
|
39397
41479
|
);
|
|
@@ -39555,10 +41637,10 @@ function warnUnprefixedMigrations(files) {
|
|
|
39555
41637
|
}
|
|
39556
41638
|
async function migrate(adapter, options) {
|
|
39557
41639
|
const db = adapter ?? getAdapter();
|
|
39558
|
-
const dir =
|
|
41640
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
39559
41641
|
const delimiter2 = options?.delimiter ?? ";";
|
|
39560
41642
|
const result = { applied: [], skipped: [], failed: [] };
|
|
39561
|
-
if (!
|
|
41643
|
+
if (!existsSync27(dir)) {
|
|
39562
41644
|
return result;
|
|
39563
41645
|
}
|
|
39564
41646
|
await ensureMigrationTableOn(db);
|
|
@@ -39594,7 +41676,7 @@ async function migrate(adapter, options) {
|
|
|
39594
41676
|
result.skipped.push(file);
|
|
39595
41677
|
continue;
|
|
39596
41678
|
}
|
|
39597
|
-
const sqlContent = readFileSync25(
|
|
41679
|
+
const sqlContent = readFileSync25(join31(dir, file), "utf-8").trim();
|
|
39598
41680
|
if (!sqlContent) {
|
|
39599
41681
|
result.skipped.push(file);
|
|
39600
41682
|
continue;
|
|
@@ -39629,9 +41711,9 @@ async function migrate(adapter, options) {
|
|
|
39629
41711
|
}
|
|
39630
41712
|
async function status(adapter, options) {
|
|
39631
41713
|
const db = adapter ?? getAdapter();
|
|
39632
|
-
const dir =
|
|
41714
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
39633
41715
|
const result = { completed: [], pending: [] };
|
|
39634
|
-
if (!
|
|
41716
|
+
if (!existsSync27(dir)) {
|
|
39635
41717
|
return result;
|
|
39636
41718
|
}
|
|
39637
41719
|
if (!await adapterTableExists(db, MIGRATION_TABLE)) {
|
|
@@ -39677,13 +41759,13 @@ async function createMigration(description, options) {
|
|
|
39677
41759
|
if (kind === "code" || kind === "class") {
|
|
39678
41760
|
return createClassMigration(description, options);
|
|
39679
41761
|
}
|
|
39680
|
-
const dir =
|
|
39681
|
-
if (!
|
|
39682
|
-
|
|
41762
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
41763
|
+
if (!existsSync27(dir)) {
|
|
41764
|
+
mkdirSync19(dir, { recursive: true });
|
|
39683
41765
|
}
|
|
39684
41766
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
39685
41767
|
const now = /* @__PURE__ */ new Date();
|
|
39686
|
-
const
|
|
41768
|
+
const timestamp2 = [
|
|
39687
41769
|
now.getFullYear(),
|
|
39688
41770
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
39689
41771
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -39691,10 +41773,10 @@ async function createMigration(description, options) {
|
|
|
39691
41773
|
String(now.getMinutes()).padStart(2, "0"),
|
|
39692
41774
|
String(now.getSeconds()).padStart(2, "0")
|
|
39693
41775
|
].join("");
|
|
39694
|
-
const upFileName = `${
|
|
39695
|
-
const downFileName = `${
|
|
39696
|
-
const upPath =
|
|
39697
|
-
const downPath =
|
|
41776
|
+
const upFileName = `${timestamp2}_${safeName}.sql`;
|
|
41777
|
+
const downFileName = `${timestamp2}_${safeName}.down.sql`;
|
|
41778
|
+
const upPath = join31(dir, upFileName);
|
|
41779
|
+
const downPath = join31(dir, downFileName);
|
|
39698
41780
|
const upTemplate = `-- Migration: ${description}
|
|
39699
41781
|
-- Created: ${now.toISOString()}
|
|
39700
41782
|
|
|
@@ -39703,19 +41785,19 @@ async function createMigration(description, options) {
|
|
|
39703
41785
|
-- Created: ${now.toISOString()}
|
|
39704
41786
|
|
|
39705
41787
|
`;
|
|
39706
|
-
|
|
39707
|
-
|
|
41788
|
+
writeFileSync17(upPath, upTemplate, "utf-8");
|
|
41789
|
+
writeFileSync17(downPath, downTemplate, "utf-8");
|
|
39708
41790
|
return { upPath, downPath };
|
|
39709
41791
|
}
|
|
39710
41792
|
async function createClassMigration(description, options) {
|
|
39711
|
-
const dir =
|
|
39712
|
-
if (!
|
|
39713
|
-
|
|
41793
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
41794
|
+
if (!existsSync27(dir)) {
|
|
41795
|
+
mkdirSync19(dir, { recursive: true });
|
|
39714
41796
|
}
|
|
39715
41797
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
39716
41798
|
const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
39717
41799
|
const now = /* @__PURE__ */ new Date();
|
|
39718
|
-
const
|
|
41800
|
+
const timestamp2 = [
|
|
39719
41801
|
now.getFullYear(),
|
|
39720
41802
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
39721
41803
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -39723,8 +41805,8 @@ async function createClassMigration(description, options) {
|
|
|
39723
41805
|
String(now.getMinutes()).padStart(2, "0"),
|
|
39724
41806
|
String(now.getSeconds()).padStart(2, "0")
|
|
39725
41807
|
].join("");
|
|
39726
|
-
const fileName = `${
|
|
39727
|
-
const filePath =
|
|
41808
|
+
const fileName = `${timestamp2}_${safeName}.ts`;
|
|
41809
|
+
const filePath = join31(dir, fileName);
|
|
39728
41810
|
const content = `// Migration: ${description}
|
|
39729
41811
|
// Created: ${now.toISOString()}
|
|
39730
41812
|
|
|
@@ -39740,7 +41822,7 @@ export class ${className} {
|
|
|
39740
41822
|
}
|
|
39741
41823
|
}
|
|
39742
41824
|
`;
|
|
39743
|
-
|
|
41825
|
+
writeFileSync17(filePath, content, "utf-8");
|
|
39744
41826
|
return filePath;
|
|
39745
41827
|
}
|
|
39746
41828
|
var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
|
|
@@ -39821,8 +41903,8 @@ var init_migration = __esm({
|
|
|
39821
41903
|
}
|
|
39822
41904
|
/** Return sorted list of all migration files on disk (excludes .down.sql). */
|
|
39823
41905
|
getFiles() {
|
|
39824
|
-
const dir =
|
|
39825
|
-
if (!
|
|
41906
|
+
const dir = resolve19(this.dir);
|
|
41907
|
+
if (!existsSync27(dir)) return [];
|
|
39826
41908
|
return sortMigrationFiles(
|
|
39827
41909
|
readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
|
|
39828
41910
|
);
|
|
@@ -42606,8 +44688,8 @@ var init_seeder = __esm({
|
|
|
42606
44688
|
// src/docstore.ts
|
|
42607
44689
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
42608
44690
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
42609
|
-
import { mkdirSync as
|
|
42610
|
-
import { dirname as dirname14, isAbsolute as isAbsolute6, join as
|
|
44691
|
+
import { mkdirSync as mkdirSync20 } from "node:fs";
|
|
44692
|
+
import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
|
|
42611
44693
|
function iso(d) {
|
|
42612
44694
|
return d.toISOString();
|
|
42613
44695
|
}
|
|
@@ -42872,8 +44954,8 @@ function resolveStorePath(dbPath) {
|
|
|
42872
44954
|
if (dbPath === ":memory:") return dbPath;
|
|
42873
44955
|
let path8 = dbPath;
|
|
42874
44956
|
if (!isAbsolute6(path8)) {
|
|
42875
|
-
path8 =
|
|
42876
|
-
|
|
44957
|
+
path8 = join32(process.cwd(), path8);
|
|
44958
|
+
mkdirSync20(dirname14(path8), { recursive: true });
|
|
42877
44959
|
}
|
|
42878
44960
|
return path8;
|
|
42879
44961
|
}
|
|
@@ -44156,8 +46238,8 @@ var init_attachment = __esm({
|
|
|
44156
46238
|
|
|
44157
46239
|
// src/realtime/storage.ts
|
|
44158
46240
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
44159
|
-
import { mkdirSync as
|
|
44160
|
-
import { resolve as
|
|
46241
|
+
import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
46242
|
+
import { resolve as resolve20, sep as sep6 } from "node:path";
|
|
44161
46243
|
import { createRequire as createRequire8 } from "node:module";
|
|
44162
46244
|
function storageKey(filename = "") {
|
|
44163
46245
|
let ext = "";
|
|
@@ -44193,19 +46275,19 @@ var init_storage = __esm({
|
|
|
44193
46275
|
LocalStorage = class {
|
|
44194
46276
|
directory;
|
|
44195
46277
|
constructor(directory) {
|
|
44196
|
-
this.directory =
|
|
44197
|
-
|
|
46278
|
+
this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
|
|
46279
|
+
mkdirSync21(this.directory, { recursive: true });
|
|
44198
46280
|
}
|
|
44199
46281
|
// Resolve inside the root and reject any traversal attempt.
|
|
44200
46282
|
pathFor(key) {
|
|
44201
|
-
const target =
|
|
44202
|
-
if (target !== this.directory && !target.startsWith(this.directory +
|
|
46283
|
+
const target = resolve20(this.directory, key);
|
|
46284
|
+
if (target !== this.directory && !target.startsWith(this.directory + sep6)) {
|
|
44203
46285
|
throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
|
|
44204
46286
|
}
|
|
44205
46287
|
return target;
|
|
44206
46288
|
}
|
|
44207
46289
|
put(key, data) {
|
|
44208
|
-
|
|
46290
|
+
writeFileSync18(this.pathFor(key), data);
|
|
44209
46291
|
}
|
|
44210
46292
|
get(key) {
|
|
44211
46293
|
try {
|