@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
@@ -56,6 +56,7 @@ import {
56
56
  } from "@metaobjectsdev/migrate-ts";
57
57
  import type { AgentSchemaInput, SchemaColumnLike } from "@metaobjectsdev/codegen-ts";
58
58
  import { reportLoadError } from "../lib/load-error.js";
59
+ import { describeError } from "../lib/error-text.js";
59
60
 
60
61
  /**
61
62
  * Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -286,6 +287,12 @@ function buildAgentSchemaInput(
286
287
  );
287
288
  return undefined;
288
289
  }
290
+ // MySQL: MetaObjects does not own the schema, so there is no migrate-built schema to
291
+ // describe. The generated Drizzle tables are the description.
292
+ if (configured === "mysql") {
293
+ log.warn("docs: agent/schema.md skipped — MetaObjects does not own a MySQL schema (the generated Drizzle tables describe it).");
294
+ return undefined;
295
+ }
289
296
  const dialect = configured ?? DEFAULT_DIALECT;
290
297
  try {
291
298
  const built = buildExpectedSchemaWithProvenance(root, {
@@ -313,7 +320,7 @@ function buildAgentSchemaInput(
313
320
  // agent pages still emit.
314
321
  log.warn(
315
322
  `docs: agent/schema.md skipped — the expected schema could not be built ` +
316
- `(${(err as Error).message}). Run 'meta migrate' for the full diagnosis.`,
323
+ `(${describeError(err)}). Run 'meta migrate' for the full diagnosis.`,
317
324
  );
318
325
  return undefined;
319
326
  }
@@ -331,7 +338,7 @@ export async function docsCommand(
331
338
  try {
332
339
  flags = parseDocsArgs(args, cwd);
333
340
  } catch (err) {
334
- log.error(`docs: ${(err as Error).message}`);
341
+ log.error(`docs: ${describeError(err)}`);
335
342
  return 2;
336
343
  }
337
344
 
@@ -418,7 +425,7 @@ export async function docsCommand(
418
425
  flags.projectRootProvided ? { explicitDir: metaRoot } : undefined,
419
426
  );
420
427
  } catch (err) {
421
- log.error(`docs: ${(err as Error).message}`);
428
+ log.error(`docs: ${describeError(err)}`);
422
429
  return 2;
423
430
  }
424
431
 
@@ -468,7 +475,7 @@ export async function docsCommand(
468
475
  configLoadOptions = loadMemoryOptionsFrom(loadedConfig);
469
476
  } catch (err) {
470
477
  log.warn(
471
- `docs: metaobjects.config.ts failed to load (${(err as Error).message}); ` +
478
+ `docs: metaobjects.config.ts failed to load (${describeError(err)}); ` +
472
479
  `generating docs without its providers`,
473
480
  );
474
481
  loadedConfig = undefined;
@@ -558,7 +565,7 @@ export async function docsCommand(
558
565
  suite = resolveGenerators(loadedConfig?.generators ?? []);
559
566
  } catch (err) {
560
567
  log.warn(
561
- `docs: could not resolve the generator suite (${(err as Error).message}) — the agent ` +
568
+ `docs: could not resolve the generator suite (${describeError(err)}) — the agent ` +
562
569
  `pages fall back to the generators declared as values, so a tier wired by name may ` +
563
570
  `be missing from them.`,
564
571
  );
@@ -618,7 +625,7 @@ export async function docsCommand(
618
625
  try {
619
626
  modelFiles = await docsFile(modelOpts).generate(ctx);
620
627
  } catch (err) {
621
- const msg = (err as Error).message;
628
+ const msg = describeError(err);
622
629
  // Duplicate output path (silent-overwrite backstop): the generator already
623
630
  // names both colliding FQNs + the path and starts with "docs:". Surface it
624
631
  // verbatim as a clean non-zero exit (no double prefix, no stack trace).
@@ -674,7 +681,7 @@ export async function docsCommand(
674
681
  );
675
682
  }
676
683
  } catch (err) {
677
- const msg = (err as Error).message;
684
+ const msg = describeError(err);
678
685
  if (msg.startsWith("docs: duplicate output path")) {
679
686
  log.error(msg);
680
687
  return 1;
@@ -737,7 +744,7 @@ export async function docsCommand(
737
744
  await writeFile(path, f.content, "utf8");
738
745
  }
739
746
  } catch (err) {
740
- log.error(`docs: failed to write pages: ${(err as Error).message}`);
747
+ log.error(`docs: failed to write pages: ${describeError(err)}`);
741
748
  return 1;
742
749
  }
743
750
 
@@ -806,7 +813,7 @@ async function scaffoldSiteCommand(projectRoot: string): Promise<number> {
806
813
  created.push(rel);
807
814
  }
808
815
  } catch (err) {
809
- log.error(`docs: failed to scaffold site templates: ${(err as Error).message}`);
816
+ log.error(`docs: failed to scaffold site templates: ${describeError(err)}`);
810
817
  return 1;
811
818
  }
812
819
  log.info(
@@ -913,7 +920,7 @@ async function emitSite(
913
920
  log.info(`meta docs --site — wrote ${r.pages.length} page(s) → ${siteOutDir}`);
914
921
  return 0;
915
922
  } catch (err) {
916
- log.error(`docs: failed to generate site: ${(err as Error).message}`);
923
+ log.error(`docs: failed to generate site: ${describeError(err)}`);
917
924
  return 1;
918
925
  }
919
926
  }
@@ -931,7 +938,7 @@ async function metamodelDocsCommand(cwd: string, out: string): Promise<number> {
931
938
  try {
932
939
  docs = renderCoreMetamodelDocs(composeRegistry(coreProviders));
933
940
  } catch (err) {
934
- log.error(`docs: failed to render metamodel docs: ${(err as Error).message}`);
941
+ log.error(`docs: failed to render metamodel docs: ${describeError(err)}`);
935
942
  return 1;
936
943
  }
937
944
  try {
@@ -942,7 +949,7 @@ async function metamodelDocsCommand(cwd: string, out: string): Promise<number> {
942
949
  await writeFile(path, content, "utf8");
943
950
  }
944
951
  } catch (err) {
945
- log.error(`docs: failed to write metamodel pages: ${(err as Error).message}`);
952
+ log.error(`docs: failed to write metamodel pages: ${describeError(err)}`);
946
953
  return 1;
947
954
  }
948
955
  log.info(`meta docs --metamodel — wrote ${docs.size} page(s) → ${outDir}`);
@@ -36,6 +36,7 @@ import {
36
36
  ejectLibrary, ejectableLibraryNames, isLibraryName, libraryStaleness,
37
37
  type LibraryEjectResult,
38
38
  } from "../lib/library-eject.js";
39
+ import { describeError } from "../lib/error-text.js";
39
40
 
40
41
  // Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
41
42
  // same never-clobber-without-consent contract. Kept as its own local constant rather
@@ -255,7 +256,12 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
255
256
  // The adapter source the OUTPUT imports is part of what is handed over — copied whether
256
257
  // or not the generator file itself was already here, and under the same never-clobber
257
258
  // rule, so re-running eject fills in a missing copy without touching a kept one.
258
- const runtime = ejectsRuntime(opts.name)
259
+ // Except when the KEPT copy predates that hand-over (1.0.9): with no `runtimeImport`
260
+ // option its output imports the published package, so the copy would be dead code —
261
+ // a Drizzle adapter, and its dependencies, in a project that may have no database.
262
+ const keptCopyImportsPackage =
263
+ existing !== undefined && !opts.force && !existing.includes("runtimeImport");
264
+ const runtime = ejectsRuntime(opts.name) && !keptCopyImportsPackage
259
265
  ? await ejectRuntime(opts.cwd, [opts.name], opts.force === true)
260
266
  : undefined;
261
267
 
@@ -613,7 +619,7 @@ export async function ejectCommand(
613
619
  try {
614
620
  flags = parseEjectArgs(args);
615
621
  } catch (err) {
616
- log.error((err as Error).message);
622
+ log.error(describeError(err));
617
623
  return 2;
618
624
  }
619
625
 
@@ -687,7 +693,7 @@ export async function ejectCommand(
687
693
  if (fmt === "text") reportOne(result, name);
688
694
  }
689
695
  } catch (err) {
690
- log.error((err as Error).message);
696
+ log.error(describeError(err));
691
697
  return 1;
692
698
  }
693
699
 
@@ -6,13 +6,14 @@ import { FileSource } from "@metaobjectsdev/metadata/core";
6
6
  import { TypeRegistry, registerCoreTypes, MetaDataLoader, canonicalSerialize } from "@metaobjectsdev/metadata";
7
7
  import { registerForgeTypes, resolveCollection } from "@metaobjectsdev/sdk";
8
8
  import { collectionLoadOptions } from "../lib/collection-load-options.js";
9
+ import { describeError } from "../lib/error-text.js";
9
10
 
10
11
  export async function exportCommand(args: string[], cwd: string): Promise<number> {
11
12
  let flags;
12
13
  try {
13
14
  flags = parseExportArgs(args);
14
15
  } catch (err) {
15
- log.error((err as Error).message);
16
+ log.error(describeError(err));
16
17
  return 2;
17
18
  }
18
19
 
@@ -34,7 +35,7 @@ export async function exportCommand(args: string[], cwd: string): Promise<number
34
35
  try {
35
36
  collection = await resolveCollection(projectRoot);
36
37
  } catch (err) {
37
- log.error((err as Error).message);
38
+ log.error(describeError(err));
38
39
  return 1;
39
40
  }
40
41
 
@@ -29,6 +29,8 @@ import {
29
29
  } from "../lib/catalog-listing.js";
30
30
  import { emitStructured } from "../lib/format.js";
31
31
  import { composeCatalog } from "../lib/catalog.js";
32
+ import { describeError } from "../lib/error-text.js";
33
+ import { findRuntimeBoundaryCrossings, runtimeBoundaryWarnings } from "../lib/runtime-boundary-advisory.js";
32
34
 
33
35
  /**
34
36
  * Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -65,7 +67,7 @@ export function mapStatus(s: WriteStatus): GenFileStatus {
65
67
  export async function genCommand(args: string[], cwd: string, fmt: OutputFormat = "text"): Promise<number> {
66
68
  let flags;
67
69
  try { flags = parseGenArgs(args); }
68
- catch (err) { log.error((err as Error).message); return 2; }
70
+ catch (err) { log.error(describeError(err)); return 2; }
69
71
 
70
72
  // ADR-0021 D3 — `meta gen --list`: print the generator CATALOG and exit 0 WITHOUT
71
73
  // running codegen. No project is required (like `meta types`): "what can this engine
@@ -97,7 +99,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
97
99
  try {
98
100
  collection = await resolveCollection(cwd);
99
101
  } catch (err) {
100
- log.error((err as Error).message);
102
+ log.error(describeError(err));
101
103
  return 2;
102
104
  }
103
105
  // ...but `metaobjects.config.ts` is not named by the metadata: it is this
@@ -136,7 +138,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
136
138
  try {
137
139
  forgeConfig = await loadMetaobjectsConfig(projectRoot);
138
140
  } catch (err) {
139
- log.error((err as Error).message);
141
+ log.error(describeError(err));
140
142
  return 2;
141
143
  }
142
144
 
@@ -204,7 +206,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
204
206
  ...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
205
207
  });
206
208
  } catch (err) {
207
- log.error(`gen failed: ${(err as Error).message}`);
209
+ log.error(`gen failed: ${describeError(err)}`);
208
210
  return 1;
209
211
  }
210
212
 
@@ -234,6 +236,14 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
234
236
  info: "",
235
237
  }));
236
238
 
239
+ // Advisory: generated code importing the owned adapter copy from outside its own
240
+ // workspace package (a monorepo) will not build. Warning only.
241
+ if (!cliConfig.dryRun) {
242
+ for (const w of runtimeBoundaryWarnings(
243
+ findRuntimeBoundaryCrossings(projectRoot, result.files.map((f) => f.path)),
244
+ )) log.warn(`warning: ${w}`);
245
+ }
246
+
237
247
  const targetDirs = Array.from(new Set(
238
248
  (forgeConfig.targets ? Object.values(forgeConfig.targets).map((t) => t.outDir) : [])
239
249
  .concat([forgeConfig.outDir]),
@@ -369,7 +379,7 @@ function runAntiPatternScan(
369
379
  projectRoot, ignore !== undefined ? { ignore } : undefined)));
370
380
  } catch (err) {
371
381
  // Never let an advisory scan break gen — but never claim it found nothing either.
372
- return skippedSection(`the scan failed: ${(err as Error).message}`);
382
+ return skippedSection(`the scan failed: ${describeError(err)}`);
373
383
  }
374
384
  }
375
385
 
@@ -402,7 +412,7 @@ async function listCatalogCommand(
402
412
  collection = await resolveCollection(cwd);
403
413
  } catch (err) {
404
414
  log.error(
405
- `meta gen --list --probe needs a project to probe: ${(err as Error).message}\n` +
415
+ `meta gen --list --probe needs a project to probe: ${describeError(err)}\n` +
406
416
  "Run `meta gen --list` (no --probe) for the catalog on its own.",
407
417
  );
408
418
  return 2;
@@ -414,7 +424,7 @@ async function listCatalogCommand(
414
424
  try {
415
425
  forgeConfig = await loadMetaobjectsConfig(projectRoot);
416
426
  } catch (err) {
417
- log.error((err as Error).message);
427
+ log.error(describeError(err));
418
428
  return 2;
419
429
  }
420
430
 
@@ -24,6 +24,7 @@ import {
24
24
  OWNED_GENERATORS_DIR,
25
25
  type GeneratorScope,
26
26
  } from "./generator-help.js";
27
+ import { describeError } from "../lib/error-text.js";
27
28
 
28
29
  export { GENERATOR_HELP, GENERATOR_OPTIONS, GENERATOR_SCOPES, OWNED_GENERATORS_DIR } from "./generator-help.js";
29
30
  export type { GeneratorScope } from "./generator-help.js";
@@ -334,7 +335,7 @@ export async function generatorCommand(args: string[], cwd: string): Promise<num
334
335
  try {
335
336
  ({ values, positionals } = parseArgs({ args, options: GENERATOR_OPTIONS, strict: true, allowPositionals: true }));
336
337
  } catch (err) {
337
- log.error((err as Error).message);
338
+ log.error(describeError(err));
338
339
  return 2;
339
340
  }
340
341
  const [sub, name, ...extra] = positionals;
@@ -358,7 +359,7 @@ export async function generatorCommand(args: string[], cwd: string): Promise<num
358
359
  wire: values["no-wire"] !== true,
359
360
  });
360
361
  } catch (err) {
361
- log.error((err as Error).message);
362
+ log.error(describeError(err));
362
363
  return 2;
363
364
  }
364
365
 
@@ -14,6 +14,7 @@ import { log } from "../lib/log.js";
14
14
  import { cliVersion } from "../lib/version.js";
15
15
  import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
16
16
  import { DEFAULT_DOCS_DIR } from "@metaobjectsdev/codegen-ts";
17
+ import { describeError } from "../lib/error-text.js";
17
18
 
18
19
  // ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
19
20
  // the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
@@ -157,6 +158,8 @@ Next steps:
157
158
  4. meta gen # generate from exactly what you wired
158
159
  5. meta docs # neutral model + API docs (on by default)
159
160
  6. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
161
+ A new database later (a fresh clone, CI, tests) replays the committed migrations:
162
+ meta migrate apply-pending --db file:other.sqlite
160
163
  `;
161
164
 
162
165
  export interface InitOptions {
@@ -861,7 +864,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
861
864
  // init failure gets. `init()` refuses it too, which is what covers every other door.
862
865
  assertKnownStackValues({ servers: flags.servers ?? [], clients: flags.clients ?? [] });
863
866
  } catch (err) {
864
- log.error((err as Error).message);
867
+ log.error(describeError(err));
865
868
  return 2;
866
869
  }
867
870
 
@@ -923,7 +926,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
923
926
  }
924
927
  return 0;
925
928
  } catch (err) {
926
- log.error((err as Error).message);
929
+ log.error(describeError(err));
927
930
  return 1;
928
931
  }
929
932
  }
@@ -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
  }
@@ -1409,6 +1418,7 @@ export async function runOfflineGenerate(
1409
1418
  );
1410
1419
  await writeSnapshot(path, nextSnapshot);
1411
1420
  log.info(`migrate: wrote ${res.upPath}`);
1421
+ log.info(`migrate: wrote ${res.downPath}`);
1412
1422
  return 0;
1413
1423
  }
1414
1424
 
@@ -1438,7 +1448,7 @@ async function runRollback(
1438
1448
  try {
1439
1449
  kysely = await buildKyselyFromUrl(databaseUrl, config.dialect);
1440
1450
  } catch (err) {
1441
- log.error(`migrate: ${(err as Error).message}`);
1451
+ log.error(`migrate: ${describeError(err)}`);
1442
1452
  return 2;
1443
1453
  }
1444
1454
  // kysely.dialect is "sqlite" | "postgres" here — d1 is rejected upstream.
@@ -1456,13 +1466,13 @@ async function runRollback(
1456
1466
  }
1457
1467
  return 0;
1458
1468
  } catch (err) {
1459
- log.error(`migrate: rollback failed: ${(err as Error).message}`);
1469
+ log.error(`migrate: rollback failed: ${describeError(err)}`);
1460
1470
  return 1;
1461
1471
  } finally {
1462
1472
  try {
1463
1473
  await kysely.close();
1464
1474
  } catch (err) {
1465
- log.warn(`migrate: failed to close DB cleanly: ${(err as Error).message}`);
1475
+ log.warn(`migrate: failed to close DB cleanly: ${describeError(err)}`);
1466
1476
  }
1467
1477
  }
1468
1478
  }
@@ -1496,7 +1506,7 @@ async function runD1Migrate(
1496
1506
  try {
1497
1507
  binding = resolveD1Binding(parsed.d1Bindings, config.d1.binding);
1498
1508
  } catch (err) {
1499
- log.error(`migrate: ${(err as Error).message}`);
1509
+ log.error(`migrate: ${describeError(err)}`);
1500
1510
  return 2;
1501
1511
  }
1502
1512
  } else {
@@ -1538,7 +1548,7 @@ async function runD1Migrate(
1538
1548
  try {
1539
1549
  collection = await resolveCollection(metaRoot);
1540
1550
  } catch (err) {
1541
- log.error((err as Error).message);
1551
+ log.error(describeError(err));
1542
1552
  return 2;
1543
1553
  }
1544
1554
 
@@ -1579,7 +1589,7 @@ async function runD1Migrate(
1579
1589
  configPath: wranglerConfigPath,
1580
1590
  });
1581
1591
  } catch (err) {
1582
- log.error(`migrate: failed to introspect D1: ${(err as Error).message}`);
1592
+ log.error(`migrate: failed to introspect D1: ${describeError(err)}`);
1583
1593
  return 2;
1584
1594
  }
1585
1595
 
@@ -1612,7 +1622,7 @@ async function runD1Migrate(
1612
1622
  // #258 — a primary-key move has no expressible migration; refuse loudly.
1613
1623
  if (isPrimaryKeyChangeError(err)) return refuseEngineError(err, PK_CHANGE_HINT, fmt);
1614
1624
  if (isDeclaredRenameError(err)) return refuseEngineError(err, DECLARED_RENAME_HINT, fmt);
1615
- if ((err as Error).message.includes("aborted by onAmbiguous")) {
1625
+ if (describeError(err).includes("aborted by onAmbiguous")) {
1616
1626
  const entries = ambiguousToEntries(collectedAmbiguous);
1617
1627
  for (const e of entries) {
1618
1628
  log.error(` ambiguous ${e.kind}: ${e.description}${e.hint ? ` [${e.hint}]` : ""}`);
@@ -1708,7 +1718,7 @@ async function runWranglerApply(
1708
1718
  return await new Promise<number>((resolve) => {
1709
1719
  const child = spawn("wrangler", applyArgs, { stdio: "inherit" });
1710
1720
  child.on("error", (err) => {
1711
- log.error(`migrate: failed to run wrangler: ${(err as Error).message}`);
1721
+ log.error(`migrate: failed to run wrangler: ${describeError(err)}`);
1712
1722
  resolve(2);
1713
1723
  });
1714
1724
  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