@metaobjectsdev/cli 1.0.9 → 1.0.11

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.
Files changed (102) hide show
  1. package/dist/src/commands/deps.d.ts.map +1 -1
  2. package/dist/src/commands/deps.js +9 -8
  3. package/dist/src/commands/deps.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts.map +1 -1
  5. package/dist/src/commands/docs.js +19 -12
  6. package/dist/src/commands/docs.js.map +1 -1
  7. package/dist/src/commands/eject.d.ts.map +1 -1
  8. package/dist/src/commands/eject.js +8 -3
  9. package/dist/src/commands/eject.js.map +1 -1
  10. package/dist/src/commands/export.d.ts.map +1 -1
  11. package/dist/src/commands/export.js +3 -2
  12. package/dist/src/commands/export.js.map +1 -1
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +15 -7
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.d.ts.map +1 -1
  17. package/dist/src/commands/generator.js +3 -2
  18. package/dist/src/commands/generator.js.map +1 -1
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +5 -2
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts.map +1 -1
  23. package/dist/src/commands/migrate.js +31 -23
  24. package/dist/src/commands/migrate.js.map +1 -1
  25. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  26. package/dist/src/commands/prompt-snapshot.js +5 -4
  27. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  28. package/dist/src/commands/types.d.ts.map +1 -1
  29. package/dist/src/commands/types.js +7 -1
  30. package/dist/src/commands/types.js.map +1 -1
  31. package/dist/src/commands/upgrade.d.ts.map +1 -1
  32. package/dist/src/commands/upgrade.js +4 -3
  33. package/dist/src/commands/upgrade.js.map +1 -1
  34. package/dist/src/commands/verify.d.ts.map +1 -1
  35. package/dist/src/commands/verify.js +73 -24
  36. package/dist/src/commands/verify.js.map +1 -1
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +14 -2
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/lib/args.d.ts +11 -0
  41. package/dist/src/lib/args.d.ts.map +1 -1
  42. package/dist/src/lib/args.js +19 -6
  43. package/dist/src/lib/args.js.map +1 -1
  44. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  45. package/dist/src/lib/catalog-listing.js +5 -1
  46. package/dist/src/lib/catalog-listing.js.map +1 -1
  47. package/dist/src/lib/dependency-sync.d.ts.map +1 -1
  48. package/dist/src/lib/dependency-sync.js +4 -3
  49. package/dist/src/lib/dependency-sync.js.map +1 -1
  50. package/dist/src/lib/error-text.d.ts +2 -0
  51. package/dist/src/lib/error-text.d.ts.map +1 -0
  52. package/dist/src/lib/error-text.js +15 -0
  53. package/dist/src/lib/error-text.js.map +1 -0
  54. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  55. package/dist/src/lib/fk-index-advisory.js +4 -1
  56. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  57. package/dist/src/lib/gen-migrate-advice.d.ts +1 -1
  58. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  59. package/dist/src/lib/gen-migrate-advice.js +24 -0
  60. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  61. package/dist/src/lib/git-ignore.d.ts.map +1 -1
  62. package/dist/src/lib/git-ignore.js +2 -1
  63. package/dist/src/lib/git-ignore.js.map +1 -1
  64. package/dist/src/lib/name-lint.d.ts +12 -0
  65. package/dist/src/lib/name-lint.d.ts.map +1 -0
  66. package/dist/src/lib/name-lint.js +95 -0
  67. package/dist/src/lib/name-lint.js.map +1 -0
  68. package/dist/src/lib/output.d.ts +3 -2
  69. package/dist/src/lib/output.d.ts.map +1 -1
  70. package/dist/src/lib/output.js.map +1 -1
  71. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  72. package/dist/src/lib/requirement-check.js +43 -2
  73. package/dist/src/lib/requirement-check.js.map +1 -1
  74. package/dist/src/lib/runtime-boundary-advisory.d.ts +24 -0
  75. package/dist/src/lib/runtime-boundary-advisory.d.ts.map +1 -0
  76. package/dist/src/lib/runtime-boundary-advisory.js +47 -0
  77. package/dist/src/lib/runtime-boundary-advisory.js.map +1 -0
  78. package/package.json +10 -10
  79. package/src/commands/deps.ts +9 -8
  80. package/src/commands/docs.ts +19 -12
  81. package/src/commands/eject.ts +9 -3
  82. package/src/commands/export.ts +3 -2
  83. package/src/commands/gen.ts +17 -7
  84. package/src/commands/generator.ts +3 -2
  85. package/src/commands/init.ts +5 -2
  86. package/src/commands/migrate.ts +33 -23
  87. package/src/commands/prompt-snapshot.ts +5 -4
  88. package/src/commands/types.ts +6 -1
  89. package/src/commands/upgrade.ts +4 -3
  90. package/src/commands/verify.ts +83 -24
  91. package/src/index.ts +14 -2
  92. package/src/lib/args.ts +28 -6
  93. package/src/lib/catalog-listing.ts +5 -1
  94. package/src/lib/dependency-sync.ts +4 -3
  95. package/src/lib/error-text.ts +13 -0
  96. package/src/lib/fk-index-advisory.ts +4 -1
  97. package/src/lib/gen-migrate-advice.ts +24 -1
  98. package/src/lib/git-ignore.ts +2 -1
  99. package/src/lib/name-lint.ts +106 -0
  100. package/src/lib/output.ts +3 -2
  101. package/src/lib/requirement-check.ts +46 -2
  102. package/src/lib/runtime-boundary-advisory.ts +74 -0
@@ -39,6 +39,7 @@ import {
39
39
  } from "../lib/requirement-check.js";
40
40
  import { lintRequirements } from "../lib/requirement-lint.js";
41
41
  import { lintOverlays } from "../lib/overlay-lint.js";
42
+ import { lintNodeNames } from "../lib/name-lint.js";
42
43
  import { FileSource } from "@metaobjectsdev/metadata/core";
43
44
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
44
45
  import {
@@ -106,6 +107,7 @@ import {
106
107
  } from "@metaobjectsdev/metadata";
107
108
  import { verify, ERR_REQUIRED_SLOT_UNUSED, ERR_PARTIAL_UNRESOLVED } from "@metaobjectsdev/render";
108
109
  import { describeLoadError, reportLoadError } from "../lib/load-error.js";
110
+ import { describeError } from "../lib/error-text.js";
109
111
 
110
112
  const DEFAULT_PROMPTS_DIR = "prompts";
111
113
 
@@ -162,7 +164,7 @@ export async function verifyCommand(
162
164
  try {
163
165
  flags = parseVerifyArgs(args);
164
166
  } catch (err) {
165
- const msg = (err as Error).message;
167
+ const msg = describeError(err);
166
168
  log.error(msg);
167
169
  // A structured caller gets a structured refusal. Exiting 2 with an EMPTY stdout
168
170
  // is the same silence this command is being fixed for, one level up.
@@ -206,7 +208,7 @@ export async function verifyCommand(
206
208
  try {
207
209
  collection = await resolveCollection(cwd);
208
210
  } catch (err) {
209
- const msg = (err as Error).message;
211
+ const msg = describeError(err);
210
212
  log.error(msg);
211
213
  emitStructured({ error: msg, hint: "declare metadata `sources` in .metaobjects/config.json, or run `meta init`" }, fmt);
212
214
  return 2;
@@ -274,7 +276,7 @@ export async function verifyCommand(
274
276
  strict: !flags.lax,
275
277
  });
276
278
  } catch (err) {
277
- const msg = (err as Error).message;
279
+ const msg = describeError(err);
278
280
  // Everything the loader's envelope carried, not just the message: the stable code a
279
281
  // CI job keys on, and the FILE + json path the message itself can never name.
280
282
  const report = describeLoadError(err);
@@ -349,6 +351,11 @@ export async function verifyCommand(
349
351
  // way every other advisory pass does.
350
352
  let overlaySection: AdvisorySection<AdvisoryDiagnosticRow> =
351
353
  skippedSection("the overlay lint did not run");
354
+ // The node-name authoring lint — its own section for the same reason: a model
355
+ // carrying a few hundred legacy names must not push another section's findings
356
+ // off the end of a capped run.
357
+ let nameSection: AdvisorySection<AdvisoryDiagnosticRow> =
358
+ skippedSection("the name lint did not run");
352
359
  // The ledger counts `meta verify` prints on every run. Undefined for a project
353
360
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
354
361
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -385,6 +392,10 @@ export async function verifyCommand(
385
392
  // Warnings ONLY — never changes the exit code.
386
393
  await runOverlayLintAdvisory();
387
394
 
395
+ // Whitespace in a node's `name` — loads clean on every port, almost never meant.
396
+ // Runs on every `meta verify`; warnings ONLY, never changes the exit code.
397
+ runNameLintAdvisory();
398
+
388
399
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
389
400
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
390
401
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
@@ -433,6 +444,7 @@ export async function verifyCommand(
433
444
  requirementCounts,
434
445
  antiPatterns: antiPatternSection,
435
446
  overlays: overlaySection,
447
+ names: nameSection,
436
448
  }),
437
449
  fmt,
438
450
  );
@@ -481,8 +493,15 @@ export async function verifyCommand(
481
493
  // .metaobjects/config.json — the spelling a TypeScript project is least likely
482
494
  // to have. migrate's own resolution still WINS where it has an answer, because
483
495
  // it is the more specific statement about the chain being replayed.
496
+ if (flags.dialect === undefined && migrateConfig.dialect === undefined && forgeConfig?.dialect === "mysql") {
497
+ log.error(
498
+ "meta verify --replay: MetaObjects does not own a MySQL schema, so there is no migration chain to replay.",
499
+ );
500
+ return 2;
501
+ }
502
+ const configuredDialect = forgeConfig?.dialect === "mysql" ? undefined : forgeConfig?.dialect;
484
503
  const dialect: Dialect | undefined =
485
- flags.dialect ?? migrateConfig.dialect ?? forgeConfig?.dialect;
504
+ flags.dialect ?? migrateConfig.dialect ?? configuredDialect;
486
505
  if (dialect === undefined) {
487
506
  log.error(
488
507
  `meta verify --replay: no dialect — pass --dialect <postgres|sqlite>, ` +
@@ -505,7 +524,7 @@ export async function verifyCommand(
505
524
  } catch (err) {
506
525
  // A missing optional driver lands here, and its message already carries the
507
526
  // install hint. Operational, not drift.
508
- log.error(`meta verify --replay: ${(err as Error).message}`);
527
+ log.error(`meta verify --replay: ${describeError(err)}`);
509
528
  return 2;
510
529
  }
511
530
 
@@ -514,7 +533,7 @@ export async function verifyCommand(
514
533
  try {
515
534
  applied = await applyPending(engine.db, dir, { dryRun: false, dialect });
516
535
  } catch (err) {
517
- log.error(`meta verify --replay: ${(err as Error).message}`);
536
+ log.error(`meta verify --replay: ${describeError(err)}`);
518
537
  for (const line of replayRemedy(err)) log.error(`meta verify --replay: ${line}`);
519
538
  return 1;
520
539
  }
@@ -787,7 +806,7 @@ export async function verifyCommand(
787
806
  findings = await lintOverlays(collection, (path) => new FileSource(path));
788
807
  } catch (err) {
789
808
  // Never let an advisory scan break verify — and never report it as clean.
790
- overlaySection = skippedSection(`the overlay lint failed: ${(err as Error).message}`);
809
+ overlaySection = skippedSection(`the overlay lint failed: ${describeError(err)}`);
791
810
  return;
792
811
  }
793
812
  overlaySection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
@@ -800,6 +819,29 @@ export async function verifyCommand(
800
819
  }
801
820
  }
802
821
 
822
+ // -- node-name authoring lint (advisory) -----------------------------------
823
+ // Reads the LOADED model (see name-lint.ts). Its own section, its own cap, and
824
+ // a skip that carries its reason rather than looking like a clean scan.
825
+ function runNameLintAdvisory(): void {
826
+ if (flags.noNameLint) {
827
+ nameSection = skippedSection("suppressed by --no-name-lint");
828
+ return;
829
+ }
830
+ if (process.env.META_NO_NAME_LINT === "1") {
831
+ nameSection = skippedSection("suppressed by META_NO_NAME_LINT=1");
832
+ return;
833
+ }
834
+ const findings = lintNodeNames(root);
835
+ nameSection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
836
+ if (findings.length > 0) {
837
+ log.warn(
838
+ `meta verify — names: ${findings.length} authoring warning(s) ` +
839
+ `(advisory — does not fail the build):`,
840
+ );
841
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
842
+ }
843
+ }
844
+
803
845
  // -- verify-as-teacher (advisory) ------------------------------------------
804
846
  // Records its result EITHER WAY — a skip carries its reason rather than looking
805
847
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
@@ -819,7 +861,7 @@ export async function verifyCommand(
819
861
  findings = scanSourceForAntiPatterns(projectRoot, ignore !== undefined ? { ignore } : undefined);
820
862
  } catch (err) {
821
863
  // Never let an advisory scan break verify — and never report it as clean.
822
- antiPatternSection = skippedSection(`the scan failed: ${(err as Error).message}`);
864
+ antiPatternSection = skippedSection(`the scan failed: ${describeError(err)}`);
823
865
  return;
824
866
  }
825
867
  // F52 — the one gate that can see a provider silently missing its base. Folded into
@@ -860,12 +902,17 @@ export async function verifyCommand(
860
902
  // each one and says how to declare it (`index.lookup`) instead. Read off the SAME
861
903
  // expected schema `meta migrate` builds, so "covered" means covered in its DDL.
862
904
  let unindexedFks: UnindexedFkFinding[] = [];
905
+ // Skipped on MySQL: MetaObjects emits no DDL there, and InnoDB indexes every foreign
906
+ // key itself.
907
+ const scanDialect = flags.dialect ?? forgeConfig?.dialect ?? "postgres";
863
908
  try {
864
- unindexedFks = scanForUnindexedForeignKeys(root, {
865
- dialect: flags.dialect ?? forgeConfig?.dialect ?? "postgres",
866
- columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
867
- skip: collection.imported,
868
- });
909
+ if (scanDialect !== "mysql") {
910
+ unindexedFks = scanForUnindexedForeignKeys(root, {
911
+ dialect: scanDialect,
912
+ columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
913
+ skip: collection.imported,
914
+ });
915
+ }
869
916
  } catch {
870
917
  // Same discipline as its siblings: an advisory scan never breaks verify. A model the
871
918
  // schema builder refuses is migrate's to report, with its own message.
@@ -1039,7 +1086,8 @@ export async function verifyCommand(
1039
1086
  const text = provider.resolve(ref);
1040
1087
  if (text === undefined) {
1041
1088
  log.error(
1042
- `[${tmpl.name}] (${label}) ${ERR_PARTIAL_UNRESOLVED}: ref "${ref}" did not resolve under ${promptsDir}`,
1089
+ `[${tmpl.name}] (${label}) ${ERR_PARTIAL_UNRESOLVED}: ref "${ref}" did not resolve under ${promptsDir}` +
1090
+ ` — create ${join(promptsDir, `${ref}.mustache`)} (or .txt), or point --prompts at the directory that holds it`,
1043
1091
  );
1044
1092
  errorCount++;
1045
1093
  anyErrorHere = true;
@@ -1144,7 +1192,7 @@ export async function verifyCommand(
1144
1192
  try {
1145
1193
  kysely = await buildKyselyFromUrl(flags.db as string, flags.dialect as Dialect | undefined);
1146
1194
  } catch (err) {
1147
- log.error(`verify: ${(err as Error).message}`);
1195
+ log.error(`verify: ${describeError(err)}`);
1148
1196
  return 1;
1149
1197
  }
1150
1198
 
@@ -1170,7 +1218,7 @@ export async function verifyCommand(
1170
1218
  ...importedOption(collection),
1171
1219
  });
1172
1220
  } catch (err) {
1173
- log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
1221
+ log.error(`verify: failed to introspect ${kysely.displayUrl}: ${describeError(err)}`);
1174
1222
  return 1;
1175
1223
  }
1176
1224
 
@@ -1184,7 +1232,7 @@ export async function verifyCommand(
1184
1232
  try {
1185
1233
  await kysely.close();
1186
1234
  } catch (err) {
1187
- log.warn(`verify: failed to close DB cleanly: ${(err as Error).message}`);
1235
+ log.warn(`verify: failed to close DB cleanly: ${describeError(err)}`);
1188
1236
  }
1189
1237
  }
1190
1238
  }
@@ -1217,7 +1265,7 @@ export async function verifyCommand(
1217
1265
  try {
1218
1266
  binding = resolveD1Binding(parsed.d1Bindings, d1Config.binding);
1219
1267
  } catch (err) {
1220
- log.error(`verify: ${(err as Error).message}`);
1268
+ log.error(`verify: ${describeError(err)}`);
1221
1269
  return 2;
1222
1270
  }
1223
1271
  } else {
@@ -1241,7 +1289,7 @@ export async function verifyCommand(
1241
1289
  try {
1242
1290
  actual = await introspectD1({ runner: d1Runner, binding: binding.binding, remote, configPath: wranglerConfigPath });
1243
1291
  } catch (err) {
1244
- log.error(`verify: failed to introspect D1 binding '${binding.binding}': ${(err as Error).message}`);
1292
+ log.error(`verify: failed to introspect D1 binding '${binding.binding}': ${describeError(err)}`);
1245
1293
  return 1;
1246
1294
  }
1247
1295
 
@@ -1258,7 +1306,7 @@ export async function verifyCommand(
1258
1306
  ...importedOption(collection),
1259
1307
  });
1260
1308
  } catch (err) {
1261
- log.error(`verify: ${(err as Error).message}`);
1309
+ log.error(`verify: ${describeError(err)}`);
1262
1310
  return 1;
1263
1311
  }
1264
1312
 
@@ -1444,7 +1492,7 @@ export async function verifyCommand(
1444
1492
  try {
1445
1493
  result = await computeCodegenDrift(forgeConfig, codegenRoot, genConfigDir, genCollection.inScope);
1446
1494
  } catch (err) {
1447
- log.error(`verify --codegen: regeneration failed: ${(err as Error).message}`);
1495
+ log.error(`verify --codegen: regeneration failed: ${describeError(err)}`);
1448
1496
  return 1;
1449
1497
  }
1450
1498
 
@@ -1541,7 +1589,7 @@ export async function verifyCommand(
1541
1589
  cwd: genConfigDir,
1542
1590
  });
1543
1591
  } catch (err) {
1544
- log.error(`verify --docs: regeneration failed: ${(err as Error).message}`);
1592
+ log.error(`verify --docs: regeneration failed: ${describeError(err)}`);
1545
1593
  return 1;
1546
1594
  }
1547
1595
 
@@ -1617,7 +1665,7 @@ export async function verifyCommand(
1617
1665
  try {
1618
1666
  lock = await readLockOrThrow(collection.configDir);
1619
1667
  } catch (err) {
1620
- log.error(`verify --deps: ${(err as Error).message}`);
1668
+ log.error(`verify --deps: ${describeError(err)}`);
1621
1669
  return 2;
1622
1670
  }
1623
1671
 
@@ -1807,6 +1855,7 @@ function buildVerifyPayload(input: {
1807
1855
  requirementCounts: RequirementCounts | undefined;
1808
1856
  antiPatterns: AdvisorySection<AdvisoryFindingRow>;
1809
1857
  overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1858
+ names: AdvisorySection<AdvisoryDiagnosticRow>;
1810
1859
  }): Record<string, unknown> {
1811
1860
  const ran = input.gates.filter((g) => g.ran);
1812
1861
  const failed = ran.filter((g) => !g.ok);
@@ -1826,6 +1875,9 @@ function buildVerifyPayload(input: {
1826
1875
  if (input.overlays.status === "ran" && input.overlays.total > 0) {
1827
1876
  parts.push(`${input.overlays.total} overlay authoring finding(s)`);
1828
1877
  }
1878
+ if (input.names.status === "ran" && input.names.total > 0) {
1879
+ parts.push(`${input.names.total} name authoring finding(s)`);
1880
+ }
1829
1881
 
1830
1882
  const help: string[] = [];
1831
1883
  if (failed.length > 0) {
@@ -1841,11 +1893,17 @@ function buildVerifyPayload(input: {
1841
1893
  `${input.overlays.total} unflagged cross-file redeclaration(s) — see overlays.rows[]; add overlay: true so a renamed or removed target fails loudly instead of silently becoming a new object`,
1842
1894
  );
1843
1895
  }
1896
+ if (input.names.total > 0) {
1897
+ help.push(
1898
+ `${input.names.total} node name(s) containing whitespace — see names.rows[]; rename each node, and pin @column on a field whose physical column must keep its name`,
1899
+ );
1900
+ }
1844
1901
  if (
1845
1902
  failed.length === 0 &&
1846
1903
  input.antiPatterns.total === 0 &&
1847
1904
  input.requirements.total === 0 &&
1848
- input.overlays.total === 0
1905
+ input.overlays.total === 0 &&
1906
+ input.names.total === 0
1849
1907
  ) {
1850
1908
  help.push("no drift and nothing advisory to answer — nothing to do");
1851
1909
  }
@@ -1858,6 +1916,7 @@ function buildVerifyPayload(input: {
1858
1916
  antiPatterns: input.antiPatterns,
1859
1917
  requirements: input.requirements,
1860
1918
  overlays: input.overlays,
1919
+ names: input.names,
1861
1920
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1862
1921
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1863
1922
  // text on stderr — but it is not in this document, and a reader must not have
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { cliVersion } from "./lib/version.js";
4
4
  import { resolveFormat, isValidFormat, VALID_FORMATS, type OutputFormat } from "./lib/format.js";
5
5
  import { resolveCollection } from "@metaobjectsdev/sdk";
6
6
  import { GENERATOR_HELP } from "./commands/generator-help.js";
7
+ import { describeError } from "./lib/error-text.js";
7
8
  export { defineConfig } from "@metaobjectsdev/codegen-ts";
8
9
  export type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
9
10
 
@@ -126,6 +127,8 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
126
127
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
127
128
  --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint
128
129
  (never a gate — this lint can't fail the build)
130
+ --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace
131
+ in a name; never a gate — this lint can't fail the build)
129
132
  --limit <n|all> How many advisory lines TEXT output prints per section before
130
133
  it truncates (default 20). Never applies to --format
131
134
  toon/json, which carry every finding and every diagnostic.
@@ -319,6 +322,8 @@ FLAGS:
319
322
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
320
323
  --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint —
321
324
  never a gate; this lint can't fail the build
325
+ --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace in
326
+ a name) — never a gate; this lint can't fail the build
322
327
  --limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
323
328
  (default 20; per-section so the authoring lint can never push
324
329
  the gate's own warnings off the end)
@@ -349,6 +354,13 @@ redeclaration lacks 'overlay: true' — today's default merge rule reuses it
349
354
  silently, but the same unflagged redeclaration silently becomes a NEW object
350
355
  the day the target is renamed or removed upstream. Warnings only — it can
351
356
  never fail the build. Opt out with --no-overlay-lint or META_NO_OVERLAY_LINT=1.
357
+
358
+ verify also prints an ADVISORY node-name authoring lint, in its own section: any
359
+ node (object, field, identity, relationship, ...) whose name contains whitespace.
360
+ Leading or trailing whitespace is almost certainly a copy-paste accident — rename
361
+ the node, and pin @column on a field whose physical column must keep its name.
362
+ Requirement names are left to the requirement lint above. Warnings only — it can
363
+ never fail the build. Opt out with --no-name-lint or META_NO_NAME_LINT=1.
352
364
  `,
353
365
  export: `meta export — flatten loaded metadata to one canonical JSON artifact
354
366
 
@@ -591,7 +603,7 @@ export async function run(argv: string[]): Promise<number> {
591
603
  try {
592
604
  flags = parseAgentDocsArgs(rest);
593
605
  } catch (err) {
594
- log.error((err as Error).message);
606
+ log.error(describeError(err));
595
607
  return 2;
596
608
  }
597
609
  const targetCwd = flags.out !== undefined ? resolve(cwd, flags.out) : cwd;
@@ -609,7 +621,7 @@ export async function run(argv: string[]): Promise<number> {
609
621
  log.info("Re-run agent-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
610
622
  return 0;
611
623
  } catch (err) {
612
- log.error((err as Error).message);
624
+ log.error(describeError(err));
613
625
  return 1;
614
626
  }
615
627
  }
package/src/lib/args.ts CHANGED
@@ -197,6 +197,23 @@ export function parseExportArgs(argv: string[]): ExportFlags {
197
197
  const DIALECTS = ["sqlite", "postgres", "d1"] as const;
198
198
  type Dialect = (typeof DIALECTS)[number];
199
199
 
200
+ /**
201
+ * `--dialect` names a database whose schema `meta migrate` / `meta verify --db` OWN. MySQL is
202
+ * a codegen + runtime dialect (`dialect: "mysql"` in metaobjects.config.ts) and is refused
203
+ * here by name, so the message says what to do instead of only listing the others.
204
+ */
205
+ function assertSchemaDialect(dialect: string | undefined): void {
206
+ if (dialect === undefined || DIALECTS.includes(dialect as Dialect)) return;
207
+ if (dialect === "mysql") {
208
+ throw new Error(
209
+ "invalid --dialect 'mysql': MetaObjects does not own a MySQL schema. " +
210
+ `Write the DDL yourself; \`dialect: "mysql"\` in metaobjects.config.ts is for generated code. ` +
211
+ `Schema dialects: ${DIALECTS.join(", ")}`,
212
+ );
213
+ }
214
+ throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
215
+ }
216
+
200
217
  /**
201
218
  * #192 — migration output-format adapters. Orthogonal to dialect: a Flyway shop
202
219
  * is still on postgres or sqlite. "default" is the homegrown
@@ -345,6 +362,13 @@ export interface VerifyFlags {
345
362
  * lint has no gate half at all — it can never fail the build).
346
363
  */
347
364
  noOverlayLint: boolean;
365
+ /**
366
+ * Suppress the advisory node-NAME authoring lint — whitespace in a node's
367
+ * `name` (a trailing space copied from legacy XML loads clean on every port).
368
+ * Advisory only, like its siblings: it has no gate half and can never fail
369
+ * the build.
370
+ */
371
+ noNameLint: boolean;
348
372
  /**
349
373
  * ADR-0023 strict-attr load opt-OUT (#96). `verify` is strict-by-default — an
350
374
  * undeclared/typo'd own `@attr` fails verify (ERR_UNKNOWN_ATTR). `--lax`
@@ -386,6 +410,7 @@ export const VERIFY_OPTIONS = {
386
410
  "no-antipatterns": { type: "boolean", default: false },
387
411
  "no-requirement-lint": { type: "boolean", default: false },
388
412
  "no-overlay-lint": { type: "boolean", default: false },
413
+ "no-name-lint": { type: "boolean", default: false },
389
414
  lax: { type: "boolean", default: false },
390
415
  "d1": { type: "string" },
391
416
  "remote": { type: "boolean", default: false },
@@ -428,9 +453,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
428
453
  });
429
454
 
430
455
  const dialect = values.dialect as string | undefined;
431
- if (dialect !== undefined && !DIALECTS.includes(dialect as Dialect)) {
432
- throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
433
- }
456
+ assertSchemaDialect(dialect);
434
457
 
435
458
  const allowTokens = parseAllowTokens(values.allow as string | string[] | undefined);
436
459
 
@@ -472,6 +495,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
472
495
  noAntipatterns: !!values["no-antipatterns"],
473
496
  noRequirementLint: !!values["no-requirement-lint"],
474
497
  noOverlayLint: !!values["no-overlay-lint"],
498
+ noNameLint: !!values["no-name-lint"],
475
499
  lax: !!values.lax,
476
500
  d1: values.d1 as string | undefined,
477
501
  remote: !!values.remote,
@@ -588,9 +612,7 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
588
612
  }
589
613
 
590
614
  const dialect = values.dialect as string | undefined;
591
- if (dialect !== undefined && !DIALECTS.includes(dialect as Dialect)) {
592
- throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
593
- }
615
+ assertSchemaDialect(dialect);
594
616
 
595
617
  // NOTE: the flag is --migration-format, not --format: `--format` is already a
596
618
  // GLOBAL cli flag selecting output rendering (toon|json|text), consumed in index.ts.
@@ -41,6 +41,7 @@ import {
41
41
  } from "./runtime-eject.js";
42
42
  import { OWNED_RUNTIME_DIR } from "@metaobjectsdev/codegen-ts";
43
43
  import { readPackageManifest, declaredDependencyNames } from "./package-manifest.js";
44
+ import { describeError } from "./error-text.js";
44
45
 
45
46
  /**
46
47
  * One `--list` row. The cross-port subset is name / layer / tier / description.
@@ -239,7 +240,7 @@ async function probeOne(
239
240
  // Fall through: the original error is the one worth reporting.
240
241
  }
241
242
  }
242
- return { error: (err as Error).message };
243
+ return { error: describeError(err) };
243
244
  }
244
245
  }
245
246
 
@@ -432,6 +433,9 @@ export const REFERENCE_HELPER_NOTE: readonly string[] = [
432
433
  "Each generator is a reference helper, not a guarantee: `meta eject <name>` copies it into",
433
434
  "your repo and the copy is yours to change. What MetaObjects guarantees (migrate, verify,",
434
435
  "render) is not a generator and is not listed here.",
436
+ "Your database, store or framework is not listed (MongoDB, DynamoDB, Express, NestJS, Vue, …)?",
437
+ "Model it anyway and write or retarget a generator: the metaobjects-codegen skill's",
438
+ "references/any-stack.md has the procedure. MySQL is `dialect: \"mysql\"`.",
435
439
  ];
436
440
 
437
441
  /** The footer line explaining the `package-only` row mark. */
@@ -37,6 +37,7 @@ import {
37
37
  type Lock,
38
38
  type LockEntry,
39
39
  } from "@metaobjectsdev/sdk";
40
+ import { describeError } from "./error-text.js";
40
41
 
41
42
  /** `dependency-sync.ts`'s own `ParseError` factory for every
42
43
  * `ERR_DEPENDENCY_MANIFEST_INVALID` case (DESIGN §4.1 step 2 and step 4):
@@ -128,7 +129,7 @@ export async function readManifestDir(dir: string, name: string): Promise<Manife
128
129
  try {
129
130
  parsed = JSON.parse(raw);
130
131
  } catch (err) {
131
- manifestInvalid(name, "readManifestDir", `${manifestPath} is not valid JSON: ${(err as Error).message}`);
132
+ manifestInvalid(name, "readManifestDir", `${manifestPath} is not valid JSON: ${describeError(err)}`);
132
133
  }
133
134
 
134
135
  const result = DependencyManifestSchema.safeParse(parsed);
@@ -491,7 +492,7 @@ export async function readLockOrThrow(configDir: string): Promise<Lock | undefin
491
492
  return await readLock(configDir);
492
493
  } catch (err) {
493
494
  throw new Error(
494
- `${DEFAULT_METAOBJECTS_DIR}/${LOCK_FILE} is corrupted and could not be read: ${(err as Error).message}. ` +
495
+ `${DEFAULT_METAOBJECTS_DIR}/${LOCK_FILE} is corrupted and could not be read: ${describeError(err)}. ` +
495
496
  "Fix it by hand, or delete it and re-run `meta deps sync` to regenerate it.",
496
497
  );
497
498
  }
@@ -613,7 +614,7 @@ export async function checkDependencies(
613
614
  lockIntegrity: lockEntry?.integrity,
614
615
  installedVersion: undefined,
615
616
  installedIntegrity: undefined,
616
- detail: stripDependencyPrefix(name, (err as Error).message),
617
+ detail: stripDependencyPrefix(name, describeError(err)),
617
618
  });
618
619
  }
619
620
  }
@@ -0,0 +1,13 @@
1
+ // The text to print for a caught error. `(err as Error).message` is empty for an
2
+ // AggregateError such as a refused Postgres connection on a dual-stack host, where the
3
+ // detail lives on the inner errors, so a bare `.message` printed nothing after the colon.
4
+
5
+ export function describeError(err: unknown): string {
6
+ if (!(err instanceof Error)) return String(err);
7
+ if (err.message) return err.message;
8
+ if (err instanceof AggregateError && err.errors.length > 0) {
9
+ return err.errors.map(describeError).join("; ");
10
+ }
11
+ const code = (err as { code?: unknown }).code;
12
+ return typeof code === "string" && code ? code : err.name;
13
+ }
@@ -96,7 +96,10 @@ export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOpt
96
96
  });
97
97
  const fields = ref !== undefined ? ref.fields : fk.columns;
98
98
  const label = ref !== undefined ? `${entity.name}.${ref.name}` : `${entity.name} (${fk.name})`;
99
- const indexName = `by${fields.map(pascal).join("")}`;
99
+ // Entity-prefixed: an index name is schema-wide in Postgres, so a bare `by<Field>`
100
+ // suggested the SAME name to every entity sharing the FK field, and applying the advice
101
+ // verbatim failed `meta migrate` with ERR_DUPLICATE_SQL_NAME.
102
+ const indexName = `${entity.name.charAt(0).toLowerCase()}${entity.name.slice(1)}By${fields.map(pascal).join("")}`;
100
103
  const fieldList = fields.length === 1 ? fields[0]! : `[${fields.join(", ")}]`;
101
104
  out.push({
102
105
  file: files[0] ?? "",
@@ -40,7 +40,7 @@ const VIEW_CHANGE_KINDS: ReadonlySet<Change["kind"]> = new Set(["create-view", "
40
40
  export interface GenMigrateAdviceInput {
41
41
  metadata: MetaRoot;
42
42
  /** `metaobjects.config.ts` `dialect` — undefined when no DB code is generated. */
43
- dialect: "sqlite" | "postgres" | undefined;
43
+ dialect: "sqlite" | "postgres" | "mysql" | undefined;
44
44
  /** `.metaobjects/config.json` `migrate.dialect`, when set (may say `d1`). */
45
45
  migrateDialect: "sqlite" | "postgres" | "d1" | undefined;
46
46
  columnNamingStrategy: "snake_case" | "literal" | "kebab-case" | undefined;
@@ -55,6 +55,8 @@ export interface GenMigrateAdviceInput {
55
55
  export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<string | undefined> {
56
56
  if (input.dialect === undefined || input.changedFiles.length === 0) return undefined;
57
57
 
58
+ if (input.dialect === "mysql") return mysqlAdvice(input);
59
+
58
60
  const d1 = d1BindingFor(input);
59
61
  const schemaDialect = d1 !== undefined ? "d1" : input.dialect;
60
62
  let built;
@@ -94,6 +96,27 @@ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<st
94
96
  : `migrate the schema change with \`meta migrate --db <url> --dialect ${input.dialect} --slug <name> --apply\``;
95
97
  }
96
98
 
99
+ /**
100
+ * MySQL: `meta migrate` does not own the schema, so there is no migration to write. When the
101
+ * run touched a table-backed object, say that the adopter's DDL has to follow. The provenance
102
+ * (which generated file belongs to which table-backed object) does not depend on the dialect,
103
+ * so it is read through the Postgres expected-schema builder.
104
+ */
105
+ function mysqlAdvice(input: GenMigrateAdviceInput): string | undefined {
106
+ let provenance: ReadonlyMap<string, string>;
107
+ try {
108
+ provenance = buildExpectedSchemaWithProvenance(input.metadata, {
109
+ dialect: "postgres",
110
+ ...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
111
+ }).provenance;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ if (!touchesTableBackedObject(input.changedFiles, provenance)) return undefined;
116
+ return "MetaObjects does not manage a MySQL schema: apply the matching change to your MySQL DDL " +
117
+ "(the generated Drizzle tables describe the columns it must have)";
118
+ }
119
+
97
120
  /** The project's D1 binding when it migrates through D1, else undefined. */
98
121
  function d1BindingFor(input: GenMigrateAdviceInput): D1Binding | undefined {
99
122
  if (input.dialect !== "sqlite" && input.migrateDialect !== "d1") return undefined;
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { spawnSync } from "node:child_process";
19
19
  import { toPosix } from "./rel-posix.js";
20
+ import { describeError } from "./error-text.js";
20
21
 
21
22
  /** Ignored paths, or the reason the question could not be asked. */
22
23
  export type GitIgnoreResult =
@@ -75,7 +76,7 @@ export function gitIgnored(
75
76
  try {
76
77
  res = spawnSync(gitBin, args, { encoding: "utf-8", input });
77
78
  } catch (err) {
78
- return { unavailable: `git could not be run (${(err as Error).message})` };
79
+ return { unavailable: `git could not be run (${describeError(err)})` };
79
80
  }
80
81
  if (res.error !== undefined) {
81
82
  return { unavailable: "git is not on PATH" };