@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.
- package/dist/src/commands/deps.d.ts.map +1 -1
- package/dist/src/commands/deps.js +9 -8
- package/dist/src/commands/deps.js.map +1 -1
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -12
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +3 -2
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +3 -2
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +8 -7
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/generator.d.ts.map +1 -1
- package/dist/src/commands/generator.js +3 -2
- package/dist/src/commands/generator.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +5 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +30 -23
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +5 -4
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +7 -1
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +4 -3
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +35 -23
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +17 -6
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts.map +1 -1
- package/dist/src/lib/catalog-listing.js +5 -1
- package/dist/src/lib/catalog-listing.js.map +1 -1
- package/dist/src/lib/dependency-sync.d.ts.map +1 -1
- package/dist/src/lib/dependency-sync.js +4 -3
- package/dist/src/lib/dependency-sync.js.map +1 -1
- package/dist/src/lib/error-text.d.ts +2 -0
- package/dist/src/lib/error-text.d.ts.map +1 -0
- package/dist/src/lib/error-text.js +15 -0
- package/dist/src/lib/error-text.js.map +1 -0
- package/dist/src/lib/gen-migrate-advice.d.ts +1 -1
- package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
- package/dist/src/lib/gen-migrate-advice.js +24 -0
- package/dist/src/lib/gen-migrate-advice.js.map +1 -1
- package/dist/src/lib/git-ignore.d.ts.map +1 -1
- package/dist/src/lib/git-ignore.js +2 -1
- package/dist/src/lib/git-ignore.js.map +1 -1
- package/dist/src/lib/output.d.ts +3 -2
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +43 -2
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/deps.ts +9 -8
- package/src/commands/docs.ts +19 -12
- package/src/commands/eject.ts +3 -2
- package/src/commands/export.ts +3 -2
- package/src/commands/gen.ts +8 -7
- package/src/commands/generator.ts +3 -2
- package/src/commands/init.ts +5 -2
- package/src/commands/migrate.ts +32 -23
- package/src/commands/prompt-snapshot.ts +5 -4
- package/src/commands/types.ts +6 -1
- package/src/commands/upgrade.ts +4 -3
- package/src/commands/verify.ts +37 -23
- package/src/index.ts +3 -2
- package/src/lib/args.ts +19 -6
- package/src/lib/catalog-listing.ts +5 -1
- package/src/lib/dependency-sync.ts +4 -3
- package/src/lib/error-text.ts +13 -0
- package/src/lib/gen-migrate-advice.ts +24 -1
- package/src/lib/git-ignore.ts +2 -1
- package/src/lib/output.ts +3 -2
- package/src/lib/requirement-check.ts +46 -2
package/src/commands/migrate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
139
|
+
log.error(`[${tmpl.name}] render failed: ${describeError(err)}`);
|
|
139
140
|
errorCount++;
|
|
140
141
|
continue;
|
|
141
142
|
}
|
package/src/commands/types.ts
CHANGED
|
@@ -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
|
|
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)
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
86
|
+
log.error(describeError(err));
|
|
86
87
|
return 1;
|
|
87
88
|
}
|
|
88
89
|
|
package/src/commands/verify.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|