@metaobjectsdev/cli 1.0.11 → 1.0.13

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.
@@ -0,0 +1,128 @@
1
+ // server/typescript/packages/cli/src/lib/fmt-engine.ts
2
+ //
3
+ // `meta fmt` (#304) orchestration: resolve the project's own metadata files
4
+ // (resolveCollection — never a hardcoded `metaobjects/`), format each one
5
+ // standalone (@metaobjectsdev/metadata's formatMetadataFile — own-mode,
6
+ // declared-here layer only), and gate every rewrite behind a whole-project
7
+ // reload-and-compare safety check before it ever touches disk.
8
+ //
9
+ // The safety check (design rule 4): swap a candidate in via an in-memory
10
+ // source carrying the SAME file id the real load would use, reload the WHOLE
11
+ // project, and require both that it loads with no errors AND that its
12
+ // canonical serialization is byte-identical to the untouched baseline. Only
13
+ // then is the candidate written — so `--check` (which skips the write) and a
14
+ // real run share one code path, and a bug in the formatter can only ever
15
+ // leave a file unchanged plus an error, never a corrupted file.
16
+ import { basename, extname, resolve as resolvePath } from "node:path";
17
+ import { readFile, writeFile } from "node:fs/promises";
18
+ import { composeRegistry, MetaDataLoader, InMemoryStringSource, canonicalSerialize, formatMetadataFile, } from "@metaobjectsdev/metadata";
19
+ import { FileSource } from "@metaobjectsdev/metadata/core";
20
+ import { resolveCollection, defaultLoadMemoryProviders } from "@metaobjectsdev/sdk";
21
+ import { loadMetaobjectsConfig, loadMemoryOptionsFrom } from "./load-metaobjects-config.js";
22
+ async function projectProviders(collection) {
23
+ try {
24
+ const cfg = await loadMetaobjectsConfig(collection.configDir);
25
+ return loadMemoryOptionsFrom(cfg).providers ?? [];
26
+ }
27
+ catch {
28
+ // No metaobjects.config.ts (common for a non-TS-server project running
29
+ // `meta fmt` just for schema-neutral formatting) — format against the
30
+ // core vocabulary only, exactly like a project with no custom providers.
31
+ return [];
32
+ }
33
+ }
34
+ export async function runFmt(opts) {
35
+ let collection;
36
+ try {
37
+ collection = await resolveCollection(opts.projectRoot);
38
+ }
39
+ catch (err) {
40
+ return { files: [], fatal: err instanceof Error ? err.message : String(err) };
41
+ }
42
+ const providers = await projectProviders(collection);
43
+ const registry = composeRegistry([...defaultLoadMemoryProviders, ...providers]);
44
+ let targets;
45
+ if (opts.files !== undefined && opts.files.length > 0) {
46
+ const want = new Set(opts.files.map((f) => resolvePath(opts.projectRoot, f)));
47
+ const owned = new Set(collection.ownFiles);
48
+ const missing = [...want].filter((f) => !owned.has(f));
49
+ if (missing.length > 0) {
50
+ return {
51
+ files: [],
52
+ fatal: `not among this project's resolved metadata sources: ${missing.join(", ")}\n` +
53
+ "meta fmt only formats files resolveCollection() already resolves — pass no " +
54
+ "arguments to format every one of them.",
55
+ };
56
+ }
57
+ targets = collection.ownFiles.filter((f) => want.has(f));
58
+ }
59
+ else {
60
+ targets = collection.ownFiles;
61
+ }
62
+ const sourceId = (p) => collection.fileIds.get(p) ?? basename(p);
63
+ const libSources = collection.libraries.length > 0
64
+ ? (await import("@metaobjectsdev/metadata/library")).librarySources([...collection.libraries])
65
+ : [];
66
+ const buildSources = (overridePath, overrideText) => [
67
+ ...libSources,
68
+ ...collection.files.map((p) => {
69
+ const id = sourceId(p);
70
+ return overridePath !== undefined && p === overridePath
71
+ ? new InMemoryStringSource(overrideText ?? "", { id })
72
+ : new FileSource(p, { id });
73
+ }),
74
+ ];
75
+ const baseline = await new MetaDataLoader({ registry }).load(buildSources());
76
+ if (baseline.errors.length > 0) {
77
+ return {
78
+ files: [],
79
+ fatal: "this project's metadata does not currently load cleanly — fix the error(s) below, " +
80
+ "then re-run fmt:\n" +
81
+ baseline.errors.map((e) => ` ${e.message}`).join("\n"),
82
+ };
83
+ }
84
+ const baselineCanonical = canonicalSerialize(baseline.root);
85
+ const reports = [];
86
+ for (const path of targets) {
87
+ const ext = extname(path).toLowerCase();
88
+ if (ext === ".yaml" || ext === ".yml") {
89
+ reports.push({
90
+ path,
91
+ status: "skipped-yaml",
92
+ detail: "no canonical YAML emitter exists (ADR-0006: JSON is the canonical interchange form) — left untouched",
93
+ });
94
+ continue;
95
+ }
96
+ const content = await readFile(path, "utf8");
97
+ const formatted = formatMetadataFile(content, { registry, sourceId: sourceId(path) });
98
+ if (!formatted.ok) {
99
+ reports.push(formatted.overlay
100
+ ? { path, status: "skipped-overlay", detail: formatted.message }
101
+ : { path, status: "error", detail: formatted.message });
102
+ continue;
103
+ }
104
+ if (formatted.text === content) {
105
+ reports.push({ path, status: "unchanged" });
106
+ continue;
107
+ }
108
+ const testLoad = await new MetaDataLoader({ registry }).load(buildSources(path, formatted.text));
109
+ const safe = testLoad.errors.length === 0 && canonicalSerialize(testLoad.root) === baselineCanonical;
110
+ if (!safe) {
111
+ reports.push({
112
+ path,
113
+ status: "error",
114
+ detail: "formatting this file would change the loaded model's meaning — left unchanged",
115
+ });
116
+ continue;
117
+ }
118
+ if (opts.check) {
119
+ reports.push({ path, status: "would-format" });
120
+ }
121
+ else {
122
+ await writeFile(path, formatted.text, "utf8");
123
+ reports.push({ path, status: "formatted" });
124
+ }
125
+ }
126
+ return { files: reports };
127
+ }
128
+ //# sourceMappingURL=fmt-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fmt-engine.js","sourceRoot":"","sources":["../../../src/lib/fmt-engine.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -51,15 +51,15 @@
51
51
  "@biomejs/js-api": "^0.7.0",
52
52
  "@biomejs/wasm-nodejs": "^1.9.4",
53
53
  "@libsql/kysely-libsql": "^0.4.0",
54
- "@metaobjectsdev/codegen-ts": "1.0.11",
55
- "@metaobjectsdev/codegen-ts-react": "1.0.11",
56
- "@metaobjectsdev/codegen-ts-tanstack": "1.0.11",
57
- "@metaobjectsdev/docs-site": "1.0.11",
58
- "@metaobjectsdev/metadata": "1.0.11",
59
- "@metaobjectsdev/migrate-ts": "1.0.11",
60
- "@metaobjectsdev/render": "1.0.11",
61
- "@metaobjectsdev/runtime-ts": "1.0.11",
62
- "@metaobjectsdev/sdk": "1.0.11",
54
+ "@metaobjectsdev/codegen-ts": "1.0.13",
55
+ "@metaobjectsdev/codegen-ts-react": "1.0.13",
56
+ "@metaobjectsdev/codegen-ts-tanstack": "1.0.13",
57
+ "@metaobjectsdev/docs-site": "1.0.13",
58
+ "@metaobjectsdev/metadata": "1.0.13",
59
+ "@metaobjectsdev/migrate-ts": "1.0.13",
60
+ "@metaobjectsdev/render": "1.0.13",
61
+ "@metaobjectsdev/runtime-ts": "1.0.13",
62
+ "@metaobjectsdev/sdk": "1.0.13",
63
63
  "@toon-format/toon": "^2.3.0",
64
64
  "jiti": "^2.4.0"
65
65
  },
@@ -0,0 +1,90 @@
1
+ // `meta fmt` (#304) — rewrite metadata files into the canonical form the
2
+ // cross-port canonical serializer already produces. `--check` lists the
3
+ // files that are not canonical and exits non-zero without changing anything.
4
+ //
5
+ // All of the actual work — file resolution, standalone per-file formatting,
6
+ // the YAML skip, and the whole-project reload-and-compare safety check — lives
7
+ // in `../lib/fmt-engine.js`, kept framework-free so it is unit-testable without
8
+ // spawning the CLI. This module is the thin text/exit-code front end.
9
+ import { relative } from "node:path";
10
+ import { parseFmtArgs } from "../lib/args.js";
11
+ import { log } from "../lib/log.js";
12
+ import { describeError } from "../lib/error-text.js";
13
+ import { runFmt, type FmtFileReport } from "../lib/fmt-engine.js";
14
+
15
+ function describe(report: FmtFileReport, cwd: string, check: boolean): string {
16
+ const rel = relative(cwd, report.path) || report.path;
17
+ switch (report.status) {
18
+ case "formatted":
19
+ return ` reformatted ${rel}`;
20
+ case "would-format":
21
+ return ` not canonical ${rel}`;
22
+ case "unchanged":
23
+ return ` ok ${rel}`;
24
+ case "skipped-yaml":
25
+ return ` skipped (yaml) ${rel} — ${report.detail}`;
26
+ case "skipped-overlay":
27
+ return ` skipped (overlay) ${rel} — this file declares an overlay fmt cannot resolve standalone`;
28
+ case "error":
29
+ return ` error ${rel} — ${report.detail}`;
30
+ default: {
31
+ const _exhaustive: never = report.status;
32
+ return _exhaustive;
33
+ }
34
+ }
35
+ }
36
+
37
+ export async function fmtCommand(args: string[], cwd: string): Promise<number> {
38
+ let flags;
39
+ try {
40
+ flags = parseFmtArgs(args);
41
+ } catch (err) {
42
+ log.error(describeError(err));
43
+ return 2;
44
+ }
45
+
46
+ const result = await runFmt({
47
+ projectRoot: cwd,
48
+ check: flags.check,
49
+ files: flags.files,
50
+ });
51
+
52
+ if (result.fatal !== undefined) {
53
+ log.error(result.fatal);
54
+ return 1;
55
+ }
56
+
57
+ if (result.files.length === 0) {
58
+ log.info("meta fmt — no metadata files to format.");
59
+ return 0;
60
+ }
61
+
62
+ for (const report of result.files) {
63
+ log.info(describe(report, cwd, flags.check));
64
+ }
65
+
66
+ const errored = result.files.filter((f) => f.status === "error");
67
+ const needsFormat = result.files.filter((f) => f.status === "would-format");
68
+ const reformatted = result.files.filter((f) => f.status === "formatted");
69
+
70
+ if (flags.check) {
71
+ if (needsFormat.length > 0 || errored.length > 0) {
72
+ log.error(
73
+ `meta fmt --check — ${needsFormat.length} file(s) not canonical` +
74
+ (errored.length > 0 ? `, ${errored.length} error(s)` : "") +
75
+ `. Run \`meta fmt\` to fix.`,
76
+ );
77
+ return 1;
78
+ }
79
+ log.info("meta fmt --check — every file is already canonical.");
80
+ return 0;
81
+ }
82
+
83
+ if (errored.length > 0) {
84
+ log.error(`meta fmt — ${errored.length} file(s) could not be formatted safely (left unchanged).`);
85
+ return 1;
86
+ }
87
+
88
+ log.info(`meta fmt — ${reformatted.length} file(s) reformatted.`);
89
+ return 0;
90
+ }
@@ -40,6 +40,7 @@ import {
40
40
  import { lintRequirements } from "../lib/requirement-lint.js";
41
41
  import { lintOverlays } from "../lib/overlay-lint.js";
42
42
  import { lintNodeNames } from "../lib/name-lint.js";
43
+ import { lintDeprecatedReferences } from "../lib/deprecation-lint.js";
43
44
  import { FileSource } from "@metaobjectsdev/metadata/core";
44
45
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
45
46
  import {
@@ -356,6 +357,9 @@ export async function verifyCommand(
356
357
  // off the end of a capped run.
357
358
  let nameSection: AdvisorySection<AdvisoryDiagnosticRow> =
358
359
  skippedSection("the name lint did not run");
360
+ // #305 — the deprecated-reference authoring lint. Its own section, same reason.
361
+ let deprecationSection: AdvisorySection<AdvisoryDiagnosticRow> =
362
+ skippedSection("the deprecated-reference lint did not run");
359
363
  // The ledger counts `meta verify` prints on every run. Undefined for a project
360
364
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
361
365
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -396,6 +400,11 @@ export async function verifyCommand(
396
400
  // Runs on every `meta verify`; warnings ONLY, never changes the exit code.
397
401
  runNameLintAdvisory();
398
402
 
403
+ // #305 — a node that depends (extends/@objectRef/@references/origin
404
+ // @from/@of/@via) on a `deprecated` node. Runs on every `meta verify`;
405
+ // warnings ONLY, never changes the exit code.
406
+ runDeprecationLintAdvisory();
407
+
399
408
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
400
409
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
401
410
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
@@ -445,6 +454,7 @@ export async function verifyCommand(
445
454
  antiPatterns: antiPatternSection,
446
455
  overlays: overlaySection,
447
456
  names: nameSection,
457
+ deprecations: deprecationSection,
448
458
  }),
449
459
  fmt,
450
460
  );
@@ -842,6 +852,29 @@ export async function verifyCommand(
842
852
  }
843
853
  }
844
854
 
855
+ // -- deprecated-reference authoring lint (advisory) -------------------------
856
+ // #305 — reads the LOADED model (see deprecation-lint.ts). Its own section,
857
+ // its own cap, and a skip that carries its reason rather than looking clean.
858
+ function runDeprecationLintAdvisory(): void {
859
+ if (flags.noDeprecationLint) {
860
+ deprecationSection = skippedSection("suppressed by --no-deprecation-lint");
861
+ return;
862
+ }
863
+ if (process.env.META_NO_DEPRECATION_LINT === "1") {
864
+ deprecationSection = skippedSection("suppressed by META_NO_DEPRECATION_LINT=1");
865
+ return;
866
+ }
867
+ const findings = lintDeprecatedReferences(root);
868
+ deprecationSection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
869
+ if (findings.length > 0) {
870
+ log.warn(
871
+ `meta verify — deprecations: ${findings.length} reference(s) to a deprecated node ` +
872
+ `(advisory — does not fail the build):`,
873
+ );
874
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
875
+ }
876
+ }
877
+
845
878
  // -- verify-as-teacher (advisory) ------------------------------------------
846
879
  // Records its result EITHER WAY — a skip carries its reason rather than looking
847
880
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
@@ -1856,6 +1889,7 @@ function buildVerifyPayload(input: {
1856
1889
  antiPatterns: AdvisorySection<AdvisoryFindingRow>;
1857
1890
  overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1858
1891
  names: AdvisorySection<AdvisoryDiagnosticRow>;
1892
+ deprecations: AdvisorySection<AdvisoryDiagnosticRow>;
1859
1893
  }): Record<string, unknown> {
1860
1894
  const ran = input.gates.filter((g) => g.ran);
1861
1895
  const failed = ran.filter((g) => !g.ok);
@@ -1878,6 +1912,9 @@ function buildVerifyPayload(input: {
1878
1912
  if (input.names.status === "ran" && input.names.total > 0) {
1879
1913
  parts.push(`${input.names.total} name authoring finding(s)`);
1880
1914
  }
1915
+ if (input.deprecations.status === "ran" && input.deprecations.total > 0) {
1916
+ parts.push(`${input.deprecations.total} deprecated-reference finding(s)`);
1917
+ }
1881
1918
 
1882
1919
  const help: string[] = [];
1883
1920
  if (failed.length > 0) {
@@ -1898,12 +1935,18 @@ function buildVerifyPayload(input: {
1898
1935
  `${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
1936
  );
1900
1937
  }
1938
+ if (input.deprecations.total > 0) {
1939
+ help.push(
1940
+ `${input.deprecations.total} reference(s) to a deprecated node — see deprecations.rows[]; each names the deprecated target and, where declared, its replacement`,
1941
+ );
1942
+ }
1901
1943
  if (
1902
1944
  failed.length === 0 &&
1903
1945
  input.antiPatterns.total === 0 &&
1904
1946
  input.requirements.total === 0 &&
1905
1947
  input.overlays.total === 0 &&
1906
- input.names.total === 0
1948
+ input.names.total === 0 &&
1949
+ input.deprecations.total === 0
1907
1950
  ) {
1908
1951
  help.push("no drift and nothing advisory to answer — nothing to do");
1909
1952
  }
@@ -1917,6 +1960,7 @@ function buildVerifyPayload(input: {
1917
1960
  requirements: input.requirements,
1918
1961
  overlays: input.overlays,
1919
1962
  names: input.names,
1963
+ deprecations: input.deprecations,
1920
1964
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1921
1965
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1922
1966
  // text on stderr — but it is not in this document, and a reader must not have
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ COMMANDS:
41
41
  deps list One line per locked dependency: name, version, hash, node/package summary
42
42
  types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
43
43
  export Flatten loaded metadata to one canonical JSON artifact
44
+ fmt [<file>...] Rewrite metadata files into canonical form; --check lists drift, exits non-zero
44
45
  docs [<project-root>] --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
45
46
  verify Drift gate — subverbs: --templates / --db / --codegen / --docs / --deps (bare = --templates)
46
47
  upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
@@ -86,6 +87,21 @@ GEN FLAGS:
86
87
  EXPORT FLAGS:
87
88
  --out <file> Write output to a file (default: stdout)
88
89
 
90
+ FMT FLAGS:
91
+ [<file>...] Optional explicit file(s) to format — must be members of this
92
+ project's resolved metadata sources (resolveCollection()); omit
93
+ to format every one of them.
94
+ --check List files that are not canonical and exit non-zero; changes nothing.
95
+ A file whose formatting would change the LOADED MODEL's meaning
96
+ is never rewritten — reported as an error instead (safety check).
97
+ A YAML file is always reported as skipped: no canonical YAML
98
+ emitter exists yet (ADR-0006 — JSON is the canonical interchange
99
+ form). A file declaring 'overlay: true' anywhere — whether or not
100
+ a same-file base exists to merge into — is skipped too: a
101
+ formatter never changes structure, and buildTree's ordinary
102
+ find-or-reuse behavior would otherwise silently fold a same-file
103
+ base + overlay into one node, dropping the overlay marker.
104
+
89
105
  DOCS FLAGS:
90
106
  [<project-root>] PROJECT ROOT to resolve metadata from — the directory that CONTAINS
91
107
  your metadata, NOT the metadata directory. Passing it SCOPES the run
@@ -129,6 +145,8 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
129
145
  (never a gate — this lint can't fail the build)
130
146
  --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace
131
147
  in a name; never a gate — this lint can't fail the build)
148
+ --no-deprecation-lint Suppress the advisory deprecated-reference AUTHORING lint
149
+ (never a gate — this lint can't fail the build)
132
150
  --limit <n|all> How many advisory lines TEXT output prints per section before
133
151
  it truncates (default 20). Never applies to --format
134
152
  toon/json, which carry every finding and every diagnostic.
@@ -324,6 +342,8 @@ FLAGS:
324
342
  never a gate; this lint can't fail the build
325
343
  --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace in
326
344
  a name) — never a gate; this lint can't fail the build
345
+ --no-deprecation-lint Suppress the advisory deprecated-reference AUTHORING lint —
346
+ never a gate; this lint can't fail the build
327
347
  --limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
328
348
  (default 20; per-section so the authoring lint can never push
329
349
  the gate's own warnings off the end)
@@ -361,6 +381,14 @@ Leading or trailing whitespace is almost certainly a copy-paste accident — ren
361
381
  the node, and pin @column on a field whose physical column must keep its name.
362
382
  Requirement names are left to the requirement lint above. Warnings only — it can
363
383
  never fail the build. Opt out with --no-name-lint or META_NO_NAME_LINT=1.
384
+
385
+ verify also prints an ADVISORY deprecated-reference authoring lint, in its own
386
+ section: any node that depends on a node carrying the documentation @deprecated
387
+ attr, via extends, @objectRef, @references, or an origin's @from/@of/@via. Where
388
+ the deprecated node also carries @replacedBy, the finding names the replacement.
389
+ A node referencing itself (a recursive FK, a same-entity passthrough) is not a
390
+ finding. Warnings only — it can never fail the build. Opt out with
391
+ --no-deprecation-lint or META_NO_DEPRECATION_LINT=1.
364
392
  `,
365
393
  export: `meta export — flatten loaded metadata to one canonical JSON artifact
366
394
 
@@ -370,6 +398,39 @@ USAGE:
370
398
  FLAGS:
371
399
  --out <file> Write output to a file (default: stdout)
372
400
  --help, -h Print this help
401
+ `,
402
+ fmt: `meta fmt — rewrite metadata files into canonical form
403
+
404
+ USAGE:
405
+ meta fmt [<file>...] [flags]
406
+
407
+ Formats every JSON metadata file this project's resolved sources contain
408
+ (resolveCollection() — never a hardcoded default directory), or just the
409
+ files named. Each file is formatted as its OWN content (own-mode,
410
+ declared-here layer only) — never inlining an inherited member or another
411
+ file's overlay.
412
+
413
+ FLAGS:
414
+ [<file>...] Optional explicit file(s) — must be members of this project's
415
+ resolved metadata sources; omit to format every one of them.
416
+ --check List files that are not canonical and exit non-zero; changes
417
+ nothing on disk.
418
+ --help, -h Print this help
419
+
420
+ SAFETY: before writing any file, fmt reloads the whole project with that file's
421
+ candidate content swapped in and requires the loaded model's canonical form to
422
+ be byte-identical to the untouched baseline. A file that fails this check is
423
+ left exactly as it was and reported as an error — fmt never changes meaning.
424
+
425
+ YAML: left untouched and reported as skipped — no canonical YAML emitter
426
+ exists yet (ADR-0006: JSON is the canonical interchange form; YAML is an
427
+ authoring format).
428
+
429
+ OVERLAY: a file declaring 'overlay: true' ANYWHERE in its tree is reported as
430
+ skipped — not only when no base exists in the same file. A formatter never
431
+ changes structure: even when a same-file base IS present, merging it with
432
+ the overlay (what the ordinary loader would do) would drop the overlay
433
+ marker from the output, so fmt refuses that too and leaves the file as-is.
373
434
  `,
374
435
  docs: `meta docs — generate neutral metadata documentation (entity + template pages)
375
436
 
@@ -645,6 +706,10 @@ export async function run(argv: string[]): Promise<number> {
645
706
  const { exportCommand } = await import("./commands/export.js");
646
707
  return exportCommand(rest, cwd);
647
708
  }
709
+ case "fmt": {
710
+ const { fmtCommand } = await import("./commands/fmt.js");
711
+ return fmtCommand(rest, cwd);
712
+ }
648
713
  case "types": {
649
714
  const { typesCommand } = await import("./commands/types.js");
650
715
  return typesCommand(rest, explicitFormat);
package/src/lib/args.ts CHANGED
@@ -190,6 +190,35 @@ export function parseExportArgs(argv: string[]): ExportFlags {
190
190
  };
191
191
  }
192
192
 
193
+ // ---------------------------------------------------------------------------
194
+ // fmt flags (#304)
195
+ // ---------------------------------------------------------------------------
196
+
197
+ export interface FmtFlags {
198
+ check: boolean;
199
+ /** Optional explicit file list — narrows the run to these files (must be
200
+ * members of the project's resolved metadata sources). Empty = every file. */
201
+ files: string[];
202
+ }
203
+
204
+ /** The flag table `parseFmtArgs` parses. Exported so the help text can be gated against it. */
205
+ export const FMT_OPTIONS = {
206
+ check: { type: "boolean", default: false },
207
+ } as const;
208
+
209
+ export function parseFmtArgs(argv: string[]): FmtFlags {
210
+ const { values, positionals } = parseArgs({
211
+ args: argv,
212
+ options: FMT_OPTIONS,
213
+ strict: true,
214
+ allowPositionals: true,
215
+ });
216
+ return {
217
+ check: !!values.check,
218
+ files: positionals,
219
+ };
220
+ }
221
+
193
222
  // ---------------------------------------------------------------------------
194
223
  // shared DB-connection vocab (used by both verify --db and migrate)
195
224
  // ---------------------------------------------------------------------------
@@ -369,6 +398,13 @@ export interface VerifyFlags {
369
398
  * the build.
370
399
  */
371
400
  noNameLint: boolean;
401
+ /**
402
+ * Suppress the advisory deprecated-REFERENCE authoring lint (#305) — a node
403
+ * depending (extends/@objectRef/@references/origin @from/@of/@via) on a
404
+ * `deprecated` node. Advisory only, like its siblings: it has no gate half
405
+ * and can never fail the build.
406
+ */
407
+ noDeprecationLint: boolean;
372
408
  /**
373
409
  * ADR-0023 strict-attr load opt-OUT (#96). `verify` is strict-by-default — an
374
410
  * undeclared/typo'd own `@attr` fails verify (ERR_UNKNOWN_ATTR). `--lax`
@@ -411,6 +447,7 @@ export const VERIFY_OPTIONS = {
411
447
  "no-requirement-lint": { type: "boolean", default: false },
412
448
  "no-overlay-lint": { type: "boolean", default: false },
413
449
  "no-name-lint": { type: "boolean", default: false },
450
+ "no-deprecation-lint": { type: "boolean", default: false },
414
451
  lax: { type: "boolean", default: false },
415
452
  "d1": { type: "string" },
416
453
  "remote": { type: "boolean", default: false },
@@ -496,6 +533,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
496
533
  noRequirementLint: !!values["no-requirement-lint"],
497
534
  noOverlayLint: !!values["no-overlay-lint"],
498
535
  noNameLint: !!values["no-name-lint"],
536
+ noDeprecationLint: !!values["no-deprecation-lint"],
499
537
  lax: !!values.lax,
500
538
  d1: values.d1 as string | undefined,
501
539
  remote: !!values.remote,