@metaobjectsdev/cli 1.0.3 → 1.0.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +13 -5
  2. package/dist/src/commands/eject.d.ts +2 -1
  3. package/dist/src/commands/eject.d.ts.map +1 -1
  4. package/dist/src/commands/eject.js +235 -75
  5. package/dist/src/commands/eject.js.map +1 -1
  6. package/dist/src/commands/gen.d.ts.map +1 -1
  7. package/dist/src/commands/gen.js +119 -29
  8. package/dist/src/commands/gen.js.map +1 -1
  9. package/dist/src/commands/init.d.ts +5 -9
  10. package/dist/src/commands/init.d.ts.map +1 -1
  11. package/dist/src/commands/init.js +80 -384
  12. package/dist/src/commands/init.js.map +1 -1
  13. package/dist/src/commands/verify.d.ts.map +1 -1
  14. package/dist/src/commands/verify.js +40 -14
  15. package/dist/src/commands/verify.js.map +1 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +36 -14
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lib/advisory.d.ts +6 -0
  20. package/dist/src/lib/advisory.d.ts.map +1 -1
  21. package/dist/src/lib/advisory.js +10 -0
  22. package/dist/src/lib/advisory.js.map +1 -1
  23. package/dist/src/lib/args.d.ts +18 -2
  24. package/dist/src/lib/args.d.ts.map +1 -1
  25. package/dist/src/lib/args.js +15 -3
  26. package/dist/src/lib/args.js.map +1 -1
  27. package/dist/src/lib/catalog-listing.d.ts +92 -0
  28. package/dist/src/lib/catalog-listing.d.ts.map +1 -0
  29. package/dist/src/lib/catalog-listing.js +268 -0
  30. package/dist/src/lib/catalog-listing.js.map +1 -0
  31. package/dist/src/lib/catalog.d.ts +41 -0
  32. package/dist/src/lib/catalog.d.ts.map +1 -0
  33. package/dist/src/lib/catalog.js +89 -0
  34. package/dist/src/lib/catalog.js.map +1 -0
  35. package/dist/src/lib/collection-load-options.d.ts +1 -1
  36. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  37. package/dist/src/lib/collection-load-options.js +6 -0
  38. package/dist/src/lib/collection-load-options.js.map +1 -1
  39. package/dist/src/lib/install-set.d.ts +31 -0
  40. package/dist/src/lib/install-set.d.ts.map +1 -0
  41. package/dist/src/lib/install-set.js +103 -0
  42. package/dist/src/lib/install-set.js.map +1 -0
  43. package/dist/src/lib/library-eject.d.ts +79 -0
  44. package/dist/src/lib/library-eject.d.ts.map +1 -0
  45. package/dist/src/lib/library-eject.js +241 -0
  46. package/dist/src/lib/library-eject.js.map +1 -0
  47. package/dist/src/lib/library-listing.d.ts +63 -0
  48. package/dist/src/lib/library-listing.d.ts.map +1 -0
  49. package/dist/src/lib/library-listing.js +178 -0
  50. package/dist/src/lib/library-listing.js.map +1 -0
  51. package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
  52. package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
  53. package/dist/src/lib/library-prefix-advisory.js +67 -0
  54. package/dist/src/lib/library-prefix-advisory.js.map +1 -0
  55. package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
  56. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  57. package/dist/src/lib/load-metaobjects-config.js +11 -17
  58. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  59. package/dist/src/lib/requirement-check.d.ts +17 -2
  60. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  61. package/dist/src/lib/requirement-check.js +51 -9
  62. package/dist/src/lib/requirement-check.js.map +1 -1
  63. package/package.json +10 -10
  64. package/src/commands/eject.ts +304 -90
  65. package/src/commands/gen.ts +128 -28
  66. package/src/commands/init.ts +82 -390
  67. package/src/commands/verify.ts +47 -15
  68. package/src/index.ts +36 -14
  69. package/src/lib/advisory.ts +13 -0
  70. package/src/lib/args.ts +29 -5
  71. package/src/lib/catalog-listing.ts +369 -0
  72. package/src/lib/catalog.ts +107 -0
  73. package/src/lib/collection-load-options.ts +7 -1
  74. package/src/lib/install-set.ts +116 -0
  75. package/src/lib/library-eject.ts +298 -0
  76. package/src/lib/library-listing.ts +258 -0
  77. package/src/lib/library-prefix-advisory.ts +78 -0
  78. package/src/lib/load-metaobjects-config.ts +15 -21
  79. package/src/lib/requirement-check.ts +71 -11
@@ -14,7 +14,9 @@ import { emitStructured, type OutputFormat } from "../lib/format.js";
14
14
  import {
15
15
  antiPatternRows, missingBaseUrlRows, removedPropRows, ranSection, skippedSection, warnCapped,
16
16
  type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
17
+ libraryPrefixRows,
17
18
  } from "../lib/advisory.js";
19
+ import { scanForUnprovenancedLibraryPrefix } from "../lib/library-prefix-advisory.js";
18
20
  import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
19
21
  import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
20
22
  import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
@@ -382,7 +384,7 @@ export async function verifyCommand(
382
384
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
383
385
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
384
386
  // noisy project (both opt-outs work on `meta verify` and `meta gen`).
385
- runAntiPatternAdvisory();
387
+ await runAntiPatternAdvisory();
386
388
 
387
389
  const exitCode = Math.max(
388
390
  templateExit,
@@ -646,15 +648,20 @@ export async function verifyCommand(
646
648
  say(
647
649
  `meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
648
650
  `${s.architectural} architectural) — ${parts.join(", ")}; ` +
649
- `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
650
- `counted over ${collection.files.length} metadata file(s)` +
651
- // FR-023 — only when there ARE dependencies. A project that declares none
652
- // must print the sentence it printed before dependencies existed, to the
653
- // byte: "0 from dependencies" is noise on every existing project, and this
654
- // line is a SURFACE the no-dependency guarantee covers like any other.
655
- (collection.dependencies.length > 0
656
- ? `, ${collection.dependencies.length} from dependencies.`
657
- : `.`),
651
+ // FR-043 §5.4 — a project whose every requirement came from a shipped library
652
+ // is not measured, and says so rather than printing a ratio. Silence would be
653
+ // worse than either: a missing clause reads as a clean coverage result.
654
+ (s.entitiesTotal === undefined
655
+ ? `coverage: not measured (no project-authored requirements).`
656
+ : `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
657
+ `counted over ${collection.files.length} metadata file(s)` +
658
+ // FR-023 — only when there ARE dependencies. A project that declares none
659
+ // must print the sentence it printed before dependencies existed, to the
660
+ // byte: "0 from dependencies" is noise on every existing project, and this
661
+ // line is a SURFACE the no-dependency guarantee covers like any other.
662
+ (collection.dependencies.length > 0
663
+ ? `, ${collection.dependencies.length} from dependencies.`
664
+ : `.`)),
658
665
  );
659
666
  if (s.undecided > 0) {
660
667
  say(
@@ -674,8 +681,12 @@ export async function verifyCommand(
674
681
  .filter((k) => (s.byStatus[k] ?? 0) > 0)
675
682
  .map((k) => ({ status: k, count: s.byStatus[k] ?? 0 })),
676
683
  undecided: s.undecided,
677
- entitiesClaimed: s.entitiesClaimed,
678
- entitiesTotal: s.entitiesTotal,
684
+ // Absent when coverage was not measured (FR-043 §5.4), never zeroed: a reader
685
+ // cannot tell `0/0 claimed` from "not measured", and the two mean opposite
686
+ // things.
687
+ ...(s.entitiesTotal === undefined
688
+ ? {}
689
+ : { entitiesClaimed: s.entitiesClaimed ?? 0, entitiesTotal: s.entitiesTotal }),
679
690
  metadataFiles: collection.files.length,
680
691
  };
681
692
  }
@@ -777,7 +788,7 @@ export async function verifyCommand(
777
788
  // Records its result EITHER WAY — a skip carries its reason rather than looking
778
789
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
779
790
  // scanner's own header: bias to under-flagging, never a non-zero exit).
780
- function runAntiPatternAdvisory(): void {
791
+ async function runAntiPatternAdvisory(): Promise<void> {
781
792
  if (flags.noAntipatterns) {
782
793
  antiPatternSection = skippedSection("suppressed by --no-antipatterns");
783
794
  return;
@@ -816,10 +827,23 @@ export async function verifyCommand(
816
827
  // advisory only — never breaks verify.
817
828
  }
818
829
 
830
+ // FR-043 §3.5 — `metaobjects::` in this project's own metadata with no ejection
831
+ // provenance. Advisory because with the library NOT opted in nothing is broken; the
832
+ // opted-in case is refused at load instead.
833
+ let libraryPrefix: Awaited<ReturnType<typeof scanForUnprovenancedLibraryPrefix>> = [];
834
+ try {
835
+ libraryPrefix = await scanForUnprovenancedLibraryPrefix(
836
+ root, collection.ownFiles, collection.configDir,
837
+ );
838
+ } catch {
839
+ // Same discipline as its siblings: an advisory scan never breaks verify.
840
+ }
841
+
819
842
  antiPatternSection = ranSection([
820
843
  ...antiPatternRows(findings),
821
844
  ...missingBaseUrlRows(baseUrl),
822
845
  ...removedPropRows(removedProps),
846
+ ...libraryPrefixRows(libraryPrefix),
823
847
  ]);
824
848
  if (findings.length > 0) {
825
849
  log.warn(
@@ -842,6 +866,13 @@ export async function verifyCommand(
842
866
  );
843
867
  warnCapped(removedProps.map((f) => ` ${f.message}`), flags.limit, { structured });
844
868
  }
869
+ if (libraryPrefix.length > 0) {
870
+ log.warn(
871
+ `meta verify — ${libraryPrefix.length} node(s) declared under "metaobjects::" with ` +
872
+ `no ejection provenance (advisory — does not fail the build):`,
873
+ );
874
+ warnCapped(libraryPrefix.map((f) => ` ${f.message}`), flags.limit, { structured });
875
+ }
845
876
  }
846
877
 
847
878
  // -- template (prompt / output) drift --------------------------------------
@@ -1572,8 +1603,9 @@ interface RequirementCounts {
1572
1603
  architectural: number;
1573
1604
  byStatus: { status: string; count: number }[];
1574
1605
  undecided: number;
1575
- entitiesClaimed: number;
1576
- entitiesTotal: number;
1606
+ /** Absent when coverage was not measured — FR-043 §5.4. */
1607
+ entitiesClaimed?: number;
1608
+ entitiesTotal?: number;
1577
1609
  /** How many metadata files the two entity counts were taken over. */
1578
1610
  metadataFiles: number;
1579
1611
  }
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ const VERSION = cliVersion();
17
17
  * human text. It is named ONCE and used by both the warning below and the help
18
18
  * text above, so the two cannot drift apart.
19
19
  */
20
- const FORMAT_AWARE_COMMANDS: readonly string[] = ["gen", "verify", "migrate", "types", "deps"];
20
+ const FORMAT_AWARE_COMMANDS: readonly string[] = ["gen", "verify", "migrate", "types", "deps", "eject"];
21
21
 
22
22
  const HELP_TEXT = `meta — MetaObjects CLI (v${VERSION})
23
23
 
@@ -58,8 +58,14 @@ GLOBAL OPTIONS:
58
58
 
59
59
  GEN FLAGS:
60
60
  --dry-run Compute and print, don't write
61
- --list Print the generator registry (name, tier, options) and exit —
62
- no config or metadata required
61
+ --list Print the generator CATALOG (name, layer, tier, what it emits,
62
+ what to install) and exit. Codegen is opt-in: nothing runs until
63
+ you wire it, and this is where you choose. No config or metadata
64
+ required — add --format json for the same catalog, machine-readable.
65
+ --probe With --list: construct every catalog generator and dry-run it
66
+ against YOUR model, reporting how many files each would emit.
67
+ A count per generator beats any category label — and it
68
+ cannot go stale, because it runs the generators. Needs a project.
63
69
  --baseline <default|adopt|fresh>
64
70
  First-time-on-existing-file behavior. Default: refuse a file that
65
71
  cannot be proved to be generated output. "adopt" records the files
@@ -156,8 +162,14 @@ USAGE:
156
162
 
157
163
  FLAGS:
158
164
  --dry-run Compute and print, don't write
159
- --list Print the generator registry (name, tier, options) and exit —
160
- no config or metadata required
165
+ --list Print the generator CATALOG (name, layer, tier, what it emits,
166
+ what to install) and exit. Codegen is opt-in: nothing runs until
167
+ you wire it, and this is where you choose. No config or metadata
168
+ required — add --format json for the same catalog, machine-readable.
169
+ --probe With --list: construct every catalog generator and dry-run it
170
+ against YOUR model, reporting how many files each would emit.
171
+ A count per generator beats any category label — and it cannot go
172
+ stale, because it runs the generators. Needs a project.
161
173
  --baseline <default|adopt|fresh>
162
174
  First-time-on-existing-file behavior. Default: refuse a file that
163
175
  cannot be proved to be generated output. "adopt" records the files
@@ -183,23 +195,33 @@ limit on the machine-readable result.
183
195
 
184
196
  NOTE: outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts
185
197
  `,
186
- eject: `meta eject — copy a reference generator into your repo so you own it
198
+ eject: `meta eject — copy reference generators into your repo so you own them
187
199
 
188
200
  USAGE:
189
- meta eject <name> Copy generator <name> into codegen/generators/<name>.ts
201
+ meta eject <name>... Copy each generator into codegen/generators/<name>.ts
190
202
  meta eject --list List every ejectable generator name, grouped by package
191
203
 
204
+ Codegen is opt-in — \`meta init\` wires nothing — so this is the door every generator
205
+ you run comes through. Take several at once: the install set is CONSOLIDATED, so
206
+ \`meta eject form hooks grid\` prints one install line, not three. An unknown name
207
+ refuses the whole call and copies nothing.
208
+
192
209
  FLAGS:
193
210
  --list List ejectable generators instead of copying one
194
211
  --force Overwrite an already-ejected file (default: never clobber)
212
+ --format <toon|json|text> Output format (global flag). The structured form carries
213
+ each file's wire lines, the consolidated install set and the
214
+ config keys the ejected generators read.
195
215
  --help, -h Print this help
196
216
 
197
- \`meta init\` copies five generators (entity, queries, routes, barrel, names) into
198
- codegen/generators/ automatically (ADR-0034 scaffold-and-own). \`meta eject\` is
199
- the same operation for ANY generator — one you skipped at init time, a UI-tier
200
- generator like form/hooks/grid, or one a package gains later. It prints the
201
- import line to paste into metaobjects.config.ts, and it never overwrites a
202
- file you already own unless you pass --force.
217
+ \`meta init\` copies NO generators — codegen is opt-in, and codegen/generators/
218
+ starts empty on purpose. Everything you run comes through here (ADR-0034
219
+ scaffold-and-own): the file is YOURS to edit, and \`meta gen\` runs your copy, not
220
+ the package's. It prints the import line and the entry to add in
221
+ metaobjects.config.ts, and never overwrites a file you already own without --force.
222
+
223
+ Find names with \`meta gen --list\` — add --probe to see how many files each would
224
+ emit for your model.
203
225
  `,
204
226
  deps: `meta deps — sync a declared metadata dependency's committed snapshot
205
227
 
@@ -584,7 +606,7 @@ export async function run(argv: string[]): Promise<number> {
584
606
  }
585
607
  case "eject": {
586
608
  const { ejectCommand } = await import("./commands/eject.js");
587
- return ejectCommand(rest, cwd);
609
+ return ejectCommand(rest, cwd, fmt);
588
610
  }
589
611
  case "deps": {
590
612
  const { depsCommand } = await import("./commands/deps.js");
@@ -150,6 +150,19 @@ export function removedPropRows(findings: readonly RemovedPropFinding[]): Adviso
150
150
  }));
151
151
  }
152
152
 
153
+ /** FR-043 §3.5 findings in the same row shape. */
154
+ export function libraryPrefixRows(
155
+ findings: readonly { file: string; fqn: string; message: string }[],
156
+ ): AdvisoryFindingRow[] {
157
+ return findings.map((f) => ({
158
+ file: f.file,
159
+ line: 0,
160
+ rule: "library-prefix-unprovenanced",
161
+ construct: f.fqn,
162
+ message: f.message,
163
+ }));
164
+ }
165
+
153
166
  // ---------------------------------------------------------------------------
154
167
  // text output
155
168
  // ---------------------------------------------------------------------------
package/src/lib/args.ts CHANGED
@@ -107,6 +107,12 @@ export interface GenFlags {
107
107
  /** ADR-0021 D3 — print the stable-name generator registry and exit without
108
108
  * running codegen. */
109
109
  list: boolean;
110
+ /**
111
+ * `--list` only: construct every catalog generator and dry-run it against THIS
112
+ * project's model, reporting a real file count per generator. Needs a project;
113
+ * plain `--list` deliberately does not (it describes the installed engine).
114
+ */
115
+ probe: boolean;
110
116
  /** Suppress the advisory anti-pattern (verify-as-teacher) pass. */
111
117
  noAntipatterns: boolean;
112
118
  /**
@@ -122,6 +128,7 @@ export const GEN_OPTIONS = {
122
128
  "dry-run": { type: "boolean", default: false },
123
129
  "baseline": { type: "string" },
124
130
  "list": { type: "boolean", default: false },
131
+ "probe": { type: "boolean", default: false },
125
132
  "no-antipatterns": { type: "boolean", default: false },
126
133
  "limit": { type: "string" },
127
134
  } as const;
@@ -149,6 +156,7 @@ export function parseGenArgs(argv: string[]): GenFlags {
149
156
  entities: positionals,
150
157
  baseline: (baselineRaw as "default" | "fresh" | "adopt" | undefined) ?? "default",
151
158
  list: !!values.list,
159
+ probe: !!values.probe,
152
160
  noAntipatterns: !!values["no-antipatterns"],
153
161
  // Throws on a bad value; the command layer reports it and exits 2, exactly as
154
162
  // it does for --baseline above.
@@ -602,8 +610,14 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
602
610
  // ---------------------------------------------------------------------------
603
611
 
604
612
  export interface EjectFlags {
605
- /** The generator name to eject; undefined when only --list was given. */
606
- name: string | undefined;
613
+ /**
614
+ * The generator names to eject, in the order given. Empty when only --list was given.
615
+ *
616
+ * MANY, not one: choosing a client tier means `form hooks grid`, and three separate
617
+ * invocations produce three separate install lines for the same package. The
618
+ * consolidated install set is the point of taking them together.
619
+ */
620
+ names: string[];
607
621
  list: boolean;
608
622
  /** Overwrite an already-ejected file; default false — eject never clobbers. */
609
623
  force: boolean;
@@ -623,12 +637,22 @@ export function parseEjectArgs(argv: string[]): EjectFlags {
623
637
  allowPositionals: true,
624
638
  });
625
639
 
626
- if (positionals.length > 1) {
627
- throw new Error(`meta eject takes at most one generator name; got: ${positionals.join(", ")}`);
640
+ // `Set.add` returns the SET, which is always truthy — so the tempting
641
+ // `positionals.filter((n) => !seen.add(n))` never reports anything.
642
+ const seen = new Set<string>();
643
+ const duplicates: string[] = [];
644
+ for (const n of positionals) {
645
+ if (seen.has(n)) duplicates.push(n);
646
+ else seen.add(n);
647
+ }
648
+ if (duplicates.length > 0) {
649
+ throw new Error(
650
+ `meta eject: repeated generator name(s): ${[...new Set(duplicates)].join(", ")}`,
651
+ );
628
652
  }
629
653
 
630
654
  return {
631
- name: positionals[0],
655
+ names: positionals,
632
656
  list: !!values.list,
633
657
  force: !!values.force,
634
658
  };