@metaobjectsdev/cli 1.0.13 → 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,4 +1,4 @@
1
- import { relative } from "node:path";
1
+ import { dirname, join, relative } from "node:path";
2
2
  import { parseGenArgs } from "../lib/args.js";
3
3
  import { resolveGenConfig, resolveMigrateDefaults } from "../lib/config.js";
4
4
  import { genMigrateAdvice } from "../lib/gen-migrate-advice.js";
@@ -16,7 +16,7 @@ import {
16
16
  type AdvisoryFindingRow, type AdvisorySection,
17
17
  } from "../lib/advisory.js";
18
18
  import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
19
- import { runGen } from "@metaobjectsdev/codegen-ts";
19
+ import { declaredGeneratorNames, runGen, undeclaredGeneratorsMessage } from "@metaobjectsdev/codegen-ts";
20
20
  import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
21
21
  import { packageOfResolutionKey } from "@metaobjectsdev/metadata";
22
22
  import type { MetaRoot } from "@metaobjectsdev/metadata";
@@ -27,10 +27,13 @@ import {
27
27
  buildCatalogListing, renderCatalogText, wiredGeneratorNames, ownedGeneratorNames,
28
28
  declaredDepsOf,
29
29
  } from "../lib/catalog-listing.js";
30
- import { emitStructured } from "../lib/format.js";
30
+ import { emitStructured, narrate } from "../lib/format.js";
31
31
  import { composeCatalog } from "../lib/catalog.js";
32
32
  import { describeError } from "../lib/error-text.js";
33
33
  import { findRuntimeBoundaryCrossings, runtimeBoundaryWarnings } from "../lib/runtime-boundary-advisory.js";
34
+ import { hasLegacyDocsBlock, legacyDocsConfigMessage, NO_CONFIG_MESSAGE } from "../lib/docs-removed.js";
35
+ import { buildAgentSchemaInput } from "../lib/agent-schema.js";
36
+ import { existsSync } from "node:fs";
34
37
 
35
38
  /**
36
39
  * Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -127,13 +130,26 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
127
130
  // would find no manifest there and silently skip the nudge. `meta verify` makes
128
131
  // the same call for the same reason; the two commands describe this and the
129
132
  // anti-pattern scan below as one advisory pass, so they must scan one tree.
130
- warnIfAgentContextStale(projectRoot);
133
+ //
134
+ // EVERY warning this run prints is also collected here, in the order printed, because
135
+ // the structured result carries them. They used to reach stderr only — and on a pipe,
136
+ // where the default format is the document, a reader of that document saw a clean run
137
+ // with every caveat missing. A new warning site adds to this list.
138
+ const warnings: string[] = [];
139
+ const advise = (line: string | undefined): void => { if (line !== undefined) warnings.push(line); };
140
+ advise(warnIfAgentContextStale(projectRoot));
131
141
  // Advisory: the committed hash manifest is what makes hand-edit detection work on a
132
142
  // machine that did not generate the output. Silent unless it is ignored. Keyed on
133
143
  // projectRoot, not cwd, for the same reason its neighbour is — the manifest belongs to
134
144
  // whichever directory `resolveCollection` decided the metadata lives in.
135
- warnIfManifestIgnored(projectRoot);
145
+ advise(warnIfManifestIgnored(projectRoot));
136
146
 
147
+ // ADR-0034 Amendment 9: the config IS the list. With none there is nothing to generate,
148
+ // documentation included, and a run that wrote nothing silently would read as a pass.
149
+ if (!existsSync(join(projectRoot, "metaobjects.config.ts"))) {
150
+ log.error(NO_CONFIG_MESSAGE);
151
+ return 2;
152
+ }
137
153
  let forgeConfig;
138
154
  try {
139
155
  forgeConfig = await loadMetaobjectsConfig(projectRoot);
@@ -141,6 +157,28 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
141
157
  log.error(describeError(err));
142
158
  return 2;
143
159
  }
160
+ // A 1.0 `docs` block configured a command that no longer exists. Nothing reads it, so the
161
+ // run would write no documentation; it fails before writing anything instead.
162
+ if (hasLegacyDocsBlock(forgeConfig)) {
163
+ log.error(legacyDocsConfigMessage("meta gen"));
164
+ return 2;
165
+ }
166
+ // `--generators` narrows to DECLARED entries, by stable name; it never adds one.
167
+ const catalog = composeCatalog();
168
+ if (flags.generators !== undefined) {
169
+ let declared: string[];
170
+ try {
171
+ declared = declaredGeneratorNames(forgeConfig, catalog);
172
+ } catch (err) {
173
+ log.error(describeError(err));
174
+ return 2;
175
+ }
176
+ const unknown = flags.generators.filter((n) => !declared.includes(n));
177
+ if (unknown.length > 0) {
178
+ log.error(`meta gen: ${undeclaredGeneratorsMessage(unknown, declared)}`);
179
+ return 2;
180
+ }
181
+ }
144
182
 
145
183
  let metadata;
146
184
  const loadOptions = {
@@ -151,7 +189,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
151
189
  metadata = await loadMemory(genCollection.configDir, loadOptions);
152
190
  } catch (err) {
153
191
  reportLoadError(log, "failed to load metadata", err);
154
- return 2;
192
+ return 1;
155
193
  }
156
194
 
157
195
  // ADR-0023: gen loads leniently, so an unknown attribute (`isAbstrakt: true`, a
@@ -162,7 +200,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
162
200
  const unknownAttrs = unknownAttrFindings.length > 0
163
201
  ? [...unknownAttrFindings, UNKNOWN_ATTR_GEN_ADVICE]
164
202
  : [];
165
- for (const w of unknownAttrs) log.warn(`warning: ${w}`);
203
+ for (const w of unknownAttrs) { log.warn(`warning: ${w}`); warnings.push(w); }
166
204
 
167
205
  // FR-023 §11.1 item 2 — refuse a positional that names ONLY objects imported
168
206
  // from a dependency and excluded from output by the default exclusion rule.
@@ -185,7 +223,8 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
185
223
  projectRoot,
186
224
  // The COMPOSED catalog, so the requires / api-framework gates can see the react
187
225
  // and tanstack entries too. codegen-ts only knows its own slice.
188
- catalog: composeCatalog(),
226
+ catalog,
227
+ ...(flags.generators !== undefined && { generatorNames: flags.generators }),
189
228
  baseline: flags.baseline,
190
229
  // --dry-run must actually preview. This was previously passed only to the
191
230
  // display object below, so a "preview" run wrote every file.
@@ -199,6 +238,15 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
199
238
  // snapshot artifact), so sharedModelFile() defaults its `files` selection
200
239
  // to exactly what this project itself declares.
201
240
  sourceFiles: genCollection.ownFiles,
241
+ // The source DIRECTORIES, with each dependency artifact's, for a generator that loads
242
+ // the model with a loader of its own (the docs site).
243
+ sourceRoots: sourceRootsOf(genCollection),
244
+ // The physical schema `agent/schema.md` describes, built only if `agentDocsFile()` asks.
245
+ agentSchema: () => buildAgentSchemaInput(
246
+ metadata,
247
+ forgeConfig.dialect,
248
+ (m) => { const w = `[agent-docs] ${m}`; log.warn(w); warnings.push(w); },
249
+ ),
202
250
  // FR-043 §6 — the shipped-library selection. Always passed, including as `[]`:
203
251
  // that is what tells the post-selection audit "this project opted into none",
204
252
  // which is a different statement from a programmatic caller that never said.
@@ -210,20 +258,23 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
210
258
  return 1;
211
259
  }
212
260
 
213
- for (const w of result.warnings) { log.warn(w); }
261
+ for (const w of result.warnings) { log.warn(w); warnings.push(w); }
214
262
 
215
263
  // A first-run POINTER, not a warning: an empty selection is the designed state of a
216
264
  // fresh `meta init`, so calling it a problem would make every new project start with
217
265
  // one. It says what to do next and disappears the moment anything is wired.
218
266
  if ((forgeConfig.generators?.length ?? 0) === 0) {
219
- log.info(
267
+ // stdout only in text format: a structured run carries the same pointer in the
268
+ // document's `help`, and a sentence in front of the document breaks `| jq`.
269
+ narrate(fmt,
220
270
  "\nNothing is generated until you choose it — `generators: []` is what `meta init` " +
221
271
  "scaffolds, by design.\n" +
222
- " meta gen --list --probe the catalog, with how many files each generator " +
223
- "would emit for YOUR model\n" +
224
- " meta eject <name>... take the ones you want; it prints the import, the " +
225
- "entry to wire, and what to install\n" +
226
- "(`meta docs` needs none of this — documentation is on by default.)",
272
+ " meta gen --list --probe the catalog: the import that runs each generator as " +
273
+ "shipped, and how many files it would emit for YOUR model\n" +
274
+ " meta eject <name>... copy one into your repo to change what it emits; it " +
275
+ "prints the import to swap in, the entry to wire, and what to install\n" +
276
+ "Documentation is chosen the same way: docsFile(), apiDocsFile() and the rest, with a " +
277
+ "`docs` target.",
227
278
  );
228
279
  }
229
280
 
@@ -241,7 +292,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
241
292
  if (!cliConfig.dryRun) {
242
293
  for (const w of runtimeBoundaryWarnings(
243
294
  findRuntimeBoundaryCrossings(projectRoot, result.files.map((f) => f.path)),
244
- )) log.warn(`warning: ${w}`);
295
+ )) { log.warn(`warning: ${w}`); warnings.push(w); }
245
296
  }
246
297
 
247
298
  const targetDirs = Array.from(new Set(
@@ -263,9 +314,9 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
263
314
  metadata,
264
315
  dialect: forgeConfig.dialect,
265
316
  migrateDialect: migrateDefaults.dialect,
266
- columnNamingStrategy: forgeConfig.columnNamingStrategy,
267
317
  projectRoot,
268
318
  migrateOutDir: migrateDefaults.outDir,
319
+ migrateFormat: migrateDefaults.format,
269
320
  changedFiles: result.files.filter((f) => CHANGED_STATUSES.has(f.status)).map((f) => f.path),
270
321
  });
271
322
 
@@ -275,7 +326,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
275
326
  dialect: forgeConfig.dialect,
276
327
  dryRun: cliConfig.dryRun,
277
328
  // Text mode already printed these to stderr above; the structured forms carry them.
278
- warnings: fmt === "text" ? [] : unknownAttrs,
329
+ warnings: fmt === "text" ? [] : warnings,
279
330
  antiPatterns,
280
331
  generatorCount: forgeConfig.generators?.length ?? 0,
281
332
  ...(migrateAdvice !== undefined ? { migrateAdvice } : {}),
@@ -436,7 +487,7 @@ async function listCatalogCommand(
436
487
  });
437
488
  } catch (err) {
438
489
  reportLoadError(log, "failed to load metadata", err);
439
- return 2;
490
+ return 1;
440
491
  }
441
492
 
442
493
  opts = {
@@ -451,9 +502,21 @@ async function listCatalogCommand(
451
502
  // codegen config it was moved out of.
452
503
  libraries: genCollection.libraries,
453
504
  },
454
- // The collection's own files, exactly as `meta gen` supplies them below —
455
- // `shared-model` cannot be counted without them.
456
- probe: { metadata, scope: genCollection.inScope, sourceFiles: genCollection.ownFiles },
505
+ // The run inputs `meta gen` supplies below: `shared-model` cannot be counted without the
506
+ // collection's own files, `docs-site` without its source directories, and `agent-docs`
507
+ // leaves out its schema page without the schema. The schema's skip reasons are not
508
+ // printed: a count is what the probe reports.
509
+ probe: {
510
+ metadata,
511
+ scope: genCollection.inScope,
512
+ sourceFiles: genCollection.ownFiles,
513
+ sourceRoots: sourceRootsOf(genCollection),
514
+ agentSchema: () => buildAgentSchemaInput(
515
+ metadata,
516
+ forgeConfig.dialect,
517
+ () => {},
518
+ ),
519
+ },
457
520
  };
458
521
  } else {
459
522
  // No probe: still report `wired` / `owned` when a project happens to be here, since
@@ -483,3 +546,12 @@ async function listCatalogCommand(
483
546
  else emitStructured(rows, fmt);
484
547
  return 0;
485
548
  }
549
+
550
+ /**
551
+ * The collection's metadata source directories, followed by the directory of each dependency's
552
+ * artifact: a loader that reads whole directories (the docs site's) needs both, or an
553
+ * `extends` onto an imported abstract would not resolve in it.
554
+ */
555
+ export function sourceRootsOf(collection: Collection): string[] {
556
+ return [...collection.sourceRoots, ...new Set(collection.dependencies.map((d) => dirname(d.artifactPath)))];
557
+ }
@@ -13,9 +13,9 @@
13
13
  import { existsSync } from "node:fs";
14
14
  import { mkdir, readFile, writeFile } from "node:fs/promises";
15
15
  import { join } from "node:path";
16
- import { parseArgs } from "node:util";
17
16
  import { catalogEntry } from "../lib/catalog.js";
18
17
  import { log } from "../lib/log.js";
18
+ import { parseCommandArgs } from "../lib/strict-args.js";
19
19
  import { dependencyNotesForTemplate } from "./eject.js";
20
20
  import {
21
21
  CONFIG_FILE,
@@ -333,7 +333,7 @@ export async function generatorCommand(args: string[], cwd: string): Promise<num
333
333
  let values: { scope?: string; force?: boolean; "no-wire"?: boolean };
334
334
  let positionals: string[];
335
335
  try {
336
- ({ values, positionals } = parseArgs({ args, options: GENERATOR_OPTIONS, strict: true, allowPositionals: true }));
336
+ ({ values, positionals } = parseCommandArgs("generator", { args, options: GENERATOR_OPTIONS, strict: true, allowPositionals: true }));
337
337
  } catch (err) {
338
338
  log.error(describeError(err));
339
339
  return 2;
@@ -11,13 +11,17 @@ import { assertKnownStackValues, resolveStack } from "../lib/detect-stack.js";
11
11
  import { reportIgnoredScaffold } from "../lib/ignored-scaffold-check.js";
12
12
  import { parseInitArgs } from "../lib/args.js";
13
13
  import { log } from "../lib/log.js";
14
- import { cliVersion } from "../lib/version.js";
14
+ import { cliVersion, isPrereleaseVersion } from "../lib/version.js";
15
+ import { currentContentHash } from "../lib/agent-context-staleness.js";
15
16
  import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
16
17
  import { DEFAULT_DOCS_DIR } from "@metaobjectsdev/codegen-ts";
17
18
  import { describeError } from "../lib/error-text.js";
19
+ import { SUBTYPE_ROOT, TYPE_METADATA, TYPE_SUBTYPE_SEPARATOR } from "@metaobjectsdev/metadata/constants";
20
+ import { docsRefForVersion } from "@metaobjectsdev/metadata";
18
21
 
19
- // ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
20
- // the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
22
+ // ADR-0034 scaffold-and-own — where `meta eject` and `meta generator new` put a generator
23
+ // the consumer OWNS. `meta init` scaffolds it empty; metaobjects.config.ts imports from it
24
+ // locally, or imports a stock generator from its package (Amendment 5).
21
25
  const OWNED_GENERATORS_DIR = "codegen/generators";
22
26
 
23
27
 
@@ -31,13 +35,10 @@ const OWNED_GENERATORS_DIR = "codegen/generators";
31
35
  // `meta gen --list` and by `meta eject routes`, to the adopter who actually chose it.
32
36
  const SCAFFOLD_OUT_DIR = "src/generated";
33
37
 
38
+ // The canonical form of an empty root, byte-for-byte what `meta fmt` writes, so a
39
+ // freshly scaffolded project passes `meta fmt --check` (it used to fail it at once).
34
40
  const META_COMMON_JSON = JSON.stringify(
35
- {
36
- metadata: {
37
- package: "",
38
- children: [] as unknown[],
39
- },
40
- },
41
+ { [`${TYPE_METADATA}${TYPE_SUBTYPE_SEPARATOR}${SUBTYPE_ROOT}`]: {} },
41
42
  null,
42
43
  2,
43
44
  ) + "\n";
@@ -107,33 +108,37 @@ export default defineConfig({
107
108
  // would emit, so you can see what your metadata is already asking for. Group by
108
109
  // \`layer\`: model / persistence / api / client / docs / capability.
109
110
  //
110
- // Then take the ones you want:
111
+ // Every row carries the import line that runs its generator as shipped
112
+ // (\`wire.import\`). Add it at the top of this file:
113
+ //
114
+ // import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators";
115
+ //
116
+ // then call each one in the array below: entityFile(), queriesFile(), barrel(). A
117
+ // generator's options are in its documentation. The row also lists what to install
118
+ // and the config keys the generator reads (\`dbImport\`, \`apiPrefix\`, \`extStyle\`, …).
119
+ // Add them here as you go.
120
+ //
121
+ // Need one to emit something different? Own it:
122
+ //
123
+ // meta eject entity
111
124
  //
112
- // meta eject entity queries routes barrel
125
+ // copies that generator into ./codegen/generators/ — YOURS to edit — and prints the
126
+ // local import to swap in for the package one. \`meta gen\` then runs your copy.
113
127
  //
114
- // That copies each generator into ./codegen/generators/ — YOURS to edit, and what
115
- // \`meta gen\` runs — and prints the import line to add here, the entry to add below,
116
- // what to install, and any config keys those generators read (\`dbImport\`,
117
- // \`apiPrefix\`, \`extStyle\`, …). Add them here as you go.
128
+ // Documentation is chosen the same way: the \`docs\` layer of the catalog (docsFile(),
129
+ // apiDocsFile(), …) writes to a \`docs\` target you declare beside \`generators\`:
130
+ //
131
+ // targets: { docs: { outDir: "${DEFAULT_DOCS_DIR}" } },
132
+ //
133
+ // Nothing is generated that this file does not declare, documentation included.
118
134
  //
119
135
  // Need an output nothing in that catalog emits — OpenAPI, JSON Schema, a client, a
120
- // service layer, docs? Write your own generator; that is the normal path:
136
+ // service layer? Write your own generator; that is the normal path:
121
137
  //
122
138
  // meta generator new openapi --scope model
123
139
  //
124
140
  // writes a working, commented generator into ./codegen/generators/ and adds it here.
125
141
  generators: [],
126
-
127
- docs: {
128
- outDir: "${DEFAULT_DOCS_DIR}", // every surface lands here (run: meta docs).
129
- // A SUB-directory on purpose: these pages are regenerated
130
- // and overwritten, and docs/ itself is usually yours.
131
- layout: "flat", // or "package" for multi-package models
132
- // surfaces defaults to ["model", "api", "requirements", "agent"] — all four.
133
- // Documentation is the one thing that IS on by default, because the requirements
134
- // and agent surfaces emit ZERO files for a project with nothing to describe, and
135
- // the always-on agent context points at the agent/ pages by name.
136
- },
137
142
  });
138
143
  `;
139
144
  }
@@ -150,14 +155,16 @@ Next steps:
150
155
  1. Author entities under metaobjects/ (start from the scaffolded meta.common.json)
151
156
  2. meta gen --list --probe # the catalog: every generator, grouped by layer, with
152
157
  # how many files each would emit for YOUR model
153
- 3. meta eject <name>... # take the ones you want — copies them into
158
+ 3. Wire the ones you want in metaobjects.config.ts: each catalog row prints the
159
+ import that runs it as shipped.
160
+ meta eject <name>... # need to change what one emits? copies it into
154
161
  # codegen/generators/ (yours to edit) and prints the
155
- # import, the entry to wire, and what to install
162
+ # import to swap in and what to install
156
163
  meta generator new <name> # an output no reference emits? write your own — this
157
164
  # scaffolds a working one and wires it
158
- 4. meta gen # generate from exactly what you wired
159
- 5. meta docs # neutral model + API docs (on by default)
160
- 6. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
165
+ 4. meta gen # generate from exactly what you wired, documentation
166
+ # included (the docs layer of the catalog)
167
+ 5. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
161
168
  A new database later (a fresh clone, CI, tests) replays the committed migrations:
162
169
  meta migrate apply-pending --db file:other.sqlite
163
170
  `;
@@ -205,6 +212,13 @@ export interface InitResult {
205
212
  * just scaffolded over your work.
206
213
  */
207
214
  refreshedDocsOnly?: boolean;
215
+ /**
216
+ * True when the project was already initialized (`metaobjects/` or `.metaobjects/`
217
+ * exists) and the run was neither `--force` nor `--refresh-docs`: nothing was written.
218
+ * A re-run asks for a state that already holds, so it is a no-op that exits 0, not
219
+ * the error it used to be.
220
+ */
221
+ alreadyInitialized?: boolean;
208
222
  }
209
223
 
210
224
  async function readManifest(cwd: string): Promise<Manifest | undefined> {
@@ -299,17 +313,31 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise
299
313
  warnIfMonorepoSubdir(opts, result);
300
314
  const prior = await readManifest(opts.cwd);
301
315
  const stack = await stackForAgentContext(opts, prior);
302
- let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack });
316
+ let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack, docsRef: docsRefForVersion(cliVersion()) });
303
317
  if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/"));
304
318
 
319
+ const generatedBy = cliVersion();
320
+ const contentHash = currentContentHash().currentContentHash;
305
321
  const decision = planScaffold({
306
322
  stack, assembled, prior,
307
323
  readCurrent: (rel) => {
308
324
  const abs = join(opts.cwd, rel);
309
325
  return existsSyncWrap(abs) ? readFileSyncWrap(abs, "utf8") : undefined;
310
326
  },
311
- generatedBy: cliVersion(),
327
+ generatedBy,
328
+ ...(contentHash !== undefined && { contentHash }),
312
329
  });
330
+ // A pre-release CLI stamps its own pre-release version into the committed
331
+ // .metaobjects/.agent-context.json ("generatedBy") and the skills/docs it scaffolds may
332
+ // differ from the eventual release's. tools/prerelease/detect-prerelease-pins.sh now
333
+ // flags that on the tree side; this is the other half — say so at the moment it is
334
+ // written, not only when someone later runs the detector.
335
+ if (isPrereleaseVersion(generatedBy)) {
336
+ result.warnings.push(
337
+ `agent context generated by pre-release CLI ${generatedBy} — refresh it again with ` +
338
+ `the release CLI before merging ('meta init --refresh-docs --docs-only').`,
339
+ );
340
+ }
313
341
 
314
342
  // --force: overwrite hand-edited docs in place rather than parking the fresh copy
315
343
  // at <path>.new (issue #163 — a forced (re)scaffold means "I mean it"; applies to
@@ -585,9 +613,10 @@ export async function init(opts: InitOptions): Promise<InitResult> {
585
613
  }
586
614
 
587
615
  if (exists && !opts.force && !opts.refreshDocs) {
588
- throw new Error(
589
- "metaobjects/ or .metaobjects/ already exists; use --force to overwrite scaffold files (existing records are preserved), or --refresh-docs to update only agent docs",
590
- );
616
+ if (metaobjectsExists) result.preserved.push(DEFAULT_METADATA_DIR);
617
+ if (agentDirExists) result.preserved.push(DEFAULT_METAOBJECTS_DIR);
618
+ result.alreadyInitialized = true;
619
+ return result;
591
620
  }
592
621
 
593
622
  const dirs = [
@@ -603,7 +632,10 @@ export async function init(opts: InitOptions): Promise<InitResult> {
603
632
  ".metaobjects/config.json",
604
633
  ".metaobjects/.gitignore",
605
634
  );
606
- result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
635
+ // The agent context forecasts itself: writeAgentContext is dry-run aware and
636
+ // reports exactly the paths a real run writes — the root CLAUDE.md/AGENTS.md wiring
637
+ // included, which a hand-kept list here used to leave out.
638
+ await writeAgentContext(opts, result);
607
639
  result.created.push(OWNED_GENERATORS_DIR, CODEGEN_TSCONFIG_REL);
608
640
  result.created.push("metaobjects.config.ts", ".gitignore");
609
641
  return result;
@@ -884,9 +916,19 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
884
916
  configOnly: flags.configOnly,
885
917
  });
886
918
 
919
+ if (result.alreadyInitialized) {
920
+ log.info(
921
+ `meta init: already initialized (${result.preserved.join(" and ")} exist) — nothing written (no-op).\n` +
922
+ " meta init --refresh-docs update the agent docs after a CLI upgrade\n" +
923
+ " meta init --force re-scaffold the project files (your metadata is preserved)",
924
+ );
925
+ return 0;
926
+ }
927
+
887
928
  if (flags.printOnly) {
888
929
  log.info("Would create:");
889
930
  for (const path of result.created) log.info(` ${path}`);
931
+ for (const w of result.warnings) log.info(` ${w}`);
890
932
  return 0;
891
933
  }
892
934