@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,839 +1,15 @@
1
- // `meta docs [<project-root>] --out <dir>` — STANDALONE neutral metadata docs.
1
+ // `meta docs` was removed in 1.1.0 (ADR-0034 Amendment 9). Documentation is written by the
2
+ // generators a project lists in `generators`, which `meta gen` runs and `meta verify
3
+ // --codegen` checks: `docsFile()`, `apiDocsFile()`, `requirementsFile()`, `agentDocsFile()`,
4
+ // `metamodelDocsFile()` and `docsSiteFile()`. `--scaffold-site` is `meta eject docs-site`.
2
5
  //
3
- // Emits one neutral page per entity (`<Entity>.md`) and one per
4
- // `template.output` (`<Template>.md`) from metadata ALONE — no gen config, no
5
- // codegen pipeline. It is the Tier-2 delivery (like `migrate`): runnable from
6
- // the compiled `meta` binary.
7
- //
8
- // DRY: this command does NOT re-walk objects/templates. It builds the same
9
- // GenContext the codegen runner builds for the `docsFile()` generator and
10
- // calls that generator, then writes the returned files. The neutrality of the
11
- // output is therefore guaranteed — it is byte-for-byte the same generator the
12
- // `meta gen` pipeline runs (gated by the docs conformance fixture).
13
- import { resolve as resolvePath, basename, dirname } from "node:path";
14
- import { mkdir, writeFile } from "node:fs/promises";
6
+ // The command stays only to say so, exiting 2, because "unknown command" names neither the
7
+ // replacement nor the migration section. `meta agent-docs` (the agent context) is a different
8
+ // command and is unchanged.
15
9
  import { log } from "../lib/log.js";
16
- import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
17
- import { collectionLoadOptions } from "../lib/collection-load-options.js";
18
- import { loadMemory, resolveCollection, resolveConfigDir } from "@metaobjectsdev/sdk";
19
- import { existsSync } from "node:fs";
20
- import { join } from "node:path";
21
- import { makeRenderContext, buildPkMap, buildRelationMap, buildProjectionViews, resolveDocsConfig, apiLabel, } from "@metaobjectsdev/codegen-ts";
22
- import { docsFile, apiDocsFile, requirementsFile, agentDocsFile } from "@metaobjectsdev/codegen-ts/generators";
23
- import { composeRegistry, coreProviders, DEFAULT_COLUMN_NAMING_STRATEGY, renderCoreMetamodelDocs, } from "@metaobjectsdev/metadata";
24
- import { dbEmittingObjects, DEFAULT_DIALECT, DEFAULT_DOCS_DIR, missingDialectMessage, resolveGenerators, runEmitsHonoRoutes, runEmitsUiTier } from "@metaobjectsdev/codegen-ts";
25
- import { generateSite, SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } from "@metaobjectsdev/docs-site";
26
- // The `agent` schema surface takes the physical schema as an ARGUMENT with its resolvers
27
- // injected — codegen-ts deliberately owns none of it (see agent-schema-input.ts). THIS is
28
- // where the two packages meet: `meta docs` already depends on both.
29
- import { buildExpectedSchemaWithProvenance, columnTypeSql, qualifiedDbName, } from "@metaobjectsdev/migrate-ts";
30
- import { reportLoadError } from "../lib/load-error.js";
31
- import { describeError } from "../lib/error-text.js";
32
- function parseLayout(v, flag) {
33
- if (v === undefined)
34
- throw new Error(`${flag} requires flat|package`);
35
- if (v !== "flat" && v !== "package") {
36
- throw new Error(`${flag} must be "flat" or "package" (got "${v}")`);
37
- }
38
- return v;
39
- }
40
- function parseDocsArgs(argv, cwd) {
41
- let projectRoot;
42
- let out;
43
- let templates;
44
- let prompts;
45
- let layout;
46
- let baseUrl;
47
- let wantModel = false;
48
- let wantApi = false;
49
- let wantRequirements = false;
50
- let wantAgent = false;
51
- let wantMetamodel = false;
52
- let wantSite = false;
53
- let wantScaffoldSite = false;
54
- let outProvided = false;
55
- let layoutProvided = false;
56
- for (let i = 0; i < argv.length; i++) {
57
- const a = argv[i];
58
- if (a === "--out" || a === "-o") {
59
- const v = argv[++i];
60
- if (v === undefined)
61
- throw new Error(`${a} requires a directory argument`);
62
- out = v;
63
- outProvided = true;
64
- }
65
- else if (a.startsWith("--out=")) {
66
- out = a.slice("--out=".length);
67
- outProvided = true;
68
- }
69
- else if (a === "--layout") {
70
- layout = parseLayout(argv[++i], a);
71
- layoutProvided = true;
72
- }
73
- else if (a.startsWith("--layout=")) {
74
- layout = parseLayout(a.slice("--layout=".length), "--layout");
75
- layoutProvided = true;
76
- }
77
- else if (a === "--model") {
78
- wantModel = true;
79
- }
80
- else if (a === "--api") {
81
- wantApi = true;
82
- }
83
- else if (a === "--requirements") {
84
- wantRequirements = true;
85
- }
86
- else if (a === "--agent") {
87
- wantAgent = true;
88
- }
89
- else if (a === "--metamodel") {
90
- wantMetamodel = true;
91
- }
92
- else if (a === "--site") {
93
- wantSite = true;
94
- }
95
- else if (a === "--scaffold-site") {
96
- wantScaffoldSite = true;
97
- }
98
- else if (a === "--base-url") {
99
- const v = argv[++i];
100
- if (v === undefined)
101
- throw new Error(`${a} requires a URL argument`);
102
- baseUrl = v;
103
- }
104
- else if (a.startsWith("--base-url=")) {
105
- baseUrl = a.slice("--base-url=".length);
106
- }
107
- else if (a === "--templates") {
108
- const v = argv[++i];
109
- if (v === undefined)
110
- throw new Error(`${a} requires a directory argument`);
111
- templates = v;
112
- }
113
- else if (a.startsWith("--templates=")) {
114
- templates = a.slice("--templates=".length);
115
- }
116
- else if (a === "--prompts") {
117
- const v = argv[++i];
118
- if (v === undefined)
119
- throw new Error(`${a} requires a directory argument`);
120
- prompts = v;
121
- }
122
- else if (a.startsWith("--prompts=")) {
123
- prompts = a.slice("--prompts=".length);
124
- }
125
- else if (a.startsWith("-")) {
126
- throw new Error(`unknown flag: ${a}`);
127
- }
128
- else if (projectRoot === undefined) {
129
- projectRoot = a;
130
- }
131
- else {
132
- throw new Error(`unexpected argument: ${a}`);
133
- }
134
- }
135
- // --model and/or --api narrow the MARKDOWN surfaces; both flags (or neither)
136
- // leave the surfaces unset so the resolved `docs:` config decides (default
137
- // both). `--site` is a separate (additive) surface: when it is the ONLY
138
- // surface flag we pass an explicit EMPTY markdown surface list so markdown
139
- // emission is suppressed (resolveDocsConfig honors `[]` via `??`); combined
140
- // with --model/--api the requested markdown surfaces still emit alongside it.
141
- const surfaces = [];
142
- if (wantModel)
143
- surfaces.push("model");
144
- if (wantApi)
145
- surfaces.push("api");
146
- if (wantRequirements)
147
- surfaces.push("requirements");
148
- if (wantAgent)
149
- surfaces.push("agent");
150
- return {
151
- // `<project-root>` is the project root that CONTAINS the metadata; default
152
- // cwd (mirrors how migrate/gen treat the working directory as the root).
153
- projectRoot: projectRoot ?? cwd,
154
- projectRootProvided: projectRoot !== undefined,
155
- // Default out dir, resolved against the project root below. ONE spelling of the
156
- // default, shared with resolveDocsConfig — two spellings is how a flag default and
157
- // a config default come to disagree about where the pages are.
158
- //
159
- // --metamodel keeps its own sub-path: the renderer writes under <out>/metamodel/,
160
- // and it is a different artifact (the metamodel reference, not this project's
161
- // pages), so it is not swept into the generated-docs directory.
162
- out: out ?? (wantMetamodel ? "./docs/metamodel" : DEFAULT_DOCS_DIR),
163
- // Default flat preserves today's single-package output (+ existing goldens).
164
- layout: layout ?? "flat",
165
- metamodel: wantMetamodel,
166
- site: wantSite,
167
- scaffoldSite: wantScaffoldSite,
168
- outProvided,
169
- layoutProvided,
170
- ...(surfaces.length > 0 || wantSite ? { surfaces } : {}),
171
- ...(baseUrl !== undefined ? { baseUrl } : {}),
172
- ...(templates !== undefined ? { templates } : {}),
173
- ...(prompts !== undefined ? { prompts } : {}),
174
- };
175
- }
176
- /**
177
- * The physical schema `agent/schema.md` renders from, built by the package that OWNS it
178
- * (`migrate-ts`) with its own resolvers handed across — see codegen-ts's
179
- * agent-schema-input.ts for why the docs generator refuses to compute any of this itself.
180
- *
181
- * Returns undefined, after warning, when the page is to be SKIPPED — and a skipped page is
182
- * not a silent pass: `verify --docs` convicts a committed `agent/<page>.md` that a fresh
183
- * run no longer emits (see lib/docs-drift.ts), so a page describing the previous schema
184
- * fails the gate rather than surviving it on exactly the change it most needs to flag.
185
- */
186
- function buildAgentSchemaInput(root, configured, strategy) {
187
- // THE RUNNER'S OWN GUARD DECIDES THIS, not a default applied here.
188
- //
189
- // `DEFAULT_DIALECT` is INERT: `runGen` throws when a model emits database code and the
190
- // config declares no dialect, and it throws BEFORE `normalizeConfig` fills that default
191
- // in, precisely so a DB project that forgot one gets a named error instead of
192
- // "a Postgres project quietly emitting sqlite". So a persisted model with no dialect is
193
- // not a sqlite project — it is a project `meta gen` REFUSES. Documenting it as sqlite
194
- // would state an answer the toolchain never gave, about a schema it will not build.
195
- //
196
- // Two wrong answers were tried here before this one, and both were ASSERTIONS. First a
197
- // hardcoded `?? "sqlite"`; then a skip whenever `dialect` was absent, on the theory that
198
- // an undeclared dialect is an unknown one. The compute answer is to ask the predicate
199
- // `runGen` asks — `dbEmittingObjects` — and skip only when that guard would fire. A model
200
- // with no persisted object needs no dialect and renders an empty schema page anyway, so
201
- // the inert default is correct for exactly the projects the guard lets through.
202
- const dbEmitting = dbEmittingObjects(root.objects());
203
- if (dbEmitting.length > 0 && configured === undefined) {
204
- log.warn(`docs: agent/schema.md skipped — ${missingDialectMessage(dbEmitting)} ` +
205
- `('meta gen' refuses this model for the same reason.)`);
206
- return undefined;
207
- }
208
- // MySQL: MetaObjects does not own the schema, so there is no migrate-built schema to
209
- // describe. The generated Drizzle tables are the description.
210
- if (configured === "mysql") {
211
- log.warn("docs: agent/schema.md skipped — MetaObjects does not own a MySQL schema (the generated Drizzle tables describe it).");
212
- return undefined;
213
- }
214
- const dialect = configured ?? DEFAULT_DIALECT;
215
- try {
216
- const built = buildExpectedSchemaWithProvenance(root, {
217
- dialect,
218
- columnNamingStrategy: strategy,
219
- // Views come from codegen-ts (migrate-ts never generates view DDL), exactly as
220
- // `verify --db` threads them.
221
- views: buildProjectionViews(root, { dialect, columnNamingStrategy: strategy }),
222
- });
223
- return {
224
- dialect,
225
- tables: built.snapshot.tables,
226
- views: built.snapshot.views,
227
- provenance: built.provenance,
228
- // The structural `SchemaColumnLike` is migrate-ts's own ColumnDescriptor,
229
- // narrowed to what the page reads; the cast hands the full descriptor back to
230
- // the renderer that produced it.
231
- columnType: (c) => columnTypeSql(c, dialect),
232
- qualify: qualifiedDbName,
233
- };
234
- }
235
- catch (err) {
236
- // A model the schema builder refuses is a real condition (a primary-key move, a
237
- // duplicate physical name) that `meta migrate` will report properly. Docs must not
238
- // be the command that fails on it, so the schema page is skipped and the other two
239
- // agent pages still emit.
240
- log.warn(`docs: agent/schema.md skipped — the expected schema could not be built ` +
241
- `(${describeError(err)}). Run 'meta migrate' for the full diagnosis.`);
242
- return undefined;
243
- }
244
- }
245
- export async function docsCommand(args, cwd, opts) {
246
- // Informational output only. `log.warn` / `log.error` are deliberately NOT routed
247
- // through this: a skipped surface or a failed render is a finding either way.
248
- const info = opts?.silent === true ? (_m) => { } : log.info;
249
- let flags;
250
- try {
251
- flags = parseDocsArgs(args, cwd);
252
- }
253
- catch (err) {
254
- log.error(`docs: ${describeError(err)}`);
255
- return 2;
256
- }
257
- // FR-033 S3 — `--metamodel`: document the BUILT-IN metamodel (type/subtype/
258
- // attr vocabulary) from the strict registry. Unlike --model/--api this needs
259
- // NEITHER a user's metadata NOR a config — there is nothing to load. It writes
260
- // the renderer's files under <out>/metamodel/ (default ./docs/metamodel).
261
- if (flags.metamodel) {
262
- // `--site` builds HTML from a MODEL — docs-site's own loader and templates over the
263
- // user's metadata. The metamodel surface is a different renderer entirely, over the
264
- // registry, and it emits markdown; there is no renderer here to bridge them. The flag
265
- // used to be parsed, accepted and then dropped by this very return: the command wrote
266
- // sixteen markdown files, printed a success line and exited 0, so asking for a site
267
- // produced no site and no complaint.
268
- //
269
- // It refuses rather than growing an HTML renderer, which would put a markdown-
270
- // rendering dependency into a published package for one surface. The website renders
271
- // it instead, keeping that dependency dev-only and giving the pages the metaobjects.dev
272
- // look rather than the docs-site adopter theme.
273
- // `--scaffold-site` is here for the same reason and was missed the first time: it is
274
- // the OTHER flag that asks for files to be written, and this same early return dropped
275
- // it identically — `--metamodel --scaffold-site` wrote 16 markdown pages, scaffolded no
276
- // theme anywhere, and exited 0. Fixing one of a matched pair leaves the defect wearing
277
- // a different flag name.
278
- const dropped = [
279
- ...(flags.site ? ["--site"] : []),
280
- ...(flags.scaffoldSite ? ["--scaffold-site"] : []),
281
- ];
282
- if (dropped.length > 0) {
283
- log.error(`docs: ${dropped.join(" and ")} ${dropped.length > 1 ? "are" : "is"} not supported ` +
284
- "with --metamodel. The metamodel reference is markdown; the rendered form is " +
285
- "published at https://metaobjects.dev/reference");
286
- return 2;
287
- }
288
- return metamodelDocsCommand(cwd, flags.out);
289
- }
290
- const metaRoot = resolvePath(cwd, flags.projectRoot);
291
- // Absolute prompt-source dir for the site (--prompts), for a project whose
292
- // templates live outside metaobjects/ or templates/ (e.g. data/templates/).
293
- const promptsDir = flags.prompts !== undefined ? resolvePath(cwd, flags.prompts) : undefined;
294
- // `--scaffold-site`: copy the docs-site templates + assets into codegen/docs-site/
295
- // so the consumer owns them (ADR-0034 scaffold-and-own). Scaffold and return —
296
- // it does not also generate. `resolveConfigDir` rather than `resolveCollection`:
297
- // scaffolding needs no metadata, but it must write where `emitSite` will READ
298
- // (below, under the resolved project root), and that is the same walk.
299
- if (flags.scaffoldSite) {
300
- // `resolveGenConfigDir` over the bare walk: `emitSite` reads the owned theme
301
- // from the directory holding `metaobjects.config.ts`, so scaffolding must write
302
- // to the same one or the theme is scaffolded where nothing will ever read it.
303
- return scaffoldSiteCommand(resolveGenConfigDir(metaRoot, await resolveConfigDir(metaRoot)));
304
- }
305
- // Discovery and load are two separate failure modes, kept in separate try
306
- // blocks deliberately — same reasoning as `meta gen` (gen.ts): a broad
307
- // catch around both would swallow a genuine ParseError as "no metaobjects/
308
- // found", masking the real failure.
309
- //
310
- // Discovery runs BEFORE the config read, deliberately, and `meta gen` calls
311
- // out the same ordering as the thing it fixed: the project root is whichever
312
- // directory `resolveCollection` decided the metadata belongs to, so
313
- // everything project-relative — `metaobjects.config.ts` and its providers,
314
- // the `docs.outDir` it names, the adopter `templates/` overrides, the owned
315
- // `codegen/docs-site/` theme — has to come from that same directory. Reading
316
- // the config from the ambient `<project-root>` argument while the metadata came
317
- // from an ancestor renders the ancestor's model with the subdirectory's
318
- // (absent) providers. For a run at the project root the two are the same path.
319
- //
320
- // An EXPLICIT `<project-root>` positional pins the collection rather than seeding a
321
- // walk (#327). The argument has always meant "document this": before sources were
322
- // resolvable it read `<path>/metaobjects/` and nothing else, and passing it to
323
- // discovery turned it into a starting point, so the nearest ancestor
324
- // `.metaobjects/config.json` was found and ITS sources were unioned in — a command
325
- // whose entire purpose is documenting one subset silently documenting the whole
326
- // repo, at exit 0. Pinned, `<path>` governs: its own config if it has one, the
327
- // default `<path>/metaobjects` if not. A bare `meta docs` still discovers, which is
328
- // the right default for "document the project I am standing in".
329
- let collection;
330
- try {
331
- collection = await resolveCollection(metaRoot, flags.projectRootProvided ? { explicitDir: metaRoot } : undefined);
332
- }
333
- catch (err) {
334
- log.error(`docs: ${describeError(err)}`);
335
- return 2;
336
- }
337
- // Which directory's `metaobjects.config.ts` governs is its OWN nearest-ancestor
338
- // walk, not the collection's: the two files answer different questions (design
339
- // §4.6), and in a Maven- or pip-rooted monorepo the collection is declared at the
340
- // repo root while the TS config sits in the app. Reading the second from the
341
- // first made docs silently drop that app's providers and skip its api surface
342
- // entirely (#326). Everything that config names follows it — the `docs.outDir` it
343
- // carries, the adopter `templates/` overrides and the owned `codegen/docs-site/`
344
- // theme beside it. Same directory whenever the two files sit together, which is
345
- // every `meta init` project.
346
- const genConfigDir = resolveGenConfigDir(metaRoot, collection.configDir);
347
- // The project root used to resolve adopter `templates/` overrides; the
348
- // framework defaults sit underneath via projectProvider's chain. `--templates`
349
- // is the one explicit override.
350
- const projectRoot = flags.templates !== undefined
351
- ? resolvePath(cwd, flags.templates)
352
- : genConfigDir;
353
- // Best-effort load of metaobjects.config.ts to pick up consumer-supplied
354
- // providers (e.g. a project's custom field/object subtypes). Unlike `gen`,
355
- // docs does NOT require a config — the Tier-2 "metadata alone" promise must
356
- // hold for config-less projects. If the config is absent or invalid, fall
357
- // back to defaults; the loader still surfaces a stable unknown-subtype error
358
- // if the metadata genuinely uses an unregistered type.
359
- let loadedConfig;
360
- let configProviders;
361
- // The same config's contributions to the metadata LOAD — providers plus the shipped
362
- // `libraries` a project opts into (#333). `configProviders` stays separate because the
363
- // site surface has its own loader and takes providers alone.
364
- let configLoadOptions = {};
365
- // hasConfig gates the api surface: api docs describe the GENERATED REST
366
- // surface, which only exists when there is a (loadable) gen config. A config
367
- // that EXISTS but fails to load degrades to model-only with a warning.
368
- const hasConfig = existsSync(join(genConfigDir, "metaobjects.config.ts"));
369
- // Only attempt the load when the file is actually present: absence is the
370
- // expected config-less case (stay silent), but a config that EXISTS yet fails
371
- // to load is surfaced as a warning rather than silently degrading to
372
- // provider-less docs — otherwise a custom-type project would later fail with a
373
- // cryptic unknown-subtype error instead of the real config error.
374
- if (hasConfig) {
375
- try {
376
- loadedConfig = await loadMetaobjectsConfig(genConfigDir);
377
- configProviders = loadedConfig.providers;
378
- configLoadOptions = loadMemoryOptionsFrom(loadedConfig);
379
- }
380
- catch (err) {
381
- log.warn(`docs: metaobjects.config.ts failed to load (${describeError(err)}); ` +
382
- `generating docs without its providers`);
383
- loadedConfig = undefined;
384
- configProviders = undefined;
385
- configLoadOptions = {};
386
- }
387
- }
388
- // Merge the config `docs:` block with CLI overrides over documented defaults.
389
- // CLI --out/--layout only override when explicitly passed; surfaces/baseUrl
390
- // override whenever present. The resolver supplies defaults (outDir ./docs/generated,
391
- // layout = fallback, surfaces = both) so config-less + flag-less runs are
392
- // unchanged.
393
- const cliOverrides = {
394
- ...(flags.outProvided ? { outDir: flags.out } : {}),
395
- ...(flags.layoutProvided ? { layout: flags.layout } : {}),
396
- ...(flags.surfaces ? { surfaces: flags.surfaces } : {}),
397
- ...(flags.baseUrl !== undefined ? { baseUrl: flags.baseUrl } : {}),
398
- };
399
- // Layout fallback chain: --layout (override, gated above) → docs.layout block →
400
- // the project's top-level outputLayout → flat. So docs default to the SAME page
401
- // placement as codegen when neither the docs block nor the CLI sets it.
402
- const docsCfg = resolveDocsConfig(loadedConfig?.docs, cliOverrides, loadedConfig?.outputLayout ?? "flat");
403
- // Against `genConfigDir`, not the collection's: `docs.outDir` is declared in
404
- // `metaobjects.config.ts`, so it resolves against the directory that config lives
405
- // in — the same rule `meta gen` applies to its own `outDir`. Identical whenever
406
- // the two files sit together.
407
- const outDir = resolvePath(genConfigDir, docsCfg.outDir);
408
- // SITE surface has its OWN model loader (docs-site's loadModel — NOT the sdk
409
- // loadMemory below) and needs no gen config. When the site is the ONLY
410
- // requested surface (no markdown surfaces resolved), emit it and return
411
- // WITHOUT building the markdown GenContext — decoupled and one fewer failure
412
- // surface. Combined with --model/--api it is emitted after them (below).
413
- if (flags.site && docsCfg.surfaces.length === 0) {
414
- return emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
415
- }
416
- // Load metadata standalone — same loader path as migrate/gen. Threads any
417
- // consumer providers from the config so custom types resolve.
418
- let root;
419
- try {
420
- root = await loadMemory(collection.configDir, {
421
- ...collectionLoadOptions(collection),
422
- ...configLoadOptions,
423
- });
424
- }
425
- catch (err) {
426
- reportLoadError(log, "docs: failed to load metadata", err);
427
- return 2;
428
- }
429
- // Build the same GenContext the codegen runner builds for docsFile(). The
430
- // dialect only affects column-type hints on the entity page; "sqlite" is the
431
- // neutral default (migrate's offline path uses the same fallback chain).
432
- const renderContext = makeRenderContext({
433
- dialect: "sqlite",
434
- loadedRoot: root,
435
- outDir,
436
- dbImport: "",
437
- // The project's own prefix, not the "" default. `agent/ui.md` documents an ENDPOINT,
438
- // and the routes generator mounts every one of them inside
439
- // `fastify.register(…, { prefix: apiPrefix })` — so a project configuring "/api" is
440
- // served at /api/authors and was being told, by the page whose whole job is to be
441
- // right about addresses, that it was /authors. `meta gen` threads this (runner.ts);
442
- // `meta docs` is the OTHER door onto the same page and did not.
443
- apiPrefix: loadedConfig?.apiPrefix ?? "",
444
- pkMap: buildPkMap(root),
445
- relationMap: buildRelationMap(root),
446
- });
447
- // The generator suite, with stable-name STRINGS resolved to the generators they name
448
- // (ADR-0021 #1 allows `generators: ["routes-hono"]`). Both aggregations below read a
449
- // generator MARKER, and a string carries none — so reading the raw array answered
450
- // "no Hono routes / no UI tier" for a project that wires them by name, which is the
451
- // same two-doors-one-question defect these flags exist to close. `meta gen` resolves
452
- // the identical array through `normalizeConfig`.
453
- //
454
- // An unknown name throws (the message names it and lists the valid ones). `meta gen`
455
- // fails on it; `meta docs` has never had to load the suite at all, so it degrades to
456
- // the already-typed entries rather than becoming a new failure — and SAYS it did,
457
- // because a page quietly losing a section is exactly what this whole fix is about.
458
- let suite;
459
- try {
460
- suite = resolveGenerators(loadedConfig?.generators ?? []);
461
- }
462
- catch (err) {
463
- log.warn(`docs: could not resolve the generator suite (${describeError(err)}) — the agent ` +
464
- `pages fall back to the generators declared as values, so a tier wired by name may ` +
465
- `be missing from them.`);
466
- suite = (loadedConfig?.generators ?? []).filter((g) => typeof g !== "string");
467
- }
468
- const ctx = {
469
- entities: root.objects(),
470
- loadedRoot: root,
471
- matches: () => true,
472
- config: {
473
- outDir,
474
- extStyle: "none",
475
- dbImport: "",
476
- dialect: "sqlite",
477
- outputLayout: docsCfg.layout,
478
- // api-docs reads this to decide whether to document the opt-in Hono CRUD
479
- // surface. Aggregate it from the generator set exactly as the gen runner
480
- // does (a generator opts in via emitsHonoRoutes), so `meta docs` auto-detects
481
- // routesFileHono() rather than relying on a field users don't normally set.
482
- includeHonoRoutes: loadedConfig?.includeHonoRoutes ?? runEmitsHonoRoutes(suite),
483
- // `agent/ui.md` describes forms, grids and the endpoints their hooks call. Whether
484
- // any of that is emitted is a GENERATOR fact — the page's own predicate is
485
- // metadata-only and answered "could a UI be generated?", so a project with
486
- // `generators: []` got a confident page naming endpoints nothing serves. Aggregate
487
- // it through the same helper the gen runner uses, so `meta docs` and `meta gen` —
488
- // the two doors onto this page — cannot answer it differently.
489
- includeUiTier: loadedConfig?.includeUiTier ?? runEmitsUiTier(suite),
490
- },
491
- renderContext,
492
- projectRoot,
493
- warn: (msg) => log.warn(`docs: ${msg}`),
494
- };
495
- const emit = [];
496
- let modelFiles = [];
497
- // The declared api surfaces, each tagged with its human label (apiLabel maps
498
- // the language key → label; never hardcode labels). The model page links one
499
- // reference per declared surface — across ALL ports, not just the surfaces
500
- // THIS command emits — so a polyglot model page points at every port's docs.
501
- const labeled = docsCfg.apiSurfaces.map((s) => ({ ...s, label: apiLabel(s.lang) }));
502
- // The api surface only materializes with a loadable gen config (there is
503
- // nothing generated to document otherwise), so gate api emit + cross-linking
504
- // on that. When false, the model surface emits its historical standalone form.
505
- const apiSelected = docsCfg.surfaces.includes("api") && loadedConfig !== undefined;
506
- // MODEL surface — the neutral metadata pages (<Entity>.md / <Template>.md +
507
- // README.md). Keep the render-error handling tight around docsFile() only.
508
- if (docsCfg.surfaces.includes("model")) {
509
- // When api is selected, link EVERY declared surface from the model page (so a
510
- // polyglot model page references each port's api docs). `labeled` already
511
- // carries {label, subDir, baseUrl?} (docsFile ignores the extra `lang`), so
512
- // pass it straight through. Otherwise no api refs.
513
- const modelOpts = apiSelected ? { apiSurfaces: labeled } : {};
514
- try {
515
- modelFiles = await docsFile(modelOpts).generate(ctx);
516
- }
517
- catch (err) {
518
- const msg = describeError(err);
519
- // Duplicate output path (silent-overwrite backstop): the generator already
520
- // names both colliding FQNs + the path and starts with "docs:". Surface it
521
- // verbatim as a clean non-zero exit (no double prefix, no stack trace).
522
- if (msg.startsWith("docs: duplicate output path")) {
523
- log.error(msg);
524
- return 1;
525
- }
526
- // The framework templates resolve from disk; inside the compiled `meta`
527
- // binary they live on a virtual fs the provider cannot read. Surface that
528
- // as an actionable message rather than a cryptic render failure.
529
- if (/entity-page|template-page|failed rendering|ENOENT|not found/i.test(msg)) {
530
- log.error(`docs: failed to render — templates not found. ` +
531
- `Run 'meta docs' from an installed package layout (with on-disk ` +
532
- `templates/), not the standalone binary, OR drop your own ` +
533
- `templates/docs/entity-page.md.mustache + template-page.md.mustache. (${msg})`);
534
- }
535
- else {
536
- log.error(`docs: ${msg}`);
537
- }
538
- return 1;
539
- }
540
- emit.push(...modelFiles);
541
- }
542
- // REQUIREMENTS surface — the declared ledger as documentation (requirements.md +
543
- // requirements.toon). Metadata-alone like the model surface: it reads ctx.loadedRoot
544
- // and nothing else, so it is NOT gated on a loadable gen config the way `api` is.
545
- //
546
- // Emits ZERO files when the project declares no `requirement.*` node, which is what
547
- // makes this surface safe to default ON — a project without a ledger sees no change.
548
- if (docsCfg.surfaces.includes("requirements")) {
549
- emit.push(...(await requirementsFile().generate(ctx)));
550
- }
551
- // API surface — the SDK reference for the GENERATED REST surface, side by side
552
- // under each surface's subDir. THIS command only OWNS the surfaces it can
553
- // generate — i.e. its own port (lang "ts"). Surfaces owned by other ports are
554
- // linked (above) but produced by running that port's docs command; we just log
555
- // a pointer. Only meaningful with a loadable gen config, so skip when absent.
556
- let apiFiles = [];
557
- if (docsCfg.surfaces.includes("api")) {
558
- if (loadedConfig !== undefined) {
559
- try {
560
- // Emit every surface THIS port owns (loop so it generalizes beyond the
561
- // single ts surface owned today).
562
- for (const s of labeled.filter((s) => s.lang === "ts")) {
563
- apiFiles.push(...(await apiDocsFile({
564
- subDir: s.subDir,
565
- modelSurface: docsCfg.surfaces.includes("model"),
566
- }).generate(ctx)));
567
- }
568
- }
569
- catch (err) {
570
- const msg = describeError(err);
571
- if (msg.startsWith("docs: duplicate output path")) {
572
- log.error(msg);
573
- return 1;
574
- }
575
- log.error(`docs: ${msg}`);
576
- return 1;
577
- }
578
- emit.push(...apiFiles);
579
- // Surfaces owned by other ports: link only, with a pointer to where they
580
- // get produced.
581
- for (const s of labeled.filter((s) => s.lang !== "ts")) {
582
- info(`meta docs: api surface '${s.lang}' (${s.subDir}) is produced by that port's docs command — run it to populate those pages.`);
583
- }
584
- }
585
- else if (hasConfig) {
586
- // Config present but failed to load — already warned above; don't claim an
587
- // api surface we couldn't build.
588
- info("meta docs: api surface skipped — metaobjects.config.ts failed to load.");
589
- }
590
- else {
591
- info("meta docs: api surface skipped — no metaobjects.config.ts (nothing generated to document).");
592
- }
593
- }
594
- // AGENT surface — three pages an agent reads BEFORE touching a tier (`agent/schema.md`
595
- // before persistence, `agent/ui.md` before a form or grid, `agent/requirements.md`
596
- // before adding a capability). The fourth file the always-on pointer names,
597
- // `api/AGENT-API.md`, belongs to the api surface above.
598
- //
599
- // Gated on a loadable gen config exactly as `api` is, and for a stronger reason: the
600
- // physical schema depends on the project's DIALECT and column-naming strategy, and the
601
- // neutral model surface above runs on a placeholder because it documents no SQL at all.
602
- // The dialect here is the project's own, resolved by the same default `meta gen` and
603
- // `meta migrate` apply — see `buildAgentSchemaInput`.
604
- if (docsCfg.surfaces.includes("agent")) {
605
- if (loadedConfig !== undefined) {
606
- const strategy = loadedConfig.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY;
607
- const schema = buildAgentSchemaInput(root, loadedConfig.dialect, strategy);
608
- emit.push(...(await agentDocsFile({
609
- ...(schema !== undefined && { schema }),
610
- columnNamingStrategy: strategy,
611
- }).generate(ctx)));
612
- }
613
- else if (hasConfig) {
614
- info("meta docs: agent surface skipped — metaobjects.config.ts failed to load.");
615
- }
616
- else {
617
- info("meta docs: agent surface skipped — no metaobjects.config.ts (the physical schema " +
618
- "and the generated UI are what it describes).");
619
- }
620
- }
621
- try {
622
- await mkdir(outDir, { recursive: true });
623
- for (const f of emit) {
624
- const path = resolvePath(outDir, f.path);
625
- await mkdir(resolvePath(path, ".."), { recursive: true });
626
- await writeFile(path, f.content, "utf8");
627
- }
628
- }
629
- catch (err) {
630
- log.error(`docs: failed to write pages: ${describeError(err)}`);
631
- return 1;
632
- }
633
- // SITE surface (additive) — emit after the markdown surfaces so both coexist.
634
- if (flags.site) {
635
- const siteRc = await emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
636
- if (siteRc !== 0)
637
- return siteRc;
638
- }
639
- // Summary: docsFile() emits ONE overview/index page (README.md) plus one page
640
- // per entity and one per template.output. The entity count is the matched
641
- // object count; the remaining non-overview model pages are template pages.
642
- const entityCount = root.objects().filter(ctx.matches).length;
643
- const modelOverview = modelFiles.filter((f) => f.path === "README.md").length;
644
- const modelTemplates = modelFiles.length > 0
645
- ? modelFiles.length - entityCount - modelOverview
646
- : 0;
647
- const modelSummary = docsCfg.surfaces.includes("model")
648
- ? `${modelOverview} overview + ${entityCount} entity page(s) + ${modelTemplates} template page(s)`
649
- : "model surface skipped";
650
- const apiSummary = apiFiles.length > 0
651
- ? `${apiFiles.length} api page(s)`
652
- : "no api pages";
653
- // Reported only when it actually wrote something. A project with no ledger emits no
654
- // requirements file, and saying "0 requirement pages" would advertise a surface that
655
- // did not run — the opposite of the silence the empty-ledger guard exists to produce.
656
- const requirementFiles = emit.filter((f) => f.path.startsWith("requirements.")).length;
657
- const reqSummary = requirementFiles > 0 ? `; ${requirementFiles} requirement file(s)` : "";
658
- // Same rule as the requirements line: NAMED rather than counted, and only when written.
659
- // Each agent page is skipped when its tier has nothing to describe, so "3 agent pages"
660
- // would leave a reader unable to tell which three — and the pages are the thing the
661
- // always-on agent context points at by name.
662
- const agentPages = emit
663
- .filter((f) => f.path.startsWith("agent/"))
664
- .map((f) => basename(f.path));
665
- const agentSummary = agentPages.length > 0 ? `; agent/${agentPages.sort().join(" + agent/")}` : "";
666
- info(`meta docs — wrote ${modelSummary}; ${apiSummary}${reqSummary}${agentSummary} → ${outDir}`);
667
- return 0;
668
- }
669
- /**
670
- * ADR-0034 scaffold-and-own for the docs-site: copy the bundled templates + assets
671
- * into `<root>/codegen/docs-site/{templates,assets}`, writing each file ONLY if
672
- * absent so a re-run never clobbers a hand-edited file.
673
- */
674
- async function scaffoldSiteCommand(projectRoot) {
675
- const tplDir = join(projectRoot, "codegen/docs-site/templates");
676
- const astDir = join(projectRoot, "codegen/docs-site/assets");
677
- const created = [];
678
- const preserved = [];
679
- try {
680
- await mkdir(tplDir, { recursive: true });
681
- await mkdir(astDir, { recursive: true });
682
- for (const name of SITE_TEMPLATE_NAMES) {
683
- const abs = join(tplDir, name);
684
- const rel = `codegen/docs-site/templates/${name}`;
685
- if (existsSync(abs)) {
686
- preserved.push(rel);
687
- continue;
688
- }
689
- await writeFile(abs, readSiteFile("template", name), "utf8");
690
- created.push(rel);
691
- }
692
- for (const name of SITE_ASSET_NAMES) {
693
- const abs = join(astDir, name);
694
- const rel = `codegen/docs-site/assets/${name}`;
695
- if (existsSync(abs)) {
696
- preserved.push(rel);
697
- continue;
698
- }
699
- await writeFile(abs, readSiteFile("asset", name), "utf8");
700
- created.push(rel);
701
- }
702
- }
703
- catch (err) {
704
- log.error(`docs: failed to scaffold site templates: ${describeError(err)}`);
705
- return 1;
706
- }
707
- log.info(`meta docs --scaffold-site — ${created.length} created, ${preserved.length} preserved ` +
708
- `→ ${join(projectRoot, "codegen/docs-site")} (edit these to own your theme)`);
709
- return 0;
710
- }
711
- /**
712
- * Emit the browsable HTML documentation site via `@metaobjectsdev/docs-site`.
713
- * The site loads the model with its OWN loader from the collection's declared
714
- * source ROOTS (whole directories, one page group each) rather than from the
715
- * per-file list the sdk `loadMemory` path takes, so this is independent of the
716
- * markdown surfaces. Writes under `<outDir>/site` so it can coexist with the markdown
717
- * output. Scaffold-and-own: when the consumer has copied templates/assets
718
- * into `<projectRoot>/codegen/docs-site/` (via `--scaffold-site`), those win
719
- * over the bundled defaults.
720
- *
721
- * Takes the ALREADY-RESOLVED collection: `docsCommand` resolved it to read the
722
- * config from the right directory, and resolving a second time here made the
723
- * combined `--model --site` path do the whole discovery-and-config walk twice.
724
- */
725
- async function emitSite(collection, projectRoot,
726
- /** Directory holding `metaobjects.config.ts` — where `--scaffold-site` writes the
727
- * owned theme, so where this must read it from. Distinct from `collection.configDir`
728
- * since #326: in a polyglot monorepo the collection is declared at the repo root
729
- * while the TS package (and its owned theme) sits in the app. */
730
- genConfigDir, outDir, configProviders, promptsDir) {
731
- const siteOutDir = resolvePath(outDir, "site");
732
- // The resolved metadata source dir(s) are REQUIRED (the site loads the
733
- // model from them) and always first. Prompt `.mustache` source is
734
- // additionally searched in the conventional <root>/templates/ and any
735
- // explicit --prompts dir (for a project whose templates live elsewhere,
736
- // e.g. data/templates/) — else the site can't show the prompt TEXT and
737
- // prints a "source missing" note. Only existing dirs are added.
738
- //
739
- // Deduped by resolved PATH, not by basename. Two DIFFERENT directories that
740
- // happen to share a basename are a legitimate multi-source project (`metaobjects`
741
- // plus `../shared-model/metaobjects`); `loadModel` disambiguates their site
742
- // group names, so refusing the pair here — which a basename key did, by
743
- // dropping the second — would break the feature this branch exists to ship.
744
- // The same directory named twice is the real hazard: it would be symlinked
745
- // and loaded twice.
746
- // The DECLARED source roots, not directories re-derived from the resolved
747
- // files: a declared source directory holding no metadata yet would otherwise
748
- // vanish from the site's group list entirely, and `sourceDirs` could come back
749
- // empty where the pre-branch code always passed `<root>/metaobjects`.
750
- // `collection.sourceRoots` deliberately EXCLUDES dependency artifacts (sdk's
751
- // collection.ts: "a dependency's artifact is not a source root"). But
752
- // `generateSite` below does NOT reuse the already-loaded `collection` root —
753
- // it runs its own independent `loadModel` over exactly the dirs handed to
754
- // it. Without the resolved snapshot dirs appended here, a consumer that
755
- // `extends` an imported abstract or `overlay: true`s an imported node — the
756
- // two constructs entities.md and abstracts-and-inheritance.md recommend for
757
- // this exact boundary — hits `ERR_UNRESOLVED_SUPER` / `ERR_OVERLAY_NO_TARGET`
758
- // in THIS loader even though the same metadata resolves fine through
759
- // `loadMemory` above. `loadModel` handles a dependency's snapshot dir
760
- // natively: each holds exactly the one artifact file, which its own
761
- // `.(json|ya?ml)$` file walk picks up like any other source dir.
762
- const sourceDirs = [
763
- ...collection.sourceRoots,
764
- ...new Set(collection.dependencies.map((d) => dirname(d.artifactPath))),
765
- ];
766
- const seenDirs = new Set(sourceDirs);
767
- if (promptsDir !== undefined && !existsSync(promptsDir)) {
768
- log.warn(`docs: --prompts dir does not exist: ${promptsDir}`);
769
- }
770
- for (const d of [join(projectRoot, "templates"), ...(promptsDir !== undefined ? [promptsDir] : [])]) {
771
- const abs = resolvePath(d);
772
- if (existsSync(abs) && !seenDirs.has(abs)) {
773
- sourceDirs.push(abs);
774
- seenDirs.add(abs);
775
- }
776
- }
777
- // Scaffold-and-own: when the consumer has copied templates/assets into
778
- // codegen/docs-site/ (via --scaffold-site), use those; else the bundled defaults.
779
- // Keyed on `genConfigDir`, NOT `projectRoot`: `--templates` redirects the adopter
780
- // RENDER template chain (the `templates/` above), and letting it also move the
781
- // docs-site theme would read it from somewhere `--scaffold-site` never writes.
782
- const ownedTemplates = join(genConfigDir, "codegen/docs-site/templates");
783
- const ownedAssets = join(genConfigDir, "codegen/docs-site/assets");
784
- try {
785
- const r = await generateSite({
786
- sourceDirs,
787
- outDir: siteOutDir,
788
- title: basename(collection.configDir) || "Metadata",
789
- stamp: new Date().toISOString().slice(0, 10),
790
- commit: "",
791
- core: { n: 15 },
792
- // Thread any consumer providers from metaobjects.config.ts so the site's
793
- // own loader resolves custom field/view/object subtypes — same providers
794
- // the markdown surfaces get via loadMemory.
795
- ...(configProviders !== undefined ? { extraProviders: configProviders } : {}),
796
- ...(existsSync(ownedTemplates) ? { templatesDir: ownedTemplates } : {}),
797
- ...(existsSync(ownedAssets) ? { assetsDir: ownedAssets } : {}),
798
- });
799
- log.info(`meta docs --site — wrote ${r.pages.length} page(s) → ${siteOutDir}`);
800
- return 0;
801
- }
802
- catch (err) {
803
- log.error(`docs: failed to generate site: ${describeError(err)}`);
804
- return 1;
805
- }
806
- }
807
- /**
808
- * FR-033 S3 — emit the metamodel reference docs (INDEX.md + per-type pages +
809
- * providers.md) from the BUILT-IN strict registry. No metadata, no config:
810
- * `composeRegistry(coreProviders)` IS the source. Files land under
811
- * `<out>` (which defaults to `./docs/metamodel`), preserving the renderer's
812
- * `types/<family>.md` subtree.
813
- */
814
- async function metamodelDocsCommand(cwd, out) {
815
- const outDir = resolvePath(cwd, out);
816
- let docs;
817
- try {
818
- docs = renderCoreMetamodelDocs(composeRegistry(coreProviders));
819
- }
820
- catch (err) {
821
- log.error(`docs: failed to render metamodel docs: ${describeError(err)}`);
822
- return 1;
823
- }
824
- try {
825
- await mkdir(outDir, { recursive: true });
826
- for (const [rel, content] of docs) {
827
- const path = resolvePath(outDir, rel);
828
- await mkdir(resolvePath(path, ".."), { recursive: true });
829
- await writeFile(path, content, "utf8");
830
- }
831
- }
832
- catch (err) {
833
- log.error(`docs: failed to write metamodel pages: ${describeError(err)}`);
834
- return 1;
835
- }
836
- log.info(`meta docs --metamodel — wrote ${docs.size} page(s) → ${outDir}`);
837
- return 0;
10
+ import { REMOVED_DOCS_COMMAND } from "../lib/docs-removed.js";
11
+ export async function docsCommand(_args, _cwd) {
12
+ log.error(REMOVED_DOCS_COMMAND);
13
+ return 2;
838
14
  }
839
15
  //# sourceMappingURL=docs.js.map