tina4-nodejs 3.13.120 → 3.13.121
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 +10724 -10668
- 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 +2394 -339
- package/packages/core/src/mcp.ts +62 -11
- package/packages/orm/dist/index.js +2443 -388
- package/types/cli/src/commands/generate.d.ts +16 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -1
|
@@ -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)) {
|
|
@@ -7161,7 +7161,7 @@ ${s}\r
|
|
|
7161
7161
|
connect() {
|
|
7162
7162
|
if (this.connected) return Promise.resolve();
|
|
7163
7163
|
if (this.connecting) return this.connecting;
|
|
7164
|
-
this.connecting = new Promise((
|
|
7164
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
7165
7165
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
7166
7166
|
sock.setNoDelay(true);
|
|
7167
7167
|
const onError = (err) => {
|
|
@@ -7198,7 +7198,7 @@ ${s}\r
|
|
|
7198
7198
|
sock.on("error", (e) => {
|
|
7199
7199
|
this.brokenError = e;
|
|
7200
7200
|
});
|
|
7201
|
-
|
|
7201
|
+
resolve21();
|
|
7202
7202
|
} catch (e) {
|
|
7203
7203
|
onError(e);
|
|
7204
7204
|
}
|
|
@@ -7261,12 +7261,12 @@ ${s}\r
|
|
|
7261
7261
|
}
|
|
7262
7262
|
/** Send one command and await its reply (assumes socket is up). */
|
|
7263
7263
|
raw(args) {
|
|
7264
|
-
return new Promise((
|
|
7264
|
+
return new Promise((resolve21, reject) => {
|
|
7265
7265
|
if (!this.sock || this.sock.destroyed) {
|
|
7266
7266
|
reject(this.brokenError ?? new Error("redis socket not connected"));
|
|
7267
7267
|
return;
|
|
7268
7268
|
}
|
|
7269
|
-
this.waiters.push({ resolve:
|
|
7269
|
+
this.waiters.push({ resolve: resolve21, reject });
|
|
7270
7270
|
this.sock.write(_RespClient.encode(args));
|
|
7271
7271
|
});
|
|
7272
7272
|
}
|
|
@@ -7608,7 +7608,7 @@ ${s}\r
|
|
|
7608
7608
|
connect() {
|
|
7609
7609
|
if (this.connected) return Promise.resolve();
|
|
7610
7610
|
if (this.connecting) return this.connecting;
|
|
7611
|
-
this.connecting = new Promise((
|
|
7611
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
7612
7612
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
7613
7613
|
sock.setNoDelay(true);
|
|
7614
7614
|
sock.once("error", (err) => {
|
|
@@ -7629,7 +7629,7 @@ ${s}\r
|
|
|
7629
7629
|
p.resolve(this.buffer.toString("utf-8"));
|
|
7630
7630
|
}
|
|
7631
7631
|
});
|
|
7632
|
-
|
|
7632
|
+
resolve21();
|
|
7633
7633
|
});
|
|
7634
7634
|
});
|
|
7635
7635
|
return this.connecting;
|
|
@@ -7662,13 +7662,13 @@ ${s}\r
|
|
|
7662
7662
|
async send(payload, terminator) {
|
|
7663
7663
|
await this.connect();
|
|
7664
7664
|
if (!this.sock || this.sock.destroyed) return "";
|
|
7665
|
-
return new Promise((
|
|
7665
|
+
return new Promise((resolve21) => {
|
|
7666
7666
|
this.buffer = Buffer.alloc(0);
|
|
7667
|
-
this.pending = { terminator, resolve:
|
|
7667
|
+
this.pending = { terminator, resolve: resolve21 };
|
|
7668
7668
|
const timer = setTimeout(() => {
|
|
7669
|
-
if (this.pending && this.pending.resolve ===
|
|
7669
|
+
if (this.pending && this.pending.resolve === resolve21) {
|
|
7670
7670
|
this.pending = null;
|
|
7671
|
-
|
|
7671
|
+
resolve21(this.buffer.toString("utf-8"));
|
|
7672
7672
|
}
|
|
7673
7673
|
}, 4e3);
|
|
7674
7674
|
if (timer.unref) timer.unref();
|
|
@@ -9434,7 +9434,7 @@ async function parseBody(req2) {
|
|
|
9434
9434
|
}
|
|
9435
9435
|
const contentType = req2.headers["content-type"] ?? "";
|
|
9436
9436
|
const chunks = [];
|
|
9437
|
-
await new Promise((
|
|
9437
|
+
await new Promise((resolve21, reject) => {
|
|
9438
9438
|
let received = 0;
|
|
9439
9439
|
let refused = false;
|
|
9440
9440
|
req2.on("data", (chunk) => {
|
|
@@ -9449,7 +9449,7 @@ async function parseBody(req2) {
|
|
|
9449
9449
|
chunks.push(chunk);
|
|
9450
9450
|
});
|
|
9451
9451
|
req2.on("end", () => {
|
|
9452
|
-
if (!refused)
|
|
9452
|
+
if (!refused) resolve21();
|
|
9453
9453
|
});
|
|
9454
9454
|
req2.on("error", reject);
|
|
9455
9455
|
});
|
|
@@ -12682,6 +12682,2038 @@ var init_version = __esm({
|
|
|
12682
12682
|
}
|
|
12683
12683
|
});
|
|
12684
12684
|
|
|
12685
|
+
// ../cli/src/commands/generate.ts
|
|
12686
|
+
var generate_exports = {};
|
|
12687
|
+
__export(generate_exports, {
|
|
12688
|
+
DEFAULT_FIELDS: () => DEFAULT_FIELDS,
|
|
12689
|
+
GENERATORS: () => GENERATORS,
|
|
12690
|
+
RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
|
|
12691
|
+
SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
|
|
12692
|
+
aiFill: () => aiFill,
|
|
12693
|
+
currentResolution: () => currentResolution,
|
|
12694
|
+
extend: () => extend,
|
|
12695
|
+
fieldsOrDefault: () => fieldsOrDefault,
|
|
12696
|
+
generate: () => generate,
|
|
12697
|
+
generateMigration: () => generateMigration,
|
|
12698
|
+
generateProgrammatic: () => generateProgrammatic,
|
|
12699
|
+
parseCliArgs: () => parseCliArgs,
|
|
12700
|
+
parseEvery: () => parseEvery,
|
|
12701
|
+
parseFields: () => parseFields,
|
|
12702
|
+
pluralizeReserved: () => pluralizeReserved,
|
|
12703
|
+
toPascal: () => toPascal,
|
|
12704
|
+
toSnake: () => toSnake,
|
|
12705
|
+
toTableName: () => toTableName
|
|
12706
|
+
});
|
|
12707
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
|
|
12708
|
+
import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
|
|
12709
|
+
function ensureDir(dir) {
|
|
12710
|
+
if (__resolution.dryRun) return;
|
|
12711
|
+
if (!existsSync13(dir)) {
|
|
12712
|
+
mkdirSync8(dir, { recursive: true });
|
|
12713
|
+
}
|
|
12714
|
+
}
|
|
12715
|
+
function writeFileSafe(path8, content) {
|
|
12716
|
+
captureEditHints(path8, content);
|
|
12717
|
+
if (__resolution.dryRun) {
|
|
12718
|
+
return;
|
|
12719
|
+
}
|
|
12720
|
+
if (existsSync13(path8)) {
|
|
12721
|
+
if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
|
|
12722
|
+
return;
|
|
12723
|
+
}
|
|
12724
|
+
writeFileSync7(path8, content, "utf-8");
|
|
12725
|
+
__resolution.actionsTaken.push(`wrote ${path8}`);
|
|
12726
|
+
if (!__resolution.jsonMode) console.log(` Created ${path8}`);
|
|
12727
|
+
}
|
|
12728
|
+
function toSnake(name) {
|
|
12729
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
12730
|
+
}
|
|
12731
|
+
function pluralizeReserved(name) {
|
|
12732
|
+
if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
|
|
12733
|
+
if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
|
|
12734
|
+
return name + "s";
|
|
12735
|
+
}
|
|
12736
|
+
function toTableName(name) {
|
|
12737
|
+
const raw = toSnake(name);
|
|
12738
|
+
if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
|
|
12739
|
+
const safe = pluralizeReserved(raw);
|
|
12740
|
+
recordTransformation({
|
|
12741
|
+
kind: "reserved_word_pluralize",
|
|
12742
|
+
from: raw,
|
|
12743
|
+
to: safe,
|
|
12744
|
+
reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
|
|
12745
|
+
override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
|
|
12746
|
+
});
|
|
12747
|
+
return safe;
|
|
12748
|
+
}
|
|
12749
|
+
return raw;
|
|
12750
|
+
}
|
|
12751
|
+
function resetResolution(target, input, opts) {
|
|
12752
|
+
__resolution.target = target;
|
|
12753
|
+
__resolution.input = input;
|
|
12754
|
+
__resolution.body = { transformations: [] };
|
|
12755
|
+
__resolution.actionsTaken = [];
|
|
12756
|
+
__resolution.dryRun = opts.dryRun;
|
|
12757
|
+
__resolution.jsonMode = opts.jsonMode;
|
|
12758
|
+
}
|
|
12759
|
+
function recordTransformation(t) {
|
|
12760
|
+
__resolution.body.transformations.push(t);
|
|
12761
|
+
}
|
|
12762
|
+
function currentResolution() {
|
|
12763
|
+
const body = {
|
|
12764
|
+
...__resolution.body,
|
|
12765
|
+
transformations: [...__resolution.body.transformations]
|
|
12766
|
+
};
|
|
12767
|
+
if (__resolution.body.edit_hints) {
|
|
12768
|
+
body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
|
|
12769
|
+
}
|
|
12770
|
+
if (__resolution.body.next) {
|
|
12771
|
+
body.next = [...__resolution.body.next];
|
|
12772
|
+
}
|
|
12773
|
+
if (__resolution.body.test_paths) {
|
|
12774
|
+
body.test_paths = [...__resolution.body.test_paths];
|
|
12775
|
+
}
|
|
12776
|
+
if (__resolution.body.routes) {
|
|
12777
|
+
body.routes = [...__resolution.body.routes];
|
|
12778
|
+
}
|
|
12779
|
+
return {
|
|
12780
|
+
command: "generate",
|
|
12781
|
+
target: __resolution.target,
|
|
12782
|
+
input: { ...__resolution.input },
|
|
12783
|
+
resolution: body,
|
|
12784
|
+
actions_taken: [...__resolution.actionsTaken],
|
|
12785
|
+
dry_run: __resolution.dryRun
|
|
12786
|
+
};
|
|
12787
|
+
}
|
|
12788
|
+
function setResolutionField(key, value) {
|
|
12789
|
+
__resolution.body[key] = value;
|
|
12790
|
+
}
|
|
12791
|
+
function pushRoute(routePattern) {
|
|
12792
|
+
if (!__resolution.body.routes) __resolution.body.routes = [];
|
|
12793
|
+
__resolution.body.routes.push(routePattern);
|
|
12794
|
+
}
|
|
12795
|
+
function pushTestPath(path8) {
|
|
12796
|
+
if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
|
|
12797
|
+
__resolution.body.test_paths.push(path8);
|
|
12798
|
+
}
|
|
12799
|
+
function pushEditHint(hint) {
|
|
12800
|
+
if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
|
|
12801
|
+
__resolution.body.edit_hints.push(hint);
|
|
12802
|
+
}
|
|
12803
|
+
function setNextSteps(steps) {
|
|
12804
|
+
if (steps.length === 0) return;
|
|
12805
|
+
__resolution.body.next = [...steps];
|
|
12806
|
+
}
|
|
12807
|
+
function toRelPath(absPath) {
|
|
12808
|
+
const cwd = process.cwd();
|
|
12809
|
+
const rel = relative2(cwd, absPath);
|
|
12810
|
+
if (!rel) return absPath;
|
|
12811
|
+
return sep3 === "/" ? rel : rel.split(sep3).join("/");
|
|
12812
|
+
}
|
|
12813
|
+
function captureEditHints(absPath, content) {
|
|
12814
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
12815
|
+
const relPath = toRelPath(absPath);
|
|
12816
|
+
const lines = content.split("\n");
|
|
12817
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12818
|
+
const match = TINA4_EDIT_MARKER.exec(lines[i]);
|
|
12819
|
+
if (match) {
|
|
12820
|
+
pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
|
|
12821
|
+
}
|
|
12822
|
+
}
|
|
12823
|
+
}
|
|
12824
|
+
function printResolution() {
|
|
12825
|
+
if (__resolution.jsonMode) {
|
|
12826
|
+
process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
|
|
12827
|
+
return;
|
|
12828
|
+
}
|
|
12829
|
+
const b = __resolution.body;
|
|
12830
|
+
const lines = [];
|
|
12831
|
+
lines.push("");
|
|
12832
|
+
lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
|
|
12833
|
+
if (b.class_name || b.file_path) {
|
|
12834
|
+
const where = b.file_path ? ` (in ${b.file_path})` : "";
|
|
12835
|
+
lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
|
|
12836
|
+
}
|
|
12837
|
+
if (b.table_name) {
|
|
12838
|
+
const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
|
|
12839
|
+
const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
|
|
12840
|
+
lines.push(` table ${b.table_name}${note}`);
|
|
12841
|
+
}
|
|
12842
|
+
if (b.routes && b.routes.length) {
|
|
12843
|
+
lines.push(` routes ${b.routes.join(", ")}`);
|
|
12844
|
+
}
|
|
12845
|
+
if (b.migration_path) {
|
|
12846
|
+
lines.push(` migration ${b.migration_path}`);
|
|
12847
|
+
}
|
|
12848
|
+
const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
|
|
12849
|
+
if (reserved && reserved.from && reserved.override) {
|
|
12850
|
+
lines.push("");
|
|
12851
|
+
lines.push(` To keep the raw name '${reserved.from}' as the table:`);
|
|
12852
|
+
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
|
|
12853
|
+
}
|
|
12854
|
+
if (b.test_paths && b.test_paths.length > 0) {
|
|
12855
|
+
lines.push("");
|
|
12856
|
+
lines.push(" Tests:");
|
|
12857
|
+
for (const testPath of b.test_paths) lines.push(` ${testPath}`);
|
|
12858
|
+
}
|
|
12859
|
+
if (b.edit_hints && b.edit_hints.length > 0) {
|
|
12860
|
+
lines.push("");
|
|
12861
|
+
lines.push(" Edit these lines:");
|
|
12862
|
+
for (const hint of b.edit_hints) {
|
|
12863
|
+
lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
|
|
12864
|
+
}
|
|
12865
|
+
}
|
|
12866
|
+
if (b.next && b.next.length > 0) {
|
|
12867
|
+
lines.push("");
|
|
12868
|
+
lines.push(" Next:");
|
|
12869
|
+
for (const step of b.next) lines.push(` ${step}`);
|
|
12870
|
+
}
|
|
12871
|
+
lines.push("");
|
|
12872
|
+
process.stderr.write(lines.join("\n"));
|
|
12873
|
+
}
|
|
12874
|
+
function toPlural(name) {
|
|
12875
|
+
const lower = name.toLowerCase();
|
|
12876
|
+
if (lower.endsWith("s")) return lower;
|
|
12877
|
+
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
|
|
12878
|
+
return lower + "s";
|
|
12879
|
+
}
|
|
12880
|
+
function toCamel(name) {
|
|
12881
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
12882
|
+
}
|
|
12883
|
+
function toPascal(name) {
|
|
12884
|
+
return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
12885
|
+
}
|
|
12886
|
+
function parseFields(fieldsStr) {
|
|
12887
|
+
if (!fieldsStr || !fieldsStr.trim()) return [];
|
|
12888
|
+
const result = [];
|
|
12889
|
+
for (const part of fieldsStr.split(",")) {
|
|
12890
|
+
const trimmed = part.trim();
|
|
12891
|
+
if (trimmed.includes(":")) {
|
|
12892
|
+
const [fname, ftype] = trimmed.split(":", 2);
|
|
12893
|
+
if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
|
|
12894
|
+
} else if (trimmed) {
|
|
12895
|
+
result.push([trimmed, "string"]);
|
|
12896
|
+
}
|
|
12897
|
+
}
|
|
12898
|
+
return result;
|
|
12899
|
+
}
|
|
12900
|
+
function fieldsOrDefault(fieldsStr) {
|
|
12901
|
+
const parsed = parseFields(fieldsStr);
|
|
12902
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
12903
|
+
}
|
|
12904
|
+
function parseCliArgs(args) {
|
|
12905
|
+
const booleanFlags = /* @__PURE__ */ new Set([
|
|
12906
|
+
"no-browser",
|
|
12907
|
+
"no-reload",
|
|
12908
|
+
"production",
|
|
12909
|
+
"managed",
|
|
12910
|
+
"all",
|
|
12911
|
+
"clear",
|
|
12912
|
+
"public",
|
|
12913
|
+
"no-migration",
|
|
12914
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
12915
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
12916
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
12917
|
+
"no-test",
|
|
12918
|
+
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
12919
|
+
"json",
|
|
12920
|
+
"dry-run"
|
|
12921
|
+
]);
|
|
12922
|
+
const flags = {};
|
|
12923
|
+
const positional = [];
|
|
12924
|
+
let i = 0;
|
|
12925
|
+
while (i < args.length) {
|
|
12926
|
+
if (args[i].startsWith("--")) {
|
|
12927
|
+
const key = args[i].slice(2);
|
|
12928
|
+
if (booleanFlags.has(key)) {
|
|
12929
|
+
flags[key] = true;
|
|
12930
|
+
i += 1;
|
|
12931
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
12932
|
+
flags[key] = args[i + 1];
|
|
12933
|
+
i += 2;
|
|
12934
|
+
} else {
|
|
12935
|
+
flags[key] = true;
|
|
12936
|
+
i += 1;
|
|
12937
|
+
}
|
|
12938
|
+
} else {
|
|
12939
|
+
positional.push(args[i]);
|
|
12940
|
+
i += 1;
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
return { flags, positional };
|
|
12944
|
+
}
|
|
12945
|
+
function parseEvery(every) {
|
|
12946
|
+
if (!every || every === true) return 60;
|
|
12947
|
+
const s = String(every).trim().toLowerCase();
|
|
12948
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
12949
|
+
const unit = s.slice(-1);
|
|
12950
|
+
if (unit in units) {
|
|
12951
|
+
const n2 = parseFloat(s.slice(0, -1));
|
|
12952
|
+
return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
|
|
12953
|
+
}
|
|
12954
|
+
const n = parseFloat(s);
|
|
12955
|
+
return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
|
|
12956
|
+
}
|
|
12957
|
+
function aiFill(fn, spec, indent = " ") {
|
|
12958
|
+
const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
|
|
12959
|
+
const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
|
|
12960
|
+
lines.push(`${indent}// Intent: ${spec.intent}`);
|
|
12961
|
+
if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
|
|
12962
|
+
lines.push(`${indent}// Use: ${spec.use}`);
|
|
12963
|
+
if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
|
|
12964
|
+
lines.push(`${indent}// Ground: ${spec.ground}`);
|
|
12965
|
+
lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
|
|
12966
|
+
lines.push(`${indent}// ${"\u2500".repeat(52)}`);
|
|
12967
|
+
return lines.join("\n") + "\n";
|
|
12968
|
+
}
|
|
12969
|
+
function extend(note, hint = "", indent = " ") {
|
|
12970
|
+
let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
|
|
12971
|
+
`;
|
|
12972
|
+
if (hint) out += `${indent}// ${hint}
|
|
12973
|
+
`;
|
|
12974
|
+
return out;
|
|
12975
|
+
}
|
|
12976
|
+
function timestamp() {
|
|
12977
|
+
const now = /* @__PURE__ */ new Date();
|
|
12978
|
+
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");
|
|
12979
|
+
}
|
|
12980
|
+
function isoNow() {
|
|
12981
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
12982
|
+
}
|
|
12983
|
+
async function generate(what, name, extraArgs = []) {
|
|
12984
|
+
if (!what) {
|
|
12985
|
+
console.error(" Usage: tina4nodejs generate <what> <name> [options]");
|
|
12986
|
+
console.error(` Generators: ${GENERATOR_LIST}`);
|
|
12987
|
+
console.error(' Options: --fields "name:string,price:float" --model ModelName');
|
|
12988
|
+
console.error(" --public open a route's writes (default: secure)");
|
|
12989
|
+
console.error(' --every 5m | --cron "\u2026" service schedule');
|
|
12990
|
+
console.error(" --json emit machine-readable resolution envelope on stdout");
|
|
12991
|
+
console.error(" --dry-run report resolution without writing any files");
|
|
12992
|
+
process.exit(1);
|
|
12993
|
+
}
|
|
12994
|
+
const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
|
|
12995
|
+
if (noNameGenerators.has(what) && name.startsWith("--")) {
|
|
12996
|
+
extraArgs = [name, ...extraArgs];
|
|
12997
|
+
name = "";
|
|
12998
|
+
}
|
|
12999
|
+
if (!noNameGenerators.has(what) && !name) {
|
|
13000
|
+
console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
|
|
13001
|
+
process.exit(1);
|
|
13002
|
+
}
|
|
13003
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
13004
|
+
const jsonMode = Boolean(flags.json);
|
|
13005
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
13006
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
|
|
13007
|
+
const spec = GENERATORS[what];
|
|
13008
|
+
if (spec) {
|
|
13009
|
+
spec.handler(name, flags);
|
|
13010
|
+
} else {
|
|
13011
|
+
console.error(` Unknown generator: ${what}`);
|
|
13012
|
+
console.error(` Available: ${GENERATOR_LIST}`);
|
|
13013
|
+
process.exit(1);
|
|
13014
|
+
}
|
|
13015
|
+
const nextFn = NEXT_STEPS[what];
|
|
13016
|
+
if (nextFn) {
|
|
13017
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
13018
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
13019
|
+
}
|
|
13020
|
+
printResolution();
|
|
13021
|
+
}
|
|
13022
|
+
async function generateProgrammatic(what, name, extraArgs = []) {
|
|
13023
|
+
const spec = GENERATORS[what];
|
|
13024
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
13025
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
13026
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
13027
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
|
|
13028
|
+
spec.handler(name, flags);
|
|
13029
|
+
const nextFn = NEXT_STEPS[what];
|
|
13030
|
+
if (nextFn) {
|
|
13031
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
13032
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
13033
|
+
}
|
|
13034
|
+
return currentResolution();
|
|
13035
|
+
}
|
|
13036
|
+
function generateModel(name, flags, emitTest = true) {
|
|
13037
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13038
|
+
const table2 = toTableName(name);
|
|
13039
|
+
const dir = resolve6("src/models");
|
|
13040
|
+
ensureDir(dir);
|
|
13041
|
+
const path8 = join15(dir, `${name}.ts`);
|
|
13042
|
+
setResolutionField("class_name", name);
|
|
13043
|
+
setResolutionField("table_name", table2);
|
|
13044
|
+
setResolutionField("file_path", `src/models/${name}.ts`);
|
|
13045
|
+
pushTestPath(`tests/${table2}_model.test.ts`);
|
|
13046
|
+
const fieldLines = [
|
|
13047
|
+
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
13048
|
+
` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
|
|
13049
|
+
];
|
|
13050
|
+
for (const [fname, ftype] of fields) {
|
|
13051
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
13052
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
13053
|
+
}
|
|
13054
|
+
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
13055
|
+
const content = `import { BaseModel } from "tina4-nodejs/orm";
|
|
13056
|
+
|
|
13057
|
+
export default class ${name} extends BaseModel {
|
|
13058
|
+
static tableName = "${table2}";
|
|
13059
|
+
static fields = {
|
|
13060
|
+
${fieldLines.join("\n")}
|
|
13061
|
+
};
|
|
13062
|
+
}
|
|
13063
|
+
`;
|
|
13064
|
+
writeFileSafe(path8, content);
|
|
13065
|
+
if (!flags["no-migration"]) {
|
|
13066
|
+
generateMigration(`create_${table2}`, flags, fields, table2, false);
|
|
13067
|
+
}
|
|
13068
|
+
if (emitTest) emitModelTest(name, table2, fields);
|
|
13069
|
+
}
|
|
13070
|
+
function secureOptOut(isPublic) {
|
|
13071
|
+
return isPublic ? `export const secure = false;
|
|
13072
|
+
|
|
13073
|
+
` : "";
|
|
13074
|
+
}
|
|
13075
|
+
function generateRoute(name, flags, emitTest = true) {
|
|
13076
|
+
const routePath = name.replace(/^\//, "");
|
|
13077
|
+
const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
|
|
13078
|
+
const model = flags.model;
|
|
13079
|
+
const isPublic = Boolean(flags.public);
|
|
13080
|
+
const base = resolve6("src/routes/api", routePath);
|
|
13081
|
+
const idDir = join15(base, "[id]");
|
|
13082
|
+
ensureDir(base);
|
|
13083
|
+
ensureDir(idDir);
|
|
13084
|
+
pushRoute(`/api/${routePath}`);
|
|
13085
|
+
pushRoute(`/api/${routePath}/{id}`);
|
|
13086
|
+
if (__resolution.target === "route") {
|
|
13087
|
+
setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
|
|
13088
|
+
}
|
|
13089
|
+
const table2 = model ? toTableName(model) : "";
|
|
13090
|
+
const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
|
|
13091
|
+
` : "";
|
|
13092
|
+
const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
|
|
13093
|
+
` : "";
|
|
13094
|
+
const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
|
|
13095
|
+
if (model) {
|
|
13096
|
+
writeFileSafe(
|
|
13097
|
+
join15(base, "get.ts"),
|
|
13098
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13099
|
+
${modelImportBase}
|
|
13100
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
13101
|
+
|
|
13102
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13103
|
+
// tina4:edit tune pagination defaults or add filter/sort parsing here
|
|
13104
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
13105
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
13106
|
+
const offset = (page - 1) * limit;
|
|
13107
|
+
const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
|
|
13108
|
+
res.json({ data: rows.map((r) => r.toObject()), page, limit });
|
|
13109
|
+
}
|
|
13110
|
+
`
|
|
13111
|
+
);
|
|
13112
|
+
} else {
|
|
13113
|
+
writeFileSafe(
|
|
13114
|
+
join15(base, "get.ts"),
|
|
13115
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13116
|
+
|
|
13117
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
13118
|
+
|
|
13119
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13120
|
+
${aiFill(`list_${routePath}`, {
|
|
13121
|
+
intent: `return the ${routePath} collection (add pagination if it grows)`,
|
|
13122
|
+
given: "req.query -> filters/paging",
|
|
13123
|
+
use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
|
|
13124
|
+
ret: "res.json({ data: rows })",
|
|
13125
|
+
ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13126
|
+
raise: `${routePath} list not implemented`
|
|
13127
|
+
})}}
|
|
13128
|
+
`
|
|
13129
|
+
);
|
|
13130
|
+
}
|
|
13131
|
+
if (model) {
|
|
13132
|
+
writeFileSafe(
|
|
13133
|
+
join15(base, "post.ts"),
|
|
13134
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13135
|
+
${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
13136
|
+
|
|
13137
|
+
// ${writeDoc}
|
|
13138
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13139
|
+
// tina4:edit validate the body before persist (Validator or hand-checks)
|
|
13140
|
+
${extend(
|
|
13141
|
+
"validate / business rules before persist",
|
|
13142
|
+
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
|
|
13143
|
+
)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
13144
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
13145
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
13146
|
+
if ((await item.save()) === false) {
|
|
13147
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
13148
|
+
return;
|
|
13149
|
+
}
|
|
13150
|
+
res.json({ data: item.toObject() }, 201);
|
|
13151
|
+
}
|
|
13152
|
+
`
|
|
13153
|
+
);
|
|
13154
|
+
} else {
|
|
13155
|
+
writeFileSafe(
|
|
13156
|
+
join15(base, "post.ts"),
|
|
13157
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13158
|
+
|
|
13159
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
13160
|
+
|
|
13161
|
+
// ${writeDoc}
|
|
13162
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13163
|
+
// tina4:edit fill the create handler (see AI-FILL fill-spec below)
|
|
13164
|
+
${aiFill(`create_${singular}`, {
|
|
13165
|
+
intent: `validate the body and persist a new ${singular}`,
|
|
13166
|
+
given: "req.body -> the posted fields",
|
|
13167
|
+
use: "new Model(req.body).save() then item.toObject() (import your model)",
|
|
13168
|
+
ret: "res.json({ data: item }, 201)",
|
|
13169
|
+
ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13170
|
+
raise: `create ${singular} not implemented`
|
|
13171
|
+
})}}
|
|
13172
|
+
`
|
|
13173
|
+
);
|
|
13174
|
+
}
|
|
13175
|
+
if (model) {
|
|
13176
|
+
writeFileSafe(
|
|
13177
|
+
join15(idDir, "get.ts"),
|
|
13178
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13179
|
+
${modelImportId}
|
|
13180
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
13181
|
+
|
|
13182
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13183
|
+
const { id } = req.params;
|
|
13184
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13185
|
+
if (!item) {
|
|
13186
|
+
res.json({ error: "Not found" }, 404);
|
|
13187
|
+
return;
|
|
13188
|
+
}
|
|
13189
|
+
res.json({ data: item.toObject() });
|
|
13190
|
+
}
|
|
13191
|
+
`
|
|
13192
|
+
);
|
|
13193
|
+
} else {
|
|
13194
|
+
writeFileSafe(
|
|
13195
|
+
join15(idDir, "get.ts"),
|
|
13196
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13197
|
+
|
|
13198
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
13199
|
+
|
|
13200
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13201
|
+
${aiFill(`get_${singular}`, {
|
|
13202
|
+
intent: `fetch one ${singular} by id`,
|
|
13203
|
+
given: "req.params.id -> the record id",
|
|
13204
|
+
use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
|
|
13205
|
+
ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
|
|
13206
|
+
ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13207
|
+
raise: `get ${singular} not implemented`
|
|
13208
|
+
})}}
|
|
13209
|
+
`
|
|
13210
|
+
);
|
|
13211
|
+
}
|
|
13212
|
+
if (model) {
|
|
13213
|
+
writeFileSafe(
|
|
13214
|
+
join15(idDir, "put.ts"),
|
|
13215
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13216
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
13217
|
+
|
|
13218
|
+
// ${writeDoc}
|
|
13219
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13220
|
+
const { id } = req.params;
|
|
13221
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13222
|
+
if (!item) {
|
|
13223
|
+
res.json({ error: "Not found" }, 404);
|
|
13224
|
+
return;
|
|
13225
|
+
}
|
|
13226
|
+
// tina4:edit guard which fields may be updated and who may update this row
|
|
13227
|
+
${extend(
|
|
13228
|
+
"guard which fields / who may update",
|
|
13229
|
+
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
|
|
13230
|
+
)} Object.assign(item, req.body as Record<string, unknown>);
|
|
13231
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
13232
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
13233
|
+
if ((await item.save()) === false) {
|
|
13234
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
13235
|
+
return;
|
|
13236
|
+
}
|
|
13237
|
+
res.json({ data: item.toObject() });
|
|
13238
|
+
}
|
|
13239
|
+
`
|
|
13240
|
+
);
|
|
13241
|
+
} else {
|
|
13242
|
+
writeFileSafe(
|
|
13243
|
+
join15(idDir, "put.ts"),
|
|
13244
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13245
|
+
|
|
13246
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
13247
|
+
|
|
13248
|
+
// ${writeDoc}
|
|
13249
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13250
|
+
// tina4:edit fill the update handler (see AI-FILL fill-spec below)
|
|
13251
|
+
${aiFill(`update_${singular}`, {
|
|
13252
|
+
intent: `load, mutate and save an existing ${singular}`,
|
|
13253
|
+
given: "req.params.id -> id; req.body -> changed fields",
|
|
13254
|
+
use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
|
|
13255
|
+
ret: "res.json({ data: item }) or 404",
|
|
13256
|
+
ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13257
|
+
raise: `update ${singular} not implemented`
|
|
13258
|
+
})}}
|
|
13259
|
+
`
|
|
13260
|
+
);
|
|
13261
|
+
}
|
|
13262
|
+
if (model) {
|
|
13263
|
+
writeFileSafe(
|
|
13264
|
+
join15(idDir, "delete.ts"),
|
|
13265
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13266
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
13267
|
+
|
|
13268
|
+
// ${writeDoc}
|
|
13269
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13270
|
+
const { id } = req.params;
|
|
13271
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
13272
|
+
if (!item) {
|
|
13273
|
+
res.json({ error: "Not found" }, 404);
|
|
13274
|
+
return;
|
|
13275
|
+
}
|
|
13276
|
+
await item.delete();
|
|
13277
|
+
res.json({ message: "deleted", id });
|
|
13278
|
+
}
|
|
13279
|
+
`
|
|
13280
|
+
);
|
|
13281
|
+
} else {
|
|
13282
|
+
writeFileSafe(
|
|
13283
|
+
join15(idDir, "delete.ts"),
|
|
13284
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13285
|
+
|
|
13286
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
13287
|
+
|
|
13288
|
+
// ${writeDoc}
|
|
13289
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13290
|
+
${aiFill(`delete_${singular}`, {
|
|
13291
|
+
intent: `delete a ${singular} by id`,
|
|
13292
|
+
given: "req.params.id -> id",
|
|
13293
|
+
use: "Model.selectOne(\u2026) then item.delete()",
|
|
13294
|
+
ret: "res.json({ message: 'deleted', id }) or 404",
|
|
13295
|
+
ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13296
|
+
raise: `delete ${singular} not implemented`
|
|
13297
|
+
})}}
|
|
13298
|
+
`
|
|
13299
|
+
);
|
|
13300
|
+
}
|
|
13301
|
+
if (emitTest) {
|
|
13302
|
+
if (model) {
|
|
13303
|
+
generateTest(routePath, { model, "secure-writes": true, public: isPublic });
|
|
13304
|
+
} else {
|
|
13305
|
+
emitRouteStubTest(routePath);
|
|
13306
|
+
}
|
|
13307
|
+
}
|
|
13308
|
+
}
|
|
13309
|
+
function generateCrud(name, flags) {
|
|
13310
|
+
const table2 = toTableName(name);
|
|
13311
|
+
const routeName = toPlural(table2);
|
|
13312
|
+
const isPublic = Boolean(flags.public);
|
|
13313
|
+
if (!__resolution.jsonMode) console.log(`
|
|
13314
|
+
Generating CRUD for ${name}...
|
|
13315
|
+
`);
|
|
13316
|
+
generateModel(name, flags, false);
|
|
13317
|
+
generateRoute(routeName, { ...flags, model: name }, false);
|
|
13318
|
+
generateForm(name, flags);
|
|
13319
|
+
generateView(name, flags);
|
|
13320
|
+
generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
|
|
13321
|
+
if (!__resolution.jsonMode) {
|
|
13322
|
+
console.log(`
|
|
13323
|
+
CRUD generation complete for ${name}.`);
|
|
13324
|
+
console.log(" Run: tina4nodejs migrate");
|
|
13325
|
+
console.log(" Visit: /swagger to see the API docs");
|
|
13326
|
+
}
|
|
13327
|
+
}
|
|
13328
|
+
function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
|
|
13329
|
+
const ts = timestamp();
|
|
13330
|
+
const dir = resolve6("migrations");
|
|
13331
|
+
ensureDir(dir);
|
|
13332
|
+
let table2;
|
|
13333
|
+
if (tableOverride) {
|
|
13334
|
+
table2 = tableOverride;
|
|
13335
|
+
} else {
|
|
13336
|
+
const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
|
|
13337
|
+
table2 = toTableName(raw);
|
|
13338
|
+
}
|
|
13339
|
+
if (__resolution.target === "migration") {
|
|
13340
|
+
setResolutionField("table_name", table2);
|
|
13341
|
+
}
|
|
13342
|
+
const fields = fieldsOverride || parseFields(flags.fields || "");
|
|
13343
|
+
const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
|
|
13344
|
+
const fileName = `${ts}_${name}.sql`;
|
|
13345
|
+
const path8 = join15(dir, fileName);
|
|
13346
|
+
setResolutionField("migration_path", `migrations/${fileName}`);
|
|
13347
|
+
if (__resolution.target === "migration") {
|
|
13348
|
+
setResolutionField("file_path", `migrations/${fileName}`);
|
|
13349
|
+
pushTestPath(`tests/${table2}_migration.test.ts`);
|
|
13350
|
+
}
|
|
13351
|
+
let upSql;
|
|
13352
|
+
let downSql;
|
|
13353
|
+
if (isCreate) {
|
|
13354
|
+
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
13355
|
+
for (const [fname, ftype] of fields) {
|
|
13356
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
13357
|
+
const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
|
|
13358
|
+
colLines.push(` ${fname} ${info.sql}${defaultClause}`);
|
|
13359
|
+
}
|
|
13360
|
+
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
13361
|
+
upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
|
|
13362
|
+
-- tina4:edit add columns beyond id + created_at
|
|
13363
|
+
${colLines.join(",\n")}
|
|
13364
|
+
);`;
|
|
13365
|
+
downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
|
|
13366
|
+
DROP TABLE IF EXISTS ${table2};`;
|
|
13367
|
+
} else {
|
|
13368
|
+
upSql = `-- tina4:edit write your UP migration SQL here
|
|
13369
|
+
-- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
13370
|
+
downSql = `-- tina4:edit write your DOWN rollback SQL here
|
|
13371
|
+
-- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
|
|
13372
|
+
}
|
|
13373
|
+
const now = isoNow();
|
|
13374
|
+
const content = `-- Migration: ${name}
|
|
13375
|
+
-- Created: ${now}
|
|
13376
|
+
|
|
13377
|
+
-- UP
|
|
13378
|
+
${upSql}
|
|
13379
|
+
|
|
13380
|
+
-- DOWN
|
|
13381
|
+
${downSql}
|
|
13382
|
+
`;
|
|
13383
|
+
writeFileSafe(path8, content);
|
|
13384
|
+
const downPath = join15(dir, `${ts}_${name}.down.sql`);
|
|
13385
|
+
const downContent = `-- Rollback: ${name}
|
|
13386
|
+
-- Created: ${now}
|
|
13387
|
+
|
|
13388
|
+
${downSql}
|
|
13389
|
+
`;
|
|
13390
|
+
writeFileSafe(downPath, downContent);
|
|
13391
|
+
if (emitTest && isCreate) emitMigrationTest(name, table2);
|
|
13392
|
+
}
|
|
13393
|
+
function generateMiddleware(name, _flags) {
|
|
13394
|
+
const snake = toSnake(name);
|
|
13395
|
+
const dir = resolve6("src/middleware");
|
|
13396
|
+
ensureDir(dir);
|
|
13397
|
+
const path8 = join15(dir, `${snake}.ts`);
|
|
13398
|
+
if (__resolution.target === "middleware") {
|
|
13399
|
+
setResolutionField("class_name", name);
|
|
13400
|
+
setResolutionField("file_path", `src/middleware/${snake}.ts`);
|
|
13401
|
+
pushTestPath(`tests/${snake}.test.ts`);
|
|
13402
|
+
}
|
|
13403
|
+
const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13404
|
+
|
|
13405
|
+
/**
|
|
13406
|
+
* ${name} middleware \u2014 runs before and after route handlers.
|
|
13407
|
+
*
|
|
13408
|
+
* Usage:
|
|
13409
|
+
* import { before${name}, after${name} } from "../middleware/${snake}.js";
|
|
13410
|
+
*/
|
|
13411
|
+
|
|
13412
|
+
export async function before${name}(
|
|
13413
|
+
req: Tina4Request,
|
|
13414
|
+
res: Tina4Response,
|
|
13415
|
+
next: () => Promise<void>,
|
|
13416
|
+
): Promise<void> {
|
|
13417
|
+
// tina4:edit replace the Authorization check with the real pre-request rule
|
|
13418
|
+
const auth = req.headers["authorization"];
|
|
13419
|
+
if (!auth) {
|
|
13420
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
13421
|
+
return;
|
|
13422
|
+
}
|
|
13423
|
+
await next();
|
|
13424
|
+
}
|
|
13425
|
+
|
|
13426
|
+
export async function after${name}(
|
|
13427
|
+
req: Tina4Request,
|
|
13428
|
+
res: Tina4Response,
|
|
13429
|
+
next: () => Promise<void>,
|
|
13430
|
+
): Promise<void> {
|
|
13431
|
+
// tina4:edit add post-processing (logging, header injection, telemetry)
|
|
13432
|
+
await next();
|
|
13433
|
+
}
|
|
13434
|
+
`;
|
|
13435
|
+
writeFileSafe(path8, content);
|
|
13436
|
+
emitMiddlewareTest(name, snake);
|
|
13437
|
+
}
|
|
13438
|
+
function generateTest(name, flags) {
|
|
13439
|
+
const snake = toSnake(name);
|
|
13440
|
+
const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
|
|
13441
|
+
const model = flags.model;
|
|
13442
|
+
const dir = resolve6("tests");
|
|
13443
|
+
ensureDir(dir);
|
|
13444
|
+
const path8 = join15(dir, `${snake}.test.ts`);
|
|
13445
|
+
if (model && flags["secure-writes"]) {
|
|
13446
|
+
const isPublic = Boolean(flags.public);
|
|
13447
|
+
const posture = isPublic ? "open (--public)" : "gated";
|
|
13448
|
+
const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
|
|
13449
|
+
assert("anonymous POST is public -> 201",
|
|
13450
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
|
|
13451
|
+
assert("anonymous POST is gated -> 401",
|
|
13452
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
|
|
13453
|
+
// A valid Bearer token passes the gate and creates -> 201.
|
|
13454
|
+
const token = getToken({ userId: 1 });
|
|
13455
|
+
assert("authenticated POST creates -> 201",
|
|
13456
|
+
(await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
|
|
13457
|
+
const content2 = `/**
|
|
13458
|
+
* ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
|
|
13459
|
+
*
|
|
13460
|
+
* Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
|
|
13461
|
+
* JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
|
|
13462
|
+
*/
|
|
13463
|
+
import { dirname, resolve } from "node:path";
|
|
13464
|
+
import { fileURLToPath } from "node:url";
|
|
13465
|
+
import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
13466
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
13467
|
+
import ${model} from "../src/models/${model}.js";
|
|
13468
|
+
|
|
13469
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
13470
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13471
|
+
|
|
13472
|
+
let pass = 0;
|
|
13473
|
+
let fail = 0;
|
|
13474
|
+
function assert(label: string, ok: boolean): void {
|
|
13475
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
13476
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
13477
|
+
}
|
|
13478
|
+
|
|
13479
|
+
await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
|
|
13480
|
+
await ${model}.createTable();
|
|
13481
|
+
|
|
13482
|
+
const router = new Router();
|
|
13483
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
13484
|
+
const client = new TestClient(router);
|
|
13485
|
+
|
|
13486
|
+
// Reads are public.
|
|
13487
|
+
assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
|
|
13488
|
+
${writeCase}
|
|
13489
|
+
|
|
13490
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
13491
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
13492
|
+
`;
|
|
13493
|
+
writeFileSafe(path8, content2);
|
|
13494
|
+
return;
|
|
13495
|
+
}
|
|
13496
|
+
let content;
|
|
13497
|
+
if (model) {
|
|
13498
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
13499
|
+
|
|
13500
|
+
/**
|
|
13501
|
+
* Tests for ${name} CRUD operations.
|
|
13502
|
+
*/
|
|
13503
|
+
|
|
13504
|
+
const list${model}s = tests(
|
|
13505
|
+
assertTrue([]),
|
|
13506
|
+
)(function list${model}s() {
|
|
13507
|
+
// tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
|
|
13508
|
+
return true;
|
|
13509
|
+
});
|
|
13510
|
+
|
|
13511
|
+
const get${model} = tests(
|
|
13512
|
+
assertTrue([]),
|
|
13513
|
+
)(function get${model}() {
|
|
13514
|
+
// tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
|
|
13515
|
+
return true;
|
|
13516
|
+
});
|
|
13517
|
+
|
|
13518
|
+
const create${model} = tests(
|
|
13519
|
+
assertTrue([]),
|
|
13520
|
+
)(function create${model}() {
|
|
13521
|
+
// tina4:edit POST a valid + an invalid body, assert 201 vs 400
|
|
13522
|
+
return true;
|
|
13523
|
+
});
|
|
13524
|
+
|
|
13525
|
+
const update${model} = tests(
|
|
13526
|
+
assertTrue([]),
|
|
13527
|
+
)(function update${model}() {
|
|
13528
|
+
// tina4:edit PUT changed fields, assert the row was persisted
|
|
13529
|
+
return true;
|
|
13530
|
+
});
|
|
13531
|
+
|
|
13532
|
+
const delete${model} = tests(
|
|
13533
|
+
assertTrue([]),
|
|
13534
|
+
)(function delete${model}() {
|
|
13535
|
+
// tina4:edit DELETE the id, assert 200 then GET returns 404
|
|
13536
|
+
return true;
|
|
13537
|
+
});
|
|
13538
|
+
|
|
13539
|
+
void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
|
|
13540
|
+
`;
|
|
13541
|
+
} else {
|
|
13542
|
+
const titleName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
13543
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
13544
|
+
|
|
13545
|
+
/**
|
|
13546
|
+
* Tests for ${name}.
|
|
13547
|
+
*/
|
|
13548
|
+
|
|
13549
|
+
const test${titleName} = tests(
|
|
13550
|
+
assertTrue([]),
|
|
13551
|
+
)(function test${titleName}() {
|
|
13552
|
+
// tina4:edit assert against the real behaviour under test (no mocks)
|
|
13553
|
+
return true;
|
|
13554
|
+
});
|
|
13555
|
+
|
|
13556
|
+
void test${titleName};
|
|
13557
|
+
`;
|
|
13558
|
+
}
|
|
13559
|
+
writeFileSafe(path8, content);
|
|
13560
|
+
}
|
|
13561
|
+
function generateForm(name, flags) {
|
|
13562
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13563
|
+
const table2 = toTableName(name);
|
|
13564
|
+
const routeName = toPlural(table2);
|
|
13565
|
+
const inputTypes = {
|
|
13566
|
+
string: "text",
|
|
13567
|
+
str: "text",
|
|
13568
|
+
text: "textarea",
|
|
13569
|
+
int: "number",
|
|
13570
|
+
integer: "number",
|
|
13571
|
+
float: "number",
|
|
13572
|
+
numeric: "number",
|
|
13573
|
+
decimal: "number",
|
|
13574
|
+
bool: "checkbox",
|
|
13575
|
+
boolean: "checkbox",
|
|
13576
|
+
datetime: "datetime-local",
|
|
13577
|
+
blob: "file"
|
|
13578
|
+
};
|
|
13579
|
+
const dir = resolve6("src/templates/forms");
|
|
13580
|
+
ensureDir(dir);
|
|
13581
|
+
const path8 = join15(dir, `${table2}.twig`);
|
|
13582
|
+
let fieldHtml = "";
|
|
13583
|
+
for (const [fname, ftype] of fields) {
|
|
13584
|
+
const itype = inputTypes[ftype] || "text";
|
|
13585
|
+
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
13586
|
+
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
13587
|
+
if (itype === "textarea") {
|
|
13588
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13589
|
+
<label for="${fname}">${label}</label>
|
|
13590
|
+
<textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
|
|
13591
|
+
</div>
|
|
13592
|
+
`;
|
|
13593
|
+
} else if (itype === "checkbox") {
|
|
13594
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13595
|
+
<label>
|
|
13596
|
+
<input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
|
|
13597
|
+
${label}
|
|
13598
|
+
</label>
|
|
13599
|
+
</div>
|
|
13600
|
+
`;
|
|
13601
|
+
} else {
|
|
13602
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
13603
|
+
<label for="${fname}">${label}</label>
|
|
13604
|
+
<input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
|
|
13605
|
+
</div>
|
|
13606
|
+
`;
|
|
13607
|
+
}
|
|
13608
|
+
}
|
|
13609
|
+
const content = `{% extends "base.twig" %}
|
|
13610
|
+
{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
|
|
13611
|
+
{% block content %}
|
|
13612
|
+
<div class="container mt-4">
|
|
13613
|
+
<h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
|
|
13614
|
+
{# tina4:edit restyle the form beyond the scaffolded defaults #}
|
|
13615
|
+
<form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
|
|
13616
|
+
{{ form_token() }}
|
|
13617
|
+
` + fieldHtml + ` <button type="submit" class="btn btn-primary">
|
|
13618
|
+
{% if item.id %}Update{% else %}Create{% endif %}
|
|
13619
|
+
</button>
|
|
13620
|
+
<a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
|
|
13621
|
+
</form>
|
|
13622
|
+
</div>
|
|
13623
|
+
{% endblock %}
|
|
13624
|
+
`;
|
|
13625
|
+
writeFileSafe(path8, content);
|
|
13626
|
+
}
|
|
13627
|
+
function generateView(name, flags) {
|
|
13628
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
13629
|
+
const table2 = toTableName(name);
|
|
13630
|
+
const routeName = toPlural(table2);
|
|
13631
|
+
const cols = fields.map(([f]) => f);
|
|
13632
|
+
const dir = resolve6("src/templates/pages");
|
|
13633
|
+
ensureDir(dir);
|
|
13634
|
+
const listPath = join15(dir, `${routeName}.twig`);
|
|
13635
|
+
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
13636
|
+
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
13637
|
+
const listContent = `{% extends "base.twig" %}
|
|
13638
|
+
{% block title %}${name}s{% endblock %}
|
|
13639
|
+
{% block content %}
|
|
13640
|
+
<div class="container mt-4">
|
|
13641
|
+
{# tina4:edit add sort / filter / pagination controls to the list #}
|
|
13642
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
13643
|
+
<h1>${name}s</h1>
|
|
13644
|
+
<a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
|
|
13645
|
+
</div>
|
|
13646
|
+
<table class="table">
|
|
13647
|
+
<thead>
|
|
13648
|
+
<tr>
|
|
13649
|
+
<th>ID</th>
|
|
13650
|
+
${th}
|
|
13651
|
+
<th>Actions</th>
|
|
13652
|
+
</tr>
|
|
13653
|
+
</thead>
|
|
13654
|
+
<tbody>
|
|
13655
|
+
{% for item in items %}
|
|
13656
|
+
<tr>
|
|
13657
|
+
<td>{{ item.id }}</td>
|
|
13658
|
+
${td}
|
|
13659
|
+
<td>
|
|
13660
|
+
<a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
|
|
13661
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
|
|
13662
|
+
</td>
|
|
13663
|
+
</tr>
|
|
13664
|
+
{% endfor %}
|
|
13665
|
+
</tbody>
|
|
13666
|
+
</table>
|
|
13667
|
+
</div>
|
|
13668
|
+
{% endblock %}
|
|
13669
|
+
`;
|
|
13670
|
+
writeFileSafe(listPath, listContent);
|
|
13671
|
+
const detailPath = join15(dir, `${table2}.twig`);
|
|
13672
|
+
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");
|
|
13673
|
+
const detailContent = `{% extends "base.twig" %}
|
|
13674
|
+
{% block title %}${name} Detail{% endblock %}
|
|
13675
|
+
{% block content %}
|
|
13676
|
+
<div class="container mt-4">
|
|
13677
|
+
{# tina4:edit extend the detail view with related records or actions #}
|
|
13678
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
13679
|
+
<h1>${name} #{{ item.id }}</h1>
|
|
13680
|
+
<div>
|
|
13681
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
|
|
13682
|
+
<a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
|
|
13683
|
+
</div>
|
|
13684
|
+
</div>
|
|
13685
|
+
${detailFields}
|
|
13686
|
+
</div>
|
|
13687
|
+
{% endblock %}
|
|
13688
|
+
`;
|
|
13689
|
+
writeFileSafe(detailPath, detailContent);
|
|
13690
|
+
}
|
|
13691
|
+
function generateAuth(_flags) {
|
|
13692
|
+
if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
|
|
13693
|
+
generateModel("User", { fields: "email:string,password:string,role:string" }, false);
|
|
13694
|
+
const registerDir = resolve6("src/routes/api/auth/register");
|
|
13695
|
+
const loginDir = resolve6("src/routes/api/auth/login");
|
|
13696
|
+
const meDir = resolve6("src/routes/api/auth/me");
|
|
13697
|
+
ensureDir(registerDir);
|
|
13698
|
+
ensureDir(loginDir);
|
|
13699
|
+
ensureDir(meDir);
|
|
13700
|
+
writeFileSafe(
|
|
13701
|
+
join15(registerDir, "post.ts"),
|
|
13702
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13703
|
+
import { hashPassword } from "tina4-nodejs";
|
|
13704
|
+
import User from "../../../../models/User.js";
|
|
13705
|
+
|
|
13706
|
+
// Public: registration mints an account for a user who has no token yet.
|
|
13707
|
+
export const secure = false;
|
|
13708
|
+
|
|
13709
|
+
export const meta = { summary: "Register a new user", tags: ["auth"] };
|
|
13710
|
+
|
|
13711
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13712
|
+
// tina4:edit add password-strength / email-format / captcha rules before mint
|
|
13713
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
13714
|
+
|
|
13715
|
+
if (!email || !password) {
|
|
13716
|
+
res.json({ error: "Email and password required" }, 400);
|
|
13717
|
+
return;
|
|
13718
|
+
}
|
|
13719
|
+
|
|
13720
|
+
const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
13721
|
+
if (existing) {
|
|
13722
|
+
res.json({ error: "Email already registered" }, 409);
|
|
13723
|
+
return;
|
|
13724
|
+
}
|
|
13725
|
+
|
|
13726
|
+
const user = new User({ email, password: hashPassword(password), role: "user" });
|
|
13727
|
+
await user.save();
|
|
13728
|
+
res.json({ message: "Registered", id: user.toObject().id }, 201);
|
|
13729
|
+
}
|
|
13730
|
+
`
|
|
13731
|
+
);
|
|
13732
|
+
writeFileSafe(
|
|
13733
|
+
join15(loginDir, "post.ts"),
|
|
13734
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13735
|
+
import { checkPassword, getToken } from "tina4-nodejs";
|
|
13736
|
+
import User from "../../../../models/User.js";
|
|
13737
|
+
|
|
13738
|
+
// Public: login authenticates by password and mints the token.
|
|
13739
|
+
export const secure = false;
|
|
13740
|
+
|
|
13741
|
+
export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
|
|
13742
|
+
|
|
13743
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13744
|
+
// tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
|
|
13745
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
13746
|
+
|
|
13747
|
+
if (!email || !password) {
|
|
13748
|
+
res.json({ error: "Email and password required" }, 400);
|
|
13749
|
+
return;
|
|
13750
|
+
}
|
|
13751
|
+
|
|
13752
|
+
const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
13753
|
+
if (!user || !checkPassword(password, user.toObject().password as string)) {
|
|
13754
|
+
res.json({ error: "Invalid credentials" }, 401);
|
|
13755
|
+
return;
|
|
13756
|
+
}
|
|
13757
|
+
|
|
13758
|
+
const data = user.toObject();
|
|
13759
|
+
// tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
|
|
13760
|
+
const token = getToken({ userId: data.id, email: data.email, role: data.role });
|
|
13761
|
+
res.json({ token });
|
|
13762
|
+
}
|
|
13763
|
+
`
|
|
13764
|
+
);
|
|
13765
|
+
writeFileSafe(
|
|
13766
|
+
join15(meDir, "get.ts"),
|
|
13767
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
13768
|
+
import { authenticateRequest } from "tina4-nodejs";
|
|
13769
|
+
|
|
13770
|
+
export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
|
|
13771
|
+
|
|
13772
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
13773
|
+
const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
|
|
13774
|
+
if (!payload) {
|
|
13775
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
13776
|
+
return;
|
|
13777
|
+
}
|
|
13778
|
+
res.json({ user: payload });
|
|
13779
|
+
}
|
|
13780
|
+
`
|
|
13781
|
+
);
|
|
13782
|
+
const formsDir = resolve6("src/templates/forms");
|
|
13783
|
+
ensureDir(formsDir);
|
|
13784
|
+
writeFileSafe(
|
|
13785
|
+
join15(formsDir, "login.twig"),
|
|
13786
|
+
`{% extends "base.twig" %}
|
|
13787
|
+
{% block title %}Login{% endblock %}
|
|
13788
|
+
{% block content %}
|
|
13789
|
+
<div class="container mt-4" style="max-width:400px">
|
|
13790
|
+
<h1>Login</h1>
|
|
13791
|
+
<form method="post" action="/api/auth/login">
|
|
13792
|
+
{{ form_token() }}
|
|
13793
|
+
<div class="form-group mb-3">
|
|
13794
|
+
<label for="email">Email</label>
|
|
13795
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
13796
|
+
</div>
|
|
13797
|
+
<div class="form-group mb-3">
|
|
13798
|
+
<label for="password">Password</label>
|
|
13799
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
|
|
13800
|
+
</div>
|
|
13801
|
+
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
13802
|
+
<p class="mt-3 text-center"><a href="/register">Create an account</a></p>
|
|
13803
|
+
</form>
|
|
13804
|
+
</div>
|
|
13805
|
+
{% endblock %}
|
|
13806
|
+
`
|
|
13807
|
+
);
|
|
13808
|
+
writeFileSafe(
|
|
13809
|
+
join15(formsDir, "register.twig"),
|
|
13810
|
+
`{% extends "base.twig" %}
|
|
13811
|
+
{% block title %}Register{% endblock %}
|
|
13812
|
+
{% block content %}
|
|
13813
|
+
<div class="container mt-4" style="max-width:400px">
|
|
13814
|
+
<h1>Register</h1>
|
|
13815
|
+
<form method="post" action="/api/auth/register">
|
|
13816
|
+
{{ form_token() }}
|
|
13817
|
+
<div class="form-group mb-3">
|
|
13818
|
+
<label for="email">Email</label>
|
|
13819
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
13820
|
+
</div>
|
|
13821
|
+
<div class="form-group mb-3">
|
|
13822
|
+
<label for="password">Password</label>
|
|
13823
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
|
|
13824
|
+
</div>
|
|
13825
|
+
<button type="submit" class="btn btn-primary w-100">Register</button>
|
|
13826
|
+
<p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
|
|
13827
|
+
</form>
|
|
13828
|
+
</div>
|
|
13829
|
+
{% endblock %}
|
|
13830
|
+
`
|
|
13831
|
+
);
|
|
13832
|
+
emitAuthTest();
|
|
13833
|
+
if (!__resolution.jsonMode) {
|
|
13834
|
+
console.log("\n Authentication scaffolding complete.");
|
|
13835
|
+
console.log(" Run: tina4nodejs migrate");
|
|
13836
|
+
console.log(" POST /api/auth/register \u2014 create account (public)");
|
|
13837
|
+
console.log(" POST /api/auth/login \u2014 get JWT token (public)");
|
|
13838
|
+
console.log(" GET /api/auth/me \u2014 get profile (requires token)");
|
|
13839
|
+
}
|
|
13840
|
+
}
|
|
13841
|
+
function generateService(name, flags) {
|
|
13842
|
+
const snake = toSnake(name);
|
|
13843
|
+
const camel = toCamel(toPascal(name)) || snake;
|
|
13844
|
+
const cron = flags.cron;
|
|
13845
|
+
const dir = resolve6("src/services");
|
|
13846
|
+
ensureDir(dir);
|
|
13847
|
+
const path8 = join15(dir, `${snake}.ts`);
|
|
13848
|
+
let scheduleField;
|
|
13849
|
+
let note;
|
|
13850
|
+
if (cron && cron !== true) {
|
|
13851
|
+
scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
|
|
13852
|
+
note = `cron '${cron}'`;
|
|
13853
|
+
} else {
|
|
13854
|
+
const seconds = parseEvery(flags.every);
|
|
13855
|
+
scheduleField = ` interval: ${seconds},`;
|
|
13856
|
+
note = `every ${seconds}s`;
|
|
13857
|
+
}
|
|
13858
|
+
const body = aiFill(`${camel}Task`, {
|
|
13859
|
+
intent: "do the scheduled work for this service",
|
|
13860
|
+
given: "context -> ServiceContext (.name, .running, .lastRun)",
|
|
13861
|
+
use: "your ORM / Api / Messenger code (re-run on schedule)",
|
|
13862
|
+
ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13863
|
+
raise: `service ${snake} not implemented`
|
|
13864
|
+
});
|
|
13865
|
+
const content = `import type { ServiceContext } from "tina4-nodejs";
|
|
13866
|
+
|
|
13867
|
+
/**
|
|
13868
|
+
* ${name} background service \u2014 runs ${note} via ServiceRunner.
|
|
13869
|
+
*
|
|
13870
|
+
* Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
|
|
13871
|
+
* does NOT auto-start services:
|
|
13872
|
+
*
|
|
13873
|
+
* import { ServiceRunner } from "tina4-nodejs";
|
|
13874
|
+
* await ServiceRunner.discover("src/services"); // registers this default export
|
|
13875
|
+
* ServiceRunner.start();
|
|
13876
|
+
*/
|
|
13877
|
+
|
|
13878
|
+
export async function ${camel}Task(context: ServiceContext): Promise<void> {
|
|
13879
|
+
// tina4:edit replace the AI-FILL stub below with the scheduled work
|
|
13880
|
+
${body}}
|
|
13881
|
+
|
|
13882
|
+
// Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
|
|
13883
|
+
// (+ interval or timing) off this default export.
|
|
13884
|
+
export default {
|
|
13885
|
+
name: "${snake}",
|
|
13886
|
+
handler: ${camel}Task,
|
|
13887
|
+
${scheduleField}
|
|
13888
|
+
};
|
|
13889
|
+
`;
|
|
13890
|
+
writeFileSafe(path8, content);
|
|
13891
|
+
emitServiceTest(name, snake, camel);
|
|
13892
|
+
}
|
|
13893
|
+
function generateQueue(name, _flags) {
|
|
13894
|
+
const topic = name.replace(/^\//, "");
|
|
13895
|
+
const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
13896
|
+
const pascal = toPascal(topic) || "Topic";
|
|
13897
|
+
const dir = resolve6("src/services");
|
|
13898
|
+
ensureDir(dir);
|
|
13899
|
+
const path8 = join15(dir, `${slug}_consumer.ts`);
|
|
13900
|
+
const body = aiFill(`handle${pascal}`, {
|
|
13901
|
+
intent: `process ONE ${topic} job payload`,
|
|
13902
|
+
given: "payload -> the produced job data (job.payload)",
|
|
13903
|
+
use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
|
|
13904
|
+
ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
13905
|
+
raise: `queue ${topic} handler not implemented`
|
|
13906
|
+
});
|
|
13907
|
+
const content = `import { Queue } from "tina4-nodejs";
|
|
13908
|
+
import type { ServiceContext } from "tina4-nodejs";
|
|
13909
|
+
|
|
13910
|
+
/**
|
|
13911
|
+
* ${topic} queue \u2014 producer + consumer worker.
|
|
13912
|
+
*
|
|
13913
|
+
* Produce from anywhere: publish${pascal}({ ... })
|
|
13914
|
+
* The consumer is a long-running worker wired as a ServiceRunner daemon:
|
|
13915
|
+
* await ServiceRunner.discover("src/services"); ServiceRunner.start();
|
|
13916
|
+
*/
|
|
13917
|
+
|
|
13918
|
+
/** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
|
|
13919
|
+
export function publish${pascal}(payload: Record<string, unknown>): string {
|
|
13920
|
+
return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
/** Process ONE ${topic} job payload. */
|
|
13924
|
+
export async function handle${pascal}(payload: unknown): Promise<void> {
|
|
13925
|
+
// tina4:edit implement the per-job handler; return to ack, throw to nack
|
|
13926
|
+
${body}}
|
|
13927
|
+
|
|
13928
|
+
/** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
|
|
13929
|
+
export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
|
|
13930
|
+
const queue = new Queue({ topic: "${topic}" });
|
|
13931
|
+
for await (const job of queue.consume("${topic}")) {
|
|
13932
|
+
const one = Array.isArray(job) ? job[0] : job;
|
|
13933
|
+
try {
|
|
13934
|
+
await handle${pascal}(one.payload);
|
|
13935
|
+
one.complete(); // ack \u2014 remove from the queue
|
|
13936
|
+
} catch (err) {
|
|
13937
|
+
one.fail(String(err)); // nack \u2014 retry / dead-letter
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
}
|
|
13941
|
+
|
|
13942
|
+
// Discovered by ServiceRunner.discover("src/services"); daemon:true because
|
|
13943
|
+
// consume${pascal} owns its own loop. The topic + per-job handle keys let
|
|
13944
|
+
// \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
|
|
13945
|
+
// loop / bounded --once drain) without wiring a ServiceRunner.
|
|
13946
|
+
export default {
|
|
13947
|
+
name: "${topic}-consumer",
|
|
13948
|
+
topic: "${topic}",
|
|
13949
|
+
handler: consume${pascal},
|
|
13950
|
+
handle: handle${pascal},
|
|
13951
|
+
daemon: true,
|
|
13952
|
+
};
|
|
13953
|
+
`;
|
|
13954
|
+
writeFileSafe(path8, content);
|
|
13955
|
+
emitQueueTest(topic, slug, pascal);
|
|
13956
|
+
}
|
|
13957
|
+
function generateValidator(name, _flags) {
|
|
13958
|
+
const dir = resolve6("src/validators");
|
|
13959
|
+
ensureDir(dir);
|
|
13960
|
+
const path8 = join15(dir, `${toSnake(name)}.ts`);
|
|
13961
|
+
const rules = extend(
|
|
13962
|
+
"add / adjust the validation rules for this payload",
|
|
13963
|
+
`e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
|
|
13964
|
+
);
|
|
13965
|
+
const content = `import { Validator } from "tina4-nodejs";
|
|
13966
|
+
|
|
13967
|
+
/**
|
|
13968
|
+
* Validate a ${name} payload. Returns a Validator (chainable rules).
|
|
13969
|
+
*
|
|
13970
|
+
* Usage in a route:
|
|
13971
|
+
* const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
|
|
13972
|
+
* if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
|
|
13973
|
+
*/
|
|
13974
|
+
export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
|
|
13975
|
+
const validator = new Validator(data);
|
|
13976
|
+
// tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
|
|
13977
|
+
${rules} validator.required("name"); // starter rule (matches the model's default field)
|
|
13978
|
+
return validator;
|
|
13979
|
+
}
|
|
13980
|
+
`;
|
|
13981
|
+
writeFileSafe(path8, content);
|
|
13982
|
+
emitValidatorTest(name, toSnake(name), toPascal(name));
|
|
13983
|
+
}
|
|
13984
|
+
function generateSeeder(name, _flags) {
|
|
13985
|
+
const table2 = toTableName(name);
|
|
13986
|
+
const dir = resolve6("src/seeds");
|
|
13987
|
+
ensureDir(dir);
|
|
13988
|
+
const path8 = join15(dir, `${table2}_seeder.ts`);
|
|
13989
|
+
const overrides = extend(
|
|
13990
|
+
"override fields that need a specific shape (seedOrm auto-fills the rest)",
|
|
13991
|
+
`e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
|
|
13992
|
+
);
|
|
13993
|
+
const content = `import { pathToFileURL } from "node:url";
|
|
13994
|
+
import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
|
|
13995
|
+
import ${name} from "../models/${name}.js";
|
|
13996
|
+
|
|
13997
|
+
/**
|
|
13998
|
+
* Seeder for ${name} \u2014 run with: tina4nodejs seed
|
|
13999
|
+
*
|
|
14000
|
+
* seedOrm auto-fills every field by type/name; override the ones that need a
|
|
14001
|
+
* specific shape below. Each callable receives a FakeData instance.
|
|
14002
|
+
*/
|
|
14003
|
+
export function fieldOverrides(fake: FakeData): Record<string, unknown> {
|
|
14004
|
+
// tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
|
|
14005
|
+
${overrides} void fake; // available for overrides above
|
|
14006
|
+
return {};
|
|
14007
|
+
}
|
|
14008
|
+
|
|
14009
|
+
/** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
|
|
14010
|
+
export async function run(): Promise<void> {
|
|
14011
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
|
|
14012
|
+
const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
|
|
14013
|
+
console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
|
|
14014
|
+
}
|
|
14015
|
+
|
|
14016
|
+
// Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
|
|
14017
|
+
// importing this module (e.g. in a test) must NOT trigger seeding.
|
|
14018
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
14019
|
+
await run();
|
|
14020
|
+
}
|
|
14021
|
+
`;
|
|
14022
|
+
writeFileSafe(path8, content);
|
|
14023
|
+
emitSeederTest(name, table2);
|
|
14024
|
+
}
|
|
14025
|
+
function generateWebsocket(name, _flags) {
|
|
14026
|
+
const raw = name.trim();
|
|
14027
|
+
const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
|
|
14028
|
+
let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
14029
|
+
const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
|
|
14030
|
+
const handlerName = `${toCamel(toPascal(base))}Ws`;
|
|
14031
|
+
const dir = resolve6("src/routes");
|
|
14032
|
+
ensureDir(dir);
|
|
14033
|
+
const path8 = join15(dir, `ws_${base}.ts`);
|
|
14034
|
+
const body = aiFill(handlerName, {
|
|
14035
|
+
intent: `handle an inbound "message" frame on ${wsPath}`,
|
|
14036
|
+
given: "data -> the message payload (string); connection -> WebSocketConnection",
|
|
14037
|
+
use: "connection.broadcast(data) or connection.sendJson({ ... })",
|
|
14038
|
+
ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
14039
|
+
raise: `websocket ${wsPath} not implemented`
|
|
14040
|
+
});
|
|
14041
|
+
const content = `import { websocket } from "tina4-nodejs";
|
|
14042
|
+
import type { WebSocketConnection } from "tina4-nodejs";
|
|
14043
|
+
|
|
14044
|
+
/**
|
|
14045
|
+
* ${wsPath} WebSocket route.
|
|
14046
|
+
*
|
|
14047
|
+
* Registered on import by websocket(). Node has NO file-based WS
|
|
14048
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it (add
|
|
14049
|
+
* \`.secure()\` to require a JWT on the upgrade):
|
|
14050
|
+
*
|
|
14051
|
+
* import "./src/routes/ws_${base}.js";
|
|
14052
|
+
*
|
|
14053
|
+
* The server invokes the handler as (connection, event, data) for each event:
|
|
14054
|
+
* "open" (connect), "message" (inbound frame), "close" (disconnect).
|
|
14055
|
+
*/
|
|
14056
|
+
export async function ${handlerName}(
|
|
14057
|
+
connection: WebSocketConnection,
|
|
14058
|
+
event: "open" | "message" | "close",
|
|
14059
|
+
data: string,
|
|
14060
|
+
): Promise<void> {
|
|
14061
|
+
if (event === "open") {
|
|
14062
|
+
// tina4:edit customize the welcome frame (or drop it)
|
|
14063
|
+
connection.sendJson({ type: "welcome" });
|
|
14064
|
+
return;
|
|
14065
|
+
}
|
|
14066
|
+
if (event === "close") {
|
|
14067
|
+
return;
|
|
14068
|
+
}
|
|
14069
|
+
// event === "message"
|
|
14070
|
+
// tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
|
|
14071
|
+
${body}}
|
|
14072
|
+
|
|
14073
|
+
websocket("${wsPath}", ${handlerName});
|
|
14074
|
+
`;
|
|
14075
|
+
writeFileSafe(path8, content);
|
|
14076
|
+
emitWebsocketTest(wsPath, base, handlerName);
|
|
14077
|
+
}
|
|
14078
|
+
function generateListener(name, _flags) {
|
|
14079
|
+
const event = name.trim();
|
|
14080
|
+
const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
14081
|
+
const handlerName = `on${toPascal(slug)}`;
|
|
14082
|
+
const dir = resolve6("src/listeners");
|
|
14083
|
+
ensureDir(dir);
|
|
14084
|
+
const path8 = join15(dir, `${slug}.ts`);
|
|
14085
|
+
const body = aiFill(handlerName, {
|
|
14086
|
+
intent: `react to the '${event}' event`,
|
|
14087
|
+
given: `args -> whatever Events.emit("${event}", ...args) passed`,
|
|
14088
|
+
use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
|
|
14089
|
+
ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
14090
|
+
raise: `listener ${event} not implemented`
|
|
14091
|
+
});
|
|
14092
|
+
const content = `import { Events } from "tina4-nodejs";
|
|
14093
|
+
|
|
14094
|
+
/**
|
|
14095
|
+
* Listener for the '${event}' event.
|
|
14096
|
+
*
|
|
14097
|
+
* Registered on import by Events.on(). Node has NO src/listeners/
|
|
14098
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it:
|
|
14099
|
+
*
|
|
14100
|
+
* import "./src/listeners/${slug}.js";
|
|
14101
|
+
*
|
|
14102
|
+
* Fires when something calls Events.emit("${event}", ...args).
|
|
14103
|
+
*/
|
|
14104
|
+
export function ${handlerName}(...args: unknown[]): void {
|
|
14105
|
+
// tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
|
|
14106
|
+
${body}}
|
|
14107
|
+
|
|
14108
|
+
Events.on("${event}", ${handlerName});
|
|
14109
|
+
`;
|
|
14110
|
+
writeFileSafe(path8, content);
|
|
14111
|
+
emitListenerTest(event, slug);
|
|
14112
|
+
}
|
|
14113
|
+
function writeTest(testName, content) {
|
|
14114
|
+
const dir = resolve6("tests");
|
|
14115
|
+
ensureDir(dir);
|
|
14116
|
+
writeFileSafe(join15(dir, `${testName}.test.ts`), content);
|
|
14117
|
+
}
|
|
14118
|
+
function standaloneTest(doc, body) {
|
|
14119
|
+
return `${doc}
|
|
14120
|
+
|
|
14121
|
+
let pass = 0;
|
|
14122
|
+
let fail = 0;
|
|
14123
|
+
function assert(label: string, ok: boolean): void {
|
|
14124
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
14125
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
14126
|
+
}
|
|
14127
|
+
async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
|
|
14128
|
+
try { await fn(); assert(label, false); }
|
|
14129
|
+
catch { assert(label, true); }
|
|
14130
|
+
}
|
|
14131
|
+
|
|
14132
|
+
${body}
|
|
14133
|
+
|
|
14134
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
14135
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
14136
|
+
`;
|
|
14137
|
+
}
|
|
14138
|
+
function sampleLiteral(fieldType) {
|
|
14139
|
+
switch ((fieldType || "string").toLowerCase()) {
|
|
14140
|
+
case "int":
|
|
14141
|
+
case "integer":
|
|
14142
|
+
return "1";
|
|
14143
|
+
case "float":
|
|
14144
|
+
case "number":
|
|
14145
|
+
case "numeric":
|
|
14146
|
+
case "decimal":
|
|
14147
|
+
return "1.5";
|
|
14148
|
+
case "bool":
|
|
14149
|
+
case "boolean":
|
|
14150
|
+
return "true";
|
|
14151
|
+
case "datetime":
|
|
14152
|
+
return '"2020-01-01 00:00:00"';
|
|
14153
|
+
case "blob":
|
|
14154
|
+
return '"x"';
|
|
14155
|
+
default:
|
|
14156
|
+
return '"sample"';
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
function emitModelTest(model, table2, fields) {
|
|
14160
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
14161
|
+
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
14162
|
+
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
14163
|
+
const valueAssert = stringField ? `
|
|
14164
|
+
assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
|
|
14165
|
+
const doc = `/**
|
|
14166
|
+
* Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
|
|
14167
|
+
*
|
|
14168
|
+
* Generated with src/models/${model}.ts by \`tina4nodejs generate model
|
|
14169
|
+
* ${model}\`. The model scaffold is working code, so this passes on generation:
|
|
14170
|
+
* binds a real in-memory SQLite DB, creates the table, saves a row, reads it
|
|
14171
|
+
* back. Run with: npx tsx tests/${table2}_model.test.ts
|
|
14172
|
+
*/
|
|
14173
|
+
import ${model} from "../src/models/${model}.js";
|
|
14174
|
+
import { initDatabase } from "tina4-nodejs/orm";`;
|
|
14175
|
+
const body = `await initDatabase({ url: "sqlite:///:memory:" });
|
|
14176
|
+
await ${model}.createTable();
|
|
14177
|
+
|
|
14178
|
+
const row = new ${model}({ ${payload} });
|
|
14179
|
+
const saved = await row.save();
|
|
14180
|
+
assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
|
|
14181
|
+
|
|
14182
|
+
const id = row.toObject().id;
|
|
14183
|
+
const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
14184
|
+
assert("row reads back by id", fetched !== null);
|
|
14185
|
+
assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
|
|
14186
|
+
|
|
14187
|
+
const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
|
|
14188
|
+
assert("find missing returns null", missing === null);`;
|
|
14189
|
+
writeTest(`${table2}_model`, standaloneTest(doc, body));
|
|
14190
|
+
}
|
|
14191
|
+
function emitRouteStubTest(route) {
|
|
14192
|
+
const doc = `/**
|
|
14193
|
+
* Routing test for ${route} \u2014 no mocks, real Router + route discovery.
|
|
14194
|
+
*
|
|
14195
|
+
* Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
|
|
14196
|
+
* ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
|
|
14197
|
+
* implement them, so this tests what IS live on generation: all five routes
|
|
14198
|
+
* register on the REAL Router, and the list handler fails loud until filled.
|
|
14199
|
+
* Run with: npx tsx tests/${route}.test.ts
|
|
14200
|
+
*/
|
|
14201
|
+
import { dirname, resolve } from "node:path";
|
|
14202
|
+
import { fileURLToPath } from "node:url";
|
|
14203
|
+
import { Router, discoverRoutes } from "tina4-nodejs";
|
|
14204
|
+
import listHandler from "../src/routes/api/${route}/get.js";`;
|
|
14205
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
14206
|
+
const router = new Router();
|
|
14207
|
+
const defs = await discoverRoutes(resolve(here, "../src/routes"));
|
|
14208
|
+
for (const def of defs) router.addRoute(def);
|
|
14209
|
+
|
|
14210
|
+
const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
|
|
14211
|
+
for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
|
|
14212
|
+
assert(\`route registered: \${sig}\`, sigs.includes(sig));
|
|
14213
|
+
}
|
|
14214
|
+
|
|
14215
|
+
// The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
|
|
14216
|
+
await assertThrows("list handler is a live stub (throws until filled)",
|
|
14217
|
+
() => listHandler({} as never, {} as never));`;
|
|
14218
|
+
writeTest(route, standaloneTest(doc, body));
|
|
14219
|
+
}
|
|
14220
|
+
function emitMiddlewareTest(name, snake) {
|
|
14221
|
+
const doc = `/**
|
|
14222
|
+
* Real dispatch test for the ${name} middleware \u2014 no mocks.
|
|
14223
|
+
*
|
|
14224
|
+
* Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
|
|
14225
|
+
* ${name}\`. Drives the scaffolded before/after functions through the REAL
|
|
14226
|
+
* MiddlewareChain with a real Tina4Request/Response (built from real node http
|
|
14227
|
+
* objects) \u2014 the same continuation dispatch the live server runs.
|
|
14228
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14229
|
+
*/
|
|
14230
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
14231
|
+
import { Socket } from "node:net";
|
|
14232
|
+
import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
|
|
14233
|
+
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
14234
|
+
import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
|
|
14235
|
+
const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
|
|
14236
|
+
const socket = new Socket();
|
|
14237
|
+
const rawReq = new IncomingMessage(socket);
|
|
14238
|
+
rawReq.method = "GET";
|
|
14239
|
+
rawReq.url = "/";
|
|
14240
|
+
rawReq.headers = { ...headers, host: "localhost" };
|
|
14241
|
+
rawReq.push(null);
|
|
14242
|
+
const rawRes = new ServerResponse(rawReq);
|
|
14243
|
+
rawRes.write = (() => true) as typeof rawRes.write;
|
|
14244
|
+
rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
|
|
14245
|
+
return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
|
|
14246
|
+
}
|
|
14247
|
+
|
|
14248
|
+
// before(): blocks an unauthenticated request (401, does not call next()).
|
|
14249
|
+
{
|
|
14250
|
+
const chain = new MiddlewareChain();
|
|
14251
|
+
chain.use(before${name});
|
|
14252
|
+
let reached = false;
|
|
14253
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14254
|
+
const { req, res, raw } = realPair({});
|
|
14255
|
+
await chain.run(req, res);
|
|
14256
|
+
assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
|
|
14257
|
+
}
|
|
14258
|
+
|
|
14259
|
+
// before(): lets an authenticated request through to the next middleware.
|
|
14260
|
+
{
|
|
14261
|
+
const chain = new MiddlewareChain();
|
|
14262
|
+
chain.use(before${name});
|
|
14263
|
+
let reached = false;
|
|
14264
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14265
|
+
const { req, res } = realPair({ authorization: "Bearer test" });
|
|
14266
|
+
await chain.run(req, res);
|
|
14267
|
+
assert("before passes an authenticated request through", reached === true);
|
|
14268
|
+
}
|
|
14269
|
+
|
|
14270
|
+
// after(): always continues the chain.
|
|
14271
|
+
{
|
|
14272
|
+
const chain = new MiddlewareChain();
|
|
14273
|
+
chain.use(after${name});
|
|
14274
|
+
let reached = false;
|
|
14275
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
14276
|
+
const { req, res } = realPair({});
|
|
14277
|
+
await chain.run(req, res);
|
|
14278
|
+
assert("after runs and continues the chain", reached === true);
|
|
14279
|
+
}`;
|
|
14280
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14281
|
+
}
|
|
14282
|
+
function emitServiceTest(name, snake, camel) {
|
|
14283
|
+
const doc = `/**
|
|
14284
|
+
* Real ServiceRunner test for the ${name} service \u2014 no mocks.
|
|
14285
|
+
*
|
|
14286
|
+
* Generated with src/services/${snake}.ts by \`tina4nodejs generate service
|
|
14287
|
+
* ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
|
|
14288
|
+
* descriptor; the task body is an AI-FILL stub that throws until filled.
|
|
14289
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14290
|
+
*/
|
|
14291
|
+
import { ServiceRunner } from "tina4-nodejs";
|
|
14292
|
+
import service, { ${camel}Task } from "../src/services/${snake}.js";`;
|
|
14293
|
+
const body = `assert("descriptor has a name + callable handler",
|
|
14294
|
+
service.name === "${snake}" && typeof service.handler === "function");
|
|
14295
|
+
|
|
14296
|
+
// Register on a REAL ServiceRunner and confirm it is listed.
|
|
14297
|
+
ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
|
|
14298
|
+
assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
|
|
14299
|
+
ServiceRunner.remove("${snake}");
|
|
14300
|
+
|
|
14301
|
+
// The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
|
|
14302
|
+
await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
|
|
14303
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14304
|
+
}
|
|
14305
|
+
function emitQueueTest(topic, slug, pascal) {
|
|
14306
|
+
const doc = `/**
|
|
14307
|
+
* Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
|
|
14308
|
+
*
|
|
14309
|
+
* Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
|
|
14310
|
+
* queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
|
|
14311
|
+
* asserts it is enqueued, and that the consumer is wired as a daemon. The
|
|
14312
|
+
* per-job handle is an AI-FILL stub that throws until filled.
|
|
14313
|
+
* Run with: npx tsx tests/${slug}.test.ts
|
|
14314
|
+
*/
|
|
14315
|
+
import { Queue } from "tina4-nodejs";
|
|
14316
|
+
import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
|
|
14317
|
+
const body = `const jobId = publish${pascal}({ hello: "world" });
|
|
14318
|
+
assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
|
|
14319
|
+
assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
|
|
14320
|
+
|
|
14321
|
+
assert("consumer default export is a daemon", worker.daemon === true);
|
|
14322
|
+
assert("consumer handler is wired", typeof worker.handler === "function");
|
|
14323
|
+
|
|
14324
|
+
// The per-job handler is an AI-FILL stub \u2014 it throws until filled.
|
|
14325
|
+
await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
|
|
14326
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
14327
|
+
}
|
|
14328
|
+
function emitValidatorTest(name, snake, pascal) {
|
|
14329
|
+
const doc = `/**
|
|
14330
|
+
* Real validation test for validate${pascal} \u2014 no mocks.
|
|
14331
|
+
*
|
|
14332
|
+
* Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
|
|
14333
|
+
* ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
|
|
14334
|
+
* on generation \u2014 adjust the rules for your payload and update these cases.
|
|
14335
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
14336
|
+
*/
|
|
14337
|
+
import { validate${pascal} } from "../src/validators/${snake}.js";`;
|
|
14338
|
+
const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
|
|
14339
|
+
|
|
14340
|
+
const bad = validate${pascal}({});
|
|
14341
|
+
assert("invalid input fails", bad.isValid() === false);
|
|
14342
|
+
assert("invalid input reports errors", bad.errors().length > 0);`;
|
|
14343
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
14344
|
+
}
|
|
14345
|
+
function emitSeederTest(model, table2) {
|
|
14346
|
+
const doc = `/**
|
|
14347
|
+
* Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
|
|
14348
|
+
*
|
|
14349
|
+
* Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
|
|
14350
|
+
* ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
|
|
14351
|
+
* seeder (auto-fills every field via FakeData) and asserts rows were created.
|
|
14352
|
+
* Run with: npx tsx tests/${table2}_seeder.test.ts
|
|
14353
|
+
*/
|
|
14354
|
+
import { initDatabase, FakeData } from "tina4-nodejs/orm";
|
|
14355
|
+
import ${model} from "../src/models/${model}.js";
|
|
14356
|
+
import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
|
|
14357
|
+
const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
|
|
14358
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL });
|
|
14359
|
+
await ${model}.createTable();
|
|
14360
|
+
|
|
14361
|
+
assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
|
|
14362
|
+
|
|
14363
|
+
await run(); // run() re-binds the same DB URL and seeds via seedOrm
|
|
14364
|
+
const rows = await ${model}.all();
|
|
14365
|
+
assert("run() seeds real rows", rows.length >= 1);`;
|
|
14366
|
+
writeTest(`${table2}_seeder`, standaloneTest(doc, body));
|
|
14367
|
+
}
|
|
14368
|
+
function emitWebsocketTest(wsPath, base, handler) {
|
|
14369
|
+
const doc = `/**
|
|
14370
|
+
* Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
|
|
14371
|
+
*
|
|
14372
|
+
* Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
|
|
14373
|
+
* ...\`. Confirms the handler registers on the REAL Router (importing runs the
|
|
14374
|
+
* module-level websocket() call) and drives the real handler for the "close"
|
|
14375
|
+
* event (no socket needed). The "message" branch is an AI-FILL stub that throws
|
|
14376
|
+
* until filled. Run with: npx tsx tests/ws_${base}.test.ts
|
|
14377
|
+
*/
|
|
14378
|
+
import { Router } from "tina4-nodejs";
|
|
14379
|
+
import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
|
|
14380
|
+
const body = `assert("handler registered on the real router",
|
|
14381
|
+
Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
|
|
14382
|
+
|
|
14383
|
+
// The "close" branch returns cleanly without a live connection.
|
|
14384
|
+
const closed = await ${handler}(null as never, "close", "");
|
|
14385
|
+
assert("close event handled cleanly", closed === undefined);
|
|
14386
|
+
|
|
14387
|
+
// The "message" branch is an AI-FILL stub \u2014 it throws until filled.
|
|
14388
|
+
await assertThrows("message branch is a live stub (throws until filled)",
|
|
14389
|
+
() => ${handler}(null as never, "message", "hi"));`;
|
|
14390
|
+
writeTest(`ws_${base}`, standaloneTest(doc, body));
|
|
14391
|
+
}
|
|
14392
|
+
function emitListenerTest(event, slug) {
|
|
14393
|
+
const doc = `/**
|
|
14394
|
+
* Real event-bus test for the '${event}' listener \u2014 no mocks.
|
|
14395
|
+
*
|
|
14396
|
+
* Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
|
|
14397
|
+
* ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
|
|
14398
|
+
* the module-level Events.on) and that emitting the event reaches it. The
|
|
14399
|
+
* reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
|
|
14400
|
+
* ran). Run with: npx tsx tests/${slug}.test.ts
|
|
14401
|
+
*/
|
|
14402
|
+
import { Events } from "tina4-nodejs";
|
|
14403
|
+
import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
|
|
14404
|
+
const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
|
|
14405
|
+
|
|
14406
|
+
// strict emit re-raises the stub error, proving the listener actually ran.
|
|
14407
|
+
await assertThrows("emitting the event reaches the (stub) listener",
|
|
14408
|
+
() => Events.emit("${event}", { strict: true }, { id: 1 }));`;
|
|
14409
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
14410
|
+
}
|
|
14411
|
+
function emitAuthTest() {
|
|
14412
|
+
const doc = `/**
|
|
14413
|
+
* Real auth test \u2014 register / login / me via the real TestClient.
|
|
14414
|
+
*
|
|
14415
|
+
* Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
|
|
14416
|
+
* real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
|
|
14417
|
+
* + login are public; the token from login authenticates GET /api/auth/me.
|
|
14418
|
+
* Run with: npx tsx tests/auth.test.ts
|
|
14419
|
+
*/
|
|
14420
|
+
import { dirname, resolve } from "node:path";
|
|
14421
|
+
import { fileURLToPath } from "node:url";
|
|
14422
|
+
import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
|
|
14423
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
14424
|
+
import User from "../src/models/User.js";
|
|
14425
|
+
|
|
14426
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
14427
|
+
delete process.env.TINA4_API_KEY;
|
|
14428
|
+
const here = dirname(fileURLToPath(import.meta.url));`;
|
|
14429
|
+
const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
|
|
14430
|
+
await User.createTable();
|
|
14431
|
+
for (const existing of await User.all()) await existing.delete(); // start from an empty table
|
|
14432
|
+
|
|
14433
|
+
const router = new Router();
|
|
14434
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
14435
|
+
const client = new TestClient(router);
|
|
14436
|
+
|
|
14437
|
+
const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
14438
|
+
assert("register a new user -> 201", registered.status === 201);
|
|
14439
|
+
|
|
14440
|
+
const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
14441
|
+
assert("duplicate register -> 409", duplicate.status === 409);
|
|
14442
|
+
|
|
14443
|
+
const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
|
|
14444
|
+
assert("login -> 200", login.status === 200);
|
|
14445
|
+
const token = (login.json() as { token?: string }).token;
|
|
14446
|
+
assert("login returns a token", typeof token === "string" && token.length > 0);
|
|
14447
|
+
|
|
14448
|
+
const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
|
|
14449
|
+
assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
|
|
14450
|
+
assert("me returns the authenticated user's email",
|
|
14451
|
+
((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
|
|
14452
|
+
|
|
14453
|
+
const anon = await client.get("/api/auth/me");
|
|
14454
|
+
assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
|
|
14455
|
+
|
|
14456
|
+
const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
|
|
14457
|
+
assert("wrong password -> 401", bad.status === 401);`;
|
|
14458
|
+
writeTest("auth", standaloneTest(doc, body));
|
|
14459
|
+
}
|
|
14460
|
+
function emitMigrationTest(migrationName, table2) {
|
|
14461
|
+
const doc = `/**
|
|
14462
|
+
* Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
|
|
14463
|
+
*
|
|
14464
|
+
* Generated with the migration by \`tina4nodejs generate migration
|
|
14465
|
+
* ${migrationName}\`. Applies the generated UP SQL against a fresh real
|
|
14466
|
+
* in-memory SQLite database and asserts the table exists, then applies the DOWN
|
|
14467
|
+
* SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
|
|
14468
|
+
* Run with: npx tsx tests/${table2}_migration.test.ts
|
|
14469
|
+
*/
|
|
14470
|
+
import { dirname, join, resolve } from "node:path";
|
|
14471
|
+
import { fileURLToPath } from "node:url";
|
|
14472
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
14473
|
+
import { SQLiteAdapter } from "tina4-nodejs/orm";`;
|
|
14474
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
14475
|
+
const migrationsDir = resolve(here, "../migrations");
|
|
14476
|
+
|
|
14477
|
+
const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
|
|
14478
|
+
assert("generated UP migration file exists", Boolean(upFile));
|
|
14479
|
+
const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
|
|
14480
|
+
|
|
14481
|
+
function statements(sql: string): string[] {
|
|
14482
|
+
const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
|
|
14483
|
+
return noComments.split(";").map((s) => s.trim()).filter(Boolean);
|
|
14484
|
+
}
|
|
14485
|
+
|
|
14486
|
+
const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
|
|
14487
|
+
const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
|
|
14488
|
+
const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
|
|
14489
|
+
|
|
14490
|
+
const db = new SQLiteAdapter(":memory:");
|
|
14491
|
+
for (const stmt of statements(upSql)) db.execute(stmt);
|
|
14492
|
+
assert("UP creates the ${table2} table", db.tableExists("${table2}"));
|
|
14493
|
+
|
|
14494
|
+
for (const stmt of statements(downSql)) db.execute(stmt);
|
|
14495
|
+
assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
|
|
14496
|
+
writeTest(`${table2}_migration`, standaloneTest(doc, body));
|
|
14497
|
+
}
|
|
14498
|
+
var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
|
|
14499
|
+
var init_generate = __esm({
|
|
14500
|
+
"../cli/src/commands/generate.ts"() {
|
|
14501
|
+
"use strict";
|
|
14502
|
+
FIELD_TYPE_MAP = {
|
|
14503
|
+
string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14504
|
+
str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14505
|
+
int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14506
|
+
integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14507
|
+
float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14508
|
+
number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14509
|
+
numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14510
|
+
decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
14511
|
+
bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14512
|
+
boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14513
|
+
text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14514
|
+
datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
|
|
14515
|
+
blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
|
|
14516
|
+
};
|
|
14517
|
+
SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
14518
|
+
"order",
|
|
14519
|
+
"group",
|
|
14520
|
+
"user",
|
|
14521
|
+
"table",
|
|
14522
|
+
"select",
|
|
14523
|
+
"from",
|
|
14524
|
+
"where",
|
|
14525
|
+
"index",
|
|
14526
|
+
"key",
|
|
14527
|
+
"values",
|
|
14528
|
+
"column",
|
|
14529
|
+
"constraint",
|
|
14530
|
+
"check",
|
|
14531
|
+
"default",
|
|
14532
|
+
"primary",
|
|
14533
|
+
"foreign",
|
|
14534
|
+
"references",
|
|
14535
|
+
"unique",
|
|
14536
|
+
"join",
|
|
14537
|
+
"union",
|
|
14538
|
+
"having",
|
|
14539
|
+
"limit",
|
|
14540
|
+
"offset",
|
|
14541
|
+
"desc",
|
|
14542
|
+
"asc",
|
|
14543
|
+
"case",
|
|
14544
|
+
"when",
|
|
14545
|
+
"then",
|
|
14546
|
+
"else",
|
|
14547
|
+
"end",
|
|
14548
|
+
"and",
|
|
14549
|
+
"or",
|
|
14550
|
+
"not",
|
|
14551
|
+
"null",
|
|
14552
|
+
"insert",
|
|
14553
|
+
"update",
|
|
14554
|
+
"delete",
|
|
14555
|
+
"create",
|
|
14556
|
+
"drop",
|
|
14557
|
+
"alter",
|
|
14558
|
+
"grant",
|
|
14559
|
+
"revoke",
|
|
14560
|
+
"commit",
|
|
14561
|
+
"rollback",
|
|
14562
|
+
"view",
|
|
14563
|
+
"trigger",
|
|
14564
|
+
"procedure",
|
|
14565
|
+
"function",
|
|
14566
|
+
"database",
|
|
14567
|
+
"schema",
|
|
14568
|
+
"session",
|
|
14569
|
+
"set",
|
|
14570
|
+
"into",
|
|
14571
|
+
"as",
|
|
14572
|
+
"on",
|
|
14573
|
+
"by",
|
|
14574
|
+
"inner",
|
|
14575
|
+
"outer",
|
|
14576
|
+
"left",
|
|
14577
|
+
"right",
|
|
14578
|
+
"full",
|
|
14579
|
+
"natural",
|
|
14580
|
+
"using",
|
|
14581
|
+
"with",
|
|
14582
|
+
"distinct",
|
|
14583
|
+
"between",
|
|
14584
|
+
"exists",
|
|
14585
|
+
"like",
|
|
14586
|
+
"in",
|
|
14587
|
+
"is",
|
|
14588
|
+
"all",
|
|
14589
|
+
"any",
|
|
14590
|
+
"cross",
|
|
14591
|
+
"add",
|
|
14592
|
+
"row",
|
|
14593
|
+
"rows",
|
|
14594
|
+
"range",
|
|
14595
|
+
"current",
|
|
14596
|
+
"to"
|
|
14597
|
+
]);
|
|
14598
|
+
RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
|
|
14599
|
+
__resolution = {
|
|
14600
|
+
target: "",
|
|
14601
|
+
input: { name: "", fields: null },
|
|
14602
|
+
body: { transformations: [] },
|
|
14603
|
+
actionsTaken: [],
|
|
14604
|
+
dryRun: false,
|
|
14605
|
+
jsonMode: false
|
|
14606
|
+
};
|
|
14607
|
+
TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
14608
|
+
DEFAULT_FIELDS = [["name", "string"]];
|
|
14609
|
+
GENERATORS = {
|
|
14610
|
+
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
14611
|
+
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
14612
|
+
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
14613
|
+
migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
14614
|
+
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
14615
|
+
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
14616
|
+
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
14617
|
+
view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
|
|
14618
|
+
auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
|
|
14619
|
+
service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
14620
|
+
queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
|
|
14621
|
+
validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
14622
|
+
seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
|
|
14623
|
+
websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
|
|
14624
|
+
listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
|
|
14625
|
+
};
|
|
14626
|
+
GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
|
|
14627
|
+
NEXT_STEPS = {
|
|
14628
|
+
model: ({ name, table: table2 }) => [
|
|
14629
|
+
`Edit src/models/${name}.ts to add fields beyond the default 'name'`,
|
|
14630
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14631
|
+
`Run its test: npx tsx tests/${table2}_model.test.ts`,
|
|
14632
|
+
`Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
|
|
14633
|
+
],
|
|
14634
|
+
route: ({ name, table: table2 }) => [
|
|
14635
|
+
`Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
|
|
14636
|
+
`Run its test: npx tsx tests/${table2}.test.ts`,
|
|
14637
|
+
`Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
|
|
14638
|
+
],
|
|
14639
|
+
crud: ({ name, table: table2 }) => [
|
|
14640
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14641
|
+
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
14642
|
+
`Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
|
|
14643
|
+
`Change fields: edit src/models/${name}.ts then re-run generate crud`
|
|
14644
|
+
],
|
|
14645
|
+
migration: () => [
|
|
14646
|
+
`Apply pending migrations: npx tina4nodejs migrate`,
|
|
14647
|
+
`Check status: npx tina4nodejs migrate:status`,
|
|
14648
|
+
`Roll back the batch: npx tina4nodejs migrate:rollback`
|
|
14649
|
+
],
|
|
14650
|
+
middleware: ({ name }) => [
|
|
14651
|
+
`Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
|
|
14652
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14653
|
+
],
|
|
14654
|
+
test: ({ name }) => [
|
|
14655
|
+
`Fill the TODOs in tests/${toSnake(name)}.test.ts`,
|
|
14656
|
+
`Run it: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14657
|
+
],
|
|
14658
|
+
form: ({ name, table: table2 }) => [
|
|
14659
|
+
`Render from a route: res.render("forms/${table2}.twig", { item })`,
|
|
14660
|
+
`Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
|
|
14661
|
+
],
|
|
14662
|
+
view: ({ table: table2 }) => [
|
|
14663
|
+
`Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
|
|
14664
|
+
`Customize the templates in src/templates/pages/`
|
|
14665
|
+
],
|
|
14666
|
+
auth: () => [
|
|
14667
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
14668
|
+
`Run the auth test: npx tsx tests/auth.test.ts`,
|
|
14669
|
+
`Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
14670
|
+
`Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
|
|
14671
|
+
],
|
|
14672
|
+
service: ({ name }) => [
|
|
14673
|
+
`Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
|
|
14674
|
+
`Fill the task body in src/services/${toSnake(name)}.ts`,
|
|
14675
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14676
|
+
],
|
|
14677
|
+
queue: ({ name }) => {
|
|
14678
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
14679
|
+
return [
|
|
14680
|
+
`Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
|
|
14681
|
+
`Produce a job: publish${toPascal(name)}({ ... })`,
|
|
14682
|
+
`Run the worker: npx tina4nodejs queue work ${name}`,
|
|
14683
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
14684
|
+
];
|
|
14685
|
+
},
|
|
14686
|
+
validator: ({ name }) => [
|
|
14687
|
+
`Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
|
|
14688
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
14689
|
+
],
|
|
14690
|
+
seeder: ({ name, table: table2 }) => [
|
|
14691
|
+
`Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
|
|
14692
|
+
`Seed the table: npx tina4nodejs seed`,
|
|
14693
|
+
`Run its test: npx tsx tests/${table2}_seeder.test.ts`
|
|
14694
|
+
],
|
|
14695
|
+
websocket: ({ name }) => {
|
|
14696
|
+
const raw = name.trim();
|
|
14697
|
+
const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
14698
|
+
const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
|
|
14699
|
+
return [
|
|
14700
|
+
`Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
|
|
14701
|
+
`Fill the "message" branch in src/routes/ws_${base}.ts`,
|
|
14702
|
+
`Run its test: npx tsx tests/ws_${base}.test.ts`
|
|
14703
|
+
];
|
|
14704
|
+
},
|
|
14705
|
+
listener: ({ name }) => {
|
|
14706
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
14707
|
+
return [
|
|
14708
|
+
`Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
|
|
14709
|
+
`Fill the reaction in src/listeners/${slug}.ts`,
|
|
14710
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
14711
|
+
];
|
|
14712
|
+
}
|
|
14713
|
+
};
|
|
14714
|
+
}
|
|
14715
|
+
});
|
|
14716
|
+
|
|
12685
14717
|
// ../core/src/mcp.ts
|
|
12686
14718
|
var mcp_exports = {};
|
|
12687
14719
|
__export(mcp_exports, {
|
|
@@ -13182,10 +15214,10 @@ function registerDevTools(server) {
|
|
|
13182
15214
|
"swagger_spec",
|
|
13183
15215
|
(_args) => {
|
|
13184
15216
|
try {
|
|
13185
|
-
const { generate:
|
|
15217
|
+
const { generate: generate3 } = reqSibling("swagger");
|
|
13186
15218
|
const { defaultRouter: defaultRouter2 } = req("./router.js");
|
|
13187
15219
|
const routes = defaultRouter2?.getRoutes?.() ?? [];
|
|
13188
|
-
return
|
|
15220
|
+
return generate3?.(routes, []) ?? { info: "Swagger not available" };
|
|
13189
15221
|
} catch (e) {
|
|
13190
15222
|
return { error: e.message };
|
|
13191
15223
|
}
|
|
@@ -13344,20 +15376,43 @@ function registerDevTools(server) {
|
|
|
13344
15376
|
);
|
|
13345
15377
|
server.registerTool(
|
|
13346
15378
|
"migration_create",
|
|
13347
|
-
(args) => {
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
15379
|
+
async (args) => {
|
|
15380
|
+
try {
|
|
15381
|
+
const rawDesc = String(args.description ?? "").trim();
|
|
15382
|
+
if (!rawDesc) return { ok: false, error: "description is required" };
|
|
15383
|
+
const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
15384
|
+
if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
|
|
15385
|
+
const migrationsDir = path3.join(projectRoot3, "migrations");
|
|
15386
|
+
if (fs4.existsSync(migrationsDir)) {
|
|
15387
|
+
const upSuffix = `_${slug}.sql`;
|
|
15388
|
+
const downSuffix = `_${slug}.down.sql`;
|
|
15389
|
+
const existing = fs4.readdirSync(migrationsDir).filter(
|
|
15390
|
+
(f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
|
|
15391
|
+
);
|
|
15392
|
+
if (existing.length > 0) {
|
|
15393
|
+
return {
|
|
15394
|
+
ok: false,
|
|
15395
|
+
error: `A migration with slug "${slug}" already exists`,
|
|
15396
|
+
existing
|
|
15397
|
+
};
|
|
15398
|
+
}
|
|
15399
|
+
}
|
|
15400
|
+
const originalCwd = process.cwd();
|
|
15401
|
+
try {
|
|
15402
|
+
process.chdir(projectRoot3);
|
|
15403
|
+
const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
15404
|
+
const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
|
|
15405
|
+
const migrationPath = envelope.resolution?.migration_path;
|
|
15406
|
+
const created = migrationPath ? path3.basename(migrationPath) : "";
|
|
15407
|
+
return { ok: true, created, resolution: envelope };
|
|
15408
|
+
} finally {
|
|
15409
|
+
process.chdir(originalCwd);
|
|
15410
|
+
}
|
|
15411
|
+
} catch (e) {
|
|
15412
|
+
return { ok: false, error: e.message };
|
|
15413
|
+
}
|
|
13359
15414
|
},
|
|
13360
|
-
"Create a new migration file",
|
|
15415
|
+
"Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
|
|
13361
15416
|
schemaFromParams([{ name: "description", type: "string" }])
|
|
13362
15417
|
);
|
|
13363
15418
|
server.registerTool(
|
|
@@ -14125,14 +16180,14 @@ data: ${channel.buffer.shift()}
|
|
|
14125
16180
|
`;
|
|
14126
16181
|
continue;
|
|
14127
16182
|
}
|
|
14128
|
-
const gotMessage = await new Promise((
|
|
16183
|
+
const gotMessage = await new Promise((resolve21) => {
|
|
14129
16184
|
const timer = setTimeout(() => {
|
|
14130
16185
|
channel.wake = null;
|
|
14131
|
-
|
|
16186
|
+
resolve21(false);
|
|
14132
16187
|
}, keepaliveMs);
|
|
14133
16188
|
channel.wake = () => {
|
|
14134
16189
|
clearTimeout(timer);
|
|
14135
|
-
|
|
16190
|
+
resolve21(true);
|
|
14136
16191
|
};
|
|
14137
16192
|
});
|
|
14138
16193
|
if (!gotMessage) yield `: keep-alive
|
|
@@ -15451,8 +17506,8 @@ __export(context_exports, {
|
|
|
15451
17506
|
fts5Supported: () => fts5Supported
|
|
15452
17507
|
});
|
|
15453
17508
|
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
|
|
17509
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
|
|
17510
|
+
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
17511
|
function fts5Supported() {
|
|
15457
17512
|
try {
|
|
15458
17513
|
const conn = new DatabaseSync2(":memory:");
|
|
@@ -15475,13 +17530,13 @@ function realResolve(abs) {
|
|
|
15475
17530
|
} catch {
|
|
15476
17531
|
}
|
|
15477
17532
|
try {
|
|
15478
|
-
return
|
|
17533
|
+
return join17(realpathSync5(dirname8(abs)), basename4(abs));
|
|
15479
17534
|
} catch {
|
|
15480
17535
|
return abs;
|
|
15481
17536
|
}
|
|
15482
17537
|
}
|
|
15483
17538
|
function dbKey(db) {
|
|
15484
|
-
return
|
|
17539
|
+
return resolve8(db ? String(db) : join17(process.cwd(), ".tina4", "context.db"));
|
|
15485
17540
|
}
|
|
15486
17541
|
function defaultContext(root, db) {
|
|
15487
17542
|
const key = dbKey(db);
|
|
@@ -15565,7 +17620,7 @@ var init_context = __esm({
|
|
|
15565
17620
|
if (!this.available) return;
|
|
15566
17621
|
const parent = dirname8(this.path);
|
|
15567
17622
|
if (parent !== "" && parent !== ".") {
|
|
15568
|
-
|
|
17623
|
+
mkdirSync10(parent, { recursive: true });
|
|
15569
17624
|
}
|
|
15570
17625
|
this.conn = new DatabaseSync2(this.path);
|
|
15571
17626
|
this.ensureTable();
|
|
@@ -15637,7 +17692,7 @@ var init_context = __esm({
|
|
|
15637
17692
|
*/
|
|
15638
17693
|
indexRoot(root) {
|
|
15639
17694
|
if (!this.available) return 0;
|
|
15640
|
-
const rootAbs = realResolve(
|
|
17695
|
+
const rootAbs = realResolve(resolve8(String(root)));
|
|
15641
17696
|
this.root = rootAbs;
|
|
15642
17697
|
let total = 0;
|
|
15643
17698
|
const walk2 = (dir) => {
|
|
@@ -15654,11 +17709,11 @@ var init_context = __esm({
|
|
|
15654
17709
|
files.sort();
|
|
15655
17710
|
for (const fn of files) {
|
|
15656
17711
|
if (!_Context.eligible(fn)) continue;
|
|
15657
|
-
const full =
|
|
15658
|
-
const rel =
|
|
17712
|
+
const full = join17(dir, fn);
|
|
17713
|
+
const rel = relative4(rootAbs, full);
|
|
15659
17714
|
total += this.indexPath(full, rel);
|
|
15660
17715
|
}
|
|
15661
|
-
for (const d of subdirs) walk2(
|
|
17716
|
+
for (const d of subdirs) walk2(join17(dir, d));
|
|
15662
17717
|
};
|
|
15663
17718
|
walk2(rootAbs);
|
|
15664
17719
|
return total;
|
|
@@ -15674,9 +17729,9 @@ var init_context = __esm({
|
|
|
15674
17729
|
reindexFile(changedPath) {
|
|
15675
17730
|
if (!this.available || this.root === null) return -1;
|
|
15676
17731
|
const raw = String(changedPath);
|
|
15677
|
-
const abs = isAbsolute4(raw) ? raw :
|
|
15678
|
-
const resolved = realResolve(
|
|
15679
|
-
const rel =
|
|
17732
|
+
const abs = isAbsolute4(raw) ? raw : join17(process.cwd(), raw);
|
|
17733
|
+
const resolved = realResolve(resolve8(abs));
|
|
17734
|
+
const rel = relative4(this.root, resolved);
|
|
15680
17735
|
if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
15681
17736
|
return -1;
|
|
15682
17737
|
}
|
|
@@ -15687,7 +17742,7 @@ var init_context = __esm({
|
|
|
15687
17742
|
}
|
|
15688
17743
|
if (!_Context.eligible(basename4(rel))) return -1;
|
|
15689
17744
|
const stored = rel;
|
|
15690
|
-
if (!
|
|
17745
|
+
if (!existsSync15(abs)) {
|
|
15691
17746
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
15692
17747
|
return 0;
|
|
15693
17748
|
}
|
|
@@ -16433,7 +18488,7 @@ var init_websocket = __esm({
|
|
|
16433
18488
|
* Start the WebSocket server.
|
|
16434
18489
|
*/
|
|
16435
18490
|
async start() {
|
|
16436
|
-
return new Promise((
|
|
18491
|
+
return new Promise((resolve21, reject) => {
|
|
16437
18492
|
this.server = createServer((req2, res) => {
|
|
16438
18493
|
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
16439
18494
|
res.end("Upgrade Required");
|
|
@@ -16443,7 +18498,7 @@ var init_websocket = __esm({
|
|
|
16443
18498
|
});
|
|
16444
18499
|
this.server.listen(this.port, () => {
|
|
16445
18500
|
this.startIdleReaper();
|
|
16446
|
-
|
|
18501
|
+
resolve21();
|
|
16447
18502
|
});
|
|
16448
18503
|
this.server.on("error", (err) => {
|
|
16449
18504
|
this.emit("error", err);
|
|
@@ -17664,8 +19719,8 @@ var init_job = __esm({
|
|
|
17664
19719
|
});
|
|
17665
19720
|
|
|
17666
19721
|
// ../core/src/queueBackends/liteBackend.ts
|
|
17667
|
-
import { mkdirSync as
|
|
17668
|
-
import { join as
|
|
19722
|
+
import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
|
|
19723
|
+
import { join as join18 } from "node:path";
|
|
17669
19724
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17670
19725
|
var LiteBackend;
|
|
17671
19726
|
var init_liteBackend = __esm({
|
|
@@ -17689,22 +19744,22 @@ var init_liteBackend = __esm({
|
|
|
17689
19744
|
this.visibilityTimeout = visibilityTimeout;
|
|
17690
19745
|
}
|
|
17691
19746
|
ensureDir(queue) {
|
|
17692
|
-
const dir =
|
|
17693
|
-
|
|
19747
|
+
const dir = join18(this.basePath, queue);
|
|
19748
|
+
mkdirSync11(dir, { recursive: true });
|
|
17694
19749
|
return dir;
|
|
17695
19750
|
}
|
|
17696
19751
|
ensureFailedDir(queue) {
|
|
17697
|
-
const dir =
|
|
17698
|
-
|
|
19752
|
+
const dir = join18(this.basePath, queue, "failed");
|
|
19753
|
+
mkdirSync11(dir, { recursive: true });
|
|
17699
19754
|
return dir;
|
|
17700
19755
|
}
|
|
17701
19756
|
ensureReservedDir(queue) {
|
|
17702
|
-
const dir =
|
|
17703
|
-
|
|
19757
|
+
const dir = join18(this.basePath, queue, "reserved");
|
|
19758
|
+
mkdirSync11(dir, { recursive: true });
|
|
17704
19759
|
return dir;
|
|
17705
19760
|
}
|
|
17706
19761
|
reservedPath(queue, jobId) {
|
|
17707
|
-
return
|
|
19762
|
+
return join18(this.ensureReservedDir(queue), `${jobId}.queue-data`);
|
|
17708
19763
|
}
|
|
17709
19764
|
nowIso() {
|
|
17710
19765
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -17742,7 +19797,7 @@ var init_liteBackend = __esm({
|
|
|
17742
19797
|
error: void 0
|
|
17743
19798
|
};
|
|
17744
19799
|
const prefix = this.nextPrefix();
|
|
17745
|
-
|
|
19800
|
+
writeFileSync9(join18(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
|
|
17746
19801
|
return id;
|
|
17747
19802
|
}
|
|
17748
19803
|
/**
|
|
@@ -17761,7 +19816,7 @@ var init_liteBackend = __esm({
|
|
|
17761
19816
|
}
|
|
17762
19817
|
const candidates = [];
|
|
17763
19818
|
for (const filename of filenames) {
|
|
17764
|
-
const filePath =
|
|
19819
|
+
const filePath = join18(dir, filename);
|
|
17765
19820
|
let job;
|
|
17766
19821
|
try {
|
|
17767
19822
|
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -17804,7 +19859,7 @@ var init_liteBackend = __esm({
|
|
|
17804
19859
|
createdAt: job.createdAt ?? now,
|
|
17805
19860
|
topic: job.topic ?? queue
|
|
17806
19861
|
};
|
|
17807
|
-
|
|
19862
|
+
writeFileSync9(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
|
|
17808
19863
|
}
|
|
17809
19864
|
/**
|
|
17810
19865
|
* Return expired reservations to the queue (at-least-once delivery).
|
|
@@ -17825,7 +19880,7 @@ var init_liteBackend = __esm({
|
|
|
17825
19880
|
return;
|
|
17826
19881
|
}
|
|
17827
19882
|
for (const filename of filenames) {
|
|
17828
|
-
const filePath =
|
|
19883
|
+
const filePath = join18(reservedDir, filename);
|
|
17829
19884
|
let record;
|
|
17830
19885
|
try {
|
|
17831
19886
|
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -17863,7 +19918,7 @@ var init_liteBackend = __esm({
|
|
|
17863
19918
|
this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
|
|
17864
19919
|
const now = this.nowIso();
|
|
17865
19920
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
17866
|
-
const filePath =
|
|
19921
|
+
const filePath = join18(dir, filename);
|
|
17867
19922
|
job.topic = queue;
|
|
17868
19923
|
job.priority = job.priority ?? 0;
|
|
17869
19924
|
this.writeReserved(queue, job);
|
|
@@ -17888,7 +19943,7 @@ var init_liteBackend = __esm({
|
|
|
17888
19943
|
const results = [];
|
|
17889
19944
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
17890
19945
|
if (results.length >= count) break;
|
|
17891
|
-
const filePath =
|
|
19946
|
+
const filePath = join18(dir, filename);
|
|
17892
19947
|
job.topic = queue;
|
|
17893
19948
|
job.priority = job.priority ?? 0;
|
|
17894
19949
|
this.writeReserved(queue, job);
|
|
@@ -17941,7 +19996,7 @@ var init_liteBackend = __esm({
|
|
|
17941
19996
|
let count = 0;
|
|
17942
19997
|
for (const file of files) {
|
|
17943
19998
|
try {
|
|
17944
|
-
const job = JSON.parse(readFileSync14(
|
|
19999
|
+
const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
|
|
17945
20000
|
if (job.status === status2) count++;
|
|
17946
20001
|
} catch {
|
|
17947
20002
|
}
|
|
@@ -17954,28 +20009,28 @@ var init_liteBackend = __esm({
|
|
|
17954
20009
|
try {
|
|
17955
20010
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
17956
20011
|
for (const file of files) {
|
|
17957
|
-
unlinkSync6(
|
|
20012
|
+
unlinkSync6(join18(dir, file));
|
|
17958
20013
|
count++;
|
|
17959
20014
|
}
|
|
17960
20015
|
} catch {
|
|
17961
20016
|
}
|
|
17962
|
-
const failedDir =
|
|
20017
|
+
const failedDir = join18(dir, "failed");
|
|
17963
20018
|
try {
|
|
17964
|
-
if (
|
|
20019
|
+
if (existsSync16(failedDir)) {
|
|
17965
20020
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
17966
20021
|
for (const file of files) {
|
|
17967
|
-
unlinkSync6(
|
|
20022
|
+
unlinkSync6(join18(failedDir, file));
|
|
17968
20023
|
count++;
|
|
17969
20024
|
}
|
|
17970
20025
|
}
|
|
17971
20026
|
} catch {
|
|
17972
20027
|
}
|
|
17973
|
-
const reservedDir =
|
|
20028
|
+
const reservedDir = join18(dir, "reserved");
|
|
17974
20029
|
try {
|
|
17975
|
-
if (
|
|
20030
|
+
if (existsSync16(reservedDir)) {
|
|
17976
20031
|
const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
|
|
17977
20032
|
for (const file of files) {
|
|
17978
|
-
unlinkSync6(
|
|
20033
|
+
unlinkSync6(join18(reservedDir, file));
|
|
17979
20034
|
count++;
|
|
17980
20035
|
}
|
|
17981
20036
|
}
|
|
@@ -17998,7 +20053,7 @@ var init_liteBackend = __esm({
|
|
|
17998
20053
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
17999
20054
|
for (const file of files) {
|
|
18000
20055
|
try {
|
|
18001
|
-
const job = JSON.parse(readFileSync14(
|
|
20056
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
18002
20057
|
const attempts = job.attempts || 0;
|
|
18003
20058
|
if (attempts > 0 && attempts < maxRetries) {
|
|
18004
20059
|
results.push(job);
|
|
@@ -18021,9 +20076,9 @@ var init_liteBackend = __esm({
|
|
|
18021
20076
|
try {
|
|
18022
20077
|
const queues = readdirSync8(this.basePath);
|
|
18023
20078
|
for (const q of queues) {
|
|
18024
|
-
const failedDir =
|
|
18025
|
-
const filePath =
|
|
18026
|
-
if (
|
|
20079
|
+
const failedDir = join18(this.basePath, q, "failed");
|
|
20080
|
+
const filePath = join18(failedDir, `${jobId}.queue-data`);
|
|
20081
|
+
if (existsSync16(filePath)) {
|
|
18027
20082
|
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18028
20083
|
job.status = "pending";
|
|
18029
20084
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -18031,8 +20086,8 @@ var init_liteBackend = __esm({
|
|
|
18031
20086
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18032
20087
|
job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
|
|
18033
20088
|
const prefix = this.nextPrefix();
|
|
18034
|
-
const queueDir =
|
|
18035
|
-
|
|
20089
|
+
const queueDir = join18(this.basePath, q);
|
|
20090
|
+
writeFileSync9(join18(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
|
|
18036
20091
|
unlinkSync6(filePath);
|
|
18037
20092
|
return true;
|
|
18038
20093
|
}
|
|
@@ -18048,7 +20103,7 @@ var init_liteBackend = __esm({
|
|
|
18048
20103
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
18049
20104
|
for (const file of files) {
|
|
18050
20105
|
try {
|
|
18051
|
-
const job = JSON.parse(readFileSync14(
|
|
20106
|
+
const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
|
|
18052
20107
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18053
20108
|
job.status = "dead";
|
|
18054
20109
|
results.push(job);
|
|
@@ -18069,7 +20124,7 @@ var init_liteBackend = __esm({
|
|
|
18069
20124
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
18070
20125
|
for (const file of files) {
|
|
18071
20126
|
try {
|
|
18072
|
-
unlinkSync6(
|
|
20127
|
+
unlinkSync6(join18(failedDir, file));
|
|
18073
20128
|
count++;
|
|
18074
20129
|
} catch {
|
|
18075
20130
|
}
|
|
@@ -18082,9 +20137,9 @@ var init_liteBackend = __esm({
|
|
|
18082
20137
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
18083
20138
|
for (const file of files) {
|
|
18084
20139
|
try {
|
|
18085
|
-
const job = JSON.parse(readFileSync14(
|
|
20140
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
18086
20141
|
if (job.status === status2) {
|
|
18087
|
-
unlinkSync6(
|
|
20142
|
+
unlinkSync6(join18(dir, file));
|
|
18088
20143
|
count++;
|
|
18089
20144
|
}
|
|
18090
20145
|
} catch {
|
|
@@ -18108,7 +20163,7 @@ var init_liteBackend = __esm({
|
|
|
18108
20163
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
18109
20164
|
for (const file of files) {
|
|
18110
20165
|
try {
|
|
18111
|
-
const filePath =
|
|
20166
|
+
const filePath = join18(failedDir, file);
|
|
18112
20167
|
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18113
20168
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18114
20169
|
continue;
|
|
@@ -18118,7 +20173,7 @@ var init_liteBackend = __esm({
|
|
|
18118
20173
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18119
20174
|
job.delayUntil = null;
|
|
18120
20175
|
const prefix = this.nextPrefix();
|
|
18121
|
-
|
|
20176
|
+
writeFileSync9(join18(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
18122
20177
|
unlinkSync6(filePath);
|
|
18123
20178
|
count++;
|
|
18124
20179
|
} catch {
|
|
@@ -18138,7 +20193,7 @@ var init_liteBackend = __esm({
|
|
|
18138
20193
|
}
|
|
18139
20194
|
for (const file of files) {
|
|
18140
20195
|
if (!file.includes(id)) continue;
|
|
18141
|
-
const filePath =
|
|
20196
|
+
const filePath = join18(dir, file);
|
|
18142
20197
|
let job;
|
|
18143
20198
|
try {
|
|
18144
20199
|
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
@@ -18186,7 +20241,7 @@ var init_liteBackend = __esm({
|
|
|
18186
20241
|
error
|
|
18187
20242
|
};
|
|
18188
20243
|
const prefix = this.nextPrefix();
|
|
18189
|
-
|
|
20244
|
+
writeFileSync9(join18(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
18190
20245
|
}
|
|
18191
20246
|
/**
|
|
18192
20247
|
* Move the job to the dead-letter (failed/) directory. Terminal until a
|
|
@@ -18206,7 +20261,7 @@ var init_liteBackend = __esm({
|
|
|
18206
20261
|
error,
|
|
18207
20262
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18208
20263
|
};
|
|
18209
|
-
|
|
20264
|
+
writeFileSync9(join18(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
18210
20265
|
}
|
|
18211
20266
|
/**
|
|
18212
20267
|
* Record a failed attempt.
|
|
@@ -18242,7 +20297,7 @@ var init_liteBackend = __esm({
|
|
|
18242
20297
|
retryJob(queue, job, delaySeconds) {
|
|
18243
20298
|
this.clearReservation(queue, job.id);
|
|
18244
20299
|
try {
|
|
18245
|
-
unlinkSync6(
|
|
20300
|
+
unlinkSync6(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
18246
20301
|
} catch {
|
|
18247
20302
|
}
|
|
18248
20303
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -18661,7 +20716,7 @@ var init_queue = __esm({
|
|
|
18661
20716
|
const jobs = this.popBatch(resolvedBatchSize);
|
|
18662
20717
|
if (jobs.length === 0) {
|
|
18663
20718
|
if (resolvedPollInterval <= 0) break;
|
|
18664
|
-
await new Promise((
|
|
20719
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
18665
20720
|
continue;
|
|
18666
20721
|
}
|
|
18667
20722
|
yield jobs;
|
|
@@ -18671,7 +20726,7 @@ var init_queue = __esm({
|
|
|
18671
20726
|
const raw = this.pop();
|
|
18672
20727
|
if (raw === null) {
|
|
18673
20728
|
if (resolvedPollInterval <= 0) break;
|
|
18674
|
-
await new Promise((
|
|
20729
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
18675
20730
|
continue;
|
|
18676
20731
|
}
|
|
18677
20732
|
yield createJob(raw, this);
|
|
@@ -20746,8 +22801,8 @@ ${end}
|
|
|
20746
22801
|
|
|
20747
22802
|
// ../core/src/devAdmin.ts
|
|
20748
22803
|
import { cpus as osCpus } from "node:os";
|
|
20749
|
-
import { readFileSync as readFileSync18, writeFileSync as
|
|
20750
|
-
import { join as
|
|
22804
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync14 } from "node:fs";
|
|
22805
|
+
import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative8 } from "node:path";
|
|
20751
22806
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
20752
22807
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
20753
22808
|
function escapeHtml(value) {
|
|
@@ -20861,12 +22916,12 @@ function mapQueueJob(job, topic, status2) {
|
|
|
20861
22916
|
};
|
|
20862
22917
|
}
|
|
20863
22918
|
function readQueueDir(dir, topic, status2) {
|
|
20864
|
-
if (!
|
|
22919
|
+
if (!existsSync20(dir)) return [];
|
|
20865
22920
|
const jobs = [];
|
|
20866
22921
|
for (const filename of readdirSync12(dir).sort()) {
|
|
20867
22922
|
if (!filename.endsWith(".queue-data")) continue;
|
|
20868
22923
|
try {
|
|
20869
|
-
jobs.push(mapQueueJob(JSON.parse(readFileSync18(
|
|
22924
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
|
|
20870
22925
|
} catch {
|
|
20871
22926
|
}
|
|
20872
22927
|
}
|
|
@@ -20983,8 +23038,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
|
|
|
20983
23038
|
function resolveDevEnvVar(key) {
|
|
20984
23039
|
const live = process.env[key];
|
|
20985
23040
|
if (live !== void 0 && live !== "") return live;
|
|
20986
|
-
const envPath =
|
|
20987
|
-
if (!
|
|
23041
|
+
const envPath = join22(process.cwd(), ".env");
|
|
23042
|
+
if (!existsSync20(envPath)) return "";
|
|
20988
23043
|
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
20989
23044
|
const t = line.trim();
|
|
20990
23045
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
@@ -20994,8 +23049,8 @@ function resolveDevEnvVar(key) {
|
|
|
20994
23049
|
return "";
|
|
20995
23050
|
}
|
|
20996
23051
|
function upsertDevEnvVar(key, value) {
|
|
20997
|
-
const envPath =
|
|
20998
|
-
const lines =
|
|
23052
|
+
const envPath = join22(process.cwd(), ".env");
|
|
23053
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
20999
23054
|
let found = false;
|
|
21000
23055
|
const out = [];
|
|
21001
23056
|
for (const line of lines) {
|
|
@@ -21010,7 +23065,7 @@ function upsertDevEnvVar(key, value) {
|
|
|
21010
23065
|
} else out.push(line);
|
|
21011
23066
|
}
|
|
21012
23067
|
if (!found) out.push(`${key}=${value}`);
|
|
21013
|
-
|
|
23068
|
+
writeFileSync13(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
|
|
21014
23069
|
}
|
|
21015
23070
|
function formatUptime(seconds) {
|
|
21016
23071
|
const d = Math.floor(seconds / 86400);
|
|
@@ -21025,9 +23080,9 @@ function formatUptime(seconds) {
|
|
|
21025
23080
|
return parts.join(" ");
|
|
21026
23081
|
}
|
|
21027
23082
|
function parseEnvFile() {
|
|
21028
|
-
const envPath =
|
|
23083
|
+
const envPath = join22(process.cwd(), ".env");
|
|
21029
23084
|
const result = {};
|
|
21030
|
-
if (!
|
|
23085
|
+
if (!existsSync20(envPath)) return result;
|
|
21031
23086
|
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
21032
23087
|
for (const line of lines) {
|
|
21033
23088
|
const trimmed = line.trim();
|
|
@@ -21039,9 +23094,9 @@ function parseEnvFile() {
|
|
|
21039
23094
|
}
|
|
21040
23095
|
function walkDirRecursive(dir) {
|
|
21041
23096
|
const results = [];
|
|
21042
|
-
if (!
|
|
23097
|
+
if (!existsSync20(dir)) return results;
|
|
21043
23098
|
for (const entry of readdirSync12(dir)) {
|
|
21044
|
-
const full =
|
|
23099
|
+
const full = join22(dir, entry);
|
|
21045
23100
|
if (statSync14(full).isDirectory()) {
|
|
21046
23101
|
results.push(...walkDirRecursive(full));
|
|
21047
23102
|
} else {
|
|
@@ -21058,25 +23113,25 @@ function handleGalleryDeploy(router) {
|
|
|
21058
23113
|
res.json({ error: "No gallery item specified" }, 400);
|
|
21059
23114
|
return;
|
|
21060
23115
|
}
|
|
21061
|
-
const galleryDir =
|
|
21062
|
-
const gallerySrc =
|
|
21063
|
-
if (!
|
|
23116
|
+
const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
|
|
23117
|
+
const gallerySrc = join22(galleryDir, name, "src");
|
|
23118
|
+
if (!existsSync20(gallerySrc)) {
|
|
21064
23119
|
res.json({ error: `Gallery item '${name}' not found` }, 404);
|
|
21065
23120
|
return;
|
|
21066
23121
|
}
|
|
21067
|
-
const projectSrc =
|
|
23122
|
+
const projectSrc = resolve12(process.cwd(), "src");
|
|
21068
23123
|
const copied = [];
|
|
21069
23124
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
21070
23125
|
for (const srcFile of allFiles) {
|
|
21071
|
-
const rel =
|
|
21072
|
-
const dest =
|
|
21073
|
-
|
|
23126
|
+
const rel = relative8(gallerySrc, srcFile);
|
|
23127
|
+
const dest = join22(projectSrc, rel);
|
|
23128
|
+
mkdirSync14(dirname10(dest), { recursive: true });
|
|
21074
23129
|
copyFileSync(srcFile, dest);
|
|
21075
23130
|
copied.push(rel);
|
|
21076
23131
|
}
|
|
21077
23132
|
try {
|
|
21078
|
-
const routesDir =
|
|
21079
|
-
if (
|
|
23133
|
+
const routesDir = resolve12(process.cwd(), "src", "routes");
|
|
23134
|
+
if (existsSync20(routesDir)) {
|
|
21080
23135
|
const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
|
|
21081
23136
|
const routes = await discoverRoutes2(routesDir);
|
|
21082
23137
|
for (const route of routes) {
|
|
@@ -21092,7 +23147,7 @@ function handleGalleryDeploy(router) {
|
|
|
21092
23147
|
};
|
|
21093
23148
|
}
|
|
21094
23149
|
function safeJoin(projectRoot3, rel) {
|
|
21095
|
-
const resolved =
|
|
23150
|
+
const resolved = resolve12(projectRoot3, rel);
|
|
21096
23151
|
if (!resolved.startsWith(projectRoot3)) return null;
|
|
21097
23152
|
return resolved;
|
|
21098
23153
|
}
|
|
@@ -22055,16 +24110,16 @@ var init_devAdmin = __esm({
|
|
|
22055
24110
|
failed: queue.size("failed"),
|
|
22056
24111
|
reserved: queue.size("reserved")
|
|
22057
24112
|
};
|
|
22058
|
-
const topicDir =
|
|
24113
|
+
const topicDir = join22(queueBasePath2(), topic);
|
|
22059
24114
|
const jobs = [];
|
|
22060
24115
|
if (!statusFilter || statusFilter === "pending") {
|
|
22061
24116
|
jobs.push(...readQueueDir(topicDir, topic, "pending"));
|
|
22062
24117
|
}
|
|
22063
24118
|
if (!statusFilter || statusFilter === "reserved") {
|
|
22064
|
-
jobs.push(...readQueueDir(
|
|
24119
|
+
jobs.push(...readQueueDir(join22(topicDir, "reserved"), topic, "reserved"));
|
|
22065
24120
|
}
|
|
22066
24121
|
if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
|
|
22067
|
-
jobs.push(...readQueueDir(
|
|
24122
|
+
jobs.push(...readQueueDir(join22(topicDir, "failed"), topic, "dead_letter"));
|
|
22068
24123
|
}
|
|
22069
24124
|
res.json({ stats, jobs });
|
|
22070
24125
|
} catch (e) {
|
|
@@ -22080,10 +24135,10 @@ var init_devAdmin = __esm({
|
|
|
22080
24135
|
const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
|
|
22081
24136
|
const queueDir = queueBasePath2();
|
|
22082
24137
|
let topics = [];
|
|
22083
|
-
if (
|
|
24138
|
+
if (existsSync20(queueDir)) {
|
|
22084
24139
|
topics = readdirSync12(queueDir).filter((d) => {
|
|
22085
24140
|
try {
|
|
22086
|
-
return statSync14(
|
|
24141
|
+
return statSync14(join22(queueDir, d)).isDirectory();
|
|
22087
24142
|
} catch {
|
|
22088
24143
|
return false;
|
|
22089
24144
|
}
|
|
@@ -22398,7 +24453,7 @@ var init_devAdmin = __esm({
|
|
|
22398
24453
|
const count = parseInt(String(body.count ?? "10"), 10) || 10;
|
|
22399
24454
|
try {
|
|
22400
24455
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
22401
|
-
const dirs = ["src/orm", "src/models"].map((d) =>
|
|
24456
|
+
const dirs = ["src/orm", "src/models"].map((d) => resolve12(process.cwd(), d)).filter((d) => existsSync20(d));
|
|
22402
24457
|
const classes = [];
|
|
22403
24458
|
for (const dir of dirs) {
|
|
22404
24459
|
for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
|
|
@@ -22425,7 +24480,7 @@ var init_devAdmin = __esm({
|
|
|
22425
24480
|
const run = promisify(execFile);
|
|
22426
24481
|
try {
|
|
22427
24482
|
const { stdout, stderr } = await run("npm", ["test"], {
|
|
22428
|
-
cwd:
|
|
24483
|
+
cwd: resolve12(process.cwd()),
|
|
22429
24484
|
timeout: 18e4,
|
|
22430
24485
|
encoding: "utf-8",
|
|
22431
24486
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -22539,8 +24594,8 @@ var init_devAdmin = __esm({
|
|
|
22539
24594
|
return;
|
|
22540
24595
|
}
|
|
22541
24596
|
try {
|
|
22542
|
-
const envPath =
|
|
22543
|
-
const lines =
|
|
24597
|
+
const envPath = join22(process.cwd(), ".env");
|
|
24598
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
22544
24599
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
22545
24600
|
const newLines = [];
|
|
22546
24601
|
for (const line of lines) {
|
|
@@ -22567,7 +24622,7 @@ var init_devAdmin = __esm({
|
|
|
22567
24622
|
for (const [key, found] of Object.entries(keysFound)) {
|
|
22568
24623
|
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
22569
24624
|
}
|
|
22570
|
-
|
|
24625
|
+
writeFileSync13(envPath, newLines.join("\n") + "\n");
|
|
22571
24626
|
res.json({ success: true });
|
|
22572
24627
|
} catch (e) {
|
|
22573
24628
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -22577,26 +24632,26 @@ var init_devAdmin = __esm({
|
|
|
22577
24632
|
__devAdminFilename = fileURLToPath5(import.meta.url);
|
|
22578
24633
|
__devAdminDirname = dirname10(__devAdminFilename);
|
|
22579
24634
|
handleGalleryList = (_req, res) => {
|
|
22580
|
-
const galleryDir =
|
|
24635
|
+
const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
|
|
22581
24636
|
const items = [];
|
|
22582
|
-
if (
|
|
24637
|
+
if (existsSync20(galleryDir)) {
|
|
22583
24638
|
const entries = readdirSync12(galleryDir).sort();
|
|
22584
24639
|
for (const entry of entries) {
|
|
22585
|
-
const entryPath =
|
|
22586
|
-
const metaFile =
|
|
22587
|
-
if (statSync14(entryPath).isDirectory() &&
|
|
24640
|
+
const entryPath = join22(galleryDir, entry);
|
|
24641
|
+
const metaFile = join22(entryPath, "meta.json");
|
|
24642
|
+
if (statSync14(entryPath).isDirectory() && existsSync20(metaFile)) {
|
|
22588
24643
|
try {
|
|
22589
24644
|
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
22590
24645
|
meta.id = entry;
|
|
22591
|
-
const srcDir =
|
|
22592
|
-
if (
|
|
24646
|
+
const srcDir = join22(entryPath, "src");
|
|
24647
|
+
if (existsSync20(srcDir)) {
|
|
22593
24648
|
const allFiles = walkDirRecursive(srcDir);
|
|
22594
|
-
meta.files = allFiles.map((f) =>
|
|
24649
|
+
meta.files = allFiles.map((f) => relative8(srcDir, f));
|
|
22595
24650
|
}
|
|
22596
|
-
const projectSrc =
|
|
22597
|
-
if (
|
|
24651
|
+
const projectSrc = resolve12(process.cwd(), "src");
|
|
24652
|
+
if (existsSync20(srcDir) && meta.files) {
|
|
22598
24653
|
meta.deployed = meta.files.every(
|
|
22599
|
-
(f) =>
|
|
24654
|
+
(f) => existsSync20(join22(projectSrc, f))
|
|
22600
24655
|
);
|
|
22601
24656
|
} else {
|
|
22602
24657
|
meta.deployed = false;
|
|
@@ -22692,10 +24747,10 @@ var init_devAdmin = __esm({
|
|
|
22692
24747
|
handleFiles = async (req2, res) => {
|
|
22693
24748
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
22694
24749
|
const rel = url.searchParams.get("path") ?? ".";
|
|
22695
|
-
const root =
|
|
24750
|
+
const root = resolve12(process.cwd());
|
|
22696
24751
|
const target = safeJoin(root, rel);
|
|
22697
24752
|
const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
|
|
22698
|
-
if (!target || !
|
|
24753
|
+
if (!target || !existsSync20(target) || !statSync14(target).isDirectory()) {
|
|
22699
24754
|
res.json({ path: rel, branch, entries: [], error: "not a directory" });
|
|
22700
24755
|
return;
|
|
22701
24756
|
}
|
|
@@ -22708,8 +24763,8 @@ var init_devAdmin = __esm({
|
|
|
22708
24763
|
const entries = [];
|
|
22709
24764
|
for (const name of readdirSync12(target).sort()) {
|
|
22710
24765
|
if (devFilesHidden(name)) continue;
|
|
22711
|
-
const full =
|
|
22712
|
-
const entryRel =
|
|
24766
|
+
const full = join22(target, name);
|
|
24767
|
+
const entryRel = relative8(root, full).replace(/\\/g, "/");
|
|
22713
24768
|
if (isSecretPath(entryRel)) continue;
|
|
22714
24769
|
let isDir = false;
|
|
22715
24770
|
let size = null;
|
|
@@ -22754,7 +24809,7 @@ var init_devAdmin = __esm({
|
|
|
22754
24809
|
size
|
|
22755
24810
|
});
|
|
22756
24811
|
}
|
|
22757
|
-
res.json({ path:
|
|
24812
|
+
res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
22758
24813
|
};
|
|
22759
24814
|
DEV_ADMIN_LANG_MAP = {
|
|
22760
24815
|
".py": "python",
|
|
@@ -22799,15 +24854,15 @@ var init_devAdmin = __esm({
|
|
|
22799
24854
|
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
22800
24855
|
return;
|
|
22801
24856
|
}
|
|
22802
|
-
const root =
|
|
24857
|
+
const root = resolve12(process.cwd());
|
|
22803
24858
|
const target = safeJoin(root, rel);
|
|
22804
|
-
if (!target || !
|
|
24859
|
+
if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
|
|
22805
24860
|
res.json({ error: `File not found: ${rel}` }, 404);
|
|
22806
24861
|
return;
|
|
22807
24862
|
}
|
|
22808
24863
|
try {
|
|
22809
24864
|
const content = readFileSync18(target, "utf-8");
|
|
22810
|
-
const path8 =
|
|
24865
|
+
const path8 = relative8(root, target);
|
|
22811
24866
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22812
24867
|
} catch (e) {
|
|
22813
24868
|
res.json({ error: e.message }, 500);
|
|
@@ -22817,22 +24872,22 @@ var init_devAdmin = __esm({
|
|
|
22817
24872
|
const body = req2.body || {};
|
|
22818
24873
|
const rel = body.path || "";
|
|
22819
24874
|
const content = body.content ?? "";
|
|
22820
|
-
const root =
|
|
24875
|
+
const root = resolve12(process.cwd());
|
|
22821
24876
|
const target = safeJoin(root, rel);
|
|
22822
24877
|
if (!target) {
|
|
22823
24878
|
res.json({ error: `Path escapes project directory: ${rel}` }, 400);
|
|
22824
24879
|
return;
|
|
22825
24880
|
}
|
|
22826
24881
|
try {
|
|
22827
|
-
|
|
22828
|
-
const existed =
|
|
22829
|
-
|
|
24882
|
+
mkdirSync14(dirname10(target), { recursive: true });
|
|
24883
|
+
const existed = existsSync20(target);
|
|
24884
|
+
writeFileSync13(target, content, "utf-8");
|
|
22830
24885
|
try {
|
|
22831
24886
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
22832
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
24887
|
+
Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
|
|
22833
24888
|
} catch {
|
|
22834
24889
|
}
|
|
22835
|
-
res.json({ ok: true, path:
|
|
24890
|
+
res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22836
24891
|
} catch (e) {
|
|
22837
24892
|
res.json({ error: e.message }, 500);
|
|
22838
24893
|
}
|
|
@@ -22844,9 +24899,9 @@ var init_devAdmin = __esm({
|
|
|
22844
24899
|
res.json({ error: "Refused: secret file" }, 403);
|
|
22845
24900
|
return;
|
|
22846
24901
|
}
|
|
22847
|
-
const root =
|
|
24902
|
+
const root = resolve12(process.cwd());
|
|
22848
24903
|
const target = safeJoin(root, rel);
|
|
22849
|
-
if (!target || !
|
|
24904
|
+
if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
|
|
22850
24905
|
res.raw.writeHead(404);
|
|
22851
24906
|
res.raw.end("Not found");
|
|
22852
24907
|
return;
|
|
@@ -22879,22 +24934,22 @@ var init_devAdmin = __esm({
|
|
|
22879
24934
|
const body = req2.body || {};
|
|
22880
24935
|
const from = body.from || "";
|
|
22881
24936
|
const to = body.to || "";
|
|
22882
|
-
const root =
|
|
24937
|
+
const root = resolve12(process.cwd());
|
|
22883
24938
|
const src = safeJoin(root, from);
|
|
22884
24939
|
const dst = safeJoin(root, to);
|
|
22885
24940
|
if (!src || !dst) {
|
|
22886
24941
|
res.json({ error: "Invalid path" }, 400);
|
|
22887
24942
|
return;
|
|
22888
24943
|
}
|
|
22889
|
-
if (!
|
|
24944
|
+
if (!existsSync20(src)) {
|
|
22890
24945
|
res.json({ error: `Source not found: ${from}` }, 404);
|
|
22891
24946
|
return;
|
|
22892
24947
|
}
|
|
22893
24948
|
try {
|
|
22894
24949
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
22895
|
-
|
|
24950
|
+
mkdirSync14(dirname10(dst), { recursive: true });
|
|
22896
24951
|
renameSync3(src, dst);
|
|
22897
|
-
res.json({ ok: true, from:
|
|
24952
|
+
res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
|
|
22898
24953
|
} catch (e) {
|
|
22899
24954
|
res.json({ error: e.message }, 500);
|
|
22900
24955
|
}
|
|
@@ -22902,20 +24957,20 @@ var init_devAdmin = __esm({
|
|
|
22902
24957
|
handleFileDelete = async (req2, res) => {
|
|
22903
24958
|
const body = req2.body || {};
|
|
22904
24959
|
const rel = body.path || "";
|
|
22905
|
-
const root =
|
|
24960
|
+
const root = resolve12(process.cwd());
|
|
22906
24961
|
const target = safeJoin(root, rel);
|
|
22907
24962
|
if (!target) {
|
|
22908
24963
|
res.json({ error: "Invalid path" }, 400);
|
|
22909
24964
|
return;
|
|
22910
24965
|
}
|
|
22911
|
-
if (!
|
|
24966
|
+
if (!existsSync20(target)) {
|
|
22912
24967
|
res.json({ error: `Not found: ${rel}` }, 404);
|
|
22913
24968
|
return;
|
|
22914
24969
|
}
|
|
22915
24970
|
try {
|
|
22916
24971
|
const { rmSync } = await import("node:fs");
|
|
22917
24972
|
rmSync(target, { recursive: true, force: true });
|
|
22918
|
-
res.json({ ok: true, deleted:
|
|
24973
|
+
res.json({ ok: true, deleted: relative8(root, target) });
|
|
22919
24974
|
} catch (e) {
|
|
22920
24975
|
res.json({ error: e.message }, 500);
|
|
22921
24976
|
}
|
|
@@ -22953,7 +25008,7 @@ var init_devAdmin = __esm({
|
|
|
22953
25008
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
22954
25009
|
const args = ["install", dev ? "--save-dev" : "--save", pkg];
|
|
22955
25010
|
const output = execFileSync7("npm", args, {
|
|
22956
|
-
cwd:
|
|
25011
|
+
cwd: resolve12(process.cwd()),
|
|
22957
25012
|
timeout: 12e4,
|
|
22958
25013
|
encoding: "utf-8"
|
|
22959
25014
|
}).toString();
|
|
@@ -22965,7 +25020,7 @@ var init_devAdmin = __esm({
|
|
|
22965
25020
|
handleGitStatus = async (_req, res) => {
|
|
22966
25021
|
try {
|
|
22967
25022
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
22968
|
-
const cwd =
|
|
25023
|
+
const cwd = resolve12(process.cwd());
|
|
22969
25024
|
try {
|
|
22970
25025
|
execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
|
|
22971
25026
|
} catch {
|
|
@@ -23104,7 +25159,7 @@ var init_devAdmin = __esm({
|
|
|
23104
25159
|
try {
|
|
23105
25160
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
23106
25161
|
const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
|
|
23107
|
-
cwd:
|
|
25162
|
+
cwd: resolve12(process.cwd()),
|
|
23108
25163
|
timeout: 3e4,
|
|
23109
25164
|
encoding: "utf-8"
|
|
23110
25165
|
}).toString();
|
|
@@ -23249,21 +25304,21 @@ var init_devAdmin = __esm({
|
|
|
23249
25304
|
});
|
|
23250
25305
|
};
|
|
23251
25306
|
handleDevAdminJs = async (_req, res) => {
|
|
23252
|
-
const { readFileSync: readFileSync27, existsSync:
|
|
23253
|
-
const { dirname: dirname15, join:
|
|
25307
|
+
const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
|
|
25308
|
+
const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
|
|
23254
25309
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
23255
25310
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
23256
25311
|
const candidates = [
|
|
23257
|
-
|
|
25312
|
+
join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
23258
25313
|
// src/../public/js/
|
|
23259
|
-
|
|
25314
|
+
join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
23260
25315
|
// deeper nesting
|
|
23261
|
-
|
|
23262
|
-
|
|
25316
|
+
resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
|
|
25317
|
+
resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
|
|
23263
25318
|
// project public/
|
|
23264
25319
|
];
|
|
23265
25320
|
for (const jsPath of candidates) {
|
|
23266
|
-
if (
|
|
25321
|
+
if (existsSync28(jsPath)) {
|
|
23267
25322
|
try {
|
|
23268
25323
|
const content = readFileSync27(jsPath, "utf-8");
|
|
23269
25324
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -23288,8 +25343,8 @@ var init_devAdmin = __esm({
|
|
|
23288
25343
|
});
|
|
23289
25344
|
|
|
23290
25345
|
// ../core/src/i18n.ts
|
|
23291
|
-
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as
|
|
23292
|
-
import { join as
|
|
25346
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
|
|
25347
|
+
import { join as join23, resolve as resolve13 } from "node:path";
|
|
23293
25348
|
var I18n;
|
|
23294
25349
|
var init_i18n = __esm({
|
|
23295
25350
|
"../core/src/i18n.ts"() {
|
|
@@ -23309,7 +25364,7 @@ var init_i18n = __esm({
|
|
|
23309
25364
|
* (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
|
|
23310
25365
|
*/
|
|
23311
25366
|
constructor(locale, path8) {
|
|
23312
|
-
this._localeDir =
|
|
25367
|
+
this._localeDir = resolve13(
|
|
23313
25368
|
path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
|
|
23314
25369
|
);
|
|
23315
25370
|
this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
|
|
@@ -23366,7 +25421,7 @@ var init_i18n = __esm({
|
|
|
23366
25421
|
}
|
|
23367
25422
|
/** List available locale codes based on JSON files in the locale directory. */
|
|
23368
25423
|
availableLocales() {
|
|
23369
|
-
if (!
|
|
25424
|
+
if (!existsSync21(this._localeDir)) {
|
|
23370
25425
|
return [this._defaultLocale];
|
|
23371
25426
|
}
|
|
23372
25427
|
try {
|
|
@@ -23382,8 +25437,8 @@ var init_i18n = __esm({
|
|
|
23382
25437
|
if (this._translations.has(locale)) {
|
|
23383
25438
|
return;
|
|
23384
25439
|
}
|
|
23385
|
-
const filePath =
|
|
23386
|
-
if (
|
|
25440
|
+
const filePath = join23(this._localeDir, `${locale}.json`);
|
|
25441
|
+
if (existsSync21(filePath)) {
|
|
23387
25442
|
try {
|
|
23388
25443
|
const raw = readFileSync19(filePath, "utf-8");
|
|
23389
25444
|
const data = JSON.parse(raw);
|
|
@@ -23395,8 +25450,8 @@ var init_i18n = __esm({
|
|
|
23395
25450
|
}
|
|
23396
25451
|
}
|
|
23397
25452
|
for (const ext of [".yml", ".yaml"]) {
|
|
23398
|
-
const yamlPath =
|
|
23399
|
-
if (
|
|
25453
|
+
const yamlPath = join23(this._localeDir, `${locale}${ext}`);
|
|
25454
|
+
if (existsSync21(yamlPath)) {
|
|
23400
25455
|
try {
|
|
23401
25456
|
const raw = readFileSync19(yamlPath, "utf-8");
|
|
23402
25457
|
const data = _I18n._parseSimpleYaml(raw);
|
|
@@ -23741,7 +25796,7 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
23741
25796
|
return clean;
|
|
23742
25797
|
});
|
|
23743
25798
|
}
|
|
23744
|
-
function
|
|
25799
|
+
function generate2(routes, models = []) {
|
|
23745
25800
|
const info = {
|
|
23746
25801
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
23747
25802
|
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
@@ -24189,7 +26244,7 @@ __export(src_exports, {
|
|
|
24189
26244
|
addSchema: () => addSchema,
|
|
24190
26245
|
addSecurityScheme: () => addSecurityScheme,
|
|
24191
26246
|
createSwaggerRoutes: () => createSwaggerRoutes,
|
|
24192
|
-
generate: () =>
|
|
26247
|
+
generate: () => generate2,
|
|
24193
26248
|
resetRegistry: () => resetRegistry,
|
|
24194
26249
|
swaggerEnabled: () => swaggerEnabled
|
|
24195
26250
|
});
|
|
@@ -24543,8 +26598,8 @@ function writeMcpDiscovery(projectRoot3, port) {
|
|
|
24543
26598
|
const lines = contents.split(/\r?\n/);
|
|
24544
26599
|
const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
|
|
24545
26600
|
if (!already) {
|
|
24546
|
-
const
|
|
24547
|
-
fs8.writeFileSync(gitignorePath, `${contents}${
|
|
26601
|
+
const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
|
|
26602
|
+
fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
|
|
24548
26603
|
`, "utf-8");
|
|
24549
26604
|
}
|
|
24550
26605
|
}
|
|
@@ -24563,8 +26618,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
24563
26618
|
// ../core/src/server.ts
|
|
24564
26619
|
import { createServer as createServer2 } from "node:http";
|
|
24565
26620
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
24566
|
-
import { resolve as
|
|
24567
|
-
import { existsSync as
|
|
26621
|
+
import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative9 } from "node:path";
|
|
26622
|
+
import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
24568
26623
|
import { isatty } from "node:tty";
|
|
24569
26624
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
24570
26625
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -24602,8 +26657,8 @@ function swaggerAdvertised() {
|
|
|
24602
26657
|
return TRUTHY2.includes(raw);
|
|
24603
26658
|
}
|
|
24604
26659
|
async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
|
|
24605
|
-
const dir =
|
|
24606
|
-
if (!
|
|
26660
|
+
const dir = resolve15(base, migrationDir);
|
|
26661
|
+
if (!existsSync23(dir)) return;
|
|
24607
26662
|
let hasSql = false;
|
|
24608
26663
|
try {
|
|
24609
26664
|
hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
|
|
@@ -24744,12 +26799,12 @@ async function renderErrorPage(code, data, templatesDir) {
|
|
|
24744
26799
|
}
|
|
24745
26800
|
return instance;
|
|
24746
26801
|
};
|
|
24747
|
-
const userTemplatePath =
|
|
24748
|
-
if (
|
|
26802
|
+
const userTemplatePath = join25(templatesDir, templateFile);
|
|
26803
|
+
if (existsSync23(userTemplatePath)) {
|
|
24749
26804
|
return getCachedFrond(templatesDir).render(templateFile, data);
|
|
24750
26805
|
}
|
|
24751
|
-
const builtinTemplatePath =
|
|
24752
|
-
if (
|
|
26806
|
+
const builtinTemplatePath = join25(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
|
|
26807
|
+
if (existsSync23(builtinTemplatePath)) {
|
|
24753
26808
|
return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
|
|
24754
26809
|
}
|
|
24755
26810
|
return null;
|
|
@@ -24766,29 +26821,29 @@ function injectDevToolbar(html, ctx) {
|
|
|
24766
26821
|
}
|
|
24767
26822
|
function walkGalleryFiles(dir) {
|
|
24768
26823
|
const results = [];
|
|
24769
|
-
if (!
|
|
26824
|
+
if (!existsSync23(dir)) return results;
|
|
24770
26825
|
for (const f of readdirSync14(dir)) {
|
|
24771
|
-
const full =
|
|
26826
|
+
const full = join25(dir, f);
|
|
24772
26827
|
if (statSync15(full).isDirectory()) results.push(...walkGalleryFiles(full));
|
|
24773
26828
|
else results.push(full);
|
|
24774
26829
|
}
|
|
24775
26830
|
return results;
|
|
24776
26831
|
}
|
|
24777
26832
|
function getGalleryDeployedState() {
|
|
24778
|
-
const galleryDir =
|
|
26833
|
+
const galleryDir = resolve15(__dirname, "..", "gallery");
|
|
24779
26834
|
const state = {};
|
|
24780
|
-
if (!
|
|
26835
|
+
if (!existsSync23(galleryDir)) return state;
|
|
24781
26836
|
try {
|
|
24782
26837
|
const entries = readdirSync14(galleryDir).sort();
|
|
24783
26838
|
for (const entry of entries) {
|
|
24784
|
-
const entryPath =
|
|
24785
|
-
const metaFile =
|
|
24786
|
-
if (statSync15(entryPath).isDirectory() &&
|
|
24787
|
-
const srcDir =
|
|
24788
|
-
if (
|
|
26839
|
+
const entryPath = join25(galleryDir, entry);
|
|
26840
|
+
const metaFile = join25(entryPath, "meta.json");
|
|
26841
|
+
if (statSync15(entryPath).isDirectory() && existsSync23(metaFile)) {
|
|
26842
|
+
const srcDir = join25(entryPath, "src");
|
|
26843
|
+
if (existsSync23(srcDir)) {
|
|
24789
26844
|
const files = walkGalleryFiles(srcDir);
|
|
24790
|
-
const projectSrc =
|
|
24791
|
-
state[entry] = files.every((f) =>
|
|
26845
|
+
const projectSrc = resolve15(process.cwd(), "src");
|
|
26846
|
+
state[entry] = files.every((f) => existsSync23(join25(projectSrc, relative9(srcDir, f))));
|
|
24792
26847
|
} else {
|
|
24793
26848
|
state[entry] = false;
|
|
24794
26849
|
}
|
|
@@ -24816,9 +26871,9 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
24816
26871
|
const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
|
|
24817
26872
|
if (isDev2) {
|
|
24818
26873
|
if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
|
|
24819
|
-
const pagesDir =
|
|
26874
|
+
const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
|
|
24820
26875
|
for (const ext of [".twig", ".html"]) {
|
|
24821
|
-
if (
|
|
26876
|
+
if (existsSync23(resolve15(pagesDir, cleanPath + ext))) {
|
|
24822
26877
|
return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
|
|
24823
26878
|
}
|
|
24824
26879
|
}
|
|
@@ -24826,14 +26881,14 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
24826
26881
|
}
|
|
24827
26882
|
if (!templateCache) {
|
|
24828
26883
|
templateCache = /* @__PURE__ */ new Map();
|
|
24829
|
-
const pagesDir =
|
|
24830
|
-
if (
|
|
26884
|
+
const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
|
|
26885
|
+
if (existsSync23(pagesDir)) {
|
|
24831
26886
|
const scan = (dir, prefix) => {
|
|
24832
26887
|
for (const entry of readdirSync14(dir, { withFileTypes: true })) {
|
|
24833
26888
|
if (entry.name.startsWith("_")) continue;
|
|
24834
26889
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
24835
26890
|
if (entry.isDirectory()) {
|
|
24836
|
-
scan(
|
|
26891
|
+
scan(resolve15(dir, entry.name), rel);
|
|
24837
26892
|
} else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
|
|
24838
26893
|
const urlPath = rel.replace(/\.(twig|html)$/, "");
|
|
24839
26894
|
if (!templateCache.has(urlPath)) {
|
|
@@ -25238,7 +27293,7 @@ function serveTemplateFallback(ctx) {
|
|
|
25238
27293
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
25239
27294
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
25240
27295
|
if (!tplFile) return false;
|
|
25241
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(
|
|
27296
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve15(ctx.templatesDir, tplFile), "utf-8");
|
|
25242
27297
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
25243
27298
|
ctx.res.raw.end(html);
|
|
25244
27299
|
return true;
|
|
@@ -25277,9 +27332,9 @@ function serveMethodNotAllowed(ctx) {
|
|
|
25277
27332
|
}
|
|
25278
27333
|
function serveStaticAsset(ctx) {
|
|
25279
27334
|
const custom = process.env.TINA4_PUBLIC_DIR;
|
|
25280
|
-
if (custom &&
|
|
25281
|
-
if (
|
|
25282
|
-
if (
|
|
27335
|
+
if (custom && existsSync23(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
|
|
27336
|
+
if (existsSync23(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
|
|
27337
|
+
if (existsSync23(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
|
|
25283
27338
|
if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
|
|
25284
27339
|
if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
|
|
25285
27340
|
}
|
|
@@ -25303,10 +27358,10 @@ async function serveNotFound(ctx) {
|
|
|
25303
27358
|
return true;
|
|
25304
27359
|
}
|
|
25305
27360
|
async function buildDispatchContext(router, base) {
|
|
25306
|
-
const root = base ?
|
|
25307
|
-
const staticDir =
|
|
25308
|
-
const srcPublicDir =
|
|
25309
|
-
const templatesDir =
|
|
27361
|
+
const root = base ? resolve15(base) : process.cwd();
|
|
27362
|
+
const staticDir = resolve15(root, "public");
|
|
27363
|
+
const srcPublicDir = resolve15(root, "src/public");
|
|
27364
|
+
const templatesDir = resolve15(root, "src/templates");
|
|
25310
27365
|
let frondEngine = null;
|
|
25311
27366
|
try {
|
|
25312
27367
|
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
@@ -25470,13 +27525,13 @@ ${reset2}
|
|
|
25470
27525
|
};
|
|
25471
27526
|
}
|
|
25472
27527
|
}
|
|
25473
|
-
const base = config?.basePath ?
|
|
25474
|
-
const routesDir =
|
|
25475
|
-
const modelsDir =
|
|
25476
|
-
const ormDir =
|
|
25477
|
-
const staticDir =
|
|
25478
|
-
const srcPublicDir =
|
|
25479
|
-
const templatesDir =
|
|
27528
|
+
const base = config?.basePath ? resolve15(config.basePath) : process.cwd();
|
|
27529
|
+
const routesDir = resolve15(base, config?.routesDir ?? "src/routes");
|
|
27530
|
+
const modelsDir = resolve15(base, config?.modelsDir ?? "src/models");
|
|
27531
|
+
const ormDir = resolve15(base, "src/orm");
|
|
27532
|
+
const staticDir = resolve15(base, config?.staticDir ?? "public");
|
|
27533
|
+
const srcPublicDir = resolve15(base, "src/public");
|
|
27534
|
+
const templatesDir = resolve15(base, config?.templatesDir ?? "src/templates");
|
|
25480
27535
|
const router = new Router();
|
|
25481
27536
|
const middleware = new MiddlewareChain();
|
|
25482
27537
|
globalThis.__tina4_router = router;
|
|
@@ -25510,8 +27565,8 @@ ${reset2}
|
|
|
25510
27565
|
} catch {
|
|
25511
27566
|
}
|
|
25512
27567
|
if (frondEngine) {
|
|
25513
|
-
const localeDir =
|
|
25514
|
-
if (
|
|
27568
|
+
const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
27569
|
+
if (existsSync23(localeDir)) {
|
|
25515
27570
|
try {
|
|
25516
27571
|
const localeFiles = readdirSync14(localeDir).filter((f) => f.endsWith(".json"));
|
|
25517
27572
|
if (localeFiles.length > 0 && !frondEngine.globals?.t) {
|
|
@@ -25526,7 +27581,7 @@ ${reset2}
|
|
|
25526
27581
|
middleware.use(requestLogger());
|
|
25527
27582
|
middleware.use(rateLimiter());
|
|
25528
27583
|
MiddlewareRunner.use(SecurityHeadersMiddleware);
|
|
25529
|
-
if (
|
|
27584
|
+
if (existsSync23(routesDir)) {
|
|
25530
27585
|
const routes = await discoverRoutes(routesDir);
|
|
25531
27586
|
for (const route of routes) {
|
|
25532
27587
|
router.addRoute(route);
|
|
@@ -25546,8 +27601,8 @@ ${reset2}
|
|
|
25546
27601
|
console.log(`
|
|
25547
27602
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
25548
27603
|
}
|
|
25549
|
-
const hasOrmDir =
|
|
25550
|
-
const hasModelsDir =
|
|
27604
|
+
const hasOrmDir = existsSync23(ormDir);
|
|
27605
|
+
const hasModelsDir = existsSync23(modelsDir);
|
|
25551
27606
|
if (hasOrmDir || hasModelsDir) {
|
|
25552
27607
|
try {
|
|
25553
27608
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
@@ -25604,7 +27659,7 @@ ${reset2}
|
|
|
25604
27659
|
let modelDefs = [];
|
|
25605
27660
|
try {
|
|
25606
27661
|
const orm = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
25607
|
-
const allModelDirs = [ormDir, modelsDir].filter((d) =>
|
|
27662
|
+
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync23(d));
|
|
25608
27663
|
const seenTables = /* @__PURE__ */ new Set();
|
|
25609
27664
|
for (const dir of allModelDirs) {
|
|
25610
27665
|
const discovered = await orm.discoverModels(dir);
|
|
@@ -25839,8 +27894,8 @@ var init_server = __esm({
|
|
|
25839
27894
|
init_version();
|
|
25840
27895
|
__filename = fileURLToPath6(import.meta.url);
|
|
25841
27896
|
__dirname = dirname11(__filename);
|
|
25842
|
-
BUILTIN_ERROR_TEMPLATES_DIR =
|
|
25843
|
-
BUILTIN_PUBLIC_DIR =
|
|
27897
|
+
BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
|
|
27898
|
+
BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
|
|
25844
27899
|
swaggerAssetsEnabled = false;
|
|
25845
27900
|
DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
25846
27901
|
frondCache = /* @__PURE__ */ new Map();
|
|
@@ -25996,8 +28051,8 @@ var init_env = __esm({
|
|
|
25996
28051
|
|
|
25997
28052
|
// ../core/src/fakeData.ts
|
|
25998
28053
|
import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
|
|
25999
|
-
import { existsSync as
|
|
26000
|
-
import { resolve as
|
|
28054
|
+
import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
|
|
28055
|
+
import { resolve as resolve16, join as join26 } from "node:path";
|
|
26001
28056
|
function mulberry32(seed) {
|
|
26002
28057
|
let s = seed | 0;
|
|
26003
28058
|
return () => {
|
|
@@ -26431,12 +28486,12 @@ var init_fakeData = __esm({
|
|
|
26431
28486
|
* Returns an array of executed file paths.
|
|
26432
28487
|
*/
|
|
26433
28488
|
async seedDir(seedDir) {
|
|
26434
|
-
const dir =
|
|
26435
|
-
if (!
|
|
28489
|
+
const dir = resolve16(seedDir ?? "src/seeds");
|
|
28490
|
+
if (!existsSync24(dir)) return [];
|
|
26436
28491
|
const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
|
|
26437
28492
|
const executed = [];
|
|
26438
28493
|
for (const file of files) {
|
|
26439
|
-
const fullPath =
|
|
28494
|
+
const fullPath = join26(dir, file);
|
|
26440
28495
|
try {
|
|
26441
28496
|
const mod = await import(fullPath);
|
|
26442
28497
|
if (typeof mod.default === "function") {
|
|
@@ -26535,7 +28590,7 @@ var init_mqttMessage = __esm({
|
|
|
26535
28590
|
import net2 from "node:net";
|
|
26536
28591
|
import tls from "node:tls";
|
|
26537
28592
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
26538
|
-
import { existsSync as
|
|
28593
|
+
import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
|
|
26539
28594
|
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
28595
|
var init_mqtt = __esm({
|
|
26541
28596
|
"../core/src/mqtt.ts"() {
|
|
@@ -26738,7 +28793,7 @@ var init_mqtt = __esm({
|
|
|
26738
28793
|
*/
|
|
26739
28794
|
async connect() {
|
|
26740
28795
|
this.closeSocket();
|
|
26741
|
-
if (this.secure && this.tlsVerify && this.caFile && !
|
|
28796
|
+
if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
|
|
26742
28797
|
throw new MqttError(
|
|
26743
28798
|
`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
28799
|
);
|
|
@@ -26975,7 +29030,7 @@ var init_mqtt = __esm({
|
|
|
26975
29030
|
* a later client.
|
|
26976
29031
|
*/
|
|
26977
29032
|
openSocket() {
|
|
26978
|
-
return new Promise((
|
|
29033
|
+
return new Promise((resolve21, reject) => {
|
|
26979
29034
|
let settled = false;
|
|
26980
29035
|
const settle = (fn) => {
|
|
26981
29036
|
if (settled) return;
|
|
@@ -27001,9 +29056,9 @@ var init_mqtt = __esm({
|
|
|
27001
29056
|
rejectUnauthorized: this.tlsVerify
|
|
27002
29057
|
};
|
|
27003
29058
|
if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
|
|
27004
|
-
sock = tls.connect(opts, () => settle(() =>
|
|
29059
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
27005
29060
|
} else {
|
|
27006
|
-
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() =>
|
|
29061
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
27007
29062
|
}
|
|
27008
29063
|
sock.once("error", (err) => {
|
|
27009
29064
|
settle(() => {
|
|
@@ -27042,13 +29097,13 @@ var init_mqtt = __esm({
|
|
|
27042
29097
|
writePacket(header, body) {
|
|
27043
29098
|
if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
|
|
27044
29099
|
const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
|
|
27045
|
-
return new Promise((
|
|
29100
|
+
return new Promise((resolve21, reject) => {
|
|
27046
29101
|
this.socket.write(packet, (err) => {
|
|
27047
29102
|
if (err) {
|
|
27048
29103
|
reject(new MqttError(`MQTT write failed: ${err.message}`));
|
|
27049
29104
|
} else {
|
|
27050
29105
|
this.lastWriteAt = Date.now();
|
|
27051
|
-
|
|
29106
|
+
resolve21();
|
|
27052
29107
|
}
|
|
27053
29108
|
});
|
|
27054
29109
|
});
|
|
@@ -27081,7 +29136,7 @@ var init_mqtt = __esm({
|
|
|
27081
29136
|
if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
|
|
27082
29137
|
if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
|
|
27083
29138
|
if (this.socketError !== null) return Promise.reject(this.socketError);
|
|
27084
|
-
return new Promise((
|
|
29139
|
+
return new Promise((resolve21, reject) => {
|
|
27085
29140
|
let timer = null;
|
|
27086
29141
|
if (deadline !== null) {
|
|
27087
29142
|
const remaining = deadline - Date.now();
|
|
@@ -27096,7 +29151,7 @@ var init_mqtt = __esm({
|
|
|
27096
29151
|
}
|
|
27097
29152
|
}, remaining);
|
|
27098
29153
|
}
|
|
27099
|
-
this.waiter = { need, resolve:
|
|
29154
|
+
this.waiter = { need, resolve: resolve21, reject, timer };
|
|
27100
29155
|
this.serviceWaiter();
|
|
27101
29156
|
});
|
|
27102
29157
|
}
|
|
@@ -27221,7 +29276,7 @@ var init_mqtt = __esm({
|
|
|
27221
29276
|
|
|
27222
29277
|
// ../core/src/service.ts
|
|
27223
29278
|
import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
|
|
27224
|
-
import { join as
|
|
29279
|
+
import { join as join27, extname as extname6 } from "node:path";
|
|
27225
29280
|
import { pathToFileURL } from "node:url";
|
|
27226
29281
|
function matchCronField(field, value) {
|
|
27227
29282
|
if (field === "*") return true;
|
|
@@ -27395,7 +29450,7 @@ var init_service = __esm({
|
|
|
27395
29450
|
for (const entry of entries) {
|
|
27396
29451
|
const ext = extname6(entry);
|
|
27397
29452
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27398
|
-
const fullPath =
|
|
29453
|
+
const fullPath = join27(dir, entry);
|
|
27399
29454
|
const stat = statSync16(fullPath);
|
|
27400
29455
|
if (!stat.isFile()) continue;
|
|
27401
29456
|
try {
|
|
@@ -27522,7 +29577,7 @@ var init_service = __esm({
|
|
|
27522
29577
|
for (const entry of entries) {
|
|
27523
29578
|
const ext = extname6(entry);
|
|
27524
29579
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27525
|
-
const fullPath =
|
|
29580
|
+
const fullPath = join27(dir, entry);
|
|
27526
29581
|
if (watchedFiles.has(fullPath)) continue;
|
|
27527
29582
|
watchedFiles.add(fullPath);
|
|
27528
29583
|
watchFile(fullPath, { interval: 1e3 }, async () => {
|
|
@@ -28131,7 +30186,7 @@ var init_api = __esm({
|
|
|
28131
30186
|
* `res.destroy()`.
|
|
28132
30187
|
*/
|
|
28133
30188
|
openStreamRequest(method, url, headers, data, connectSec) {
|
|
28134
|
-
return new Promise((
|
|
30189
|
+
return new Promise((resolve21, reject) => {
|
|
28135
30190
|
let parsed;
|
|
28136
30191
|
try {
|
|
28137
30192
|
parsed = new URL2(url);
|
|
@@ -28153,7 +30208,7 @@ var init_api = __esm({
|
|
|
28153
30208
|
options.rejectUnauthorized = false;
|
|
28154
30209
|
}
|
|
28155
30210
|
const req2 = protocolModule.request(options, (res) => {
|
|
28156
|
-
|
|
30211
|
+
resolve21({ res });
|
|
28157
30212
|
});
|
|
28158
30213
|
req2.on("timeout", () => {
|
|
28159
30214
|
req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
|
|
@@ -28291,12 +30346,12 @@ var init_api = __esm({
|
|
|
28291
30346
|
* authenticate to.
|
|
28292
30347
|
*/
|
|
28293
30348
|
performRequest(method, url, headers, data, redirectsLeft) {
|
|
28294
|
-
return new Promise((
|
|
30349
|
+
return new Promise((resolve21) => {
|
|
28295
30350
|
let parsed;
|
|
28296
30351
|
try {
|
|
28297
30352
|
parsed = new URL2(url);
|
|
28298
30353
|
} catch (err) {
|
|
28299
|
-
|
|
30354
|
+
resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
|
|
28300
30355
|
return;
|
|
28301
30356
|
}
|
|
28302
30357
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -28321,7 +30376,7 @@ var init_api = __esm({
|
|
|
28321
30376
|
try {
|
|
28322
30377
|
nextUrl = new URL2(location, url).toString();
|
|
28323
30378
|
} catch {
|
|
28324
|
-
|
|
30379
|
+
resolve21({ kind: "response", res });
|
|
28325
30380
|
return;
|
|
28326
30381
|
}
|
|
28327
30382
|
const crossOrigin = !sameOrigin(url, nextUrl);
|
|
@@ -28339,17 +30394,17 @@ var init_api = __esm({
|
|
|
28339
30394
|
deleteHeaderCaseInsensitive(nextHeaders, name);
|
|
28340
30395
|
}
|
|
28341
30396
|
}
|
|
28342
|
-
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(
|
|
30397
|
+
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
|
|
28343
30398
|
return;
|
|
28344
30399
|
}
|
|
28345
|
-
|
|
30400
|
+
resolve21({ kind: "response", res });
|
|
28346
30401
|
});
|
|
28347
30402
|
req2.on("timeout", () => {
|
|
28348
30403
|
req2.destroy();
|
|
28349
|
-
|
|
30404
|
+
resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
|
|
28350
30405
|
});
|
|
28351
30406
|
req2.on("error", (err) => {
|
|
28352
|
-
|
|
30407
|
+
resolve21({ kind: "error", error: err.message });
|
|
28353
30408
|
});
|
|
28354
30409
|
if (data) {
|
|
28355
30410
|
req2.write(data);
|
|
@@ -28359,7 +30414,7 @@ var init_api = __esm({
|
|
|
28359
30414
|
}
|
|
28360
30415
|
/** Buffer a response body, parse JSON if possible, and store cookies. */
|
|
28361
30416
|
readResponse(res) {
|
|
28362
|
-
return new Promise((
|
|
30417
|
+
return new Promise((resolve21) => {
|
|
28363
30418
|
const chunks = [];
|
|
28364
30419
|
res.on("data", (chunk) => {
|
|
28365
30420
|
chunks.push(chunk);
|
|
@@ -28374,7 +30429,7 @@ var init_api = __esm({
|
|
|
28374
30429
|
} catch {
|
|
28375
30430
|
parsed = raw;
|
|
28376
30431
|
}
|
|
28377
|
-
|
|
30432
|
+
resolve21({
|
|
28378
30433
|
http_code: res.statusCode ?? null,
|
|
28379
30434
|
body: parsed,
|
|
28380
30435
|
headers: respHeaders,
|
|
@@ -28382,7 +30437,7 @@ var init_api = __esm({
|
|
|
28382
30437
|
});
|
|
28383
30438
|
});
|
|
28384
30439
|
res.on("error", (err) => {
|
|
28385
|
-
|
|
30440
|
+
resolve21({ http_code: null, body: null, headers: {}, error: err.message });
|
|
28386
30441
|
});
|
|
28387
30442
|
});
|
|
28388
30443
|
}
|
|
@@ -28446,7 +30501,7 @@ function parseMailRedirectList(raw) {
|
|
|
28446
30501
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
28447
30502
|
}
|
|
28448
30503
|
function readResponse(socket) {
|
|
28449
|
-
return new Promise((
|
|
30504
|
+
return new Promise((resolve21, reject) => {
|
|
28450
30505
|
let buffer = "";
|
|
28451
30506
|
const onData = (chunk) => {
|
|
28452
30507
|
buffer += chunk.toString("utf-8");
|
|
@@ -28458,7 +30513,7 @@ function readResponse(socket) {
|
|
|
28458
30513
|
if (line.length >= 4 && line[3] === " ") {
|
|
28459
30514
|
socket.removeListener("data", onData);
|
|
28460
30515
|
socket.removeListener("error", onError);
|
|
28461
|
-
|
|
30516
|
+
resolve21({ code, text: buffer.trim() });
|
|
28462
30517
|
return;
|
|
28463
30518
|
}
|
|
28464
30519
|
}
|
|
@@ -28472,10 +30527,10 @@ function readResponse(socket) {
|
|
|
28472
30527
|
});
|
|
28473
30528
|
}
|
|
28474
30529
|
function sendCommand(socket, command) {
|
|
28475
|
-
return new Promise((
|
|
30530
|
+
return new Promise((resolve21, reject) => {
|
|
28476
30531
|
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
28477
30532
|
if (err) return reject(err);
|
|
28478
|
-
readResponse(socket).then(
|
|
30533
|
+
readResponse(socket).then(resolve21, reject);
|
|
28479
30534
|
});
|
|
28480
30535
|
});
|
|
28481
30536
|
}
|
|
@@ -28575,7 +30630,7 @@ function imapQuote(s) {
|
|
|
28575
30630
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
28576
30631
|
}
|
|
28577
30632
|
function imapReadLine(socket) {
|
|
28578
|
-
return new Promise((
|
|
30633
|
+
return new Promise((resolve21, reject) => {
|
|
28579
30634
|
let buffer = "";
|
|
28580
30635
|
const onData = (chunk) => {
|
|
28581
30636
|
buffer += chunk.toString("utf-8");
|
|
@@ -28583,7 +30638,7 @@ function imapReadLine(socket) {
|
|
|
28583
30638
|
if (nlIndex !== -1) {
|
|
28584
30639
|
socket.removeListener("data", onData);
|
|
28585
30640
|
socket.removeListener("error", onError);
|
|
28586
|
-
|
|
30641
|
+
resolve21(buffer);
|
|
28587
30642
|
}
|
|
28588
30643
|
};
|
|
28589
30644
|
const onError = (err) => {
|
|
@@ -28595,7 +30650,7 @@ function imapReadLine(socket) {
|
|
|
28595
30650
|
});
|
|
28596
30651
|
}
|
|
28597
30652
|
function imapCommand(socket, command) {
|
|
28598
|
-
return new Promise((
|
|
30653
|
+
return new Promise((resolve21, reject) => {
|
|
28599
30654
|
imapTagCounter++;
|
|
28600
30655
|
const tag = `T${imapTagCounter}`;
|
|
28601
30656
|
const fullCommand = `${tag} ${command}\r
|
|
@@ -28606,7 +30661,7 @@ function imapCommand(socket, command) {
|
|
|
28606
30661
|
if (buffer.includes(`${tag} OK`)) {
|
|
28607
30662
|
socket.removeListener("data", onData);
|
|
28608
30663
|
socket.removeListener("error", onError);
|
|
28609
|
-
|
|
30664
|
+
resolve21(buffer);
|
|
28610
30665
|
return;
|
|
28611
30666
|
}
|
|
28612
30667
|
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
@@ -28911,14 +30966,14 @@ var init_messenger = __esm({
|
|
|
28911
30966
|
let socket;
|
|
28912
30967
|
if (this.port === 465) {
|
|
28913
30968
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
28914
|
-
await new Promise((
|
|
28915
|
-
socket.once("secureConnect",
|
|
30969
|
+
await new Promise((resolve21, reject) => {
|
|
30970
|
+
socket.once("secureConnect", resolve21);
|
|
28916
30971
|
socket.once("error", reject);
|
|
28917
30972
|
});
|
|
28918
30973
|
} else {
|
|
28919
30974
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
28920
|
-
await new Promise((
|
|
28921
|
-
socket.once("connect",
|
|
30975
|
+
await new Promise((resolve21, reject) => {
|
|
30976
|
+
socket.once("connect", resolve21);
|
|
28922
30977
|
socket.once("error", reject);
|
|
28923
30978
|
});
|
|
28924
30979
|
}
|
|
@@ -28942,8 +30997,8 @@ var init_messenger = __esm({
|
|
|
28942
30997
|
socket = tls2.connect(
|
|
28943
30998
|
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
28944
30999
|
);
|
|
28945
|
-
await new Promise((
|
|
28946
|
-
socket.once("secureConnect",
|
|
31000
|
+
await new Promise((resolve21, reject) => {
|
|
31001
|
+
socket.once("secureConnect", resolve21);
|
|
28947
31002
|
socket.once("error", reject);
|
|
28948
31003
|
});
|
|
28949
31004
|
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
@@ -29036,14 +31091,14 @@ var init_messenger = __esm({
|
|
|
29036
31091
|
let socket;
|
|
29037
31092
|
if (this.port === 465) {
|
|
29038
31093
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
29039
|
-
await new Promise((
|
|
29040
|
-
socket.once("secureConnect",
|
|
31094
|
+
await new Promise((resolve21, reject) => {
|
|
31095
|
+
socket.once("secureConnect", resolve21);
|
|
29041
31096
|
socket.once("error", reject);
|
|
29042
31097
|
});
|
|
29043
31098
|
} else {
|
|
29044
31099
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
29045
|
-
await new Promise((
|
|
29046
|
-
socket.once("connect",
|
|
31100
|
+
await new Promise((resolve21, reject) => {
|
|
31101
|
+
socket.once("connect", resolve21);
|
|
29047
31102
|
socket.once("error", reject);
|
|
29048
31103
|
});
|
|
29049
31104
|
}
|
|
@@ -29078,14 +31133,14 @@ var init_messenger = __esm({
|
|
|
29078
31133
|
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
29079
31134
|
if (useTls) {
|
|
29080
31135
|
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
29081
|
-
await new Promise((
|
|
29082
|
-
socket.once("secureConnect",
|
|
31136
|
+
await new Promise((resolve21, reject) => {
|
|
31137
|
+
socket.once("secureConnect", resolve21);
|
|
29083
31138
|
socket.once("error", reject);
|
|
29084
31139
|
});
|
|
29085
31140
|
} else {
|
|
29086
31141
|
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
29087
|
-
await new Promise((
|
|
29088
|
-
socket.once("connect",
|
|
31142
|
+
await new Promise((resolve21, reject) => {
|
|
31143
|
+
socket.once("connect", resolve21);
|
|
29089
31144
|
socket.once("error", reject);
|
|
29090
31145
|
});
|
|
29091
31146
|
}
|
|
@@ -29995,16 +32050,16 @@ var init_htmlElement = __esm({
|
|
|
29995
32050
|
});
|
|
29996
32051
|
|
|
29997
32052
|
// ../core/src/ai.ts
|
|
29998
|
-
import { existsSync as
|
|
32053
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
|
|
29999
32054
|
import { homedir } from "node:os";
|
|
30000
|
-
import { join as
|
|
32055
|
+
import { join as join28, resolve as resolve17, relative as relative10, dirname as dirname12 } from "node:path";
|
|
30001
32056
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
30002
32057
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
30003
32058
|
import { createInterface } from "node:readline";
|
|
30004
32059
|
function readVersion() {
|
|
30005
32060
|
try {
|
|
30006
32061
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
30007
|
-
const rootPkg =
|
|
32062
|
+
const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
|
|
30008
32063
|
const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
|
|
30009
32064
|
return pkg.version ?? "0.0.0";
|
|
30010
32065
|
} catch {
|
|
@@ -30068,8 +32123,8 @@ function downloadSkillsSync(jobs) {
|
|
|
30068
32123
|
function installSkills(root = ".", targets) {
|
|
30069
32124
|
const ref = skillsRef();
|
|
30070
32125
|
const dests = targets ?? [
|
|
30071
|
-
|
|
30072
|
-
|
|
32126
|
+
join28(resolve17(root), ".claude", "skills"),
|
|
32127
|
+
join28(homedir(), ".claude", "skills")
|
|
30073
32128
|
];
|
|
30074
32129
|
const jobs = [];
|
|
30075
32130
|
const index = /* @__PURE__ */ new Map();
|
|
@@ -30087,9 +32142,9 @@ function installSkills(root = ".", targets) {
|
|
|
30087
32142
|
const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
|
|
30088
32143
|
skillMdUrl[skill] = `${base}/SKILL.md`;
|
|
30089
32144
|
for (const dest of dests) {
|
|
30090
|
-
add(`${base}/SKILL.md`,
|
|
32145
|
+
add(`${base}/SKILL.md`, join28(dest, skill, "SKILL.md"));
|
|
30091
32146
|
for (const r of spec.references) {
|
|
30092
|
-
add(`${base}/references/${r}`,
|
|
32147
|
+
add(`${base}/references/${r}`, join28(dest, skill, "references", r));
|
|
30093
32148
|
}
|
|
30094
32149
|
}
|
|
30095
32150
|
}
|
|
@@ -30101,10 +32156,10 @@ function installSkills(root = ".", targets) {
|
|
|
30101
32156
|
return installed;
|
|
30102
32157
|
}
|
|
30103
32158
|
function isInstalled(root, tool) {
|
|
30104
|
-
return
|
|
32159
|
+
return existsSync26(join28(resolve17(root), tool.contextFile));
|
|
30105
32160
|
}
|
|
30106
32161
|
function showMenu(root = ".") {
|
|
30107
|
-
const r =
|
|
32162
|
+
const r = resolve17(root);
|
|
30108
32163
|
console.log("\n Tina4 AI Context Installer\n");
|
|
30109
32164
|
for (let i = 0; i < AI_TOOLS.length; i++) {
|
|
30110
32165
|
const tool = AI_TOOLS[i];
|
|
@@ -30122,16 +32177,16 @@ function showMenu(root = ".") {
|
|
|
30122
32177
|
const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
|
|
30123
32178
|
console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
|
|
30124
32179
|
console.log();
|
|
30125
|
-
return new Promise((
|
|
32180
|
+
return new Promise((resolve21) => {
|
|
30126
32181
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
30127
32182
|
rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
|
|
30128
32183
|
rl.close();
|
|
30129
|
-
|
|
32184
|
+
resolve21(answer.trim());
|
|
30130
32185
|
});
|
|
30131
32186
|
});
|
|
30132
32187
|
}
|
|
30133
32188
|
function installSelected(root, selection) {
|
|
30134
|
-
const rootPath =
|
|
32189
|
+
const rootPath = resolve17(root);
|
|
30135
32190
|
const created = [];
|
|
30136
32191
|
let indices;
|
|
30137
32192
|
let doInstallTina4Ai = false;
|
|
@@ -30224,35 +32279,35 @@ function looksLikeOldFrameworkInstall(existing) {
|
|
|
30224
32279
|
function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
30225
32280
|
const block = skillBlock(contextFile);
|
|
30226
32281
|
const [start2, end] = markersFor(contextFile);
|
|
30227
|
-
if (!
|
|
30228
|
-
|
|
32282
|
+
if (!existsSync26(contextPath)) {
|
|
32283
|
+
writeFileSync16(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
30229
32284
|
return "Installed";
|
|
30230
32285
|
}
|
|
30231
32286
|
const existing = readFileSync24(contextPath, "utf-8");
|
|
30232
32287
|
if (hasMarkers(existing, start2, end)) {
|
|
30233
|
-
|
|
32288
|
+
writeFileSync16(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
30234
32289
|
return "Refreshed skill block in";
|
|
30235
32290
|
}
|
|
30236
32291
|
if (looksLikeOldFrameworkInstall(existing)) {
|
|
30237
32292
|
const head = existing.replace(/^\s+/, "");
|
|
30238
32293
|
const preamble = existing.slice(0, existing.length - head.length);
|
|
30239
32294
|
const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
30240
|
-
|
|
32295
|
+
writeFileSync16(contextPath, newContent, "utf-8");
|
|
30241
32296
|
return "Migrated (replaced old framework dump in)";
|
|
30242
32297
|
}
|
|
30243
|
-
|
|
32298
|
+
writeFileSync16(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
30244
32299
|
return "Appended skill block to";
|
|
30245
32300
|
}
|
|
30246
32301
|
function installForTool(root, tool, context) {
|
|
30247
32302
|
const created = [];
|
|
30248
|
-
const contextPath =
|
|
32303
|
+
const contextPath = join28(root, tool.contextFile);
|
|
30249
32304
|
if (tool.configDir) {
|
|
30250
|
-
|
|
32305
|
+
mkdirSync17(join28(root, tool.configDir), { recursive: true });
|
|
30251
32306
|
}
|
|
30252
32307
|
const parentDir = dirname12(contextPath);
|
|
30253
|
-
|
|
32308
|
+
mkdirSync17(parentDir, { recursive: true });
|
|
30254
32309
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
30255
|
-
const rel =
|
|
32310
|
+
const rel = relative10(root, contextPath);
|
|
30256
32311
|
created.push(rel);
|
|
30257
32312
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
30258
32313
|
if (tool.name === "claude-code") {
|
|
@@ -30285,7 +32340,7 @@ function installTina4Ai() {
|
|
|
30285
32340
|
function installClaudeSkills(root) {
|
|
30286
32341
|
const created = [];
|
|
30287
32342
|
for (const skill of installSkills(root)) {
|
|
30288
|
-
created.push(
|
|
32343
|
+
created.push(join28(".claude", "skills", skill));
|
|
30289
32344
|
console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
|
|
30290
32345
|
}
|
|
30291
32346
|
return created;
|
|
@@ -30627,9 +32682,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
|
|
|
30627
32682
|
function generateClaudeCodeContext() {
|
|
30628
32683
|
try {
|
|
30629
32684
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
30630
|
-
const repoRoot =
|
|
30631
|
-
const claudeMdPath =
|
|
30632
|
-
if (
|
|
32685
|
+
const repoRoot = resolve17(thisDir, "..", "..", "..");
|
|
32686
|
+
const claudeMdPath = join28(repoRoot, "CLAUDE.md");
|
|
32687
|
+
if (existsSync26(claudeMdPath)) {
|
|
30633
32688
|
return readFileSync24(claudeMdPath, "utf-8");
|
|
30634
32689
|
}
|
|
30635
32690
|
} catch {
|
|
@@ -31176,11 +33231,11 @@ var init_aiClient = __esm({
|
|
|
31176
33231
|
const payload = JSON.stringify(body);
|
|
31177
33232
|
const controller = new AbortController();
|
|
31178
33233
|
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
31179
|
-
return new Promise((
|
|
33234
|
+
return new Promise((resolve21, reject) => {
|
|
31180
33235
|
const client = url.protocol === "https:" ? https2 : http2;
|
|
31181
33236
|
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
31182
33237
|
clearTimeout(connectTimer);
|
|
31183
|
-
|
|
33238
|
+
resolve21({ response, cleanup: () => {
|
|
31184
33239
|
clearTimeout(totalTimer);
|
|
31185
33240
|
clearTimeout(connectTimer);
|
|
31186
33241
|
} });
|
|
@@ -31209,7 +33264,7 @@ var init_aiClient = __esm({
|
|
|
31209
33264
|
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
31210
33265
|
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
31211
33266
|
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
31212
|
-
return new Promise((
|
|
33267
|
+
return new Promise((resolve21) => setTimeout(resolve21, delay));
|
|
31213
33268
|
}
|
|
31214
33269
|
static async requestJson(config, headers, body) {
|
|
31215
33270
|
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
@@ -34153,8 +36208,8 @@ __export(sqlite_exports, {
|
|
|
34153
36208
|
SQLiteAdapter: () => SQLiteAdapter
|
|
34154
36209
|
});
|
|
34155
36210
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
34156
|
-
import { mkdirSync as
|
|
34157
|
-
import { dirname as dirname13, isAbsolute as isAbsolute5, join as
|
|
36211
|
+
import { mkdirSync as mkdirSync18 } from "node:fs";
|
|
36212
|
+
import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18 } from "node:path";
|
|
34158
36213
|
function isIdentifier(str) {
|
|
34159
36214
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
|
|
34160
36215
|
}
|
|
@@ -34187,13 +36242,13 @@ function resolveSqlitePath(dbPath) {
|
|
|
34187
36242
|
if (dbPath === ":memory:") return dbPath;
|
|
34188
36243
|
let path8 = dbPath;
|
|
34189
36244
|
if (!isAbsolute5(path8)) {
|
|
34190
|
-
path8 =
|
|
34191
|
-
|
|
36245
|
+
path8 = join29(process.cwd(), path8);
|
|
36246
|
+
mkdirSync18(dirname13(path8), { recursive: true });
|
|
34192
36247
|
} else {
|
|
34193
|
-
const cwd =
|
|
34194
|
-
const abs =
|
|
36248
|
+
const cwd = resolve18(process.cwd());
|
|
36249
|
+
const abs = resolve18(path8);
|
|
34195
36250
|
if (abs.startsWith(cwd + "/") || abs === cwd) {
|
|
34196
|
-
|
|
36251
|
+
mkdirSync18(dirname13(abs), { recursive: true });
|
|
34197
36252
|
}
|
|
34198
36253
|
}
|
|
34199
36254
|
return path8;
|
|
@@ -34591,7 +36646,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
34591
36646
|
const elapsedMs = () => performance.now() - startedAt;
|
|
34592
36647
|
if (budgetMs === null) return attempt();
|
|
34593
36648
|
const started = attempt();
|
|
34594
|
-
return new Promise((
|
|
36649
|
+
return new Promise((resolve21, reject) => {
|
|
34595
36650
|
let expired = false;
|
|
34596
36651
|
const timer = setTimeout(() => {
|
|
34597
36652
|
expired = true;
|
|
@@ -34601,7 +36656,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
34601
36656
|
(arrived) => {
|
|
34602
36657
|
clearTimeout(timer);
|
|
34603
36658
|
if (expired) abandon?.(arrived);
|
|
34604
|
-
else
|
|
36659
|
+
else resolve21(arrived);
|
|
34605
36660
|
},
|
|
34606
36661
|
(failure) => {
|
|
34607
36662
|
clearTimeout(timer);
|
|
@@ -35190,10 +37245,10 @@ var init_mysql = __esm({
|
|
|
35190
37245
|
...timeoutOption
|
|
35191
37246
|
});
|
|
35192
37247
|
}
|
|
35193
|
-
return new Promise((
|
|
37248
|
+
return new Promise((resolve21, reject) => {
|
|
35194
37249
|
this.connection.connect((err) => {
|
|
35195
37250
|
if (err) reject(err);
|
|
35196
|
-
else
|
|
37251
|
+
else resolve21();
|
|
35197
37252
|
});
|
|
35198
37253
|
});
|
|
35199
37254
|
},
|
|
@@ -35215,10 +37270,10 @@ var init_mysql = __esm({
|
|
|
35215
37270
|
}
|
|
35216
37271
|
}
|
|
35217
37272
|
queryPromise(sql, params) {
|
|
35218
|
-
return new Promise((
|
|
37273
|
+
return new Promise((resolve21, reject) => {
|
|
35219
37274
|
this.connection.query(sql, params ?? [], (err, results) => {
|
|
35220
37275
|
if (err) reject(err);
|
|
35221
|
-
else
|
|
37276
|
+
else resolve21(results);
|
|
35222
37277
|
});
|
|
35223
37278
|
});
|
|
35224
37279
|
}
|
|
@@ -35604,11 +37659,11 @@ var init_mssql = __esm({
|
|
|
35604
37659
|
};
|
|
35605
37660
|
}
|
|
35606
37661
|
await withConnectTimeout(
|
|
35607
|
-
() => new Promise((
|
|
37662
|
+
() => new Promise((resolve21, reject) => {
|
|
35608
37663
|
this.connection = new Connection(tediousConfig);
|
|
35609
37664
|
this.connection.on("connect", (err) => {
|
|
35610
37665
|
if (err) reject(err);
|
|
35611
|
-
else
|
|
37666
|
+
else resolve21();
|
|
35612
37667
|
});
|
|
35613
37668
|
this.connection.connect();
|
|
35614
37669
|
}),
|
|
@@ -35653,11 +37708,11 @@ var init_mssql = __esm({
|
|
|
35653
37708
|
const tediousModule = requireTedious();
|
|
35654
37709
|
const Request = tediousModule.Request;
|
|
35655
37710
|
const TYPES = tediousModule.TYPES;
|
|
35656
|
-
return new Promise((
|
|
37711
|
+
return new Promise((resolve21, reject) => {
|
|
35657
37712
|
const rows = [];
|
|
35658
37713
|
const request = new Request(sql, (err, rowCount) => {
|
|
35659
37714
|
if (err) reject(err);
|
|
35660
|
-
else
|
|
37715
|
+
else resolve21({ rows, rowCount });
|
|
35661
37716
|
});
|
|
35662
37717
|
if (params) {
|
|
35663
37718
|
params.forEach((p, i) => {
|
|
@@ -35859,8 +37914,8 @@ var init_mssql = __esm({
|
|
|
35859
37914
|
throw new Error("Use startTransactionAsync() for MSSQL.");
|
|
35860
37915
|
}
|
|
35861
37916
|
async startTransactionAsync() {
|
|
35862
|
-
await new Promise((
|
|
35863
|
-
this.connection.beginTransaction((err) => err ? reject(err) :
|
|
37917
|
+
await new Promise((resolve21, reject) => {
|
|
37918
|
+
this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
|
|
35864
37919
|
});
|
|
35865
37920
|
this._inTransaction = true;
|
|
35866
37921
|
}
|
|
@@ -35868,8 +37923,8 @@ var init_mssql = __esm({
|
|
|
35868
37923
|
throw new Error("Use commitAsync() for MSSQL.");
|
|
35869
37924
|
}
|
|
35870
37925
|
async commitAsync() {
|
|
35871
|
-
await new Promise((
|
|
35872
|
-
this.connection.commitTransaction((err) => err ? reject(err) :
|
|
37926
|
+
await new Promise((resolve21, reject) => {
|
|
37927
|
+
this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
|
|
35873
37928
|
});
|
|
35874
37929
|
this._inTransaction = false;
|
|
35875
37930
|
}
|
|
@@ -35877,8 +37932,8 @@ var init_mssql = __esm({
|
|
|
35877
37932
|
throw new Error("Use rollbackAsync() for MSSQL.");
|
|
35878
37933
|
}
|
|
35879
37934
|
async rollbackAsync() {
|
|
35880
|
-
await new Promise((
|
|
35881
|
-
this.connection.rollbackTransaction((err) => err ? reject(err) :
|
|
37935
|
+
await new Promise((resolve21, reject) => {
|
|
37936
|
+
this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
|
|
35882
37937
|
});
|
|
35883
37938
|
this._inTransaction = false;
|
|
35884
37939
|
}
|
|
@@ -36203,8 +38258,8 @@ var init_firebird = __esm({
|
|
|
36203
38258
|
}
|
|
36204
38259
|
attachOnce(config) {
|
|
36205
38260
|
const fb = requireFirebird();
|
|
36206
|
-
return new Promise((
|
|
36207
|
-
fb.attach(config, (err, db) => err ? reject(err) :
|
|
38261
|
+
return new Promise((resolve21, reject) => {
|
|
38262
|
+
fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
|
|
36208
38263
|
});
|
|
36209
38264
|
}
|
|
36210
38265
|
/**
|
|
@@ -36223,7 +38278,7 @@ var init_firebird = __esm({
|
|
|
36223
38278
|
} catch (err) {
|
|
36224
38279
|
lastError = err;
|
|
36225
38280
|
if (attempt < attempts - 1) {
|
|
36226
|
-
await new Promise((
|
|
38281
|
+
await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
|
|
36227
38282
|
}
|
|
36228
38283
|
}
|
|
36229
38284
|
}
|
|
@@ -36306,19 +38361,19 @@ var init_firebird = __esm({
|
|
|
36306
38361
|
}
|
|
36307
38362
|
queryPromise(sql, params) {
|
|
36308
38363
|
const translated = this.translateSql(sql);
|
|
36309
|
-
return this.withReconnect(() => new Promise((
|
|
38364
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
36310
38365
|
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
36311
38366
|
if (err) reject(err);
|
|
36312
|
-
else
|
|
38367
|
+
else resolve21(result ?? []);
|
|
36313
38368
|
});
|
|
36314
38369
|
}));
|
|
36315
38370
|
}
|
|
36316
38371
|
executePromise(sql, params) {
|
|
36317
38372
|
const translated = this.translateSql(sql);
|
|
36318
|
-
return this.withReconnect(() => new Promise((
|
|
38373
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
36319
38374
|
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
36320
38375
|
if (err) reject(err);
|
|
36321
|
-
else
|
|
38376
|
+
else resolve21();
|
|
36322
38377
|
});
|
|
36323
38378
|
}));
|
|
36324
38379
|
}
|
|
@@ -36357,13 +38412,13 @@ var init_firebird = __esm({
|
|
|
36357
38412
|
* and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
|
|
36358
38413
|
*/
|
|
36359
38414
|
readBlob(blobFn) {
|
|
36360
|
-
return new Promise((
|
|
38415
|
+
return new Promise((resolve21, reject) => {
|
|
36361
38416
|
blobFn((err, _name, emitter) => {
|
|
36362
38417
|
if (err) return reject(err);
|
|
36363
|
-
if (!emitter) return
|
|
38418
|
+
if (!emitter) return resolve21(null);
|
|
36364
38419
|
const chunks = [];
|
|
36365
38420
|
emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
36366
|
-
emitter.on("end", () =>
|
|
38421
|
+
emitter.on("end", () => resolve21(Buffer.concat(chunks)));
|
|
36367
38422
|
emitter.on("error", (streamErr) => reject(streamErr));
|
|
36368
38423
|
});
|
|
36369
38424
|
});
|
|
@@ -36499,12 +38554,12 @@ var init_firebird = __esm({
|
|
|
36499
38554
|
}
|
|
36500
38555
|
async startTransactionAsync() {
|
|
36501
38556
|
this.ensureConnected();
|
|
36502
|
-
await new Promise((
|
|
38557
|
+
await new Promise((resolve21, reject) => {
|
|
36503
38558
|
this.db.transaction(0, (err, transaction) => {
|
|
36504
38559
|
if (err) reject(err);
|
|
36505
38560
|
else {
|
|
36506
38561
|
this.transaction = transaction;
|
|
36507
|
-
|
|
38562
|
+
resolve21();
|
|
36508
38563
|
}
|
|
36509
38564
|
});
|
|
36510
38565
|
});
|
|
@@ -36514,12 +38569,12 @@ var init_firebird = __esm({
|
|
|
36514
38569
|
}
|
|
36515
38570
|
async commitAsync() {
|
|
36516
38571
|
if (!this.transaction) throw new Error("No active transaction to commit.");
|
|
36517
|
-
await new Promise((
|
|
38572
|
+
await new Promise((resolve21, reject) => {
|
|
36518
38573
|
this.transaction.commit((err) => {
|
|
36519
38574
|
if (err) reject(err);
|
|
36520
38575
|
else {
|
|
36521
38576
|
this.transaction = null;
|
|
36522
|
-
|
|
38577
|
+
resolve21();
|
|
36523
38578
|
}
|
|
36524
38579
|
});
|
|
36525
38580
|
});
|
|
@@ -36529,12 +38584,12 @@ var init_firebird = __esm({
|
|
|
36529
38584
|
}
|
|
36530
38585
|
async rollbackAsync() {
|
|
36531
38586
|
if (!this.transaction) throw new Error("No active transaction to rollback.");
|
|
36532
|
-
await new Promise((
|
|
38587
|
+
await new Promise((resolve21, reject) => {
|
|
36533
38588
|
this.transaction.rollback((err) => {
|
|
36534
38589
|
if (err) reject(err);
|
|
36535
38590
|
else {
|
|
36536
38591
|
this.transaction = null;
|
|
36537
|
-
|
|
38592
|
+
resolve21();
|
|
36538
38593
|
}
|
|
36539
38594
|
});
|
|
36540
38595
|
});
|
|
@@ -39014,7 +41069,7 @@ var init_database = __esm({
|
|
|
39014
41069
|
|
|
39015
41070
|
// src/model.ts
|
|
39016
41071
|
import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
|
|
39017
|
-
import { join as
|
|
41072
|
+
import { join as join30, extname as extname7 } from "node:path";
|
|
39018
41073
|
async function discoverModels(modelsDir) {
|
|
39019
41074
|
const models = [];
|
|
39020
41075
|
let files;
|
|
@@ -39024,7 +41079,7 @@ async function discoverModels(modelsDir) {
|
|
|
39024
41079
|
return models;
|
|
39025
41080
|
}
|
|
39026
41081
|
for (const file of files) {
|
|
39027
|
-
const filePath =
|
|
41082
|
+
const filePath = join30(modelsDir, file);
|
|
39028
41083
|
const stat = statSync17(filePath);
|
|
39029
41084
|
if (!stat.isFile()) continue;
|
|
39030
41085
|
const ext = extname7(file);
|
|
@@ -39066,8 +41121,8 @@ var init_model = __esm({
|
|
|
39066
41121
|
});
|
|
39067
41122
|
|
|
39068
41123
|
// src/migration.ts
|
|
39069
|
-
import { existsSync as
|
|
39070
|
-
import { join as
|
|
41124
|
+
import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
|
|
41125
|
+
import { join as join31, resolve as resolve19 } from "node:path";
|
|
39071
41126
|
function unwrapAdapter(db) {
|
|
39072
41127
|
let cur = db;
|
|
39073
41128
|
while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
|
|
@@ -39383,15 +41438,15 @@ async function rollback(migrationsDir, delimiter2) {
|
|
|
39383
41438
|
}
|
|
39384
41439
|
return rolledBack2;
|
|
39385
41440
|
}
|
|
39386
|
-
const dir =
|
|
41441
|
+
const dir = resolve19(migrationsDir ?? "migrations");
|
|
39387
41442
|
const delim = delimiter2 ?? ";";
|
|
39388
41443
|
const db = getAdapter();
|
|
39389
41444
|
const migrations = await getLastBatchMigrations();
|
|
39390
41445
|
const rolledBack = [];
|
|
39391
41446
|
for (const migration of migrations) {
|
|
39392
41447
|
const downFile = `${migration.migration_name}.down.sql`;
|
|
39393
|
-
const downPath =
|
|
39394
|
-
if (!
|
|
41448
|
+
const downPath = join31(dir, downFile);
|
|
41449
|
+
if (!existsSync27(downPath)) {
|
|
39395
41450
|
throw new Error(
|
|
39396
41451
|
`Cannot rollback ${migration.migration_name}: no .down.sql file found`
|
|
39397
41452
|
);
|
|
@@ -39555,10 +41610,10 @@ function warnUnprefixedMigrations(files) {
|
|
|
39555
41610
|
}
|
|
39556
41611
|
async function migrate(adapter, options) {
|
|
39557
41612
|
const db = adapter ?? getAdapter();
|
|
39558
|
-
const dir =
|
|
41613
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
39559
41614
|
const delimiter2 = options?.delimiter ?? ";";
|
|
39560
41615
|
const result = { applied: [], skipped: [], failed: [] };
|
|
39561
|
-
if (!
|
|
41616
|
+
if (!existsSync27(dir)) {
|
|
39562
41617
|
return result;
|
|
39563
41618
|
}
|
|
39564
41619
|
await ensureMigrationTableOn(db);
|
|
@@ -39594,7 +41649,7 @@ async function migrate(adapter, options) {
|
|
|
39594
41649
|
result.skipped.push(file);
|
|
39595
41650
|
continue;
|
|
39596
41651
|
}
|
|
39597
|
-
const sqlContent = readFileSync25(
|
|
41652
|
+
const sqlContent = readFileSync25(join31(dir, file), "utf-8").trim();
|
|
39598
41653
|
if (!sqlContent) {
|
|
39599
41654
|
result.skipped.push(file);
|
|
39600
41655
|
continue;
|
|
@@ -39629,9 +41684,9 @@ async function migrate(adapter, options) {
|
|
|
39629
41684
|
}
|
|
39630
41685
|
async function status(adapter, options) {
|
|
39631
41686
|
const db = adapter ?? getAdapter();
|
|
39632
|
-
const dir =
|
|
41687
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
39633
41688
|
const result = { completed: [], pending: [] };
|
|
39634
|
-
if (!
|
|
41689
|
+
if (!existsSync27(dir)) {
|
|
39635
41690
|
return result;
|
|
39636
41691
|
}
|
|
39637
41692
|
if (!await adapterTableExists(db, MIGRATION_TABLE)) {
|
|
@@ -39677,13 +41732,13 @@ async function createMigration(description, options) {
|
|
|
39677
41732
|
if (kind === "code" || kind === "class") {
|
|
39678
41733
|
return createClassMigration(description, options);
|
|
39679
41734
|
}
|
|
39680
|
-
const dir =
|
|
39681
|
-
if (!
|
|
39682
|
-
|
|
41735
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
41736
|
+
if (!existsSync27(dir)) {
|
|
41737
|
+
mkdirSync19(dir, { recursive: true });
|
|
39683
41738
|
}
|
|
39684
41739
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
39685
41740
|
const now = /* @__PURE__ */ new Date();
|
|
39686
|
-
const
|
|
41741
|
+
const timestamp2 = [
|
|
39687
41742
|
now.getFullYear(),
|
|
39688
41743
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
39689
41744
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -39691,10 +41746,10 @@ async function createMigration(description, options) {
|
|
|
39691
41746
|
String(now.getMinutes()).padStart(2, "0"),
|
|
39692
41747
|
String(now.getSeconds()).padStart(2, "0")
|
|
39693
41748
|
].join("");
|
|
39694
|
-
const upFileName = `${
|
|
39695
|
-
const downFileName = `${
|
|
39696
|
-
const upPath =
|
|
39697
|
-
const downPath =
|
|
41749
|
+
const upFileName = `${timestamp2}_${safeName}.sql`;
|
|
41750
|
+
const downFileName = `${timestamp2}_${safeName}.down.sql`;
|
|
41751
|
+
const upPath = join31(dir, upFileName);
|
|
41752
|
+
const downPath = join31(dir, downFileName);
|
|
39698
41753
|
const upTemplate = `-- Migration: ${description}
|
|
39699
41754
|
-- Created: ${now.toISOString()}
|
|
39700
41755
|
|
|
@@ -39703,19 +41758,19 @@ async function createMigration(description, options) {
|
|
|
39703
41758
|
-- Created: ${now.toISOString()}
|
|
39704
41759
|
|
|
39705
41760
|
`;
|
|
39706
|
-
|
|
39707
|
-
|
|
41761
|
+
writeFileSync17(upPath, upTemplate, "utf-8");
|
|
41762
|
+
writeFileSync17(downPath, downTemplate, "utf-8");
|
|
39708
41763
|
return { upPath, downPath };
|
|
39709
41764
|
}
|
|
39710
41765
|
async function createClassMigration(description, options) {
|
|
39711
|
-
const dir =
|
|
39712
|
-
if (!
|
|
39713
|
-
|
|
41766
|
+
const dir = resolve19(options?.migrationsDir ?? "migrations");
|
|
41767
|
+
if (!existsSync27(dir)) {
|
|
41768
|
+
mkdirSync19(dir, { recursive: true });
|
|
39714
41769
|
}
|
|
39715
41770
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
39716
41771
|
const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
39717
41772
|
const now = /* @__PURE__ */ new Date();
|
|
39718
|
-
const
|
|
41773
|
+
const timestamp2 = [
|
|
39719
41774
|
now.getFullYear(),
|
|
39720
41775
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
39721
41776
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -39723,8 +41778,8 @@ async function createClassMigration(description, options) {
|
|
|
39723
41778
|
String(now.getMinutes()).padStart(2, "0"),
|
|
39724
41779
|
String(now.getSeconds()).padStart(2, "0")
|
|
39725
41780
|
].join("");
|
|
39726
|
-
const fileName = `${
|
|
39727
|
-
const filePath =
|
|
41781
|
+
const fileName = `${timestamp2}_${safeName}.ts`;
|
|
41782
|
+
const filePath = join31(dir, fileName);
|
|
39728
41783
|
const content = `// Migration: ${description}
|
|
39729
41784
|
// Created: ${now.toISOString()}
|
|
39730
41785
|
|
|
@@ -39740,7 +41795,7 @@ export class ${className} {
|
|
|
39740
41795
|
}
|
|
39741
41796
|
}
|
|
39742
41797
|
`;
|
|
39743
|
-
|
|
41798
|
+
writeFileSync17(filePath, content, "utf-8");
|
|
39744
41799
|
return filePath;
|
|
39745
41800
|
}
|
|
39746
41801
|
var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
|
|
@@ -39821,8 +41876,8 @@ var init_migration = __esm({
|
|
|
39821
41876
|
}
|
|
39822
41877
|
/** Return sorted list of all migration files on disk (excludes .down.sql). */
|
|
39823
41878
|
getFiles() {
|
|
39824
|
-
const dir =
|
|
39825
|
-
if (!
|
|
41879
|
+
const dir = resolve19(this.dir);
|
|
41880
|
+
if (!existsSync27(dir)) return [];
|
|
39826
41881
|
return sortMigrationFiles(
|
|
39827
41882
|
readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
|
|
39828
41883
|
);
|
|
@@ -42606,8 +44661,8 @@ var init_seeder = __esm({
|
|
|
42606
44661
|
// src/docstore.ts
|
|
42607
44662
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
42608
44663
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
42609
|
-
import { mkdirSync as
|
|
42610
|
-
import { dirname as dirname14, isAbsolute as isAbsolute6, join as
|
|
44664
|
+
import { mkdirSync as mkdirSync20 } from "node:fs";
|
|
44665
|
+
import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
|
|
42611
44666
|
function iso(d) {
|
|
42612
44667
|
return d.toISOString();
|
|
42613
44668
|
}
|
|
@@ -42872,8 +44927,8 @@ function resolveStorePath(dbPath) {
|
|
|
42872
44927
|
if (dbPath === ":memory:") return dbPath;
|
|
42873
44928
|
let path8 = dbPath;
|
|
42874
44929
|
if (!isAbsolute6(path8)) {
|
|
42875
|
-
path8 =
|
|
42876
|
-
|
|
44930
|
+
path8 = join32(process.cwd(), path8);
|
|
44931
|
+
mkdirSync20(dirname14(path8), { recursive: true });
|
|
42877
44932
|
}
|
|
42878
44933
|
return path8;
|
|
42879
44934
|
}
|
|
@@ -44156,8 +46211,8 @@ var init_attachment = __esm({
|
|
|
44156
46211
|
|
|
44157
46212
|
// src/realtime/storage.ts
|
|
44158
46213
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
44159
|
-
import { mkdirSync as
|
|
44160
|
-
import { resolve as
|
|
46214
|
+
import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
46215
|
+
import { resolve as resolve20, sep as sep6 } from "node:path";
|
|
44161
46216
|
import { createRequire as createRequire8 } from "node:module";
|
|
44162
46217
|
function storageKey(filename = "") {
|
|
44163
46218
|
let ext = "";
|
|
@@ -44193,19 +46248,19 @@ var init_storage = __esm({
|
|
|
44193
46248
|
LocalStorage = class {
|
|
44194
46249
|
directory;
|
|
44195
46250
|
constructor(directory) {
|
|
44196
|
-
this.directory =
|
|
44197
|
-
|
|
46251
|
+
this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
|
|
46252
|
+
mkdirSync21(this.directory, { recursive: true });
|
|
44198
46253
|
}
|
|
44199
46254
|
// Resolve inside the root and reject any traversal attempt.
|
|
44200
46255
|
pathFor(key) {
|
|
44201
|
-
const target =
|
|
44202
|
-
if (target !== this.directory && !target.startsWith(this.directory +
|
|
46256
|
+
const target = resolve20(this.directory, key);
|
|
46257
|
+
if (target !== this.directory && !target.startsWith(this.directory + sep6)) {
|
|
44203
46258
|
throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
|
|
44204
46259
|
}
|
|
44205
46260
|
return target;
|
|
44206
46261
|
}
|
|
44207
46262
|
put(key, data) {
|
|
44208
|
-
|
|
46263
|
+
writeFileSync18(this.pathFor(key), data);
|
|
44209
46264
|
}
|
|
44210
46265
|
get(key) {
|
|
44211
46266
|
try {
|