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
package/packages/cli/src/bin.ts
CHANGED
|
@@ -345,10 +345,13 @@ export const COMMANDS: Record<string, CommandSpec> = {
|
|
|
345
345
|
summary: "Run pending SQL migrations",
|
|
346
346
|
},
|
|
347
347
|
"migrate:create": {
|
|
348
|
-
|
|
348
|
+
// Pass the full argv through so `migrate:create "add users" --json --dry-run`
|
|
349
|
+
// reaches the same envelope machinery as `generate migration ...` (ADR-0063).
|
|
350
|
+
// The delegation itself lives in commands/migrateCreate.ts.
|
|
351
|
+
handler: async (a) => { await createMigration(a); },
|
|
349
352
|
usage: "<desc>",
|
|
350
353
|
args: ["description"],
|
|
351
|
-
summary: "Create a new migration file",
|
|
354
|
+
summary: "Create a new migration file (delegates to generate migration)",
|
|
352
355
|
},
|
|
353
356
|
"migrate:status": {
|
|
354
357
|
handler: async (a) => { await migrateStatus(a[0]); },
|
|
@@ -306,24 +306,41 @@ function toRelPath(absPath: string): string {
|
|
|
306
306
|
return sep === "/" ? rel : rel.split(sep).join("/");
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
// Line-anchored
|
|
309
|
+
// Line-anchored `tina4:edit LABEL` marker regex (multi-style, ADR-0063 v1.1).
|
|
310
310
|
//
|
|
311
|
-
// Line-anchored (`^\s
|
|
312
|
-
// (after optional whitespace) — a marker embedded inside a
|
|
313
|
-
// literal never falsely matches. LABEL is captured greedily
|
|
314
|
-
// line, then trimmed
|
|
315
|
-
|
|
311
|
+
// Line-anchored (`^\s*<comment-lead>`) so the marker must be at the START of
|
|
312
|
+
// a code line (after optional whitespace) — a marker embedded inside a
|
|
313
|
+
// template string literal never falsely matches. LABEL is captured greedily
|
|
314
|
+
// until end of line (or the closing `#}` for a Twig comment), then trimmed
|
|
315
|
+
// on push. Four comment styles are recognised, one regex per style — bundled
|
|
316
|
+
// into one alternation so the scanner is O(lines):
|
|
317
|
+
//
|
|
318
|
+
// // tina4:edit LABEL TS/JS/C/Java/Rust (existing)
|
|
319
|
+
// # tina4:edit LABEL Python/Ruby/shell (parity)
|
|
320
|
+
// -- tina4:edit LABEL SQL migrations
|
|
321
|
+
// {# tina4:edit LABEL #} Twig / Frond templates
|
|
322
|
+
//
|
|
323
|
+
// Ports the same shape PHP uses in `bin/tina4php::collectEditHintsFromContent`
|
|
324
|
+
// (`~(?://|--|\{#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$~`). The `#` bare-
|
|
325
|
+
// comment style is added on top so a Ruby/Python-style template (none exist
|
|
326
|
+
// today, but the scanner is now language-agnostic) is covered too.
|
|
327
|
+
const TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
316
328
|
|
|
317
329
|
/**
|
|
318
|
-
* Scan `content` for
|
|
319
|
-
* match against the given absolute path. Called
|
|
320
|
-
* — including under `--dry-run` — so the envelope
|
|
321
|
-
* preview and post-write.
|
|
322
|
-
*
|
|
323
|
-
*
|
|
330
|
+
* Scan `content` for `tina4:edit …` markers (any of the 4 comment styles)
|
|
331
|
+
* and record one EditHint per match against the given absolute path. Called
|
|
332
|
+
* from every `writeFileSafe` — including under `--dry-run` — so the envelope
|
|
333
|
+
* promises the same hints in preview and post-write.
|
|
334
|
+
*
|
|
335
|
+
* File-extension gate keeps the scan cheap: only text/code files where a
|
|
336
|
+
* marker could reasonably live are opened. TS/JS + SQL + Twig cover every
|
|
337
|
+
* template the generator emits today; expanded here (from TS/JS-only in
|
|
338
|
+
* 3.13.120) so `generate form`, `generate view` and `generate migration`
|
|
339
|
+
* carry `edit_hints[]` rather than returning `[]` (parity with PHP, whose
|
|
340
|
+
* regex has always been language-agnostic).
|
|
324
341
|
*/
|
|
325
342
|
function captureEditHints(absPath: string, content: string): void {
|
|
326
|
-
if (!/\.(ts|tsx|js|mjs|cjs|jsx)$/.test(absPath)) return;
|
|
343
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
327
344
|
const relPath = toRelPath(absPath);
|
|
328
345
|
const lines = content.split("\n");
|
|
329
346
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -454,6 +471,10 @@ export function parseCliArgs(args: string[]): { flags: Record<string, string | b
|
|
|
454
471
|
const booleanFlags = new Set([
|
|
455
472
|
"no-browser", "no-reload", "production", "managed", "all", "clear",
|
|
456
473
|
"public", "no-migration",
|
|
474
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
475
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
476
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
477
|
+
"no-test",
|
|
457
478
|
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
458
479
|
"json", "dry-run",
|
|
459
480
|
]);
|
|
@@ -573,7 +594,7 @@ export const GENERATORS: Record<string, GeneratorSpec> = {
|
|
|
573
594
|
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
574
595
|
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
575
596
|
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
576
|
-
migration: { handler: (n, f) => generateMigration(n, f), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
597
|
+
migration: { handler: (n, f) => generateMigration(n, f, undefined, undefined, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
577
598
|
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
578
599
|
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
579
600
|
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
@@ -761,6 +782,53 @@ export async function generate(what: string, name: string, extraArgs: string[] =
|
|
|
761
782
|
printResolution();
|
|
762
783
|
}
|
|
763
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Programmatic entry point for in-process consumers (MCP tools, tests, hosted
|
|
787
|
+
* agents) — does everything `generate()` does EXCEPT print.
|
|
788
|
+
*
|
|
789
|
+
* Reset the resolution → dispatch to the requested generator → populate `next[]`
|
|
790
|
+
* → return the envelope. Files still land on disk (unless `--dry-run` is passed
|
|
791
|
+
* in `extraArgs`); only the human "Created …" per-file log and the
|
|
792
|
+
* `printResolution()` output are suppressed (via `jsonMode: true`, the same
|
|
793
|
+
* suppression `--json` uses on the CLI).
|
|
794
|
+
*
|
|
795
|
+
* Used by the MCP `migration_create` tool (packages/core/src/mcp.ts) so the
|
|
796
|
+
* ADR-0063 `generate_v1_1` envelope drives every surface (CLI, MCP, tests)
|
|
797
|
+
* without a subprocess round-trip.
|
|
798
|
+
*/
|
|
799
|
+
export async function generateProgrammatic(
|
|
800
|
+
what: string,
|
|
801
|
+
name: string,
|
|
802
|
+
extraArgs: string[] = [],
|
|
803
|
+
): Promise<ResolutionEnvelope> {
|
|
804
|
+
const spec = GENERATORS[what];
|
|
805
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
806
|
+
|
|
807
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
808
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
809
|
+
// jsonMode:true suppresses writeFileSafe's per-file console.log so nothing
|
|
810
|
+
// leaks to stdout (which the JSON-RPC caller would parse as tool output).
|
|
811
|
+
// Files are still written — jsonMode gates PRINTS only, not disk writes.
|
|
812
|
+
resetResolution(what, { name, fields: (flags.fields as string) ?? null }, { dryRun, jsonMode: true });
|
|
813
|
+
|
|
814
|
+
spec.handler(name, flags);
|
|
815
|
+
|
|
816
|
+
// v1.1 (ADR-0063): populate `resolution.next[]` from the per-verb curator —
|
|
817
|
+
// same logic `generate()` runs, using the RESOLVED table_name when the
|
|
818
|
+
// dispatched handler recorded one (avoids a duplicate reserved_word
|
|
819
|
+
// transformation on the envelope).
|
|
820
|
+
const nextFn = NEXT_STEPS[what];
|
|
821
|
+
if (nextFn) {
|
|
822
|
+
const resolvedTable = __resolution.body.table_name
|
|
823
|
+
?? (name
|
|
824
|
+
? (SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name))
|
|
825
|
+
: "");
|
|
826
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return currentResolution();
|
|
830
|
+
}
|
|
831
|
+
|
|
764
832
|
// ── Model ───────────────────────────────────────────────────────────
|
|
765
833
|
|
|
766
834
|
function generateModel(name: string, flags: Record<string, string | boolean>, emitTest = true): void {
|
|
@@ -1129,7 +1197,7 @@ function generateCrud(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1129
1197
|
|
|
1130
1198
|
// ── Migration ───────────────────────────────────────────────────────
|
|
1131
1199
|
|
|
1132
|
-
function generateMigration(
|
|
1200
|
+
export function generateMigration(
|
|
1133
1201
|
name: string,
|
|
1134
1202
|
flags: Record<string, string | boolean>,
|
|
1135
1203
|
fieldsOverride?: Array<[string, string]>,
|
|
@@ -1185,6 +1253,15 @@ function generateMigration(
|
|
|
1185
1253
|
let upSql: string;
|
|
1186
1254
|
let downSql: string;
|
|
1187
1255
|
|
|
1256
|
+
// ADR-0063 (scaffolding envelope v1.1): `-- tina4:edit <label>` markers
|
|
1257
|
+
// sit above each SQL block so an operator (or an AI agent) can grep to
|
|
1258
|
+
// the genuine first-edit spot. Wording mirrors Ruby's `generate_migration`
|
|
1259
|
+
// + PHP's `sqlEditHintMarker` calls for cross-language parity. The create-
|
|
1260
|
+
// branch marker sits INSIDE the column-list parentheses but OUTSIDE
|
|
1261
|
+
// `colLines.join(",\n")` so its trailing `,` does not become part of the
|
|
1262
|
+
// parsed label (the migration runner's splitStatements strips `--` line
|
|
1263
|
+
// comments, so a comma-less marker line inside a DDL body is still
|
|
1264
|
+
// syntactically inert on every engine).
|
|
1188
1265
|
if (isCreate) {
|
|
1189
1266
|
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
1190
1267
|
for (const [fname, ftype] of fields) {
|
|
@@ -1194,11 +1271,20 @@ function generateMigration(
|
|
|
1194
1271
|
}
|
|
1195
1272
|
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
1196
1273
|
|
|
1197
|
-
upSql =
|
|
1198
|
-
|
|
1274
|
+
upSql =
|
|
1275
|
+
`CREATE TABLE IF NOT EXISTS ${table} (\n` +
|
|
1276
|
+
` -- tina4:edit add columns beyond id + created_at\n` +
|
|
1277
|
+
`${colLines.join(",\n")}\n);`;
|
|
1278
|
+
downSql =
|
|
1279
|
+
`-- tina4:edit mirror the CREATE's added columns in the rollback\n` +
|
|
1280
|
+
`DROP TABLE IF EXISTS ${table};`;
|
|
1199
1281
|
} else {
|
|
1200
|
-
upSql =
|
|
1201
|
-
|
|
1282
|
+
upSql =
|
|
1283
|
+
`-- tina4:edit write your UP migration SQL here\n` +
|
|
1284
|
+
`-- Example: ALTER TABLE ${table} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
1285
|
+
downSql =
|
|
1286
|
+
`-- tina4:edit write your DOWN rollback SQL here\n` +
|
|
1287
|
+
`-- Example: ALTER TABLE ${table} DROP COLUMN new_col;`;
|
|
1202
1288
|
}
|
|
1203
1289
|
|
|
1204
1290
|
const now = isoNow();
|
|
@@ -1470,12 +1556,18 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1470
1556
|
}
|
|
1471
1557
|
}
|
|
1472
1558
|
|
|
1559
|
+
// ADR-0063 (scaffolding envelope v1.1): `{# tina4:edit <label> #}` is the
|
|
1560
|
+
// Twig-comment partner of `// tina4:edit` in TS/JS. Baked at a genuine
|
|
1561
|
+
// first-edit spot so an operator (or an AI agent) can grep to a real
|
|
1562
|
+
// customisation point instead of scanning the whole file. Marker wording
|
|
1563
|
+
// mirrors Ruby's `generate_form` for cross-language parity.
|
|
1473
1564
|
const content =
|
|
1474
1565
|
`{% extends "base.twig" %}\n` +
|
|
1475
1566
|
`{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}\n` +
|
|
1476
1567
|
`{% block content %}\n` +
|
|
1477
1568
|
`<div class="container mt-4">\n` +
|
|
1478
1569
|
` <h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>\n` +
|
|
1570
|
+
` {# tina4:edit restyle the form beyond the scaffolded defaults #}\n` +
|
|
1479
1571
|
` <form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">\n` +
|
|
1480
1572
|
` {{ form_token() }}\n` +
|
|
1481
1573
|
fieldHtml +
|
|
@@ -1507,11 +1599,16 @@ function generateView(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1507
1599
|
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
1508
1600
|
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
1509
1601
|
|
|
1602
|
+
// ADR-0063 (scaffolding envelope v1.1): `{# tina4:edit <label> #}` markers
|
|
1603
|
+
// sit at each template's genuine first-edit spot (list = customise the
|
|
1604
|
+
// table; detail = extend the record's view). Wording mirrors Ruby's
|
|
1605
|
+
// `generate_view` for cross-language parity.
|
|
1510
1606
|
const listContent =
|
|
1511
1607
|
`{% extends "base.twig" %}\n` +
|
|
1512
1608
|
`{% block title %}${name}s{% endblock %}\n` +
|
|
1513
1609
|
`{% block content %}\n` +
|
|
1514
1610
|
`<div class="container mt-4">\n` +
|
|
1611
|
+
` {# tina4:edit add sort / filter / pagination controls to the list #}\n` +
|
|
1515
1612
|
` <div class="d-flex justify-content-between align-items-center mb-3">\n` +
|
|
1516
1613
|
` <h1>${name}s</h1>\n` +
|
|
1517
1614
|
` <a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>\n` +
|
|
@@ -1553,6 +1650,7 @@ function generateView(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1553
1650
|
`{% block title %}${name} Detail{% endblock %}\n` +
|
|
1554
1651
|
`{% block content %}\n` +
|
|
1555
1652
|
`<div class="container mt-4">\n` +
|
|
1653
|
+
` {# tina4:edit extend the detail view with related records or actions #}\n` +
|
|
1556
1654
|
` <div class="d-flex justify-content-between align-items-center mb-3">\n` +
|
|
1557
1655
|
` <h1>${name} #{{ item.id }}</h1>\n` +
|
|
1558
1656
|
` <div>\n` +
|
|
@@ -8,53 +8,48 @@
|
|
|
8
8
|
* Creates both:
|
|
9
9
|
* migrations/YYYYMMDDHHMMSS_description.sql (up migration)
|
|
10
10
|
* migrations/YYYYMMDDHHMMSS_description.down.sql (rollback)
|
|
11
|
+
*
|
|
12
|
+
* 3.13.121 (ADR-0063): this is a thin delegation to `generate migration` so a
|
|
13
|
+
* scaffolded migration has ONE shape and ONE envelope regardless of whether
|
|
14
|
+
* the developer typed `tina4 migrate:create` or `tina4 generate migration`.
|
|
15
|
+
* The two file paths emitted here (`_desc.sql` + `_desc.down.sql`) and the
|
|
16
|
+
* schema-awareness on `create_X` names are unchanged; the delegation adds the
|
|
17
|
+
* ADR-0063 `generate_v1_1` envelope, `--json` / `--dry-run` support, and the
|
|
18
|
+
* `edit_hints[]` / `next[]` machinery for free. `--no-test` preserves the
|
|
19
|
+
* pre-3.13.121 UX (just a migration, no co-emitted test).
|
|
11
20
|
*/
|
|
12
|
-
import {
|
|
13
|
-
|
|
21
|
+
import { generate } from "./generate.js";
|
|
22
|
+
|
|
23
|
+
export async function createMigration(args: string[] = []): Promise<void> {
|
|
24
|
+
// Split flags from the description so `tina4 migrate:create "add users"
|
|
25
|
+
// --json --dry-run` reaches the same envelope machinery as its
|
|
26
|
+
// `tina4 generate migration` twin (see test/migrateCreateEnvelopeParity.test.ts).
|
|
27
|
+
const positionals: string[] = [];
|
|
28
|
+
const flags: string[] = [];
|
|
29
|
+
for (const arg of args) {
|
|
30
|
+
if (arg.startsWith("--")) flags.push(arg);
|
|
31
|
+
else positionals.push(arg);
|
|
32
|
+
}
|
|
33
|
+
const description = positionals.join(" ").trim();
|
|
14
34
|
|
|
15
|
-
export async function createMigration(description?: string): Promise<void> {
|
|
16
35
|
if (!description) {
|
|
17
36
|
console.error(" Usage: tina4 migrate:create <description>");
|
|
18
37
|
console.error(' Example: tina4 migrate:create "create users table"');
|
|
19
38
|
process.exit(1);
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Sanitise description for filename
|
|
30
|
-
const safeName = description
|
|
41
|
+
// Sanitise the description into a filename-safe slug BEFORE delegation.
|
|
42
|
+
// Preserves migrate:create's pre-3.13.121 UX: `migrate:create "add users"`
|
|
43
|
+
// still yields `${ts}_add_users.sql` (space -> underscore, lowercased),
|
|
44
|
+
// never a filename with a bare space. `generate migration` takes its name
|
|
45
|
+
// verbatim by design; migrate:create is the human-friendly front door.
|
|
46
|
+
const safeDescription = description
|
|
31
47
|
.toLowerCase()
|
|
32
48
|
.replace(/[^a-z0-9]+/g, "_")
|
|
33
49
|
.replace(/^_|_$/g, "");
|
|
34
50
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
String(now.getMonth() + 1).padStart(2, "0"),
|
|
40
|
-
String(now.getDate()).padStart(2, "0"),
|
|
41
|
-
String(now.getHours()).padStart(2, "0"),
|
|
42
|
-
String(now.getMinutes()).padStart(2, "0"),
|
|
43
|
-
String(now.getSeconds()).padStart(2, "0"),
|
|
44
|
-
].join("");
|
|
45
|
-
|
|
46
|
-
const upFileName = `${timestamp}_${safeName}.sql`;
|
|
47
|
-
const downFileName = `${timestamp}_${safeName}.down.sql`;
|
|
48
|
-
const upPath = join(dir, upFileName);
|
|
49
|
-
const downPath = join(dir, downFileName);
|
|
50
|
-
|
|
51
|
-
const upTemplate = `-- Migration: ${description}\n-- Created: ${now.toISOString()}\n\n`;
|
|
52
|
-
const downTemplate = `-- Rollback: ${description}\n-- Created: ${now.toISOString()}\n\n`;
|
|
53
|
-
|
|
54
|
-
writeFileSync(upPath, upTemplate, "utf-8");
|
|
55
|
-
writeFileSync(downPath, downTemplate, "utf-8");
|
|
56
|
-
|
|
57
|
-
console.log(` Created migration: ${upFileName}`);
|
|
58
|
-
console.log(` Created rollback: ${downFileName}`);
|
|
59
|
-
console.log(` Path: ${dir}`);
|
|
51
|
+
// Delegate to `generate migration` with --no-test so migrate:create stays
|
|
52
|
+
// "just a migration, no test" — its pre-3.13.121 semantics — while getting
|
|
53
|
+
// the ADR-0063 envelope, edit_hints[], next[] and --json/--dry-run for free.
|
|
54
|
+
await generate("migration", safeDescription, ["--no-test", ...flags]);
|
|
60
55
|
}
|