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