@metaobjectsdev/cli 0.9.0 → 0.10.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.
- package/dist/src/commands/docs.d.ts +2 -0
- package/dist/src/commands/docs.d.ts.map +1 -0
- package/dist/src/commands/docs.js +395 -0
- package/dist/src/commands/docs.js.map +1 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +41 -1
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +6 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +102 -29
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +69 -15
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +20 -32
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts +7 -0
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -0
- package/dist/src/lib/agent-context-staleness.js +26 -0
- package/dist/src/lib/agent-context-staleness.js.map +1 -0
- package/dist/src/lib/args.d.ts +14 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +22 -0
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +21 -0
- package/dist/src/lib/codegen-drift.d.ts.map +1 -0
- package/dist/src/lib/codegen-drift.js +142 -0
- package/dist/src/lib/codegen-drift.js.map +1 -0
- package/dist/src/lib/detect-stack.d.ts +7 -0
- package/dist/src/lib/detect-stack.d.ts.map +1 -0
- package/dist/src/lib/detect-stack.js +38 -0
- package/dist/src/lib/detect-stack.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +8 -0
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/version.d.ts +7 -0
- package/dist/src/lib/version.d.ts.map +1 -0
- package/dist/src/lib/version.js +30 -0
- package/dist/src/lib/version.js.map +1 -0
- package/package.json +56 -45
- package/src/commands/docs.ts +438 -0
- package/src/commands/gen.ts +44 -1
- package/src/commands/init.ts +106 -31
- package/src/commands/verify.ts +81 -15
- package/src/index.ts +20 -30
- package/src/lib/agent-context-staleness.ts +24 -0
- package/src/lib/args.ts +41 -0
- package/src/lib/codegen-drift.ts +173 -0
- package/src/lib/detect-stack.ts +41 -0
- package/src/lib/load-metaobjects-config.ts +8 -0
- package/src/lib/version.ts +27 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// `meta docs <metadata> --out <dir>` — STANDALONE neutral metadata docs.
|
|
2
|
+
//
|
|
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
|
+
|
|
14
|
+
import { resolve as resolvePath } from "node:path";
|
|
15
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
16
|
+
import { log } from "../lib/log.js";
|
|
17
|
+
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
18
|
+
import { loadMemory, DEFAULT_METADATA_DIR } from "@metaobjectsdev/sdk";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
makeRenderContext,
|
|
23
|
+
buildPkMap,
|
|
24
|
+
buildRelationMap,
|
|
25
|
+
resolveDocsConfig,
|
|
26
|
+
apiLabel,
|
|
27
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
28
|
+
import type {
|
|
29
|
+
GenContext,
|
|
30
|
+
EmittedFile,
|
|
31
|
+
ResolvedDocsConfig,
|
|
32
|
+
DocsSurface,
|
|
33
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
34
|
+
import { docsFile, apiDocsFile } from "@metaobjectsdev/codegen-ts/generators";
|
|
35
|
+
import { composeRegistry, coreProviders, renderCoreMetamodelDocs } from "@metaobjectsdev/metadata";
|
|
36
|
+
|
|
37
|
+
type DocsLayout = "flat" | "package";
|
|
38
|
+
|
|
39
|
+
interface DocsFlags {
|
|
40
|
+
/** Project root holding `metaobjects/` (the metadata to document). */
|
|
41
|
+
metadata: string;
|
|
42
|
+
/** Output directory for the rendered pages. */
|
|
43
|
+
out: string;
|
|
44
|
+
/** Page-placement layout. `flat` (default) writes `<Name>.md` at the out
|
|
45
|
+
* root; `package` folds pages under package-path subdirs (collision-proof
|
|
46
|
+
* for multi-package models with repeated short names). */
|
|
47
|
+
layout: DocsLayout;
|
|
48
|
+
/** Optional override for the project root used to resolve adopter
|
|
49
|
+
* `templates/` overrides. Defaults to the metadata root. */
|
|
50
|
+
templates?: string;
|
|
51
|
+
/** Which doc surfaces to emit, when overridden on the CLI. `--model` ⇒
|
|
52
|
+
* ["model"], `--api` ⇒ ["api"], both ⇒ ["model","api"]. Unset ⇒ defer to the
|
|
53
|
+
* resolved `docs:` config (default both). */
|
|
54
|
+
surfaces?: DocsSurface[];
|
|
55
|
+
/** Optional base URL override for cross-surface links (resolveDocsConfig). */
|
|
56
|
+
baseUrl?: string;
|
|
57
|
+
/** Whether `--out` was explicitly passed (so the resolver knows to override
|
|
58
|
+
* the config's `docs.outDir` rather than fall back to the parse default). */
|
|
59
|
+
outProvided: boolean;
|
|
60
|
+
/** Whether `--layout` was explicitly passed (same override semantics). */
|
|
61
|
+
layoutProvided: boolean;
|
|
62
|
+
/** FR-033 S3 — document the METAMODEL ITSELF (the built-in type/subtype/attr
|
|
63
|
+
* vocabulary) instead of a user's entities. Needs NO metadata + NO config. */
|
|
64
|
+
metamodel: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseLayout(v: string | undefined, flag: string): DocsLayout {
|
|
68
|
+
if (v === undefined) throw new Error(`${flag} requires flat|package`);
|
|
69
|
+
if (v !== "flat" && v !== "package") {
|
|
70
|
+
throw new Error(`${flag} must be "flat" or "package" (got "${v}")`);
|
|
71
|
+
}
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
76
|
+
let metadata: string | undefined;
|
|
77
|
+
let out: string | undefined;
|
|
78
|
+
let templates: string | undefined;
|
|
79
|
+
let layout: DocsLayout | undefined;
|
|
80
|
+
let baseUrl: string | undefined;
|
|
81
|
+
let wantModel = false;
|
|
82
|
+
let wantApi = false;
|
|
83
|
+
let wantMetamodel = false;
|
|
84
|
+
let outProvided = false;
|
|
85
|
+
let layoutProvided = false;
|
|
86
|
+
for (let i = 0; i < argv.length; i++) {
|
|
87
|
+
const a = argv[i]!;
|
|
88
|
+
if (a === "--out" || a === "-o") {
|
|
89
|
+
const v = argv[++i];
|
|
90
|
+
if (v === undefined) throw new Error(`${a} requires a directory argument`);
|
|
91
|
+
out = v;
|
|
92
|
+
outProvided = true;
|
|
93
|
+
} else if (a.startsWith("--out=")) {
|
|
94
|
+
out = a.slice("--out=".length);
|
|
95
|
+
outProvided = true;
|
|
96
|
+
} else if (a === "--layout") {
|
|
97
|
+
layout = parseLayout(argv[++i], a);
|
|
98
|
+
layoutProvided = true;
|
|
99
|
+
} else if (a.startsWith("--layout=")) {
|
|
100
|
+
layout = parseLayout(a.slice("--layout=".length), "--layout");
|
|
101
|
+
layoutProvided = true;
|
|
102
|
+
} else if (a === "--model") {
|
|
103
|
+
wantModel = true;
|
|
104
|
+
} else if (a === "--api") {
|
|
105
|
+
wantApi = true;
|
|
106
|
+
} else if (a === "--metamodel") {
|
|
107
|
+
wantMetamodel = true;
|
|
108
|
+
} else if (a === "--base-url") {
|
|
109
|
+
const v = argv[++i];
|
|
110
|
+
if (v === undefined) throw new Error(`${a} requires a URL argument`);
|
|
111
|
+
baseUrl = v;
|
|
112
|
+
} else if (a.startsWith("--base-url=")) {
|
|
113
|
+
baseUrl = a.slice("--base-url=".length);
|
|
114
|
+
} else if (a === "--templates") {
|
|
115
|
+
const v = argv[++i];
|
|
116
|
+
if (v === undefined) throw new Error(`${a} requires a directory argument`);
|
|
117
|
+
templates = v;
|
|
118
|
+
} else if (a.startsWith("--templates=")) {
|
|
119
|
+
templates = a.slice("--templates=".length);
|
|
120
|
+
} else if (a.startsWith("-")) {
|
|
121
|
+
throw new Error(`unknown flag: ${a}`);
|
|
122
|
+
} else if (metadata === undefined) {
|
|
123
|
+
metadata = a;
|
|
124
|
+
} else {
|
|
125
|
+
throw new Error(`unexpected argument: ${a}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// --model and/or --api narrow the surfaces; both flags (or neither) leave the
|
|
129
|
+
// surfaces unset so the resolved `docs:` config decides (default both).
|
|
130
|
+
const surfaces: DocsSurface[] = [];
|
|
131
|
+
if (wantModel) surfaces.push("model");
|
|
132
|
+
if (wantApi) surfaces.push("api");
|
|
133
|
+
return {
|
|
134
|
+
// `<metadata>` is the project root that contains metaobjects/; default cwd
|
|
135
|
+
// (mirrors how migrate/gen treat the working directory as the root).
|
|
136
|
+
metadata: metadata ?? cwd,
|
|
137
|
+
// Default out dir, resolved against the metadata root below. In --metamodel
|
|
138
|
+
// mode the renderer writes under <out>/metamodel/, default ./docs/metamodel.
|
|
139
|
+
out: out ?? (wantMetamodel ? "./docs/metamodel" : "./docs"),
|
|
140
|
+
// Default flat preserves today's single-package output (+ existing goldens).
|
|
141
|
+
layout: layout ?? "flat",
|
|
142
|
+
metamodel: wantMetamodel,
|
|
143
|
+
outProvided,
|
|
144
|
+
layoutProvided,
|
|
145
|
+
...(surfaces.length > 0 ? { surfaces } : {}),
|
|
146
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
147
|
+
...(templates !== undefined ? { templates } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function docsCommand(args: string[], cwd: string): Promise<number> {
|
|
152
|
+
let flags: DocsFlags;
|
|
153
|
+
try {
|
|
154
|
+
flags = parseDocsArgs(args, cwd);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
log.error(`docs: ${(err as Error).message}`);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// FR-033 S3 — `--metamodel`: document the BUILT-IN metamodel (type/subtype/
|
|
161
|
+
// attr vocabulary) from the strict registry. Unlike --model/--api this needs
|
|
162
|
+
// NEITHER a user's metadata NOR a config — there is nothing to load. It writes
|
|
163
|
+
// the renderer's files under <out>/metamodel/ (default ./docs/metamodel).
|
|
164
|
+
if (flags.metamodel) {
|
|
165
|
+
return metamodelDocsCommand(cwd, flags.out);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const metaRoot = resolvePath(cwd, flags.metadata);
|
|
169
|
+
// The project root used to resolve adopter `templates/` overrides; the
|
|
170
|
+
// framework defaults sit underneath via projectProvider's chain.
|
|
171
|
+
const projectRoot = flags.templates !== undefined
|
|
172
|
+
? resolvePath(cwd, flags.templates)
|
|
173
|
+
: metaRoot;
|
|
174
|
+
|
|
175
|
+
// Best-effort load of metaobjects.config.ts to pick up consumer-supplied
|
|
176
|
+
// providers (e.g. a project's custom field/object subtypes). Unlike `gen`,
|
|
177
|
+
// docs does NOT require a config — the Tier-2 "metadata alone" promise must
|
|
178
|
+
// hold for config-less projects. If the config is absent or invalid, fall
|
|
179
|
+
// back to defaults; the loader still surfaces a stable unknown-subtype error
|
|
180
|
+
// if the metadata genuinely uses an unregistered type.
|
|
181
|
+
let loadedConfig: Awaited<ReturnType<typeof loadMetaobjectsConfig>> | undefined;
|
|
182
|
+
let configProviders: NonNullable<Awaited<ReturnType<typeof loadMetaobjectsConfig>>["providers"]> | undefined;
|
|
183
|
+
// hasConfig gates the api surface: api docs describe the GENERATED REST
|
|
184
|
+
// surface, which only exists when there is a (loadable) gen config. A config
|
|
185
|
+
// that EXISTS but fails to load degrades to model-only with a warning.
|
|
186
|
+
const hasConfig = existsSync(join(metaRoot, "metaobjects.config.ts"));
|
|
187
|
+
// The config lives alongside metaobjects/ at the metadata root (metaRoot);
|
|
188
|
+
// projectRoot only diverges when --templates overrides the template lookup.
|
|
189
|
+
// Only attempt the load when the file is actually present: absence is the
|
|
190
|
+
// expected config-less case (stay silent), but a config that EXISTS yet fails
|
|
191
|
+
// to load is surfaced as a warning rather than silently degrading to
|
|
192
|
+
// provider-less docs — otherwise a custom-type project would later fail with a
|
|
193
|
+
// cryptic unknown-subtype error instead of the real config error.
|
|
194
|
+
if (hasConfig) {
|
|
195
|
+
try {
|
|
196
|
+
loadedConfig = await loadMetaobjectsConfig(metaRoot);
|
|
197
|
+
configProviders = loadedConfig.providers;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log.warn(
|
|
200
|
+
`docs: metaobjects.config.ts failed to load (${(err as Error).message}); ` +
|
|
201
|
+
`generating docs without its providers`,
|
|
202
|
+
);
|
|
203
|
+
loadedConfig = undefined;
|
|
204
|
+
configProviders = undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Merge the config `docs:` block with CLI overrides over documented defaults.
|
|
209
|
+
// CLI --out/--layout only override when explicitly passed; surfaces/baseUrl
|
|
210
|
+
// override whenever present. The resolver supplies defaults (outDir ./docs,
|
|
211
|
+
// layout = fallback, surfaces = both) so config-less + flag-less runs are
|
|
212
|
+
// unchanged.
|
|
213
|
+
const cliOverrides: Partial<ResolvedDocsConfig> = {
|
|
214
|
+
...(flags.outProvided ? { outDir: flags.out } : {}),
|
|
215
|
+
...(flags.layoutProvided ? { layout: flags.layout } : {}),
|
|
216
|
+
...(flags.surfaces ? { surfaces: flags.surfaces } : {}),
|
|
217
|
+
...(flags.baseUrl !== undefined ? { baseUrl: flags.baseUrl } : {}),
|
|
218
|
+
};
|
|
219
|
+
// Layout fallback chain: --layout (override, gated above) → docs.layout block →
|
|
220
|
+
// the project's top-level outputLayout → flat. So docs default to the SAME page
|
|
221
|
+
// placement as codegen when neither the docs block nor the CLI sets it.
|
|
222
|
+
const docsCfg = resolveDocsConfig(
|
|
223
|
+
loadedConfig?.docs,
|
|
224
|
+
cliOverrides,
|
|
225
|
+
loadedConfig?.outputLayout ?? "flat",
|
|
226
|
+
);
|
|
227
|
+
const outDir = resolvePath(metaRoot, docsCfg.outDir);
|
|
228
|
+
|
|
229
|
+
// Load metadata standalone — same loader path as migrate/gen. Threads any
|
|
230
|
+
// consumer providers from the config so custom types resolve.
|
|
231
|
+
let root;
|
|
232
|
+
try {
|
|
233
|
+
root = await loadMemory(metaRoot, {
|
|
234
|
+
...(configProviders !== undefined ? { providers: configProviders } : {}),
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const msg = (err as Error).message;
|
|
238
|
+
if (!existsSync(join(metaRoot, DEFAULT_METADATA_DIR))) {
|
|
239
|
+
log.error(`docs: no metaobjects/ found in ${metaRoot}; run 'meta init' to scaffold`);
|
|
240
|
+
} else {
|
|
241
|
+
log.error(`docs: failed to load metadata: ${msg}`);
|
|
242
|
+
}
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Build the same GenContext the codegen runner builds for docsFile(). The
|
|
247
|
+
// dialect only affects column-type hints on the entity page; "sqlite" is the
|
|
248
|
+
// neutral default (migrate's offline path uses the same fallback chain).
|
|
249
|
+
const renderContext = makeRenderContext({
|
|
250
|
+
dialect: "sqlite",
|
|
251
|
+
loadedRoot: root,
|
|
252
|
+
outDir,
|
|
253
|
+
dbImport: "",
|
|
254
|
+
pkMap: buildPkMap(root),
|
|
255
|
+
relationMap: buildRelationMap(root),
|
|
256
|
+
});
|
|
257
|
+
const ctx: GenContext = {
|
|
258
|
+
entities: root.objects(),
|
|
259
|
+
loadedRoot: root,
|
|
260
|
+
matches: () => true,
|
|
261
|
+
config: {
|
|
262
|
+
outDir,
|
|
263
|
+
extStyle: "none",
|
|
264
|
+
dbImport: "",
|
|
265
|
+
dialect: "sqlite",
|
|
266
|
+
outputLayout: docsCfg.layout,
|
|
267
|
+
// api-docs reads this to decide whether to document the opt-in Hono CRUD
|
|
268
|
+
// surface. Aggregate it from the generator set exactly as the gen runner
|
|
269
|
+
// does (a generator opts in via emitsHonoRoutes), so `meta docs` auto-detects
|
|
270
|
+
// routesFileHono() rather than relying on a field users don't normally set.
|
|
271
|
+
includeHonoRoutes:
|
|
272
|
+
loadedConfig?.includeHonoRoutes ??
|
|
273
|
+
(loadedConfig?.generators?.some(
|
|
274
|
+
(g) => typeof g !== "string" && g.emitsHonoRoutes === true,
|
|
275
|
+
) ??
|
|
276
|
+
false),
|
|
277
|
+
} as never,
|
|
278
|
+
renderContext,
|
|
279
|
+
projectRoot,
|
|
280
|
+
warn: (msg) => log.warn(`docs: ${msg}`),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const emit: EmittedFile[] = [];
|
|
284
|
+
let modelFiles: EmittedFile[] = [];
|
|
285
|
+
|
|
286
|
+
// The declared api surfaces, each tagged with its human label (apiLabel maps
|
|
287
|
+
// the language key → label; never hardcode labels). The model page links one
|
|
288
|
+
// reference per declared surface — across ALL ports, not just the surfaces
|
|
289
|
+
// THIS command emits — so a polyglot model page points at every port's docs.
|
|
290
|
+
const labeled = docsCfg.apiSurfaces.map((s) => ({ ...s, label: apiLabel(s.lang) }));
|
|
291
|
+
|
|
292
|
+
// The api surface only materializes with a loadable gen config (there is
|
|
293
|
+
// nothing generated to document otherwise), so gate api emit + cross-linking
|
|
294
|
+
// on that. When false, the model surface emits its historical standalone form.
|
|
295
|
+
const apiSelected = docsCfg.surfaces.includes("api") && loadedConfig !== undefined;
|
|
296
|
+
|
|
297
|
+
// MODEL surface — the neutral metadata pages (<Entity>.md / <Template>.md +
|
|
298
|
+
// README.md). Keep the render-error handling tight around docsFile() only.
|
|
299
|
+
if (docsCfg.surfaces.includes("model")) {
|
|
300
|
+
// When api is selected, link EVERY declared surface from the model page (so a
|
|
301
|
+
// polyglot model page references each port's api docs). `labeled` already
|
|
302
|
+
// carries {label, subDir, baseUrl?} (docsFile ignores the extra `lang`), so
|
|
303
|
+
// pass it straight through. Otherwise no api refs.
|
|
304
|
+
const modelOpts = apiSelected ? { apiSurfaces: labeled } : {};
|
|
305
|
+
try {
|
|
306
|
+
modelFiles = await docsFile(modelOpts).generate(ctx);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const msg = (err as Error).message;
|
|
309
|
+
// Duplicate output path (silent-overwrite backstop): the generator already
|
|
310
|
+
// names both colliding FQNs + the path and starts with "docs:". Surface it
|
|
311
|
+
// verbatim as a clean non-zero exit (no double prefix, no stack trace).
|
|
312
|
+
if (msg.startsWith("docs: duplicate output path")) {
|
|
313
|
+
log.error(msg);
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
// The framework templates resolve from disk; inside the compiled `meta`
|
|
317
|
+
// binary they live on a virtual fs the provider cannot read. Surface that
|
|
318
|
+
// as an actionable message rather than a cryptic render failure.
|
|
319
|
+
if (/entity-page|template-page|failed rendering|ENOENT|not found/i.test(msg)) {
|
|
320
|
+
log.error(
|
|
321
|
+
`docs: failed to render — templates not found. ` +
|
|
322
|
+
`Run 'meta docs' from an installed package layout (with on-disk ` +
|
|
323
|
+
`templates/), not the standalone binary, OR drop your own ` +
|
|
324
|
+
`templates/docs/entity-page.md.mustache + template-page.md.mustache. (${msg})`,
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
log.error(`docs: ${msg}`);
|
|
328
|
+
}
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
emit.push(...modelFiles);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// API surface — the SDK reference for the GENERATED REST surface, side by side
|
|
335
|
+
// under each surface's subDir. THIS command only OWNS the surfaces it can
|
|
336
|
+
// generate — i.e. its own port (lang "ts"). Surfaces owned by other ports are
|
|
337
|
+
// linked (above) but produced by running that port's docs command; we just log
|
|
338
|
+
// a pointer. Only meaningful with a loadable gen config, so skip when absent.
|
|
339
|
+
let apiFiles: EmittedFile[] = [];
|
|
340
|
+
if (docsCfg.surfaces.includes("api")) {
|
|
341
|
+
if (loadedConfig !== undefined) {
|
|
342
|
+
try {
|
|
343
|
+
// Emit every surface THIS port owns (loop so it generalizes beyond the
|
|
344
|
+
// single ts surface owned today).
|
|
345
|
+
for (const s of labeled.filter((s) => s.lang === "ts")) {
|
|
346
|
+
apiFiles.push(
|
|
347
|
+
...(await apiDocsFile({
|
|
348
|
+
subDir: s.subDir,
|
|
349
|
+
modelSurface: docsCfg.surfaces.includes("model"),
|
|
350
|
+
}).generate(ctx)),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
const msg = (err as Error).message;
|
|
355
|
+
if (msg.startsWith("docs: duplicate output path")) {
|
|
356
|
+
log.error(msg);
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
log.error(`docs: ${msg}`);
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
emit.push(...apiFiles);
|
|
363
|
+
// Surfaces owned by other ports: link only, with a pointer to where they
|
|
364
|
+
// get produced.
|
|
365
|
+
for (const s of labeled.filter((s) => s.lang !== "ts")) {
|
|
366
|
+
log.info(
|
|
367
|
+
`meta docs: api surface '${s.lang}' (${s.subDir}) is produced by that port's docs command — run it to populate those pages.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else if (hasConfig) {
|
|
371
|
+
// Config present but failed to load — already warned above; don't claim an
|
|
372
|
+
// api surface we couldn't build.
|
|
373
|
+
log.info("meta docs: api surface skipped — metaobjects.config.ts failed to load.");
|
|
374
|
+
} else {
|
|
375
|
+
log.info("meta docs: api surface skipped — no metaobjects.config.ts (nothing generated to document).");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await mkdir(outDir, { recursive: true });
|
|
381
|
+
for (const f of emit) {
|
|
382
|
+
const path = resolvePath(outDir, f.path);
|
|
383
|
+
await mkdir(resolvePath(path, ".."), { recursive: true });
|
|
384
|
+
await writeFile(path, f.content, "utf8");
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
log.error(`docs: failed to write pages: ${(err as Error).message}`);
|
|
388
|
+
return 1;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Summary: docsFile() emits ONE overview/index page (README.md) plus one page
|
|
392
|
+
// per entity and one per template.output. The entity count is the matched
|
|
393
|
+
// object count; the remaining non-overview model pages are template pages.
|
|
394
|
+
const entityCount = root.objects().filter(ctx.matches).length;
|
|
395
|
+
const modelOverview = modelFiles.filter((f) => f.path === "README.md").length;
|
|
396
|
+
const modelTemplates = modelFiles.length > 0
|
|
397
|
+
? modelFiles.length - entityCount - modelOverview
|
|
398
|
+
: 0;
|
|
399
|
+
const modelSummary = docsCfg.surfaces.includes("model")
|
|
400
|
+
? `${modelOverview} overview + ${entityCount} entity page(s) + ${modelTemplates} template page(s)`
|
|
401
|
+
: "model surface skipped";
|
|
402
|
+
const apiSummary = apiFiles.length > 0
|
|
403
|
+
? `${apiFiles.length} api page(s)`
|
|
404
|
+
: "no api pages";
|
|
405
|
+
log.info(`meta docs — wrote ${modelSummary}; ${apiSummary} → ${outDir}`);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* FR-033 S3 — emit the metamodel reference docs (INDEX.md + per-type pages +
|
|
411
|
+
* providers.md) from the BUILT-IN strict registry. No metadata, no config:
|
|
412
|
+
* `composeRegistry(coreProviders)` IS the source. Files land under
|
|
413
|
+
* `<out>` (which defaults to `./docs/metamodel`), preserving the renderer's
|
|
414
|
+
* `types/<family>.md` subtree.
|
|
415
|
+
*/
|
|
416
|
+
async function metamodelDocsCommand(cwd: string, out: string): Promise<number> {
|
|
417
|
+
const outDir = resolvePath(cwd, out);
|
|
418
|
+
let docs: Map<string, string>;
|
|
419
|
+
try {
|
|
420
|
+
docs = renderCoreMetamodelDocs(composeRegistry(coreProviders));
|
|
421
|
+
} catch (err) {
|
|
422
|
+
log.error(`docs: failed to render metamodel docs: ${(err as Error).message}`);
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
await mkdir(outDir, { recursive: true });
|
|
427
|
+
for (const [rel, content] of docs) {
|
|
428
|
+
const path = resolvePath(outDir, rel);
|
|
429
|
+
await mkdir(resolvePath(path, ".."), { recursive: true });
|
|
430
|
+
await writeFile(path, content, "utf8");
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
log.error(`docs: failed to write metamodel pages: ${(err as Error).message}`);
|
|
434
|
+
return 1;
|
|
435
|
+
}
|
|
436
|
+
log.info(`meta docs --metamodel — wrote ${docs.size} page(s) → ${outDir}`);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
package/src/commands/gen.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { resolveGenConfig } from "../lib/config.js";
|
|
|
5
5
|
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
6
6
|
import { formatGenResult, type GenFileEntry, type GenFileStatus } from "../lib/output.js";
|
|
7
7
|
import { log } from "../lib/log.js";
|
|
8
|
+
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
8
9
|
import { loadMemory, DEFAULT_METADATA_DIR } from "@metaobjectsdev/sdk";
|
|
9
|
-
import { runGen } from "@metaobjectsdev/codegen-ts";
|
|
10
|
+
import { runGen, listGenerators } from "@metaobjectsdev/codegen-ts";
|
|
10
11
|
import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
|
|
11
12
|
|
|
12
13
|
function mapStatus(s: WriteStatus): GenFileStatus {
|
|
@@ -26,6 +27,15 @@ export async function genCommand(args: string[], cwd: string): Promise<number> {
|
|
|
26
27
|
try { flags = parseGenArgs(args); }
|
|
27
28
|
catch (err) { log.error((err as Error).message); return 2; }
|
|
28
29
|
|
|
30
|
+
// ADR-0021 D3 — `meta gen --list`: print the stable-name generator registry
|
|
31
|
+
// and exit 0 WITHOUT running codegen (no config/metadata required).
|
|
32
|
+
if (flags.list) {
|
|
33
|
+
return listGeneratorsCommand();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Advisory: nudge to refresh the .claude/skills docs if they predate this CLI.
|
|
37
|
+
warnIfAgentContextStale(cwd);
|
|
38
|
+
|
|
29
39
|
const projectRoot = cwd;
|
|
30
40
|
const cliConfig = resolveGenConfig(flags);
|
|
31
41
|
|
|
@@ -112,3 +122,36 @@ export async function genCommand(args: string[], cwd: string): Promise<number> {
|
|
|
112
122
|
const hasFailure = files.some((f) => f.status === "conflict" || f.status === "refused");
|
|
113
123
|
return hasFailure ? 1 : 0;
|
|
114
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `meta gen --list` — print the stable-name generator registry (ADR-0021 D3).
|
|
128
|
+
*
|
|
129
|
+
* Generators are grouped by tier: the recommended native `meta gen` suite
|
|
130
|
+
* first, then neutral artifacts (owned by `meta docs` per D1). Each line is
|
|
131
|
+
* `<stable-name> — <description>` plus an options summary and, for neutral
|
|
132
|
+
* entries, a note pointing at the canonical door. Exits 0; no codegen runs.
|
|
133
|
+
*/
|
|
134
|
+
function listGeneratorsCommand(): number {
|
|
135
|
+
const entries = listGenerators();
|
|
136
|
+
const native = entries.filter((e) => e.tier === "native");
|
|
137
|
+
const neutral = entries.filter((e) => e.tier === "neutral");
|
|
138
|
+
const width = Math.max(...entries.map((e) => e.name.length));
|
|
139
|
+
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
lines.push("Available generators (select by stable name):");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("Native (recommended `meta gen` suite):");
|
|
144
|
+
for (const e of native) {
|
|
145
|
+
lines.push(` ${e.name.padEnd(width)} — ${e.description}`);
|
|
146
|
+
if (e.options) lines.push(` ${" ".repeat(width)} options: ${e.options}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("Neutral (owned by `meta docs`; not part of the native suite):");
|
|
150
|
+
for (const e of neutral) {
|
|
151
|
+
lines.push(` ${e.name.padEnd(width)} — ${e.description}`);
|
|
152
|
+
if (e.note) lines.push(` ${" ".repeat(width)} ${e.note}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log.info(lines.join("\n"));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|