@metaobjectsdev/cli 1.0.12 → 1.1.0

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 (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -1,8 +1,9 @@
1
1
  // FR-040 §4.2(a) — `meta eject <generator>...` takes ownership of any reference-template
2
2
  // generator, in any package, at any time after `meta init`.
3
3
  //
4
- // Under opt-in codegen this is THE copy door: `meta init` scaffolds the layout and an
5
- // empty selection, so every generator an adopter runs arrives through here. It takes
4
+ // This is the copy door, for a generator an adopter wants to CHANGE: `meta init`
5
+ // scaffolds the layout and an empty selection, and a generator run as shipped is
6
+ // imported from its package without coming through here (ADR-0034 Amendment 5). It takes
6
7
  // MANY names, because a real selection is several — choosing a client tier is
7
8
  // `form hooks grid` — and three separate invocations produce three separate install
8
9
  // lines for the same package. The consolidated install set is the point.
@@ -14,6 +15,7 @@
14
15
  // config-array edit has regressed across at least four filed issues). The primary
15
16
  // consumer performs two file edits and one install trivially when told exactly what
16
17
  // they are. `meta gen` plus `tsc` is the audit.
18
+ import { existsSync } from "node:fs";
17
19
  import { mkdir, writeFile, stat, readFile } from "node:fs/promises";
18
20
  import { join } from "node:path";
19
21
  import { cliVersion } from "../lib/version.js";
@@ -24,12 +26,16 @@ import { parseEjectArgs } from "../lib/args.js";
24
26
  import { log } from "../lib/log.js";
25
27
  import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js";
26
28
  import { compareOwnedCopy, type OwnedComparison } from "../lib/owned-copy.js";
27
- import { composeCatalog, packageOf } from "../lib/catalog.js";
28
- import { installSetFor, type InstallSet, type OwnedRuntimeInstall } from "../lib/install-set.js";
29
+ import { composeCatalog, CORE_CODEGEN_PACKAGE, factoryModuleOf, packageOf } from "../lib/catalog.js";
30
+ import { dependencyRangesOf, installSetFor, type InstallSet, type OwnedRuntimeInstall } from "../lib/install-set.js";
29
31
  import {
30
32
  ejectRuntime, ejectsRuntime, runtimeCopyStatus, runtimeDiffCommand, runtimeInstall,
31
33
  type RuntimeEjectResult, type RuntimeFileResult,
32
34
  } from "../lib/runtime-eject.js";
35
+ import {
36
+ closureOf, ejectTemplates, OWNED_TEMPLATES_DIR, packagedTemplates, predatesTemplateHandOver,
37
+ templateCopyStatus, templateDiffCommand, type TemplateEjectResult, type TemplateFileResult,
38
+ } from "../lib/template-eject.js";
33
39
  import { HTTP_RUNTIME_PACKAGE, OWNED_RUNTIME_DIR } from "@metaobjectsdev/codegen-ts";
34
40
  import { emitStructured, type OutputFormat } from "../lib/format.js";
35
41
  import {
@@ -37,6 +43,7 @@ import {
37
43
  type LibraryEjectResult,
38
44
  } from "../lib/library-eject.js";
39
45
  import { describeError } from "../lib/error-text.js";
46
+ import { DOCS_SITE_NAME, ejectDocsSite, type DocsSiteFileResult } from "../lib/docs-site-eject.js";
40
47
 
41
48
  // Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
42
49
  // same never-clobber-without-consent contract. Kept as its own local constant rather
@@ -61,8 +68,6 @@ interface TemplateSource {
61
68
  // runtime by selecting this entry via `names.includes(name)`. Reading from the root
62
69
  // deletes that machinery without weakening anything: membership is still checked, once,
63
70
  // where the untrusted value enters.
64
- const CORE_CODEGEN_PACKAGE = "@metaobjectsdev/codegen-ts";
65
-
66
71
  const SOURCES: TemplateSource[] = [
67
72
  {
68
73
  packageName: CORE_CODEGEN_PACKAGE,
@@ -85,6 +90,7 @@ function resolveSource(name: string): TemplateSource | undefined {
85
90
  return SOURCES.find((s) => s.names.includes(name));
86
91
  }
87
92
 
93
+
88
94
  /** Every ejectable name, in registry order (stable — matches `meta eject --list`). */
89
95
  export function ejectableNames(): string[] {
90
96
  return SOURCES.flatMap((s) => s.names);
@@ -112,6 +118,19 @@ function extractExportName(importLine: string, name: string): string {
112
118
  return match[1];
113
119
  }
114
120
 
121
+ // The CALL a template's header documents, when it is not the bare `<export>()`. A factory
122
+ // that cannot be called without options says so where it says how to import it
123
+ // (`// generators: [sharedModelFile({ name, include })]`). Read from the header for the
124
+ // reason the import line is: the template is the one thing that knows, and the catalog
125
+ // deliberately does not derive a call from an options summary.
126
+ const HEADER_ENTRY_RE = /^\/\/\s+generators: \[(\w+\(.*\))\],?\s*$/m;
127
+
128
+ /** The call to wire: the header's, when it documents one for this template's own export. */
129
+ function extractEntry(templateSource: string, exportName: string): string {
130
+ const call = HEADER_ENTRY_RE.exec(templateSource)?.[1];
131
+ return call !== undefined && call.startsWith(`${exportName}(`) ? call : `${exportName}()`;
132
+ }
133
+
115
134
  function extractImportLine(templateSource: string, name: string): string {
116
135
  const match = HEADER_IMPORT_RE.exec(templateSource);
117
136
  if (!match?.[1]) {
@@ -144,6 +163,9 @@ export interface EjectResult {
144
163
  importLine: string;
145
164
  /** The symbol the template exports — the binding to REPLACE in the config. */
146
165
  exportName: string;
166
+ /** The call to wire, with any options the factory cannot be called without
167
+ * (`sharedModelFile({ name, include })`), as the template's header documents it. */
168
+ entry: string;
147
169
  /** The package the generator currently comes from, i.e. the import to remove. */
148
170
  packageName: string;
149
171
  /** Advisory lines about packages the ejected file imports but the project lacks. */
@@ -171,6 +193,12 @@ export interface EjectResult {
171
193
  * generator whose output imports no adapter.
172
194
  */
173
195
  runtime?: RuntimeEjectResult | undefined;
196
+ /**
197
+ * ADR-0034 Amendment 8: the template modules this generator calls to write its output,
198
+ * copied into `codegen/templates/` so the adopter owns those too. Absent for a generator
199
+ * that calls none (`barrel`).
200
+ */
201
+ templates?: TemplateEjectResult | undefined;
174
202
  }
175
203
 
176
204
  /** The `@metaobjectsdev/*` packages an ejected template imports, read from the file
@@ -205,8 +233,13 @@ function requiredPackages(templateSource: string): string[] {
205
233
  * missing package and the version to match is the useful half; silently rewriting
206
234
  * someone's manifest is not.
207
235
  */
208
- export async function dependencyNotesForTemplate(cwd: string, templateSource: string): Promise<string[]> {
209
- const required = requiredPackages(templateSource);
236
+ export async function dependencyNotesForTemplate(
237
+ cwd: string,
238
+ templateSource: string,
239
+ /** Packages the template modules copied beside the generator import, as well. */
240
+ alsoImported: readonly string[] = [],
241
+ ): Promise<string[]> {
242
+ const required = [...new Set([...requiredPackages(templateSource), ...alsoImported.filter((p) => p.startsWith("@metaobjectsdev/"))])].sort();
210
243
  if (required.length === 0) return [];
211
244
 
212
245
  const pkg = readPackageManifest(cwd);
@@ -234,19 +267,23 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
234
267
  );
235
268
  }
236
269
 
237
- const templateSource = await readFile(join(source.root(), `${opts.name}.ts`), "utf8");
270
+ // What is written is the reference with its module specifiers pointed at the project:
271
+ // its template modules are copied beside it (ADR-0034 Amendment 8).
272
+ const closure = closureOf(opts.name);
273
+ const templateSource = closure.generator.content;
238
274
  const importLine = extractImportLine(templateSource, opts.name);
239
275
  const exportName = extractExportName(importLine, opts.name);
240
276
  const rel = `${OWNED_GENERATORS_DIR}/${opts.name}.ts`;
241
277
  const abs = join(opts.cwd, rel);
242
- const notes = await dependencyNotesForTemplate(opts.cwd, templateSource);
278
+ const notes = await dependencyNotesForTemplate(opts.cwd, templateSource, closure.packages);
243
279
  const common = {
244
280
  path: rel,
245
281
  importLine,
246
282
  exportName,
283
+ entry: extractEntry(templateSource, exportName),
247
284
  packageName: source.packageName,
248
285
  dependencyNotes: notes,
249
- templatePackages: requiredPackages(templateSource),
286
+ templatePackages: closure.packages.filter((p) => p.startsWith("@metaobjectsdev/")),
250
287
  };
251
288
 
252
289
  const existing = (await fileExists(abs)) ? await readFile(abs, "utf8") : undefined;
@@ -265,8 +302,17 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
265
302
  ? await ejectRuntime(opts.cwd, [opts.name], opts.force === true)
266
303
  : undefined;
267
304
 
305
+ // The template modules follow the same rule, for the same reason: a kept copy that predates
306
+ // the hand-over still imports the emit code from the package, so copying the modules beside
307
+ // it would be dead code in the project.
308
+ const keptCopyPredatesTemplates =
309
+ existing !== undefined && !opts.force && predatesTemplateHandOver(existing, closure);
310
+ const templates = closure.templates.length > 0 && !keptCopyPredatesTemplates
311
+ ? await ejectTemplates(opts.cwd, [closure], opts.force === true)
312
+ : undefined;
313
+
268
314
  if (!opts.force && existing !== undefined) {
269
- return { ...common, status: "preserved", comparison, runtime };
315
+ return { ...common, status: "preserved", comparison, runtime, templates };
270
316
  }
271
317
 
272
318
  await mkdir(join(opts.cwd, OWNED_GENERATORS_DIR), { recursive: true });
@@ -276,6 +322,7 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
276
322
  status: existing === undefined ? "created" : "replaced",
277
323
  comparison,
278
324
  runtime,
325
+ templates,
279
326
  };
280
327
  }
281
328
 
@@ -314,6 +361,47 @@ export async function runtimeCopyLines(cwd: string): Promise<string[]> {
314
361
  return lines;
315
362
  }
316
363
 
364
+ /**
365
+ * The owned template modules under `codegen/templates/`, file by file, against what the
366
+ * installed packages would write there today. Shared by `meta eject --list` and
367
+ * `meta gen --list`. Empty when the project owns no template copy.
368
+ *
369
+ * Only the import lines of a copy differ from the package by design, so the comparison is
370
+ * the one the generator copies get: `identical` says an upgrade can be taken by re-copying
371
+ * (`meta eject <name> --force`); `DIFFERS` says the copy is yours in a way the package is
372
+ * not, and names the diff that tells whose change it is.
373
+ */
374
+ export async function templateCopyLines(cwd: string): Promise<string[]> {
375
+ if (!existsSync(join(cwd, OWNED_TEMPLATES_DIR))) return [];
376
+ const rows = await templateCopyStatus(cwd, packagedTemplates());
377
+ if (rows.length === 0) return [];
378
+ const lines: string[] = [
379
+ `Template source this project owns (${OWNED_TEMPLATES_DIR}/), against the installed codegen packages:`,
380
+ ];
381
+ const differing: string[] = [];
382
+ for (const r of rows) {
383
+ if (r.verdict === "identical") lines.push(` ${r.path} [identical to the package]`);
384
+ else if (r.verdict === "reformatted") lines.push(` ${r.path} [same content as the package, your formatting]`);
385
+ else if (r.verdict === "not-in-package") lines.push(` ${r.path} [not in the package — yours, or removed upstream]`);
386
+ else {
387
+ const diff = templateDiffCommand(r);
388
+ if (diff !== undefined) differing.push(diff);
389
+ lines.push(
390
+ ` ${r.path} [DIFFERS: ${r.referenceOnly} line(s) only in the package, ${r.localOnly} line(s) only in yours]`,
391
+ );
392
+ }
393
+ }
394
+ if (differing.length > 0) {
395
+ lines.push(
396
+ " See the difference (upstream fix, or your own?) before taking an upgrade. " +
397
+ "Only the import lines of a copy differ from the package by design:",
398
+ );
399
+ for (const d of differing) lines.push(` ${d}`);
400
+ }
401
+ lines.push("");
402
+ return lines;
403
+ }
404
+
317
405
  /**
318
406
  * `--list` doubles as the staleness report for what this project ALREADY owns.
319
407
  *
@@ -323,38 +411,120 @@ export async function runtimeCopyLines(cwd: string): Promise<string[]> {
323
411
  * gate green. Marking each owned copy `identical` / `differs` makes it a one-command
324
412
  * answer instead of a diff nobody thinks to run.
325
413
  */
326
- async function listOutput(cwd: string): Promise<string> {
327
- const lines: string[] = [];
328
- lines.push("Ejectable generators (copy any of these into codegen/generators/ and own it):");
329
- lines.push("");
330
- let anyStale = false;
414
+ /** One reference generator as `--list` reports it: is a copy owned, and how far it moved. */
415
+ interface GeneratorListRow {
416
+ name: string;
417
+ package: string;
418
+ /** `available` = not ejected; `owned` = ejected but the reference could not be read. */
419
+ status: "available" | "owned" | "identical" | "reformatted" | "differs";
420
+ /** Lines behind / of your own in the GENERATOR file alone. */
421
+ behind: number;
422
+ own: number;
423
+ /**
424
+ * The renderers this generator owns under `codegen/templates/` (those it imports that
425
+ * exist in the project), each against the installed package. A generator is `identical`
426
+ * only when its file AND every one of these is; `status` is `differs` when any differs.
427
+ * Absent when the project owns none of its renderers.
428
+ */
429
+ templates?: GeneratorTemplateRow[];
430
+ }
431
+
432
+ interface GeneratorTemplateRow {
433
+ path: string;
434
+ status: "identical" | "reformatted" | "differs";
435
+ behind: number;
436
+ own: number;
437
+ }
438
+
439
+ /** One `SOURCES` entry and the rows walked for it — the grouping the text list prints. */
440
+ interface GeneratorListGroup {
441
+ packageName: string;
442
+ rows: GeneratorListRow[];
443
+ }
444
+
445
+ async function generatorListGroups(cwd: string): Promise<GeneratorListGroup[]> {
446
+ const groups: GeneratorListGroup[] = [];
447
+ // Every renderer the project holds, against the package: attributed to a generator by what
448
+ // that generator imports, so a renderer another generator owns never flags this one.
449
+ const templateStatus = await templateCopyStatus(cwd, packagedTemplates());
331
450
  for (const source of SOURCES) {
332
- lines.push(`${source.packageName}:`);
451
+ const rows: GeneratorListRow[] = [];
452
+ groups.push({ packageName: source.packageName, rows });
333
453
  for (const name of source.names) {
454
+ const row: GeneratorListRow = { name, package: source.packageName, status: "available", behind: 0, own: 0 };
455
+ rows.push(row);
334
456
  const abs = join(cwd, OWNED_GENERATORS_DIR, `${name}.ts`);
335
- if (!(await fileExists(abs))) {
336
- lines.push(` ${name}`);
337
- continue;
338
- }
457
+ if (!(await fileExists(abs))) continue;
339
458
  let ref: string;
340
459
  try {
341
- ref = await readFile(join(source.root(), `${name}.ts`), "utf8");
460
+ ref = closureOf(name).generator.content;
342
461
  } catch {
343
- lines.push(` ${name} [owned]`);
462
+ row.status = "owned";
344
463
  continue;
345
464
  }
346
- const owned = await readFile(abs, "utf8");
347
- const cmp = await compareOwnedCopy(owned, ref);
348
- if (cmp.verdict === "identical") {
349
- lines.push(` ${name} [owned — identical to the reference]`);
350
- } else if (cmp.verdict === "reformatted") {
351
- lines.push(` ${name} [owned — same content as the reference, your formatting]`);
465
+ const cmp = await compareOwnedCopy(await readFile(abs, "utf8"), ref);
466
+ if (cmp.verdict === "identical" || cmp.verdict === "reformatted") {
467
+ row.status = cmp.verdict;
352
468
  } else {
353
- anyStale = true;
469
+ row.status = "differs";
470
+ row.behind = cmp.referenceOnly;
471
+ row.own = cmp.localOnly;
472
+ }
473
+ const reached = new Set(closureOf(name).templates.map((f) => f.path));
474
+ const renderers: GeneratorTemplateRow[] = [];
475
+ for (const t of templateStatus) {
476
+ if (!reached.has(t.path) || t.verdict === "not-in-package") continue;
477
+ renderers.push({
478
+ path: t.path,
479
+ status: t.verdict === "identical" || t.verdict === "reformatted" ? t.verdict : "differs",
480
+ behind: t.referenceOnly,
481
+ own: t.localOnly,
482
+ });
483
+ }
484
+ if (renderers.length > 0) row.templates = renderers;
485
+ if (renderers.some((r) => r.status === "differs")) row.status = "differs";
486
+ }
487
+ }
488
+ return groups;
489
+ }
490
+
491
+ /** `--list` as one structured document: the generators and the shipped libraries. */
492
+ async function listData(cwd: string): Promise<Record<string, unknown>> {
493
+ const generators = (await generatorListGroups(cwd)).flatMap((g) => g.rows);
494
+ return {
495
+ generators,
496
+ libraries: ejectableLibraryNames(),
497
+ help: [
498
+ "run `meta eject <name>...` to copy a generator into codegen/generators/ and own it",
499
+ "`meta eject --list --format text` also reports owned runtime copies and ejected-library drift",
500
+ ],
501
+ };
502
+ }
503
+
504
+ async function listOutput(cwd: string): Promise<string> {
505
+ const lines: string[] = [];
506
+ lines.push("Ejectable generators (copy any of these into codegen/generators/ and own it):");
507
+ lines.push("");
508
+ const groups = await generatorListGroups(cwd);
509
+ const anyStale = groups.some((g) => g.rows.some((r) => r.status === "differs"));
510
+ const generatorFileDiffers = (r: GeneratorListRow): boolean => r.behind > 0 || r.own > 0;
511
+ for (const group of groups) {
512
+ lines.push(`${group.packageName}:`);
513
+ for (const r of group.rows) {
514
+ if (r.status === "available") lines.push(` ${r.name}`);
515
+ else if (r.status === "owned") lines.push(` ${r.name} [owned]`);
516
+ else if (r.status === "identical") lines.push(` ${r.name} [owned — identical to the reference]`);
517
+ else if (r.status === "reformatted") lines.push(` ${r.name} [owned — same content as the reference, your formatting]`);
518
+ else {
519
+ const differing = (r.templates ?? []).filter((t) => t.status === "differs");
354
520
  lines.push(
355
- ` ${name} [owned — DIFFERS: ${cmp.referenceOnly} line(s) behind, ` +
356
- `${cmp.localOnly} line(s) of your own]`,
521
+ generatorFileDiffers(r)
522
+ ? ` ${r.name} [owned — DIFFERS: ${r.behind} line(s) behind, ${r.own} line(s) of your own]`
523
+ : ` ${r.name} [owned — DIFFERS: generator identical, ${differing.length} renderer(s) differ]`,
357
524
  );
525
+ for (const t of differing) {
526
+ lines.push(` renderer ${t.path} [DIFFERS: ${t.behind} line(s) behind, ${t.own} line(s) of your own]`);
527
+ }
358
528
  }
359
529
  }
360
530
  lines.push("");
@@ -376,6 +546,12 @@ async function listOutput(cwd: string): Promise<string> {
376
546
  lines.push("");
377
547
  }
378
548
  lines.push(...(await runtimeCopyLines(cwd)));
549
+ lines.push(...(await templateCopyLines(cwd)));
550
+ // The docs site's theme: not a generator copy (its builder stays packaged), but a name
551
+ // `meta eject` takes all the same.
552
+ lines.push("The docs site's theme (its templates and assets, into codegen/docs-site/):");
553
+ lines.push(` ${DOCS_SITE_NAME}`);
554
+ lines.push("");
379
555
  // FR-043 — the shipped libraries, and how far this project's copies have drifted.
380
556
  lines.push("Shipped libraries (copy one into your own sources and own it):");
381
557
  for (const name of ejectableLibraryNames()) lines.push(` ${name}`);
@@ -422,16 +598,28 @@ async function listOutput(cwd: string): Promise<string> {
422
598
  // the command
423
599
  // ---------------------------------------------------------------------------
424
600
 
601
+ /**
602
+ * The entry an ejected generator is wired by. Every generator, documentation included, goes
603
+ * in the top-level `generators` and is run by `meta gen` (ADR-0034 Amendment 9).
604
+ */
605
+ interface EjectWiring {
606
+ /** The call, with the options the factory cannot be called without. */
607
+ entry: string;
608
+ }
609
+
425
610
  /** One row of the `--format json` payload. */
426
611
  interface EjectedRow {
427
612
  name: string;
428
613
  path: string;
429
614
  status: EjectResult["status"];
430
- /** The two edits an adopter makes in `metaobjects.config.ts`. */
615
+ /** The two edits an adopter makes in `metaobjects.config.ts`: the import, and the entry in
616
+ * `generators`. */
431
617
  wire: { import: string; entry: string };
432
618
  requires: readonly string[];
433
619
  /** The adapter source copied beside it (ADR-0034 Amendment 3). Absent when none. */
434
620
  runtime?: RuntimeFileResult[];
621
+ /** The template modules copied beside it (ADR-0034 Amendment 8). Absent when none. */
622
+ templates?: TemplateFileResult[];
435
623
  }
436
624
 
437
625
  interface EjectPayload {
@@ -441,6 +629,22 @@ interface EjectPayload {
441
629
  config: { keys: string[] };
442
630
  /** FR-043 — libraries ejected in this call. Absent when none were. */
443
631
  libraries?: LibraryEjectResult[];
632
+ /** `meta eject docs-site`: the site's templates and assets. Absent when not asked for. */
633
+ docsSite?: DocsSiteFileResult[];
634
+ }
635
+
636
+ /** The text report for `meta eject docs-site`. */
637
+ function reportDocsSite(files: readonly DocsSiteFileResult[]): void {
638
+ const written = files.filter((f) => f.status !== "preserved").length;
639
+ log.info(
640
+ `Ejected the docs-site theme -> codegen/docs-site/: ${written} file(s) written, ` +
641
+ `${files.length - written} already there and kept (--force replaces them).`,
642
+ );
643
+ log.info(
644
+ " docsSiteFile() reads these in place of the bundled templates and assets; edit them to own " +
645
+ "the site's look. The site builder stays in the package. Wire the generator, if you have " +
646
+ "not: `docsSiteFile()` in `generators`, with a `docs` target.",
647
+ );
444
648
  }
445
649
 
446
650
  /** The text report for one ejected library. */
@@ -486,20 +690,14 @@ function reportLibrary(result: LibraryEjectResult): void {
486
690
  * already-copied half as "preserved" and the adopter cannot tell what happened.
487
691
  * Returns the unknown names, or an empty array.
488
692
  */
489
- /** Where a package-only generator's factory is imported from. The core package serves
490
- * its generator factories from the `/generators` subpath; the UI packages from the root. */
491
- function packageFactoryPath(name: string): string {
492
- const pkg = packageOf(name) ?? "";
493
- return pkg === CORE_CODEGEN_PACKAGE ? `${pkg}/generators` : pkg;
494
- }
495
693
 
496
694
  function unknownNames(names: readonly string[]): string[] {
497
- return names.filter((n) => resolveSource(n) === undefined && !isLibraryName(n));
695
+ return names.filter((n) => resolveSource(n) === undefined && !isLibraryName(n) && n !== DOCS_SITE_NAME);
498
696
  }
499
697
 
500
698
  /** Print the per-name text report — every branch below predates this command taking
501
699
  * more than one name, and each was written against a real incident. */
502
- function reportOne(result: EjectResult, name: string): void {
700
+ function reportOne(result: EjectResult, name: string, wiring: EjectWiring): void {
503
701
  if (result.status === "preserved") {
504
702
  // "already exists — left untouched" was the whole message, and it answered the
505
703
  // question nobody has. What an owner needs to know is whether their copy still
@@ -534,6 +732,7 @@ function reportOne(result: EjectResult, name: string): void {
534
732
  "See which, before deciding:",
535
733
  );
536
734
  log.info(` diff -u node_modules/${result.packageName}/src/reference/${name}.ts ${result.path}`);
735
+ log.info(" (A copy differs from the reference in its import lines by design: they point at your project.)");
537
736
  log.info(
538
737
  " To take upstream changes AND keep your customization, three-way merge them — " +
539
738
  "`git merge-file --diff3 <your copy> <the reference you ejected from> <the reference above>`. " +
@@ -556,32 +755,61 @@ function reportOne(result: EjectResult, name: string): void {
556
755
  log.info(`Ejected "${name}" -> ${result.path}. You own it now (ADR-0034 scaffold-and-own).`);
557
756
  }
558
757
 
559
- // REPLACE, never "paste". A generator reaches `generators: [...]` under ONE binding,
560
- // so a reader told to "paste" gets a duplicate identifier at best — and at worst
561
- // deletes nothing, keeps `formFile()` in the array bound to the PACKAGE import, and
562
- // silently runs the packaged generator while editing the ejected file. That failure
563
- // is invisible and is the exact one ejecting exists to prevent.
564
- //
565
- // But eject reads no config, so it cannot know WHICH of the three states this project
566
- // is in, and stating one of them as fact is wrong in the other two. Name the goal,
567
- // then the three branches; the reader knows which one they are looking at.
568
- log.info(`In metaobjects.config.ts, "${result.exportName}" must resolve to this file:`);
569
- log.info(` ${result.importLine}`);
570
- log.info(
571
- ` - If it is imported from "${result.packageName}", REPLACE that import with the ` +
572
- "line above. Adding a second one leaves `generators` bound to the PACKAGED " +
573
- "generator, and your edits to this file do nothing.",
574
- );
575
- log.info(
576
- " - If it is already imported from ./codegen/generators/, it points here already " +
577
- "— nothing to change.",
578
- );
579
- log.info(
580
- ` - If ${result.exportName}() is not in \`generators\` yet, add the import above ` +
581
- "AND the entry.",
582
- );
758
+ {
759
+ // REPLACE, never "paste". A generator reaches `generators: [...]` under ONE binding,
760
+ // so a reader told to "paste" gets a duplicate identifier at best — and at worst
761
+ // deletes nothing, keeps `formFile()` in the array bound to the PACKAGE import, and
762
+ // silently runs the packaged generator while editing the ejected file. That failure
763
+ // is invisible and is the exact one ejecting exists to prevent.
764
+ //
765
+ // But eject reads no config, so it cannot know WHICH of the three states this project
766
+ // is in, and stating one of them as fact is wrong in the other two. Name the goal,
767
+ // then the three branches; the reader knows which one they are looking at.
768
+ log.info(`In metaobjects.config.ts, "${result.exportName}" must resolve to this file:`);
769
+ log.info(` ${result.importLine}`);
770
+ log.info(
771
+ ` - If it is imported from "${factoryModuleOf(name) ?? result.packageName}", REPLACE that import with the ` +
772
+ "line above. Adding a second one leaves `generators` bound to the PACKAGED " +
773
+ "generator, and your edits to this file do nothing.",
774
+ );
775
+ log.info(
776
+ " - If it is already imported from ./codegen/generators/, it points here already " +
777
+ "— nothing to change.",
778
+ );
779
+ log.info(
780
+ ` - If ${wiring.entry} is not in \`generators\` yet, add the import above ` +
781
+ "AND the entry.",
782
+ );
783
+ }
583
784
  for (const line of result.dependencyNotes) log.info(line);
584
785
  if (result.runtime !== undefined && result.runtime.files.length > 0) reportRuntime(result.runtime.files);
786
+ if (result.templates !== undefined && result.templates.files.length > 0) reportTemplates(result.templates.files);
787
+ }
788
+
789
+ /** What the template copy handed over, and how the owned generator reaches it. */
790
+ function reportTemplates(files: readonly TemplateFileResult[]): void {
791
+ const written = files.filter((f) => f.status !== "preserved");
792
+ // A kept file that already matches the package is not news (ejecting `entity queries` copies
793
+ // the shared validators once, then finds them) — only a kept file that differs is.
794
+ const kept = files.filter((f) => f.status === "preserved" && f.comparison?.verdict !== "identical");
795
+ log.info(
796
+ `The code this generator calls to write its output is yours too: its template modules are in ` +
797
+ `${OWNED_TEMPLATES_DIR}/, not in the codegen package. What stays in the package is the engine ` +
798
+ "(the generator base, the model walks, the naming and type-mapping tables).",
799
+ );
800
+ if (written.length > 0) {
801
+ log.info(
802
+ ` Copied ${written.length} file(s) into ${OWNED_TEMPLATES_DIR}/ (from the installed package; only their import lines differ).`,
803
+ );
804
+ }
805
+ const keptDiffering = kept.filter((f) => f.comparison?.verdict === "differs");
806
+ if (kept.length > 0) {
807
+ log.info(
808
+ ` ${kept.length} file(s) were already there and were left untouched` +
809
+ (keptDiffering.length > 0 ? `, ${keptDiffering.length} of them differing from the package` : "") +
810
+ " (--force replaces them). `meta eject --list` compares each one.",
811
+ );
812
+ }
585
813
  }
586
814
 
587
815
  /** What the adapter copy handed over, and how the emitted code reaches it. */
@@ -624,7 +852,8 @@ export async function ejectCommand(
624
852
  }
625
853
 
626
854
  if (flags.list) {
627
- log.info(await listOutput(cwd));
855
+ if (fmt === "text") log.info(await listOutput(cwd));
856
+ else emitStructured(await listData(cwd), fmt);
628
857
  return 0;
629
858
  }
630
859
 
@@ -645,7 +874,7 @@ export async function ejectCommand(
645
874
  const unknown = unmatched.filter((n) => packageOf(n) === undefined);
646
875
  if (packageOnly.length > 0) {
647
876
  log.error(
648
- `package-only generator(s): ${packageOnly.map((n) => `${n} (${packageFactoryPath(n)})`).join(", ")}. ` +
877
+ `package-only generator(s): ${packageOnly.map((n) => `${n} (${factoryModuleOf(n) ?? ""})`).join(", ")}. ` +
649
878
  "No reference template ships for them, so they cannot be ejected: import the factory " +
650
879
  "from the package named beside each one and wire it in `generators: [...]`. " +
651
880
  "Nothing was ejected.",
@@ -665,11 +894,20 @@ export async function ejectCommand(
665
894
  const rows: EjectedRow[] = [];
666
895
  const libraries: LibraryEjectResult[] = [];
667
896
  const templatePackages = new Set<string>();
897
+ // Packages OUTSIDE @metaobjectsdev that the copied template modules import (a Markdown/TOON
898
+ // writer, say): read from the codegen package that ships them, never restated here.
899
+ const templateThirdParty = new Map<string, string | undefined>();
668
900
  // ADR-0034 Amendment 3: the generators whose adapter source was copied, and what it imports.
669
901
  const runtimeGenerators = new Set<string>();
670
902
  const runtimePackages = new Set<string>();
903
+ let docsSite: DocsSiteFileResult[] | undefined;
671
904
  try {
672
905
  for (const name of flags.names) {
906
+ if (name === DOCS_SITE_NAME) {
907
+ docsSite = await ejectDocsSite(cwd, flags.force);
908
+ if (fmt === "text") reportDocsSite(docsSite);
909
+ continue;
910
+ }
673
911
  if (isLibraryName(name)) {
674
912
  const lib = await ejectLibrary({ cwd, name, force: flags.force });
675
913
  libraries.push(lib);
@@ -678,19 +916,25 @@ export async function ejectCommand(
678
916
  }
679
917
  const result = await ejectGenerator({ cwd, name, force: flags.force });
680
918
  for (const p of result.templatePackages) templatePackages.add(p);
919
+ for (const p of closureOf(name).packages.filter((q) => !q.startsWith("@metaobjectsdev/"))) {
920
+ templateThirdParty.set(p, dependencyRangesOf(CORE_CODEGEN_PACKAGE)[p]);
921
+ }
681
922
  if (result.runtime !== undefined) {
682
923
  runtimeGenerators.add(name);
683
924
  for (const p of result.runtime.packages) runtimePackages.add(p);
684
925
  }
926
+ const catalogEntry = catalog[name];
927
+ const wiring: EjectWiring = { entry: result.entry };
685
928
  rows.push({
686
929
  name,
687
930
  path: result.path,
688
931
  status: result.status,
689
- wire: { import: result.importLine, entry: `${result.exportName}()` },
690
- requires: catalog[name]?.requires ?? [],
932
+ wire: { import: result.importLine, entry: wiring.entry },
933
+ requires: catalogEntry?.requires ?? [],
691
934
  ...(result.runtime !== undefined ? { runtime: result.runtime.files } : {}),
935
+ ...(result.templates !== undefined ? { templates: result.templates.files } : {}),
692
936
  });
693
- if (fmt === "text") reportOne(result, name);
937
+ if (fmt === "text") reportOne(result, name, wiring);
694
938
  }
695
939
  } catch (err) {
696
940
  log.error(describeError(err));
@@ -705,7 +949,7 @@ export async function ejectCommand(
705
949
  // names, so a summary built from the catalog alone printed a line that omitted a
706
950
  // package each per-file note had just asked for.
707
951
  const entries = flags.names
708
- .filter((n) => !isLibraryName(n))
952
+ .filter((n) => !isLibraryName(n) && n !== DOCS_SITE_NAME)
709
953
  .map((n) => catalog[n])
710
954
  .filter((e) => e !== undefined);
711
955
  // The copied adapter source imports its own dependencies (qs, the framework, the core
@@ -716,7 +960,7 @@ export async function ejectCommand(
716
960
  const { runtime, dev } = runtimeInstall([...runtimePackages]);
717
961
  ownedRuntime = { generators: runtimeGenerators, runtime, dev };
718
962
  }
719
- const install = installSetFor(entries, [...templatePackages], ownedRuntime);
963
+ const install = installSetFor(entries, [...templatePackages], ownedRuntime, templateThirdParty);
720
964
  const configKeys = [...new Set(entries.flatMap((e) => e.configKeys ?? []))].sort();
721
965
 
722
966
  if (fmt === "text") {
@@ -749,6 +993,7 @@ export async function ejectCommand(
749
993
  install,
750
994
  config: { keys: configKeys },
751
995
  ...(libraries.length > 0 ? { libraries } : {}),
996
+ ...(docsSite !== undefined ? { docsSite } : {}),
752
997
  };
753
998
  emitStructured(payload, fmt);
754
999
  }