@metaobjectsdev/cli 1.0.9-rc.6 → 1.0.10

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 (87) 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 +3 -2
  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 +8 -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 +30 -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 +35 -23
  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 +3 -2
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/lib/args.d.ts.map +1 -1
  41. package/dist/src/lib/args.js +17 -6
  42. package/dist/src/lib/args.js.map +1 -1
  43. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  44. package/dist/src/lib/catalog-listing.js +5 -1
  45. package/dist/src/lib/catalog-listing.js.map +1 -1
  46. package/dist/src/lib/dependency-sync.d.ts.map +1 -1
  47. package/dist/src/lib/dependency-sync.js +4 -3
  48. package/dist/src/lib/dependency-sync.js.map +1 -1
  49. package/dist/src/lib/error-text.d.ts +2 -0
  50. package/dist/src/lib/error-text.d.ts.map +1 -0
  51. package/dist/src/lib/error-text.js +15 -0
  52. package/dist/src/lib/error-text.js.map +1 -0
  53. package/dist/src/lib/gen-migrate-advice.d.ts +1 -1
  54. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  55. package/dist/src/lib/gen-migrate-advice.js +24 -0
  56. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  57. package/dist/src/lib/git-ignore.d.ts.map +1 -1
  58. package/dist/src/lib/git-ignore.js +2 -1
  59. package/dist/src/lib/git-ignore.js.map +1 -1
  60. package/dist/src/lib/output.d.ts +3 -2
  61. package/dist/src/lib/output.d.ts.map +1 -1
  62. package/dist/src/lib/output.js.map +1 -1
  63. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  64. package/dist/src/lib/requirement-check.js +43 -2
  65. package/dist/src/lib/requirement-check.js.map +1 -1
  66. package/package.json +10 -10
  67. package/src/commands/deps.ts +9 -8
  68. package/src/commands/docs.ts +19 -12
  69. package/src/commands/eject.ts +3 -2
  70. package/src/commands/export.ts +3 -2
  71. package/src/commands/gen.ts +8 -7
  72. package/src/commands/generator.ts +3 -2
  73. package/src/commands/init.ts +5 -2
  74. package/src/commands/migrate.ts +32 -23
  75. package/src/commands/prompt-snapshot.ts +5 -4
  76. package/src/commands/types.ts +6 -1
  77. package/src/commands/upgrade.ts +4 -3
  78. package/src/commands/verify.ts +37 -23
  79. package/src/index.ts +3 -2
  80. package/src/lib/args.ts +19 -6
  81. package/src/lib/catalog-listing.ts +5 -1
  82. package/src/lib/dependency-sync.ts +4 -3
  83. package/src/lib/error-text.ts +13 -0
  84. package/src/lib/gen-migrate-advice.ts +24 -1
  85. package/src/lib/git-ignore.ts +2 -1
  86. package/src/lib/output.ts +3 -2
  87. package/src/lib/requirement-check.ts +46 -2
@@ -65,6 +65,7 @@ import { tokensToAllowOptions, blockedEntriesFor, blockedHintLines } from "../li
65
65
  import { reportLoadError } from "../lib/load-error.js";
66
66
  import { scanForReferentialActionConflicts } from "../lib/referential-action-advisory.js";
67
67
  import type { MetaData } from "@metaobjectsdev/metadata";
68
+ import { describeError } from "../lib/error-text.js";
68
69
 
69
70
  /**
70
71
  * Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -498,7 +499,7 @@ export async function migrateCommand(
498
499
  try {
499
500
  flags = parseMigrateArgs(args);
500
501
  } catch (err) {
501
- const msg = (err as Error).message;
502
+ const msg = describeError(err);
502
503
  log.error(`migrate: ${msg}`);
503
504
  emitStructuredError(`migrate: ${msg}`, "run `meta migrate --help` for usage", fmt);
504
505
  return 2;
@@ -692,7 +693,7 @@ export async function migrateCommand(
692
693
  try {
693
694
  collection = await resolveCollection(metaRoot);
694
695
  } catch (err) {
695
- log.error((err as Error).message);
696
+ log.error(describeError(err));
696
697
  return 2;
697
698
  }
698
699
 
@@ -712,7 +713,7 @@ export async function migrateCommand(
712
713
  try {
713
714
  kysely = await buildKyselyFromUrl(config.databaseUrl, config.dialect);
714
715
  } catch (err) {
715
- log.error(`migrate: ${(err as Error).message}`);
716
+ log.error(`migrate: ${describeError(err)}`);
716
717
  return 2;
717
718
  }
718
719
 
@@ -758,7 +759,7 @@ export async function migrateCommand(
758
759
  try {
759
760
  actual = await introspect(kysely.db, kysely.dialect);
760
761
  } catch (err) {
761
- log.error(`migrate: failed to connect to ${kysely.displayUrl}: ${(err as Error).message}`);
762
+ log.error(`migrate: failed to connect to ${kysely.displayUrl}: ${describeError(err)}`);
762
763
  await kysely.close();
763
764
  return 2;
764
765
  }
@@ -801,7 +802,7 @@ export async function migrateCommand(
801
802
  }
802
803
  // diff() throws when onAmbiguous returns "abort" — surface as exit 1
803
804
  // with the collected ambiguity list.
804
- if ((err as Error).message.includes("aborted by onAmbiguous")) {
805
+ if (describeError(err).includes("aborted by onAmbiguous")) {
805
806
  ambiguous = ambiguousToEntries(collectedAmbiguous);
806
807
  const migrateResult = {
807
808
  dialect: kysely.dialect,
@@ -895,6 +896,14 @@ export async function migrateCommand(
895
896
  if (exitCode === 0 && emitted) {
896
897
  if (config.slug === undefined) {
897
898
  log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
899
+ // The common way to land here is a NEW database (a fresh clone, CI, another
900
+ // environment) whose migrations are already committed: the diff against an empty
901
+ // database is every table, so it asks to author a new migration. That database
902
+ // needs the committed chain replayed, not a new migration.
903
+ log.error(
904
+ ` New database, migrations already committed? Apply them instead: ` +
905
+ `meta migrate apply-pending --db <url>`,
906
+ );
898
907
  await kysely.close();
899
908
  return 2;
900
909
  }
@@ -936,7 +945,7 @@ export async function migrateCommand(
936
945
  });
937
946
  appliedNames = [...result.applied];
938
947
  } catch (err) {
939
- log.error(`migrate: apply failed: ${(err as Error).message}`);
948
+ log.error(`migrate: apply failed: ${describeError(err)}`);
940
949
  exitCode = 1;
941
950
  applyFailed = true;
942
951
  }
@@ -992,7 +1001,7 @@ export async function migrateCommand(
992
1001
  } catch (err) {
993
1002
  // The migration itself is written (and possibly applied) — report the
994
1003
  // bookkeeping failure for what it is rather than as an unexpected error.
995
- log.error(`migrate: failed to write the schema snapshot: ${(err as Error).message}`);
1004
+ log.error(`migrate: failed to write the schema snapshot: ${describeError(err)}`);
996
1005
  exitCode = 1;
997
1006
  }
998
1007
  }
@@ -1000,7 +1009,7 @@ export async function migrateCommand(
1000
1009
  try {
1001
1010
  await kysely.close();
1002
1011
  } catch (err) {
1003
- log.warn(`migrate: failed to close DB cleanly: ${(err as Error).message}`);
1012
+ log.warn(`migrate: failed to close DB cleanly: ${describeError(err)}`);
1004
1013
  }
1005
1014
  }
1006
1015
 
@@ -1038,7 +1047,7 @@ export async function migrateCommand(
1038
1047
  // A metadata defect, not a tool failure: two declarations generate one database name.
1039
1048
  if (isDuplicateSqlNameError(err)) return refuseEngineError(err, DUPLICATE_SQL_NAME_HINT, fmt);
1040
1049
  // Unexpected error: emit structured error on stdout in the active format, then exit 1.
1041
- const msg = (err as Error).message ?? String(err);
1050
+ const msg = describeError(err) ?? String(err);
1042
1051
  log.error(`migrate: unexpected error: ${msg}`);
1043
1052
  emitStructuredError(`migrate: unexpected error: ${msg}`, "run `meta migrate --help` for usage", fmt);
1044
1053
  return 1;
@@ -1076,7 +1085,7 @@ export async function runBaseline(
1076
1085
  try {
1077
1086
  kysely = await buildKyselyFromUrl(config.databaseUrl, config.dialect);
1078
1087
  } catch (err) {
1079
- log.error(`migrate baseline: ${(err as Error).message}`);
1088
+ log.error(`migrate baseline: ${describeError(err)}`);
1080
1089
  return 2;
1081
1090
  }
1082
1091
  try {
@@ -1183,7 +1192,7 @@ export async function runApplyPending(
1183
1192
  try {
1184
1193
  kysely = await buildKyselyFromUrl(config.databaseUrl, config.dialect);
1185
1194
  } catch (err) {
1186
- log.error(`migrate apply-pending: ${(err as Error).message}`);
1195
+ log.error(`migrate apply-pending: ${describeError(err)}`);
1187
1196
  return 2;
1188
1197
  }
1189
1198
 
@@ -1199,13 +1208,13 @@ export async function runApplyPending(
1199
1208
  pendingNames = [...result.pending];
1200
1209
  appliedNames = [...result.applied];
1201
1210
  } catch (err) {
1202
- log.error(`migrate apply-pending: apply failed: ${(err as Error).message}`);
1211
+ log.error(`migrate apply-pending: apply failed: ${describeError(err)}`);
1203
1212
  exitCode = 1;
1204
1213
  } finally {
1205
1214
  try {
1206
1215
  await kysely.close();
1207
1216
  } catch (err) {
1208
- log.warn(`migrate apply-pending: failed to close DB cleanly: ${(err as Error).message}`);
1217
+ log.warn(`migrate apply-pending: failed to close DB cleanly: ${describeError(err)}`);
1209
1218
  }
1210
1219
  }
1211
1220
  if (exitCode !== 0) return exitCode;
@@ -1304,7 +1313,7 @@ export async function runOfflineGenerate(
1304
1313
  try {
1305
1314
  snapshot = await readSnapshot(path);
1306
1315
  } catch (err) {
1307
- log.error(`migrate: cannot read schema snapshot at ${path}: ${(err as Error).message}`);
1316
+ log.error(`migrate: cannot read schema snapshot at ${path}: ${describeError(err)}`);
1308
1317
  return 2;
1309
1318
  }
1310
1319
  if (snapshot === null) {
@@ -1352,7 +1361,7 @@ export async function runOfflineGenerate(
1352
1361
  // #258 — a primary-key move has no expressible migration; refuse loudly.
1353
1362
  if (isPrimaryKeyChangeError(err)) return refuseEngineError(err, PK_CHANGE_HINT, fmt);
1354
1363
  if (isDeclaredRenameError(err)) return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
1355
- if ((err as Error).message.includes("aborted by onAmbiguous")) {
1364
+ if (describeError(err).includes("aborted by onAmbiguous")) {
1356
1365
  log.error(`migrate: ambiguous rename/drop detected; re-run with --on-ambiguous rename|drop-add`);
1357
1366
  return 1;
1358
1367
  }
@@ -1438,7 +1447,7 @@ async function runRollback(
1438
1447
  try {
1439
1448
  kysely = await buildKyselyFromUrl(databaseUrl, config.dialect);
1440
1449
  } catch (err) {
1441
- log.error(`migrate: ${(err as Error).message}`);
1450
+ log.error(`migrate: ${describeError(err)}`);
1442
1451
  return 2;
1443
1452
  }
1444
1453
  // kysely.dialect is "sqlite" | "postgres" here — d1 is rejected upstream.
@@ -1456,13 +1465,13 @@ async function runRollback(
1456
1465
  }
1457
1466
  return 0;
1458
1467
  } catch (err) {
1459
- log.error(`migrate: rollback failed: ${(err as Error).message}`);
1468
+ log.error(`migrate: rollback failed: ${describeError(err)}`);
1460
1469
  return 1;
1461
1470
  } finally {
1462
1471
  try {
1463
1472
  await kysely.close();
1464
1473
  } catch (err) {
1465
- log.warn(`migrate: failed to close DB cleanly: ${(err as Error).message}`);
1474
+ log.warn(`migrate: failed to close DB cleanly: ${describeError(err)}`);
1466
1475
  }
1467
1476
  }
1468
1477
  }
@@ -1496,7 +1505,7 @@ async function runD1Migrate(
1496
1505
  try {
1497
1506
  binding = resolveD1Binding(parsed.d1Bindings, config.d1.binding);
1498
1507
  } catch (err) {
1499
- log.error(`migrate: ${(err as Error).message}`);
1508
+ log.error(`migrate: ${describeError(err)}`);
1500
1509
  return 2;
1501
1510
  }
1502
1511
  } else {
@@ -1538,7 +1547,7 @@ async function runD1Migrate(
1538
1547
  try {
1539
1548
  collection = await resolveCollection(metaRoot);
1540
1549
  } catch (err) {
1541
- log.error((err as Error).message);
1550
+ log.error(describeError(err));
1542
1551
  return 2;
1543
1552
  }
1544
1553
 
@@ -1579,7 +1588,7 @@ async function runD1Migrate(
1579
1588
  configPath: wranglerConfigPath,
1580
1589
  });
1581
1590
  } catch (err) {
1582
- log.error(`migrate: failed to introspect D1: ${(err as Error).message}`);
1591
+ log.error(`migrate: failed to introspect D1: ${describeError(err)}`);
1583
1592
  return 2;
1584
1593
  }
1585
1594
 
@@ -1612,7 +1621,7 @@ async function runD1Migrate(
1612
1621
  // #258 — a primary-key move has no expressible migration; refuse loudly.
1613
1622
  if (isPrimaryKeyChangeError(err)) return refuseEngineError(err, PK_CHANGE_HINT, fmt);
1614
1623
  if (isDeclaredRenameError(err)) return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
1615
- if ((err as Error).message.includes("aborted by onAmbiguous")) {
1624
+ if (describeError(err).includes("aborted by onAmbiguous")) {
1616
1625
  const entries = ambiguousToEntries(collectedAmbiguous);
1617
1626
  for (const e of entries) {
1618
1627
  log.error(` ambiguous ${e.kind}: ${e.description}${e.hint ? ` [${e.hint}]` : ""}`);
@@ -1708,7 +1717,7 @@ async function runWranglerApply(
1708
1717
  return await new Promise<number>((resolve) => {
1709
1718
  const child = spawn("wrangler", applyArgs, { stdio: "inherit" });
1710
1719
  child.on("error", (err) => {
1711
- log.error(`migrate: failed to run wrangler: ${(err as Error).message}`);
1720
+ log.error(`migrate: failed to run wrangler: ${describeError(err)}`);
1712
1721
  resolve(2);
1713
1722
  });
1714
1723
  child.on("close", (code) => resolve(code ?? 1));
@@ -20,6 +20,7 @@ import { collectionLoadOptions } from "../lib/collection-load-options.js";
20
20
  import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
21
21
  import { TYPE_TEMPLATE, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
22
22
  import { render, ESCAPERS, type RenderFormat } from "@metaobjectsdev/render";
23
+ import { describeError } from "../lib/error-text.js";
23
24
 
24
25
  const DEFAULT_PROMPTS_DIR = "prompts";
25
26
 
@@ -28,7 +29,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
28
29
  try {
29
30
  flags = parsePromptSnapshotArgs(args);
30
31
  } catch (err) {
31
- log.error((err as Error).message);
32
+ log.error(describeError(err));
32
33
  return 2;
33
34
  }
34
35
 
@@ -44,7 +45,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
44
45
  try {
45
46
  collection = await resolveCollection(cwd);
46
47
  } catch (err) {
47
- log.error((err as Error).message);
48
+ log.error(describeError(err));
48
49
  return 2;
49
50
  }
50
51
 
@@ -117,7 +118,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
117
118
  try {
118
119
  payload = JSON.parse(readFileSync(payloadPath, "utf8"));
119
120
  } catch (err) {
120
- log.error(`[${tmpl.name}] invalid payload.json: ${(err as Error).message}`);
121
+ log.error(`[${tmpl.name}] invalid payload.json: ${describeError(err)}`);
121
122
  errorCount++;
122
123
  continue;
123
124
  }
@@ -135,7 +136,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
135
136
  try {
136
137
  rendered = render({ ref: textRef, payload, provider, ...(format ? { format } : {}) });
137
138
  } catch (err) {
138
- log.error(`[${tmpl.name}] render failed: ${(err as Error).message}`);
139
+ log.error(`[${tmpl.name}] render failed: ${describeError(err)}`);
139
140
  errorCount++;
140
141
  continue;
141
142
  }
@@ -24,6 +24,7 @@ import { composeRegistry, buildVocabularyCatalog } from "@metaobjectsdev/metadat
24
24
  import { defaultLoadMemoryProviders } from "@metaobjectsdev/sdk";
25
25
  import { log } from "../lib/log.js";
26
26
  import { emitStructured, type OutputFormat } from "../lib/format.js";
27
+ import { describeError } from "../lib/error-text.js";
27
28
 
28
29
  interface TypesFlags {
29
30
  query: string | null;
@@ -210,7 +211,7 @@ export async function typesCommand(args: string[], fmt: OutputFormat = "text"):
210
211
  try {
211
212
  f = parse(args);
212
213
  } catch (err) {
213
- const msg = (err as Error).message;
214
+ const msg = describeError(err);
214
215
  log.error(msg);
215
216
  // A structured caller gets a structured refusal — exiting 2 with an EMPTY stdout is
216
217
  // the same silence a `| jq` cannot tell from "no results". Mirrors verify.ts.
@@ -317,7 +318,11 @@ export async function typesCommand(args: string[], fmt: OutputFormat = "text"):
317
318
  const shown = capped ? matches.slice(0, f.limit) : matches;
318
319
 
319
320
  if (f.detail) {
321
+ // A subtype's block lists its @attrs with their full descriptions, so an attr row
322
+ // whose owner subtype is also shown would print the same text a second time.
323
+ const shownSubtypes = new Set(shown.filter((e) => e.kind === "subtype").map((e) => e.owner));
320
324
  for (const e of shown) {
325
+ if (e.kind === "attr" && shownSubtypes.has(e.owner)) continue;
321
326
  log.info(`\n${e.name} (${e.kind})`);
322
327
  if (e.description) log.info(` ${e.description}`);
323
328
  if (e.sharedRoot)
@@ -29,6 +29,7 @@ import {
29
29
  retirementSuggestions,
30
30
  } from "@metaobjectsdev/metadata";
31
31
  import { log } from "../lib/log.js";
32
+ import { describeError } from "../lib/error-text.js";
32
33
 
33
34
  /** YAML authoring (ADR-0006). Rewritten by the `yaml`-backed arm, loaded on demand below. */
34
35
  const YAML_EXTENSIONS = new Set([".yaml", ".yml"]);
@@ -61,7 +62,7 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
61
62
  try {
62
63
  flags = parseArgs(args);
63
64
  } catch (err) {
64
- if ((err as Error).message === "__help__") {
65
+ if (describeError(err) === "__help__") {
65
66
  log.info(
66
67
  "meta upgrade [<project>] [--to <version>] [--apply]\n\n" +
67
68
  " Rewrites metadata the current loader no longer accepts, in JSON and YAML alike:\n" +
@@ -72,7 +73,7 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
72
73
  );
73
74
  return 0;
74
75
  }
75
- log.error((err as Error).message);
76
+ log.error(describeError(err));
76
77
  return 2;
77
78
  }
78
79
 
@@ -82,7 +83,7 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
82
83
  try {
83
84
  files = (await resolveCollection(projectRoot)).files;
84
85
  } catch (err) {
85
- log.error((err as Error).message);
86
+ log.error(describeError(err));
86
87
  return 1;
87
88
  }
88
89
 
@@ -106,6 +106,7 @@ import {
106
106
  } from "@metaobjectsdev/metadata";
107
107
  import { verify, ERR_REQUIRED_SLOT_UNUSED, ERR_PARTIAL_UNRESOLVED } from "@metaobjectsdev/render";
108
108
  import { describeLoadError, reportLoadError } from "../lib/load-error.js";
109
+ import { describeError } from "../lib/error-text.js";
109
110
 
110
111
  const DEFAULT_PROMPTS_DIR = "prompts";
111
112
 
@@ -162,7 +163,7 @@ export async function verifyCommand(
162
163
  try {
163
164
  flags = parseVerifyArgs(args);
164
165
  } catch (err) {
165
- const msg = (err as Error).message;
166
+ const msg = describeError(err);
166
167
  log.error(msg);
167
168
  // A structured caller gets a structured refusal. Exiting 2 with an EMPTY stdout
168
169
  // is the same silence this command is being fixed for, one level up.
@@ -206,7 +207,7 @@ export async function verifyCommand(
206
207
  try {
207
208
  collection = await resolveCollection(cwd);
208
209
  } catch (err) {
209
- const msg = (err as Error).message;
210
+ const msg = describeError(err);
210
211
  log.error(msg);
211
212
  emitStructured({ error: msg, hint: "declare metadata `sources` in .metaobjects/config.json, or run `meta init`" }, fmt);
212
213
  return 2;
@@ -274,7 +275,7 @@ export async function verifyCommand(
274
275
  strict: !flags.lax,
275
276
  });
276
277
  } catch (err) {
277
- const msg = (err as Error).message;
278
+ const msg = describeError(err);
278
279
  // Everything the loader's envelope carried, not just the message: the stable code a
279
280
  // CI job keys on, and the FILE + json path the message itself can never name.
280
281
  const report = describeLoadError(err);
@@ -481,8 +482,15 @@ export async function verifyCommand(
481
482
  // .metaobjects/config.json — the spelling a TypeScript project is least likely
482
483
  // to have. migrate's own resolution still WINS where it has an answer, because
483
484
  // it is the more specific statement about the chain being replayed.
485
+ if (flags.dialect === undefined && migrateConfig.dialect === undefined && forgeConfig?.dialect === "mysql") {
486
+ log.error(
487
+ "meta verify --replay: MetaObjects does not own a MySQL schema, so there is no migration chain to replay.",
488
+ );
489
+ return 2;
490
+ }
491
+ const configuredDialect = forgeConfig?.dialect === "mysql" ? undefined : forgeConfig?.dialect;
484
492
  const dialect: Dialect | undefined =
485
- flags.dialect ?? migrateConfig.dialect ?? forgeConfig?.dialect;
493
+ flags.dialect ?? migrateConfig.dialect ?? configuredDialect;
486
494
  if (dialect === undefined) {
487
495
  log.error(
488
496
  `meta verify --replay: no dialect — pass --dialect <postgres|sqlite>, ` +
@@ -505,7 +513,7 @@ export async function verifyCommand(
505
513
  } catch (err) {
506
514
  // A missing optional driver lands here, and its message already carries the
507
515
  // install hint. Operational, not drift.
508
- log.error(`meta verify --replay: ${(err as Error).message}`);
516
+ log.error(`meta verify --replay: ${describeError(err)}`);
509
517
  return 2;
510
518
  }
511
519
 
@@ -514,7 +522,7 @@ export async function verifyCommand(
514
522
  try {
515
523
  applied = await applyPending(engine.db, dir, { dryRun: false, dialect });
516
524
  } catch (err) {
517
- log.error(`meta verify --replay: ${(err as Error).message}`);
525
+ log.error(`meta verify --replay: ${describeError(err)}`);
518
526
  for (const line of replayRemedy(err)) log.error(`meta verify --replay: ${line}`);
519
527
  return 1;
520
528
  }
@@ -787,7 +795,7 @@ export async function verifyCommand(
787
795
  findings = await lintOverlays(collection, (path) => new FileSource(path));
788
796
  } catch (err) {
789
797
  // Never let an advisory scan break verify — and never report it as clean.
790
- overlaySection = skippedSection(`the overlay lint failed: ${(err as Error).message}`);
798
+ overlaySection = skippedSection(`the overlay lint failed: ${describeError(err)}`);
791
799
  return;
792
800
  }
793
801
  overlaySection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
@@ -819,7 +827,7 @@ export async function verifyCommand(
819
827
  findings = scanSourceForAntiPatterns(projectRoot, ignore !== undefined ? { ignore } : undefined);
820
828
  } catch (err) {
821
829
  // Never let an advisory scan break verify — and never report it as clean.
822
- antiPatternSection = skippedSection(`the scan failed: ${(err as Error).message}`);
830
+ antiPatternSection = skippedSection(`the scan failed: ${describeError(err)}`);
823
831
  return;
824
832
  }
825
833
  // F52 — the one gate that can see a provider silently missing its base. Folded into
@@ -860,12 +868,17 @@ export async function verifyCommand(
860
868
  // each one and says how to declare it (`index.lookup`) instead. Read off the SAME
861
869
  // expected schema `meta migrate` builds, so "covered" means covered in its DDL.
862
870
  let unindexedFks: UnindexedFkFinding[] = [];
871
+ // Skipped on MySQL: MetaObjects emits no DDL there, and InnoDB indexes every foreign
872
+ // key itself.
873
+ const scanDialect = flags.dialect ?? forgeConfig?.dialect ?? "postgres";
863
874
  try {
864
- unindexedFks = scanForUnindexedForeignKeys(root, {
865
- dialect: flags.dialect ?? forgeConfig?.dialect ?? "postgres",
866
- columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
867
- skip: collection.imported,
868
- });
875
+ if (scanDialect !== "mysql") {
876
+ unindexedFks = scanForUnindexedForeignKeys(root, {
877
+ dialect: scanDialect,
878
+ columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
879
+ skip: collection.imported,
880
+ });
881
+ }
869
882
  } catch {
870
883
  // Same discipline as its siblings: an advisory scan never breaks verify. A model the
871
884
  // schema builder refuses is migrate's to report, with its own message.
@@ -1039,7 +1052,8 @@ export async function verifyCommand(
1039
1052
  const text = provider.resolve(ref);
1040
1053
  if (text === undefined) {
1041
1054
  log.error(
1042
- `[${tmpl.name}] (${label}) ${ERR_PARTIAL_UNRESOLVED}: ref "${ref}" did not resolve under ${promptsDir}`,
1055
+ `[${tmpl.name}] (${label}) ${ERR_PARTIAL_UNRESOLVED}: ref "${ref}" did not resolve under ${promptsDir}` +
1056
+ ` — create ${join(promptsDir, `${ref}.mustache`)} (or .txt), or point --prompts at the directory that holds it`,
1043
1057
  );
1044
1058
  errorCount++;
1045
1059
  anyErrorHere = true;
@@ -1144,7 +1158,7 @@ export async function verifyCommand(
1144
1158
  try {
1145
1159
  kysely = await buildKyselyFromUrl(flags.db as string, flags.dialect as Dialect | undefined);
1146
1160
  } catch (err) {
1147
- log.error(`verify: ${(err as Error).message}`);
1161
+ log.error(`verify: ${describeError(err)}`);
1148
1162
  return 1;
1149
1163
  }
1150
1164
 
@@ -1170,7 +1184,7 @@ export async function verifyCommand(
1170
1184
  ...importedOption(collection),
1171
1185
  });
1172
1186
  } catch (err) {
1173
- log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
1187
+ log.error(`verify: failed to introspect ${kysely.displayUrl}: ${describeError(err)}`);
1174
1188
  return 1;
1175
1189
  }
1176
1190
 
@@ -1184,7 +1198,7 @@ export async function verifyCommand(
1184
1198
  try {
1185
1199
  await kysely.close();
1186
1200
  } catch (err) {
1187
- log.warn(`verify: failed to close DB cleanly: ${(err as Error).message}`);
1201
+ log.warn(`verify: failed to close DB cleanly: ${describeError(err)}`);
1188
1202
  }
1189
1203
  }
1190
1204
  }
@@ -1217,7 +1231,7 @@ export async function verifyCommand(
1217
1231
  try {
1218
1232
  binding = resolveD1Binding(parsed.d1Bindings, d1Config.binding);
1219
1233
  } catch (err) {
1220
- log.error(`verify: ${(err as Error).message}`);
1234
+ log.error(`verify: ${describeError(err)}`);
1221
1235
  return 2;
1222
1236
  }
1223
1237
  } else {
@@ -1241,7 +1255,7 @@ export async function verifyCommand(
1241
1255
  try {
1242
1256
  actual = await introspectD1({ runner: d1Runner, binding: binding.binding, remote, configPath: wranglerConfigPath });
1243
1257
  } catch (err) {
1244
- log.error(`verify: failed to introspect D1 binding '${binding.binding}': ${(err as Error).message}`);
1258
+ log.error(`verify: failed to introspect D1 binding '${binding.binding}': ${describeError(err)}`);
1245
1259
  return 1;
1246
1260
  }
1247
1261
 
@@ -1258,7 +1272,7 @@ export async function verifyCommand(
1258
1272
  ...importedOption(collection),
1259
1273
  });
1260
1274
  } catch (err) {
1261
- log.error(`verify: ${(err as Error).message}`);
1275
+ log.error(`verify: ${describeError(err)}`);
1262
1276
  return 1;
1263
1277
  }
1264
1278
 
@@ -1444,7 +1458,7 @@ export async function verifyCommand(
1444
1458
  try {
1445
1459
  result = await computeCodegenDrift(forgeConfig, codegenRoot, genConfigDir, genCollection.inScope);
1446
1460
  } catch (err) {
1447
- log.error(`verify --codegen: regeneration failed: ${(err as Error).message}`);
1461
+ log.error(`verify --codegen: regeneration failed: ${describeError(err)}`);
1448
1462
  return 1;
1449
1463
  }
1450
1464
 
@@ -1541,7 +1555,7 @@ export async function verifyCommand(
1541
1555
  cwd: genConfigDir,
1542
1556
  });
1543
1557
  } catch (err) {
1544
- log.error(`verify --docs: regeneration failed: ${(err as Error).message}`);
1558
+ log.error(`verify --docs: regeneration failed: ${describeError(err)}`);
1545
1559
  return 1;
1546
1560
  }
1547
1561
 
@@ -1617,7 +1631,7 @@ export async function verifyCommand(
1617
1631
  try {
1618
1632
  lock = await readLockOrThrow(collection.configDir);
1619
1633
  } catch (err) {
1620
- log.error(`verify --deps: ${(err as Error).message}`);
1634
+ log.error(`verify --deps: ${describeError(err)}`);
1621
1635
  return 2;
1622
1636
  }
1623
1637
 
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
 
@@ -591,7 +592,7 @@ export async function run(argv: string[]): Promise<number> {
591
592
  try {
592
593
  flags = parseAgentDocsArgs(rest);
593
594
  } catch (err) {
594
- log.error((err as Error).message);
595
+ log.error(describeError(err));
595
596
  return 2;
596
597
  }
597
598
  const targetCwd = flags.out !== undefined ? resolve(cwd, flags.out) : cwd;
@@ -609,7 +610,7 @@ export async function run(argv: string[]): Promise<number> {
609
610
  log.info("Re-run agent-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
610
611
  return 0;
611
612
  } catch (err) {
612
- log.error((err as Error).message);
613
+ log.error(describeError(err));
613
614
  return 1;
614
615
  }
615
616
  }
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
@@ -428,9 +445,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
428
445
  });
429
446
 
430
447
  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
- }
448
+ assertSchemaDialect(dialect);
434
449
 
435
450
  const allowTokens = parseAllowTokens(values.allow as string | string[] | undefined);
436
451
 
@@ -588,9 +603,7 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
588
603
  }
589
604
 
590
605
  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
- }
606
+ assertSchemaDialect(dialect);
594
607
 
595
608
  // NOTE: the flag is --migration-format, not --format: `--format` is already a
596
609
  // 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
+ }