tina4-nodejs 3.13.124 → 3.13.130

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.
@@ -1186,8 +1186,8 @@ function hashPassword(password, salt, iterations = 26e4) {
1186
1186
  }
1187
1187
  function checkPassword(password, hash) {
1188
1188
  try {
1189
- const delimiter4 = hash.includes("$") ? "$" : ":";
1190
- const parts = hash.split(delimiter4);
1189
+ const delimiter5 = hash.includes("$") ? "$" : ":";
1190
+ const parts = hash.split(delimiter5);
1191
1191
  if (parts.length !== 4 || parts[0] !== "pbkdf2_sha256") return false;
1192
1192
  const iterations = parseInt(parts[1], 10);
1193
1193
  const salt = parts[2];
@@ -6375,6 +6375,38 @@ var init_sqlTranslator = __esm({
6375
6375
  return sql;
6376
6376
  }
6377
6377
  }
6378
+ /**
6379
+ * Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
6380
+ * target engine.
6381
+ *
6382
+ * ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
6383
+ * that happens to contain the word `TEXT` (a column name, a string literal) is
6384
+ * never rewritten. Complements `autoIncrementSyntax` (which maps the id
6385
+ * keyword) so ONE portable migration — and every `Model.createTable()` DDL,
6386
+ * which is also SQLite-canonical — applies on every engine instead of failing
6387
+ * on Firebird/MSSQL.
6388
+ *
6389
+ * * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
6390
+ * EXISTS`.
6391
+ * * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
6392
+ * rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
6393
+ * * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
6394
+ * column maps to `DATETIME` (matching the adapters' createTableAsync).
6395
+ */
6396
+ static ddlTypes(sql, engine) {
6397
+ const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
6398
+ if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
6399
+ switch ((engine ?? "").toLowerCase()) {
6400
+ case "firebird":
6401
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bBLOB\s+SUB_TYPE\s+TEXT\b/gi, "\0FBTEXT\0").replace(/\bTEXT\b/gi, "BLOB SUB_TYPE TEXT").replaceAll("\0FBTEXT\0", "BLOB SUB_TYPE TEXT").replace(/\bREAL\b/gi, "DOUBLE PRECISION");
6402
+ case "mssql":
6403
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bTIMESTAMP\b/gi, "DATETIME2");
6404
+ case "mysql":
6405
+ return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
6406
+ default:
6407
+ return sql;
6408
+ }
6409
+ }
6378
6410
  /**
6379
6411
  * Convert ? placeholders to engine-specific style.
6380
6412
  *
@@ -8416,6 +8448,8 @@ var init_mysql = __esm({
8416
8448
  translateSql(sql) {
8417
8449
  let translated = SQLTranslator.concatPipesToFunc(sql);
8418
8450
  translated = SQLTranslator.ilikeToLike(translated);
8451
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
8452
+ translated = SQLTranslator.ddlTypes(translated, "mysql");
8419
8453
  return translated;
8420
8454
  }
8421
8455
  execute(sql, params) {
@@ -8837,6 +8871,9 @@ var init_mssql = __esm({
8837
8871
  let translated = SQLTranslator.limitToTop(sql);
8838
8872
  translated = SQLTranslator.concatPipesToFunc(translated);
8839
8873
  translated = SQLTranslator.ilikeToLike(translated);
8874
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
8875
+ translated = SQLTranslator.booleanToInt(translated);
8876
+ translated = SQLTranslator.ddlTypes(translated, "mssql");
8840
8877
  return translated;
8841
8878
  }
8842
8879
  execSqlPromise(sql, params) {
@@ -9467,6 +9504,8 @@ var init_firebird = __esm({
9467
9504
  let translated = SQLTranslator.limitToRows(sql);
9468
9505
  translated = SQLTranslator.booleanToInt(translated);
9469
9506
  translated = SQLTranslator.ilikeToLike(translated);
9507
+ translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
9508
+ translated = SQLTranslator.ddlTypes(translated, "firebird");
9470
9509
  return translated;
9471
9510
  }
9472
9511
  /**
@@ -12555,7 +12594,7 @@ async function removeMigrationRecord(name) {
12555
12594
  [name]
12556
12595
  );
12557
12596
  }
12558
- async function rollback(migrationsDir, delimiter4) {
12597
+ async function rollback(migrationsDir, delimiter5) {
12559
12598
  if (migrationsDir instanceof Map) {
12560
12599
  const downFunctions = migrationsDir;
12561
12600
  const migrations2 = await getLastBatchMigrations();
@@ -12574,7 +12613,7 @@ async function rollback(migrationsDir, delimiter4) {
12574
12613
  return rolledBack2;
12575
12614
  }
12576
12615
  const dir = resolve5(migrationsDir ?? "migrations");
12577
- const delim = delimiter4 ?? ";";
12616
+ const delim = delimiter5 ?? ";";
12578
12617
  const db = getAdapter();
12579
12618
  const migrations = await getLastBatchMigrations();
12580
12619
  const rolledBack = [];
@@ -12625,12 +12664,12 @@ function parseSetTerm(statement) {
12625
12664
  const m = statement.trim().match(SET_TERM_RE);
12626
12665
  return m ? m[1] : null;
12627
12666
  }
12628
- function splitStatements(sql, delimiter4 = ";") {
12667
+ function splitStatements(sql, delimiter5 = ";") {
12629
12668
  sql = normalizeQuotes(sql);
12630
12669
  const statements = [];
12631
12670
  let current = "";
12632
12671
  const n = sql.length;
12633
- let dlen = delimiter4.length;
12672
+ let dlen = delimiter5.length;
12634
12673
  let i = 0;
12635
12674
  let inDollarBlock = false;
12636
12675
  let inSlashBlock = false;
@@ -12699,15 +12738,15 @@ function splitStatements(sql, delimiter4 = ";") {
12699
12738
  }
12700
12739
  continue;
12701
12740
  }
12702
- if (dlen > 0 && sql.startsWith(delimiter4, i)) {
12741
+ if (dlen > 0 && sql.startsWith(delimiter5, i)) {
12703
12742
  i += dlen;
12704
12743
  const stmt2 = current.trim();
12705
12744
  current = "";
12706
12745
  if (stmt2) {
12707
12746
  const newTerm = parseSetTerm(stmt2);
12708
12747
  if (newTerm !== null) {
12709
- delimiter4 = newTerm;
12710
- dlen = delimiter4.length;
12748
+ delimiter5 = newTerm;
12749
+ dlen = delimiter5.length;
12711
12750
  } else {
12712
12751
  statements.push(stmt2);
12713
12752
  }
@@ -12746,7 +12785,7 @@ function warnUnprefixedMigrations(files) {
12746
12785
  async function migrate(adapter, options) {
12747
12786
  const db = adapter ?? getAdapter();
12748
12787
  const dir = resolve5(options?.migrationsDir ?? "migrations");
12749
- const delimiter4 = options?.delimiter ?? ";";
12788
+ const delimiter5 = options?.delimiter ?? ";";
12750
12789
  const result = { applied: [], skipped: [], failed: [] };
12751
12790
  if (!existsSync6(dir)) {
12752
12791
  return result;
@@ -12789,7 +12828,7 @@ async function migrate(adapter, options) {
12789
12828
  result.skipped.push(file);
12790
12829
  continue;
12791
12830
  }
12792
- const statements = splitStatements(sqlContent, delimiter4);
12831
+ const statements = splitStatements(sqlContent, delimiter5);
12793
12832
  try {
12794
12833
  await adapterStartTransaction(db);
12795
12834
  for (const stmt of statements) {
@@ -15418,7 +15457,7 @@ function mulberry32(seed) {
15418
15457
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
15419
15458
  };
15420
15459
  }
15421
- var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, FakeData;
15460
+ var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, PRODUCT_ADJECTIVES, PRODUCT_NOUNS, FakeData;
15422
15461
  var init_fakeData = __esm({
15423
15462
  "../core/src/fakeData.ts"() {
15424
15463
  "use strict";
@@ -15680,6 +15719,59 @@ var init_fakeData = __esm({
15680
15719
  "ZAR",
15681
15720
  "INR"
15682
15721
  ];
15722
+ PRODUCT_ADJECTIVES = [
15723
+ "Wireless",
15724
+ "Organic",
15725
+ "Premium",
15726
+ "Classic",
15727
+ "Eco",
15728
+ "Smart",
15729
+ "Portable",
15730
+ "Deluxe",
15731
+ "Compact",
15732
+ "Rustic",
15733
+ "Handcrafted",
15734
+ "Vintage",
15735
+ "Modern",
15736
+ "Ergonomic",
15737
+ "Stainless",
15738
+ "Bamboo",
15739
+ "Recycled",
15740
+ "Artisan",
15741
+ "Professional",
15742
+ "Ultra",
15743
+ "Insulated",
15744
+ "Lightweight",
15745
+ "Adjustable",
15746
+ "Foldable"
15747
+ ];
15748
+ PRODUCT_NOUNS = [
15749
+ "Keyboard",
15750
+ "Coffee Beans",
15751
+ "Backpack",
15752
+ "Water Bottle",
15753
+ "Desk Lamp",
15754
+ "Headphones",
15755
+ "Notebook",
15756
+ "Sneakers",
15757
+ "Sunglasses",
15758
+ "Wallet",
15759
+ "Mug",
15760
+ "Chair",
15761
+ "Blender",
15762
+ "Speaker",
15763
+ "Charger",
15764
+ "Umbrella",
15765
+ "Toothbrush",
15766
+ "Jacket",
15767
+ "Watch",
15768
+ "Kettle",
15769
+ "Picture Frame",
15770
+ "Planter",
15771
+ "Cutlery Set",
15772
+ "Yoga Mat",
15773
+ "Phone Case"
15774
+ ];
15683
15775
  FakeData = class _FakeData {
15684
15776
  rng;
15685
15777
  seeded;
@@ -15751,6 +15843,16 @@ var init_fakeData = __esm({
15751
15843
  jobTitle() {
15752
15844
  return this.pick(JOB_TITLES);
15753
15845
  }
15846
+ /**
15847
+ * A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
15848
+ * Beans" — an adjective + noun from the product vocabulary. Deterministic
15849
+ * under a seed like every other generator (draws from the same instance
15850
+ * PRNG). Used to seed a generic `name` column on a product-ish table instead
15851
+ * of a person name (see the ORM FakeData `forField` heuristic).
15852
+ */
15853
+ product() {
15854
+ return `${this.pick(PRODUCT_ADJECTIVES)} ${this.pick(PRODUCT_NOUNS)}`;
15855
+ }
15754
15856
  paragraph(sentences = 4) {
15755
15857
  const parts = [];
15756
15858
  for (let i = 0; i < sentences; i++) {
@@ -15874,11 +15976,27 @@ var init_fakeData = __esm({
15874
15976
  });
15875
15977
 
15876
15978
  // ../orm/src/fakeData.ts
15877
- var FakeData2;
15979
+ function isProductTable(table2) {
15980
+ const t = (table2 ?? "").toLowerCase();
15981
+ return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
15982
+ }
15983
+ var PRODUCT_TABLE_HINTS, FakeData2;
15878
15984
  var init_fakeData2 = __esm({
15879
15985
  "../orm/src/fakeData.ts"() {
15880
15986
  "use strict";
15881
15987
  init_fakeData();
15988
+ PRODUCT_TABLE_HINTS = [
15989
+ "product",
15990
+ "item",
15991
+ "catalog",
15992
+ "inventory",
15993
+ "goods",
15994
+ "merchandise",
15995
+ "sku",
15996
+ "listing",
15997
+ "stock",
15998
+ "ware"
15999
+ ];
15882
16000
  FakeData2 = class extends FakeData {
15883
16001
  constructor(seed) {
15884
16002
  super(seed);
@@ -15900,8 +16018,11 @@ var init_fakeData2 = __esm({
15900
16018
  *
15901
16019
  * @param fieldDef - An ORM FieldDefinition object
15902
16020
  * @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
16021
+ * @param table - Optional table/model name. A generic `name`/`full_name`
16022
+ * column on a product-ish table (see {@link isProductTable}) gets a
16023
+ * product name; with no table context it stays a person name (back-compat).
15903
16024
  */
15904
- forField(fieldDef, columnName) {
16025
+ forField(fieldDef, columnName, table2) {
15905
16026
  if (fieldDef.primaryKey && fieldDef.autoIncrement) {
15906
16027
  return void 0;
15907
16028
  }
@@ -15911,7 +16032,9 @@ var init_fakeData2 = __esm({
15911
16032
  const col = (columnName ?? "").toLowerCase();
15912
16033
  if (col.includes("email")) return this.email();
15913
16034
  if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
15914
- if (col === "name" || col === "full_name" || col === "fullname") return this.name();
16035
+ if (col === "name" || col === "full_name" || col === "fullname") {
16036
+ return isProductTable(table2) ? this.product() : this.name();
16037
+ }
15915
16038
  if (col === "first_name" || col === "firstname") return this.firstName();
15916
16039
  if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
15917
16040
  if (col.includes("address")) return this.address();
@@ -15998,7 +16121,7 @@ async function autoFieldMap(db, table2, fake = new FakeData2()) {
15998
16121
  continue;
15999
16122
  }
16000
16123
  const fieldType = sqlTypeToFieldType(sqlType);
16001
- fieldMap[name] = () => fake.forField({ type: fieldType }, name);
16124
+ fieldMap[name] = () => fake.forField({ type: fieldType }, name, table2);
16002
16125
  }
16003
16126
  return fieldMap;
16004
16127
  }
@@ -16144,7 +16267,7 @@ async function seedOrm(ormClass, count = 10, overrides, seed, opts, fkPools) {
16144
16267
  } else if (pools[name] && pools[name].length > 0) {
16145
16268
  attrs[name] = fake.choice(pools[name]);
16146
16269
  } else {
16147
- attrs[name] = fake.forField(def, name);
16270
+ attrs[name] = fake.forField(def, name, modelName);
16148
16271
  }
16149
16272
  }
16150
16273
  validateTypes(fields, attrs, modelName);
@@ -21179,14 +21302,14 @@ function extractBoundary(contentType) {
21179
21302
  function parseMultipart(body, boundary) {
21180
21303
  const fields = {};
21181
21304
  const files = {};
21182
- const delimiter4 = Buffer.from(`--${boundary}`);
21305
+ const delimiter5 = Buffer.from(`--${boundary}`);
21183
21306
  const closeDelimiter = Buffer.from(`--${boundary}--`);
21184
21307
  const crlf = Buffer.from("\r\n");
21185
21308
  const doubleCrlf = Buffer.from("\r\n\r\n");
21186
21309
  let offset = 0;
21187
- const firstIdx = bufferIndexOf(body, delimiter4, offset);
21310
+ const firstIdx = bufferIndexOf(body, delimiter5, offset);
21188
21311
  if (firstIdx === -1) return { fields, files };
21189
- offset = firstIdx + delimiter4.length;
21312
+ offset = firstIdx + delimiter5.length;
21190
21313
  if (body[offset] === 13 && body[offset + 1] === 10) {
21191
21314
  offset += 2;
21192
21315
  }
@@ -21195,7 +21318,7 @@ function parseMultipart(body, boundary) {
21195
21318
  if (headersEnd === -1) break;
21196
21319
  const headersStr = body.subarray(offset, headersEnd).toString("utf-8");
21197
21320
  offset = headersEnd + doubleCrlf.length;
21198
- const nextDelimIdx = bufferIndexOf(body, delimiter4, offset);
21321
+ const nextDelimIdx = bufferIndexOf(body, delimiter5, offset);
21199
21322
  if (nextDelimIdx === -1) break;
21200
21323
  const contentEnd = nextDelimIdx - crlf.length;
21201
21324
  const content = body.subarray(offset, contentEnd);
@@ -21218,7 +21341,7 @@ function parseMultipart(body, boundary) {
21218
21341
  } else if (disposition.name) {
21219
21342
  fields[disposition.name] = content.toString("utf-8");
21220
21343
  }
21221
- offset = nextDelimIdx + delimiter4.length;
21344
+ offset = nextDelimIdx + delimiter5.length;
21222
21345
  if (body[offset] === 45 && body[offset + 1] === 45) {
21223
21346
  break;
21224
21347
  }
@@ -24391,6 +24514,7 @@ __export(generate_exports, {
24391
24514
  parseEvery: () => parseEvery,
24392
24515
  parseFields: () => parseFields,
24393
24516
  pluralizeReserved: () => pluralizeReserved,
24517
+ resolveTable: () => resolveTable,
24394
24518
  toPascal: () => toPascal,
24395
24519
  toSnake: () => toSnake,
24396
24520
  toTableName: () => toTableName
@@ -24433,12 +24557,35 @@ function toTableName(name) {
24433
24557
  from: raw,
24434
24558
  to: safe,
24435
24559
  reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
24436
- override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
24560
+ override: `--table-name <name> (table names interpolate unquoted; forcing a reserved name is yours to quote in raw SQL)`
24437
24561
  });
24438
24562
  return safe;
24439
24563
  }
24440
24564
  return raw;
24441
24565
  }
24566
+ function resolveTable(name, flags, opts = {}) {
24567
+ const announce = opts.announce ?? false;
24568
+ const override = (flags ?? {})["table-name"];
24569
+ if (typeof override === "string" && override) {
24570
+ if (announce && SQL_RESERVED_TABLE_NAMES.has(toSnake(override))) {
24571
+ console.error(
24572
+ ` ! table_name '${override}' is a SQL reserved word. Tina4 interpolates table names UNQUOTED, so the ORM's generated SQL will fail on it -- quote it yourself in raw SQL and migrations.`
24573
+ );
24574
+ }
24575
+ return override;
24576
+ }
24577
+ const bare = toSnake(name);
24578
+ if (!SQL_RESERVED_TABLE_NAMES.has(bare)) {
24579
+ return bare;
24580
+ }
24581
+ const table2 = toTableName(name);
24582
+ if (announce) {
24583
+ console.error(
24584
+ ` \xB7 '${bare}' is a SQL reserved word; using table_name '${table2}' (Tina4 interpolates table names unquoted). Override with --table-name <name>.`
24585
+ );
24586
+ }
24587
+ return table2;
24588
+ }
24442
24589
  function resetResolution(target, input, opts) {
24443
24590
  __resolution.target = target;
24444
24591
  __resolution.input = input;
@@ -24537,10 +24684,11 @@ function printResolution() {
24537
24684
  lines.push(` migration ${b.migration_path}`);
24538
24685
  }
24539
24686
  const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
24540
- if (reserved && reserved.from && reserved.override) {
24687
+ if (reserved && reserved.from) {
24541
24688
  lines.push("");
24542
- lines.push(` To keep the raw name '${reserved.from}' as the table:`);
24543
- lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
24689
+ lines.push(` To set the table name yourself:`);
24690
+ lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} --table-name <name>`);
24691
+ lines.push(` Tina4 interpolates table names unquoted; if you force the reserved '${reserved.from}', you own the quoting in raw SQL.`);
24544
24692
  }
24545
24693
  if (b.test_paths && b.test_paths.length > 0) {
24546
24694
  lines.push("");
@@ -24676,6 +24824,7 @@ async function generate(what, name, extraArgs = []) {
24676
24824
  console.error(" Usage: tina4nodejs generate <what> <name> [options]");
24677
24825
  console.error(` Generators: ${GENERATOR_LIST}`);
24678
24826
  console.error(' Options: --fields "name:string,price:float" --model ModelName');
24827
+ console.error(" --table-name <name> force the model's table name (else derived; reserved words auto-pluralise)");
24679
24828
  console.error(" --public open a route's writes (default: secure)");
24680
24829
  console.error(' --every 5m | --cron "\u2026" service schedule');
24681
24830
  console.error(" --json emit machine-readable resolution envelope on stdout");
@@ -24726,7 +24875,7 @@ async function generateProgrammatic(what, name, extraArgs = []) {
24726
24875
  }
24727
24876
  function generateModel(name, flags, emitTest = true) {
24728
24877
  const fields = fieldsOrDefault(flags.fields || "");
24729
- const table2 = toTableName(name);
24878
+ const table2 = resolveTable(name, flags, { announce: true });
24730
24879
  const dir = resolve11("src/models");
24731
24880
  ensureDir(dir);
24732
24881
  const path8 = join21(dir, `${name}.ts`);
@@ -24777,7 +24926,7 @@ function generateRoute(name, flags, emitTest = true) {
24777
24926
  if (__resolution.target === "route") {
24778
24927
  setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
24779
24928
  }
24780
- const table2 = model ? toTableName(model) : "";
24929
+ const table2 = model ? resolveTable(model, flags, { announce: false }) : "";
24781
24930
  const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
24782
24931
  ` : "";
24783
24932
  const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
@@ -24998,7 +25147,7 @@ ${aiFill(`delete_${singular}`, {
24998
25147
  }
24999
25148
  }
25000
25149
  function generateCrud(name, flags) {
25001
- const table2 = toTableName(name);
25150
+ const table2 = resolveTable(name, flags, { announce: false });
25002
25151
  const routeName = toPlural(table2);
25003
25152
  const isPublic = Boolean(flags.public);
25004
25153
  if (!__resolution.jsonMode) console.log(`
@@ -25025,7 +25174,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
25025
25174
  table2 = tableOverride;
25026
25175
  } else {
25027
25176
  const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
25028
- table2 = toTableName(raw);
25177
+ table2 = resolveTable(raw, flags, { announce: false });
25029
25178
  }
25030
25179
  if (__resolution.target === "migration") {
25031
25180
  setResolutionField("table_name", table2);
@@ -25048,7 +25197,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
25048
25197
  const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
25049
25198
  colLines.push(` ${fname} ${info.sql}${defaultClause}`);
25050
25199
  }
25051
- colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
25200
+ colLines.push(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
25052
25201
  upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
25053
25202
  -- tina4:edit add columns beyond id + created_at
25054
25203
  ${colLines.join(",\n")}
@@ -25251,7 +25400,7 @@ void test${titleName};
25251
25400
  }
25252
25401
  function generateForm(name, flags) {
25253
25402
  const fields = fieldsOrDefault(flags.fields || "");
25254
- const table2 = toTableName(name);
25403
+ const table2 = resolveTable(name, flags, { announce: false });
25255
25404
  const routeName = toPlural(table2);
25256
25405
  const inputTypes = {
25257
25406
  string: "text",
@@ -25317,7 +25466,7 @@ function generateForm(name, flags) {
25317
25466
  }
25318
25467
  function generateView(name, flags) {
25319
25468
  const fields = fieldsOrDefault(flags.fields || "");
25320
- const table2 = toTableName(name);
25469
+ const table2 = resolveTable(name, flags, { announce: false });
25321
25470
  const routeName = toPlural(table2);
25322
25471
  const cols = fields.map(([f]) => f);
25323
25472
  const dir = resolve11("src/templates/pages");
@@ -25672,8 +25821,8 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
25672
25821
  writeFileSafe(path8, content);
25673
25822
  emitValidatorTest(name, toSnake(name), toPascal(name));
25674
25823
  }
25675
- function generateSeeder(name, _flags) {
25676
- const table2 = toTableName(name);
25824
+ function generateSeeder(name, flags) {
25825
+ const table2 = resolveTable(name, flags, { announce: false });
25677
25826
  const dir = resolve11("src/seeds");
25678
25827
  ensureDir(dir);
25679
25828
  const path8 = join21(dir, `${table2}_seeder.ts`);
@@ -26191,8 +26340,8 @@ var init_generate = __esm({
26191
26340
  "src/commands/generate.ts"() {
26192
26341
  "use strict";
26193
26342
  FIELD_TYPE_MAP = {
26194
- string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26195
- str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26343
+ string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
26344
+ str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
26196
26345
  int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
26197
26346
  integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
26198
26347
  float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
@@ -26202,7 +26351,7 @@ var init_generate = __esm({
26202
26351
  bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
26203
26352
  boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
26204
26353
  text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26205
- datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
26354
+ datetime: { orm: '"datetime"', sql: "TIMESTAMP", defaultVal: "NULL" },
26206
26355
  blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
26207
26356
  };
26208
26357
  SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
@@ -26298,7 +26447,7 @@ var init_generate = __esm({
26298
26447
  TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
26299
26448
  DEFAULT_FIELDS = [["name", "string"]];
26300
26449
  GENERATORS = {
26301
- model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
26450
+ model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
26302
26451
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
26303
26452
  crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
26304
26453
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
@@ -36995,23 +37144,23 @@ var init_devAdmin = __esm({
36995
37144
  });
36996
37145
  };
36997
37146
  handleDevAdminJs = async (_req, res) => {
36998
- const { readFileSync: readFileSync29, existsSync: existsSync36 } = await import("node:fs");
36999
- const { dirname: dirname17, join: join39, resolve: resolve29 } = await import("node:path");
37147
+ const { readFileSync: readFileSync30, existsSync: existsSync37 } = await import("node:fs");
37148
+ const { dirname: dirname18, join: join40, resolve: resolve29 } = await import("node:path");
37000
37149
  const { fileURLToPath: fileURLToPath10 } = await import("node:url");
37001
- const dir = dirname17(fileURLToPath10(import.meta.url));
37150
+ const dir = dirname18(fileURLToPath10(import.meta.url));
37002
37151
  const candidates = [
37003
- join39(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
37152
+ join40(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
37004
37153
  // src/../public/js/
37005
- join39(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
37154
+ join40(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
37006
37155
  // deeper nesting
37007
37156
  resolve29(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
37008
37157
  resolve29(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
37009
37158
  // project public/
37010
37159
  ];
37011
37160
  for (const jsPath of candidates) {
37012
- if (existsSync36(jsPath)) {
37161
+ if (existsSync37(jsPath)) {
37013
37162
  try {
37014
- const content = readFileSync29(jsPath, "utf-8");
37163
+ const content = readFileSync30(jsPath, "utf-8");
37015
37164
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
37016
37165
  res.raw.end(content);
37017
37166
  return;
@@ -40888,23 +41037,23 @@ function guessContentType(filename) {
40888
41037
  }
40889
41038
  function buildMultipartBody(boundary, fieldName, filename, fileContent, contentType, extraFields) {
40890
41039
  const crlf = "\r\n";
40891
- const delimiter4 = `--${boundary}`;
41040
+ const delimiter5 = `--${boundary}`;
40892
41041
  const parts = [];
40893
41042
  if (extraFields) {
40894
41043
  for (const [key, value] of Object.entries(extraFields)) {
40895
- parts.push(Buffer.from(delimiter4 + crlf, "utf-8"));
41044
+ parts.push(Buffer.from(delimiter5 + crlf, "utf-8"));
40896
41045
  parts.push(Buffer.from(`Content-Disposition: form-data; name="${key}"` + crlf + crlf, "utf-8"));
40897
41046
  parts.push(Buffer.from(String(value) + crlf, "utf-8"));
40898
41047
  }
40899
41048
  }
40900
- parts.push(Buffer.from(delimiter4 + crlf, "utf-8"));
41049
+ parts.push(Buffer.from(delimiter5 + crlf, "utf-8"));
40901
41050
  parts.push(
40902
41051
  Buffer.from(`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"` + crlf, "utf-8")
40903
41052
  );
40904
41053
  parts.push(Buffer.from(`Content-Type: ${contentType}` + crlf + crlf, "utf-8"));
40905
41054
  parts.push(fileContent);
40906
41055
  parts.push(Buffer.from(crlf, "utf-8"));
40907
- parts.push(Buffer.from(delimiter4 + "--" + crlf, "utf-8"));
41056
+ parts.push(Buffer.from(delimiter5 + "--" + crlf, "utf-8"));
40908
41057
  return Buffer.concat(parts);
40909
41058
  }
40910
41059
  async function* parseLineStream(chunks) {
@@ -47461,8 +47610,8 @@ async function runTests(testPath) {
47461
47610
  console.log(` Found ${testFiles.length} test file(s)
47462
47611
  `);
47463
47612
  for (const file of testFiles) {
47464
- const relative11 = file.replace(cwd + "/", "");
47465
- console.log(` Running: ${relative11}`);
47613
+ const relative12 = file.replace(cwd + "/", "");
47614
+ console.log(` Running: ${relative12}`);
47466
47615
  try {
47467
47616
  execSync3(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
47468
47617
  } catch {
@@ -47472,19 +47621,233 @@ async function runTests(testPath) {
47472
47621
  process.exit(inlineFailed || fileFailed ? 1 : 0);
47473
47622
  }
47474
47623
 
47624
+ // src/commands/lint.ts
47625
+ import { existsSync as existsSync33, readdirSync as readdirSync20, readFileSync as readFileSync28, statSync as statSync20, writeFileSync as writeFileSync20 } from "node:fs";
47626
+ import { createRequire as createRequire9 } from "node:module";
47627
+ import { delimiter as delimiter2, dirname as dirname16, join as join35, relative as relative11 } from "node:path";
47628
+ import { spawnSync as spawnSync3 } from "node:child_process";
47629
+ var LINT_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
47630
+ var JS_EXTENSIONS = [".js", ".mjs", ".cjs"];
47631
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
47632
+ function isLintFile(name) {
47633
+ if (name.endsWith(".d.ts")) return false;
47634
+ return LINT_EXTENSIONS.some((ext) => name.endsWith(ext));
47635
+ }
47636
+ function walkSource2(dir, out) {
47637
+ let entries;
47638
+ try {
47639
+ entries = readdirSync20(dir);
47640
+ } catch {
47641
+ return;
47642
+ }
47643
+ for (const name of entries) {
47644
+ if (SKIP_DIRS3.has(name)) continue;
47645
+ const full = join35(dir, name);
47646
+ let st;
47647
+ try {
47648
+ st = statSync20(full);
47649
+ } catch {
47650
+ continue;
47651
+ }
47652
+ if (st.isDirectory()) {
47653
+ walkSource2(full, out);
47654
+ } else if (isLintFile(name)) {
47655
+ out.push(full);
47656
+ }
47657
+ }
47658
+ }
47659
+ function collectAppFiles(cwd) {
47660
+ const files = [];
47661
+ const srcDir = join35(cwd, "src");
47662
+ try {
47663
+ if (statSync20(srcDir).isDirectory()) walkSource2(srcDir, files);
47664
+ } catch {
47665
+ }
47666
+ for (const entry of ["app.ts", "app.mts", "app.js", "app.mjs"]) {
47667
+ const p = join35(cwd, entry);
47668
+ try {
47669
+ if (statSync20(p).isFile()) files.push(p);
47670
+ } catch {
47671
+ }
47672
+ }
47673
+ return files;
47674
+ }
47675
+ var ESLINT_FLAT_CONFIGS = [
47676
+ "eslint.config.js",
47677
+ "eslint.config.mjs",
47678
+ "eslint.config.cjs",
47679
+ "eslint.config.ts",
47680
+ "eslint.config.mts",
47681
+ "eslint.config.cts"
47682
+ ];
47683
+ var ESLINT_SCAFFOLD_FILE = "eslint.config.js";
47684
+ var ESLINT_SCAFFOLD = `import js from "@eslint/js";
47685
+ import tseslint from "typescript-eslint";
47686
+ export default [js.configs.recommended, ...tseslint.configs.recommended];
47687
+ `;
47688
+ function findEslintConfig(cwd) {
47689
+ for (const name of ESLINT_FLAT_CONFIGS) {
47690
+ const p = join35(cwd, name);
47691
+ if (existsSync33(p)) return p;
47692
+ }
47693
+ return null;
47694
+ }
47695
+ function resolvePackageBin(cwd, pkg, binRelative) {
47696
+ const direct = join35(cwd, "node_modules", pkg, binRelative);
47697
+ if (existsSync33(direct)) return direct;
47698
+ const require3 = createRequire9(join35(cwd, "package.json"));
47699
+ let entry;
47700
+ try {
47701
+ entry = require3.resolve(pkg);
47702
+ } catch {
47703
+ return null;
47704
+ }
47705
+ let dir = dirname16(entry);
47706
+ for (let i = 0; i < 8; i++) {
47707
+ const pkgJson = join35(dir, "package.json");
47708
+ if (existsSync33(pkgJson)) {
47709
+ try {
47710
+ if (JSON.parse(readFileSync28(pkgJson, "utf-8")).name === pkg) {
47711
+ const bin = join35(dir, binRelative);
47712
+ return existsSync33(bin) ? bin : null;
47713
+ }
47714
+ } catch {
47715
+ }
47716
+ }
47717
+ const parent = dirname16(dir);
47718
+ if (parent === dir) break;
47719
+ dir = parent;
47720
+ }
47721
+ return null;
47722
+ }
47723
+ function resolveEslintBin(cwd) {
47724
+ return resolvePackageBin(cwd, "eslint", "bin/eslint.js");
47725
+ }
47726
+ function hasPackage(cwd, pkg) {
47727
+ return existsSync33(join35(cwd, "node_modules", ...pkg.split("/"), "package.json"));
47728
+ }
47729
+ function resolveNpm() {
47730
+ const windows = process.platform === "win32";
47731
+ const names = windows ? ["npm.cmd", "npm.exe", "npm"] : ["npm"];
47732
+ for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
47733
+ if (!dir) continue;
47734
+ for (const name of names) {
47735
+ const candidate = join35(dir, name);
47736
+ try {
47737
+ if (statSync20(candidate).isFile()) return candidate;
47738
+ } catch {
47739
+ }
47740
+ }
47741
+ }
47742
+ return null;
47743
+ }
47744
+ function runLint(args) {
47745
+ const fix = args.includes("--fix");
47746
+ const noInstall = args.includes("--no-install");
47747
+ const cwd = process.cwd();
47748
+ const files = collectAppFiles(cwd);
47749
+ if (files.length === 0) {
47750
+ console.log(" lint: nothing to lint (no src/ or app.ts).");
47751
+ process.exit(0);
47752
+ }
47753
+ let eslintBin = resolveEslintBin(cwd);
47754
+ let eslintConfig = findEslintConfig(cwd);
47755
+ if (!noInstall && (!eslintBin || !eslintConfig)) {
47756
+ const npm = resolveNpm();
47757
+ if (!npm) {
47758
+ console.log(" \xB7 npm not found \u2014 using the zero-dependency baseline.");
47759
+ } else {
47760
+ if (!eslintBin) {
47761
+ console.log(" \xB7 installing eslint (npm i -D eslint @eslint/js typescript-eslint)...");
47762
+ const rc = spawnSync3(npm, ["install", "-D", "eslint", "@eslint/js", "typescript-eslint"], {
47763
+ cwd,
47764
+ stdio: "inherit"
47765
+ }).status;
47766
+ if (rc === 0) {
47767
+ eslintBin = resolveEslintBin(cwd);
47768
+ } else {
47769
+ console.log(" \xB7 could not install eslint \u2014 using the zero-dependency baseline.");
47770
+ }
47771
+ }
47772
+ if (eslintBin && !eslintConfig && hasPackage(cwd, "@eslint/js") && hasPackage(cwd, "typescript-eslint")) {
47773
+ const scaffold = join35(cwd, ESLINT_SCAFFOLD_FILE);
47774
+ try {
47775
+ writeFileSync20(scaffold, ESLINT_SCAFFOLD, "utf-8");
47776
+ eslintConfig = scaffold;
47777
+ console.log(` \xB7 scaffolded ${ESLINT_SCAFFOLD_FILE} (@eslint/js + typescript-eslint recommended).`);
47778
+ } catch (err) {
47779
+ console.log(
47780
+ ` \xB7 could not scaffold ${ESLINT_SCAFFOLD_FILE} (${err instanceof Error ? err.message : String(err)}).`
47781
+ );
47782
+ }
47783
+ }
47784
+ }
47785
+ }
47786
+ if (eslintBin && eslintConfig) {
47787
+ const label = fix ? "eslint --fix" : "eslint";
47788
+ const eslintArgs = [eslintBin, ...files, ...fix ? ["--fix"] : []];
47789
+ const code = spawnSync3(process.execPath, eslintArgs, { cwd, stdio: "inherit" }).status ?? 1;
47790
+ if (code !== 0) {
47791
+ console.log(` \u2717 lint failed \u2014 ${files.length} file(s) [${label}]`);
47792
+ process.exit(1);
47793
+ }
47794
+ console.log(` \u2713 lint clean \u2014 ${files.length} file(s) [${label}]`);
47795
+ process.exit(0);
47796
+ }
47797
+ if (fix) {
47798
+ console.log(" \xB7 --fix needs eslint \u2014 the baseline check has no autofix.");
47799
+ }
47800
+ const hasTsconfig = existsSync33(join35(cwd, "tsconfig.json"));
47801
+ const tscBin = hasTsconfig ? resolvePackageBin(cwd, "typescript", "bin/tsc") : null;
47802
+ if (tscBin) {
47803
+ const code = spawnSync3(process.execPath, [tscBin, "--noEmit"], { cwd, stdio: "inherit" }).status ?? 1;
47804
+ if (code !== 0) {
47805
+ console.log(` \u2717 lint failed \u2014 ${files.length} file(s) [tsc]`);
47806
+ process.exit(1);
47807
+ }
47808
+ console.log(` \u2713 lint clean \u2014 ${files.length} file(s) [tsc]`);
47809
+ process.exit(0);
47810
+ }
47811
+ const jsFiles = files.filter((f) => JS_EXTENSIONS.some((ext) => f.endsWith(ext)));
47812
+ if (jsFiles.length === 0) {
47813
+ console.log(
47814
+ " lint: no JavaScript files to check \u2014 add tsconfig.json + typescript to type-check .ts files."
47815
+ );
47816
+ process.exit(0);
47817
+ }
47818
+ let syntaxErrors = 0;
47819
+ for (const file of jsFiles) {
47820
+ const result = spawnSync3(process.execPath, ["--check", file], { cwd, encoding: "utf-8" });
47821
+ if ((result.status ?? 1) !== 0) {
47822
+ const stderr = (result.stderr || "").trim();
47823
+ const detail = stderr.split("\n").reverse().find((line) => line.includes("Error:")) || "syntax error";
47824
+ console.log(` \u2717 ${relative11(cwd, file)}: ${detail.trim()}`);
47825
+ syntaxErrors++;
47826
+ }
47827
+ }
47828
+ if (syntaxErrors > 0) {
47829
+ console.log(
47830
+ ` \u2717 lint failed \u2014 ${syntaxErrors} syntax error(s) in ${jsFiles.length} file(s) [node --check]`
47831
+ );
47832
+ process.exit(1);
47833
+ }
47834
+ console.log(` \u2713 lint clean \u2014 ${jsFiles.length} file(s) [node --check]`);
47835
+ process.exit(0);
47836
+ }
47837
+
47475
47838
  // src/bin.ts
47476
47839
  init_generate();
47477
47840
 
47478
47841
  // src/commands/seed.ts
47479
- import { existsSync as existsSync33, readdirSync as readdirSync20 } from "node:fs";
47480
- import { resolve as resolve28, join as join35 } from "node:path";
47842
+ import { existsSync as existsSync34, readdirSync as readdirSync21 } from "node:fs";
47843
+ import { resolve as resolve28, join as join36 } from "node:path";
47481
47844
  import { execSync as execSync4 } from "node:child_process";
47482
47845
  async function runSeeds(seedPath) {
47483
47846
  const cwd = process.cwd();
47484
47847
  const seedDir = resolve28(cwd, "src/seeds");
47485
47848
  if (seedPath) {
47486
47849
  const file = resolve28(seedPath);
47487
- if (!existsSync33(file)) {
47850
+ if (!existsSync34(file)) {
47488
47851
  console.error(` Error: Seed file not found: ${seedPath}`);
47489
47852
  process.exit(1);
47490
47853
  }
@@ -47497,14 +47860,14 @@ async function runSeeds(seedPath) {
47497
47860
  }
47498
47861
  return;
47499
47862
  }
47500
- if (!existsSync33(seedDir)) {
47863
+ if (!existsSync34(seedDir)) {
47501
47864
  console.log(" No seeds directory found.");
47502
47865
  console.log(" Create seed files in src/seeds/ (e.g. src/seeds/001-users.ts)");
47503
47866
  return;
47504
47867
  }
47505
47868
  let seedFiles;
47506
47869
  try {
47507
- seedFiles = readdirSync20(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join35(seedDir, f));
47870
+ seedFiles = readdirSync21(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join36(seedDir, f));
47508
47871
  } catch {
47509
47872
  console.log(" Could not read src/seeds/ directory.");
47510
47873
  return;
@@ -47517,8 +47880,8 @@ async function runSeeds(seedPath) {
47517
47880
  `);
47518
47881
  let failed = false;
47519
47882
  for (const file of seedFiles) {
47520
- const relative11 = file.replace(cwd + "/", "");
47521
- console.log(` Seeding: ${relative11}`);
47883
+ const relative12 = file.replace(cwd + "/", "");
47884
+ console.log(` Seeding: ${relative12}`);
47522
47885
  try {
47523
47886
  execSync4(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
47524
47887
  } catch {
@@ -47535,8 +47898,8 @@ async function runSeeds(seedPath) {
47535
47898
  // src/commands/queue.ts
47536
47899
  init_dotenv();
47537
47900
  init_queue();
47538
- import { readdirSync as readdirSync21, statSync as statSync20 } from "node:fs";
47539
- import { extname as extname8, join as join36 } from "node:path";
47901
+ import { readdirSync as readdirSync22, statSync as statSync21 } from "node:fs";
47902
+ import { extname as extname8, join as join37 } from "node:path";
47540
47903
  import { pathToFileURL as pathToFileURL3 } from "node:url";
47541
47904
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
47542
47905
  function parseFlags(args) {
@@ -47567,8 +47930,8 @@ function parseFlags(args) {
47567
47930
  async function resolveQueueHandler(servicesDir, topic) {
47568
47931
  let entries;
47569
47932
  try {
47570
- if (!statSync20(servicesDir).isDirectory()) return null;
47571
- entries = readdirSync21(servicesDir);
47933
+ if (!statSync21(servicesDir).isDirectory()) return null;
47934
+ entries = readdirSync22(servicesDir);
47572
47935
  } catch {
47573
47936
  return null;
47574
47937
  }
@@ -47576,9 +47939,9 @@ async function resolveQueueHandler(servicesDir, topic) {
47576
47939
  if (entry.startsWith("_")) continue;
47577
47940
  const ext = extname8(entry);
47578
47941
  if (ext !== ".ts" && ext !== ".js") continue;
47579
- const fullPath = join36(servicesDir, entry);
47942
+ const fullPath = join37(servicesDir, entry);
47580
47943
  try {
47581
- if (!statSync20(fullPath).isFile()) continue;
47944
+ if (!statSync21(fullPath).isFile()) continue;
47582
47945
  const mod = await import(pathToFileURL3(fullPath).href);
47583
47946
  const config = mod.default ?? mod;
47584
47947
  if (config && typeof config === "object" && config.topic === topic && typeof config.handle === "function") {
@@ -47728,9 +48091,9 @@ async function queueCommand(args = []) {
47728
48091
  }
47729
48092
 
47730
48093
  // src/commands/build.ts
47731
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync34, statSync as statSync21 } from "node:fs";
47732
- import { basename as basename8, delimiter as delimiter2, join as join37 } from "node:path";
47733
- import { spawnSync as spawnSync3 } from "node:child_process";
48094
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync35, statSync as statSync22 } from "node:fs";
48095
+ import { basename as basename8, delimiter as delimiter3, join as join38 } from "node:path";
48096
+ import { spawnSync as spawnSync4 } from "node:child_process";
47734
48097
  function parseFlags2(args) {
47735
48098
  const flags = {};
47736
48099
  let i = 0;
@@ -47755,13 +48118,13 @@ function whichDocker() {
47755
48118
  const pathValue = process.env.PATH || process.env.Path || "";
47756
48119
  if (!pathValue) return null;
47757
48120
  const exts = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
47758
- for (const dir of pathValue.split(delimiter2)) {
48121
+ for (const dir of pathValue.split(delimiter3)) {
47759
48122
  if (!dir) continue;
47760
48123
  for (const ext of exts) {
47761
- const candidate = join37(dir, `docker${ext}`);
48124
+ const candidate = join38(dir, `docker${ext}`);
47762
48125
  try {
47763
48126
  accessSync2(candidate, constants2.X_OK);
47764
- if (statSync21(candidate).isFile()) return candidate;
48127
+ if (statSync22(candidate).isFile()) return candidate;
47765
48128
  } catch {
47766
48129
  }
47767
48130
  }
@@ -47776,7 +48139,7 @@ function buildImage(args) {
47776
48139
  tag = `${dirName || "tina4app"}:latest`;
47777
48140
  }
47778
48141
  const dockerfile = typeof flags.file === "string" && flags.file ? flags.file : "Dockerfile";
47779
- if (!existsSync34(dockerfile) || !statSync21(dockerfile).isFile()) {
48142
+ if (!existsSync35(dockerfile) || !statSync22(dockerfile).isFile()) {
47780
48143
  console.log(` \u2717 No ${dockerfile} found.`);
47781
48144
  console.log(" A Tina4 app deploys as a container. Scaffold a Dockerfile first:");
47782
48145
  console.log(" tina4 deploy docker (or: tina4nodejs init)");
@@ -47790,7 +48153,7 @@ function buildImage(args) {
47790
48153
  process.exit(1);
47791
48154
  }
47792
48155
  console.log(` Building image ${tag} from ${dockerfile} ...`);
47793
- const result = spawnSync3(docker, ["build", "-t", tag, "-f", dockerfile, "."], {
48156
+ const result = spawnSync4(docker, ["build", "-t", tag, "-f", dockerfile, "."], {
47794
48157
  stdio: "inherit"
47795
48158
  });
47796
48159
  const code = result.status ?? 1;
@@ -47803,22 +48166,22 @@ function buildImage(args) {
47803
48166
  }
47804
48167
 
47805
48168
  // src/bin.ts
47806
- import { spawnSync as spawnSync4 } from "node:child_process";
47807
- import { existsSync as existsSync35, readFileSync as readFileSync28, statSync as statSync22 } from "node:fs";
47808
- import { delimiter as delimiter3, dirname as dirname16, join as join38 } from "node:path";
48169
+ import { spawnSync as spawnSync5 } from "node:child_process";
48170
+ import { existsSync as existsSync36, readFileSync as readFileSync29, statSync as statSync23 } from "node:fs";
48171
+ import { delimiter as delimiter4, dirname as dirname17, join as join39 } from "node:path";
47809
48172
  import { fileURLToPath as fileURLToPath9, pathToFileURL as pathToFileURL4 } from "node:url";
47810
48173
  function readCliVersion() {
47811
- let dir = dirname16(fileURLToPath9(import.meta.url));
48174
+ let dir = dirname17(fileURLToPath9(import.meta.url));
47812
48175
  for (let i = 0; i < 6; i++) {
47813
- const pkgPath = join38(dir, "package.json");
47814
- if (existsSync35(pkgPath)) {
48176
+ const pkgPath = join39(dir, "package.json");
48177
+ if (existsSync36(pkgPath)) {
47815
48178
  try {
47816
- const pkg = JSON.parse(readFileSync28(pkgPath, "utf-8"));
48179
+ const pkg = JSON.parse(readFileSync29(pkgPath, "utf-8"));
47817
48180
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
47818
48181
  } catch {
47819
48182
  }
47820
48183
  }
47821
- const parent = dirname16(dir);
48184
+ const parent = dirname17(dir);
47822
48185
  if (parent === dir) break;
47823
48186
  dir = parent;
47824
48187
  }
@@ -48034,6 +48397,13 @@ var COMMANDS = {
48034
48397
  usage: "[file]",
48035
48398
  summary: "Run project tests"
48036
48399
  },
48400
+ lint: {
48401
+ handler: (a) => {
48402
+ runLint(a);
48403
+ },
48404
+ usage: "[--fix] [--no-install]",
48405
+ summary: "Lint the project (eslint, installed dev-only on demand; else tsc/node --check baseline)"
48406
+ },
48037
48407
  queue: {
48038
48408
  handler: async (a) => {
48039
48409
  await queueCommand(a);
@@ -48108,12 +48478,12 @@ var CLIENT_INSTALL_HINT = " Install it: curl -fsSL https://tina4.com/install.s
48108
48478
  function findClient() {
48109
48479
  const windows = process.platform === "win32";
48110
48480
  const names = windows ? [`${CLIENT_BINARY}.exe`, `${CLIENT_BINARY}.cmd`, `${CLIENT_BINARY}.bat`] : [CLIENT_BINARY];
48111
- for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
48481
+ for (const dir of (process.env.PATH ?? "").split(delimiter4)) {
48112
48482
  if (!dir) continue;
48113
48483
  for (const name of names) {
48114
- const candidate = join38(dir, name);
48484
+ const candidate = join39(dir, name);
48115
48485
  try {
48116
- if (statSync22(candidate).isFile()) return candidate;
48486
+ if (statSync23(candidate).isFile()) return candidate;
48117
48487
  } catch {
48118
48488
  }
48119
48489
  }
@@ -48141,7 +48511,7 @@ ${CLIENT_INSTALL_HINT}
48141
48511
  );
48142
48512
  return EXIT_CLIENT_UNAVAILABLE;
48143
48513
  }
48144
- const result = spawnSync4(client, [command, ...args], {
48514
+ const result = spawnSync5(client, [command, ...args], {
48145
48515
  stdio: "inherit",
48146
48516
  env: { ...process.env, [DELEGATION_GUARD_ENV]: command }
48147
48517
  });