Skip to content

Commit d4911d1

Browse files
dmealingclaude
andcommitted
fix(cli): print the real reason when an error has an empty message
The adopter-estate release gate failed with `meta migrate apply-pending: apply failed: ` and nothing after the colon. The cause was a refused Postgres connection: on a dual-stack host node-postgres rejects with an AggregateError whose own message is empty, and the CLI printed `(err as Error).message`. A new `describeError` returns the message, or for an AggregateError the inner errors' messages, or the error's code or name. It replaces the raw `.message` read at all 89 sites in the CLI, so no command can print an empty reason. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1mRRB1K2WdYy82kntMQdx
1 parent 4f4717a commit d4911d1

19 files changed

Lines changed: 156 additions & 89 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ here.**
5555

5656
### Fixed
5757

58+
- **CLI errors no longer print an empty reason.** A refused Postgres connection on a dual-stack
59+
host rejects with an `AggregateError` whose own message is empty, so `meta migrate
60+
apply-pending` printed `apply failed: ` and nothing else. Every CLI command now prints the
61+
inner errors instead (`connect ECONNREFUSED 127.0.0.1:55440; …`), or the error's code or name
62+
when it carries no message.
63+
5864
- **`meta verify` counts only requirement gaps a `@disposition` could settle.** The "recorded
5965
gap(s) with no @disposition" line counted every `partial` / `planned` requirement without a
6066
disposition, including roll-up parents that are `partial` only because a descendant is. On one

‎server/typescript/packages/cli/src/commands/deps.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
planSync,
2727
readLockOrThrow,
2828
} from "../lib/dependency-sync.js";
29+
import { describeError } from "../lib/error-text.js";
2930

3031
/** The declared `dependencies` for the config governing `cwd` — read
3132
* DIRECTLY via `loadConfig`, never through `resolveCollection`/`Collection`:
@@ -50,23 +51,23 @@ async function runSync(configDir: string, specs: readonly DependencySpec[], flag
5051
// Same convention as `depsCommand`'s `declaredDependencies` catch below: a
5152
// committed project file that fails to parse is a config-class problem,
5253
// exit 2 — distinct from the exit-1 a `sync` verdict failure gets.
53-
log.error((err as Error).message);
54+
log.error(describeError(err));
5455
return 2;
5556
}
5657

5758
let plan;
5859
try {
5960
plan = await planSync(configDir, specs, lock, flags.names);
6061
} catch (err) {
61-
log.error((err as Error).message);
62+
log.error(describeError(err));
6263
return 1;
6364
}
6465

6566
let applied;
6667
try {
6768
applied = await applySync(configDir, plan, { dryRun: flags.dryRun });
6869
} catch (err) {
69-
log.error((err as Error).message);
70+
log.error(describeError(err));
7071
return 1;
7172
}
7273

@@ -100,7 +101,7 @@ async function runSync(configDir: string, specs: readonly DependencySpec[], flag
100101
try {
101102
collection = await resolveCollection(configDir);
102103
} catch (err) {
103-
log.error(`meta deps sync: the updated dependency set does not resolve: ${(err as Error).message}`);
104+
log.error(`meta deps sync: the updated dependency set does not resolve: ${describeError(err)}`);
104105
return 1;
105106
}
106107
try {
@@ -118,7 +119,7 @@ async function runList(configDir: string, fmt: OutputFormat): Promise<number> {
118119
try {
119120
lock = await readLockOrThrow(configDir);
120121
} catch (err) {
121-
log.error((err as Error).message);
122+
log.error(describeError(err));
122123
return 2;
123124
}
124125
const entries = Object.entries(lock?.dependencies ?? {});
@@ -159,7 +160,7 @@ async function runCheck(
159160
try {
160161
lock = await readLockOrThrow(configDir);
161162
} catch (err) {
162-
log.error((err as Error).message);
163+
log.error(describeError(err));
163164
return 2;
164165
}
165166

@@ -201,7 +202,7 @@ export async function depsCommand(args: string[], cwd: string, fmt: OutputFormat
201202
try {
202203
flags = parseDepsArgs(args);
203204
} catch (err) {
204-
log.error((err as Error).message);
205+
log.error(describeError(err));
205206
return 2;
206207
}
207208

@@ -216,7 +217,7 @@ export async function depsCommand(args: string[], cwd: string, fmt: OutputFormat
216217
// failure (gen.ts, migrate.ts, docs.ts) — distinct from the exit 1 a
217218
// dependency-resolution failure gets below, because this is a config
218219
// problem, not a `sync` verdict.
219-
log.error((err as Error).message);
220+
log.error(describeError(err));
220221
return 2;
221222
}
222223

‎server/typescript/packages/cli/src/commands/docs.ts‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
} from "@metaobjectsdev/migrate-ts";
5757
import type { AgentSchemaInput, SchemaColumnLike } from "@metaobjectsdev/codegen-ts";
5858
import { reportLoadError } from "../lib/load-error.js";
59+
import { describeError } from "../lib/error-text.js";
5960

6061
/**
6162
* Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -319,7 +320,7 @@ function buildAgentSchemaInput(
319320
// agent pages still emit.
320321
log.warn(
321322
`docs: agent/schema.md skipped — the expected schema could not be built ` +
322-
`(${(err as Error).message}). Run 'meta migrate' for the full diagnosis.`,
323+
`(${describeError(err)}). Run 'meta migrate' for the full diagnosis.`,
323324
);
324325
return undefined;
325326
}
@@ -337,7 +338,7 @@ export async function docsCommand(
337338
try {
338339
flags = parseDocsArgs(args, cwd);
339340
} catch (err) {
340-
log.error(`docs: ${(err as Error).message}`);
341+
log.error(`docs: ${describeError(err)}`);
341342
return 2;
342343
}
343344

@@ -424,7 +425,7 @@ export async function docsCommand(
424425
flags.projectRootProvided ? { explicitDir: metaRoot } : undefined,
425426
);
426427
} catch (err) {
427-
log.error(`docs: ${(err as Error).message}`);
428+
log.error(`docs: ${describeError(err)}`);
428429
return 2;
429430
}
430431

@@ -474,7 +475,7 @@ export async function docsCommand(
474475
configLoadOptions = loadMemoryOptionsFrom(loadedConfig);
475476
} catch (err) {
476477
log.warn(
477-
`docs: metaobjects.config.ts failed to load (${(err as Error).message}); ` +
478+
`docs: metaobjects.config.ts failed to load (${describeError(err)}); ` +
478479
`generating docs without its providers`,
479480
);
480481
loadedConfig = undefined;
@@ -564,7 +565,7 @@ export async function docsCommand(
564565
suite = resolveGenerators(loadedConfig?.generators ?? []);
565566
} catch (err) {
566567
log.warn(
567-
`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 ` +
568569
`pages fall back to the generators declared as values, so a tier wired by name may ` +
569570
`be missing from them.`,
570571
);
@@ -624,7 +625,7 @@ export async function docsCommand(
624625
try {
625626
modelFiles = await docsFile(modelOpts).generate(ctx);
626627
} catch (err) {
627-
const msg = (err as Error).message;
628+
const msg = describeError(err);
628629
// Duplicate output path (silent-overwrite backstop): the generator already
629630
// names both colliding FQNs + the path and starts with "docs:". Surface it
630631
// verbatim as a clean non-zero exit (no double prefix, no stack trace).
@@ -680,7 +681,7 @@ export async function docsCommand(
680681
);
681682
}
682683
} catch (err) {
683-
const msg = (err as Error).message;
684+
const msg = describeError(err);
684685
if (msg.startsWith("docs: duplicate output path")) {
685686
log.error(msg);
686687
return 1;
@@ -743,7 +744,7 @@ export async function docsCommand(
743744
await writeFile(path, f.content, "utf8");
744745
}
745746
} catch (err) {
746-
log.error(`docs: failed to write pages: ${(err as Error).message}`);
747+
log.error(`docs: failed to write pages: ${describeError(err)}`);
747748
return 1;
748749
}
749750

@@ -812,7 +813,7 @@ async function scaffoldSiteCommand(projectRoot: string): Promise<number> {
812813
created.push(rel);
813814
}
814815
} catch (err) {
815-
log.error(`docs: failed to scaffold site templates: ${(err as Error).message}`);
816+
log.error(`docs: failed to scaffold site templates: ${describeError(err)}`);
816817
return 1;
817818
}
818819
log.info(
@@ -919,7 +920,7 @@ async function emitSite(
919920
log.info(`meta docs --site — wrote ${r.pages.length} page(s) → ${siteOutDir}`);
920921
return 0;
921922
} catch (err) {
922-
log.error(`docs: failed to generate site: ${(err as Error).message}`);
923+
log.error(`docs: failed to generate site: ${describeError(err)}`);
923924
return 1;
924925
}
925926
}
@@ -937,7 +938,7 @@ async function metamodelDocsCommand(cwd: string, out: string): Promise<number> {
937938
try {
938939
docs = renderCoreMetamodelDocs(composeRegistry(coreProviders));
939940
} catch (err) {
940-
log.error(`docs: failed to render metamodel docs: ${(err as Error).message}`);
941+
log.error(`docs: failed to render metamodel docs: ${describeError(err)}`);
941942
return 1;
942943
}
943944
try {
@@ -948,7 +949,7 @@ async function metamodelDocsCommand(cwd: string, out: string): Promise<number> {
948949
await writeFile(path, content, "utf8");
949950
}
950951
} catch (err) {
951-
log.error(`docs: failed to write metamodel pages: ${(err as Error).message}`);
952+
log.error(`docs: failed to write metamodel pages: ${describeError(err)}`);
952953
return 1;
953954
}
954955
log.info(`meta docs --metamodel — wrote ${docs.size} page(s) → ${outDir}`);

‎server/typescript/packages/cli/src/commands/eject.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
ejectLibrary, ejectableLibraryNames, isLibraryName, libraryStaleness,
3737
type LibraryEjectResult,
3838
} from "../lib/library-eject.js";
39+
import { describeError } from "../lib/error-text.js";
3940

4041
// Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
4142
// same never-clobber-without-consent contract. Kept as its own local constant rather
@@ -613,7 +614,7 @@ export async function ejectCommand(
613614
try {
614615
flags = parseEjectArgs(args);
615616
} catch (err) {
616-
log.error((err as Error).message);
617+
log.error(describeError(err));
617618
return 2;
618619
}
619620

@@ -687,7 +688,7 @@ export async function ejectCommand(
687688
if (fmt === "text") reportOne(result, name);
688689
}
689690
} catch (err) {
690-
log.error((err as Error).message);
691+
log.error(describeError(err));
691692
return 1;
692693
}
693694

‎server/typescript/packages/cli/src/commands/export.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { FileSource } from "@metaobjectsdev/metadata/core";
66
import { TypeRegistry, registerCoreTypes, MetaDataLoader, canonicalSerialize } from "@metaobjectsdev/metadata";
77
import { registerForgeTypes, resolveCollection } from "@metaobjectsdev/sdk";
88
import { collectionLoadOptions } from "../lib/collection-load-options.js";
9+
import { describeError } from "../lib/error-text.js";
910

1011
export async function exportCommand(args: string[], cwd: string): Promise<number> {
1112
let flags;
1213
try {
1314
flags = parseExportArgs(args);
1415
} catch (err) {
15-
log.error((err as Error).message);
16+
log.error(describeError(err));
1617
return 2;
1718
}
1819

@@ -34,7 +35,7 @@ export async function exportCommand(args: string[], cwd: string): Promise<number
3435
try {
3536
collection = await resolveCollection(projectRoot);
3637
} catch (err) {
37-
log.error((err as Error).message);
38+
log.error(describeError(err));
3839
return 1;
3940
}
4041

‎server/typescript/packages/cli/src/commands/gen.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from "../lib/catalog-listing.js";
3030
import { emitStructured } from "../lib/format.js";
3131
import { composeCatalog } from "../lib/catalog.js";
32+
import { describeError } from "../lib/error-text.js";
3233

3334
/**
3435
* Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -65,7 +66,7 @@ export function mapStatus(s: WriteStatus): GenFileStatus {
6566
export async function genCommand(args: string[], cwd: string, fmt: OutputFormat = "text"): Promise<number> {
6667
let flags;
6768
try { flags = parseGenArgs(args); }
68-
catch (err) { log.error((err as Error).message); return 2; }
69+
catch (err) { log.error(describeError(err)); return 2; }
6970

7071
// ADR-0021 D3 — `meta gen --list`: print the generator CATALOG and exit 0 WITHOUT
7172
// running codegen. No project is required (like `meta types`): "what can this engine
@@ -97,7 +98,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
9798
try {
9899
collection = await resolveCollection(cwd);
99100
} catch (err) {
100-
log.error((err as Error).message);
101+
log.error(describeError(err));
101102
return 2;
102103
}
103104
// ...but `metaobjects.config.ts` is not named by the metadata: it is this
@@ -136,7 +137,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
136137
try {
137138
forgeConfig = await loadMetaobjectsConfig(projectRoot);
138139
} catch (err) {
139-
log.error((err as Error).message);
140+
log.error(describeError(err));
140141
return 2;
141142
}
142143

@@ -204,7 +205,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
204205
...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
205206
});
206207
} catch (err) {
207-
log.error(`gen failed: ${(err as Error).message}`);
208+
log.error(`gen failed: ${describeError(err)}`);
208209
return 1;
209210
}
210211

@@ -369,7 +370,7 @@ function runAntiPatternScan(
369370
projectRoot, ignore !== undefined ? { ignore } : undefined)));
370371
} catch (err) {
371372
// Never let an advisory scan break gen — but never claim it found nothing either.
372-
return skippedSection(`the scan failed: ${(err as Error).message}`);
373+
return skippedSection(`the scan failed: ${describeError(err)}`);
373374
}
374375
}
375376

@@ -402,7 +403,7 @@ async function listCatalogCommand(
402403
collection = await resolveCollection(cwd);
403404
} catch (err) {
404405
log.error(
405-
`meta gen --list --probe needs a project to probe: ${(err as Error).message}\n` +
406+
`meta gen --list --probe needs a project to probe: ${describeError(err)}\n` +
406407
"Run `meta gen --list` (no --probe) for the catalog on its own.",
407408
);
408409
return 2;
@@ -414,7 +415,7 @@ async function listCatalogCommand(
414415
try {
415416
forgeConfig = await loadMetaobjectsConfig(projectRoot);
416417
} catch (err) {
417-
log.error((err as Error).message);
418+
log.error(describeError(err));
418419
return 2;
419420
}
420421

‎server/typescript/packages/cli/src/commands/generator.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
OWNED_GENERATORS_DIR,
2525
type GeneratorScope,
2626
} from "./generator-help.js";
27+
import { describeError } from "../lib/error-text.js";
2728

2829
export { GENERATOR_HELP, GENERATOR_OPTIONS, GENERATOR_SCOPES, OWNED_GENERATORS_DIR } from "./generator-help.js";
2930
export type { GeneratorScope } from "./generator-help.js";
@@ -334,7 +335,7 @@ export async function generatorCommand(args: string[], cwd: string): Promise<num
334335
try {
335336
({ values, positionals } = parseArgs({ args, options: GENERATOR_OPTIONS, strict: true, allowPositionals: true }));
336337
} catch (err) {
337-
log.error((err as Error).message);
338+
log.error(describeError(err));
338339
return 2;
339340
}
340341
const [sub, name, ...extra] = positionals;
@@ -358,7 +359,7 @@ export async function generatorCommand(args: string[], cwd: string): Promise<num
358359
wire: values["no-wire"] !== true,
359360
});
360361
} catch (err) {
361-
log.error((err as Error).message);
362+
log.error(describeError(err));
362363
return 2;
363364
}
364365

‎server/typescript/packages/cli/src/commands/init.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { log } from "../lib/log.js";
1414
import { cliVersion } from "../lib/version.js";
1515
import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
1616
import { DEFAULT_DOCS_DIR } from "@metaobjectsdev/codegen-ts";
17+
import { describeError } from "../lib/error-text.js";
1718

1819
// ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
1920
// the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
@@ -863,7 +864,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
863864
// init failure gets. `init()` refuses it too, which is what covers every other door.
864865
assertKnownStackValues({ servers: flags.servers ?? [], clients: flags.clients ?? [] });
865866
} catch (err) {
866-
log.error((err as Error).message);
867+
log.error(describeError(err));
867868
return 2;
868869
}
869870

@@ -925,7 +926,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
925926
}
926927
return 0;
927928
} catch (err) {
928-
log.error((err as Error).message);
929+
log.error(describeError(err));
929930
return 1;
930931
}
931932
}

0 commit comments

Comments
 (0)