@metaobjectsdev/cli 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +157 -59
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +28 -6
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +66 -22
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +8 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +142 -52
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +9 -2
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +399 -73
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +40 -18
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts +2 -0
- package/dist/src/commands/upgrade.d.ts.map +1 -0
- package/dist/src/commands/upgrade.js +163 -0
- package/dist/src/commands/upgrade.js.map +1 -0
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +360 -48
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +55 -19
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +4 -0
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/args.d.ts +16 -2
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -3
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +8 -1
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +9 -1
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +22 -37
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +79 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +123 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
- package/dist/src/lib/manifest-ignored-check.js +74 -0
- package/dist/src/lib/manifest-ignored-check.js.map +1 -0
- package/dist/src/lib/migrate-scope.d.ts +49 -0
- package/dist/src/lib/migrate-scope.d.ts.map +1 -0
- package/dist/src/lib/migrate-scope.js +92 -0
- package/dist/src/lib/migrate-scope.js.map +1 -0
- package/dist/src/lib/output.d.ts +1 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -3
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +10 -57
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +11 -11
- package/src/commands/docs.ts +174 -61
- package/src/commands/export.ts +30 -6
- package/src/commands/gen.ts +68 -22
- package/src/commands/init.ts +161 -58
- package/src/commands/migrate.ts +423 -75
- package/src/commands/prompt-snapshot.ts +43 -18
- package/src/commands/upgrade.ts +179 -0
- package/src/commands/verify.ts +397 -48
- package/src/index.ts +55 -19
- package/src/lib/allow.ts +4 -0
- package/src/lib/args.ts +35 -4
- package/src/lib/codegen-drift.ts +9 -0
- package/src/lib/detect-stack.ts +20 -33
- package/src/lib/load-metaobjects-config.ts +129 -2
- package/src/lib/manifest-ignored-check.ts +75 -0
- package/src/lib/migrate-scope.ts +102 -0
- package/src/lib/output.ts +8 -4
- package/src/lib/requirement-check.ts +10 -58
- package/dist/src/lib/verified-by-scan.d.ts +0 -20
- package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
- package/dist/src/lib/verified-by-scan.js +0 -344
- package/dist/src/lib/verified-by-scan.js.map +0 -1
- package/src/lib/verified-by-scan.ts +0 -375
package/src/commands/docs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `meta docs <
|
|
1
|
+
// `meta docs [<project-root>] --out <dir>` — STANDALONE neutral metadata docs.
|
|
2
2
|
//
|
|
3
3
|
// Emits one neutral page per entity (`<Entity>.md`) and one per
|
|
4
4
|
// `template.output` (`<Template>.md`) from metadata ALONE — no gen config, no
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import { resolve as resolvePath, basename } from "node:path";
|
|
15
15
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
16
16
|
import { log } from "../lib/log.js";
|
|
17
|
-
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
18
|
-
import { loadMemory,
|
|
17
|
+
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
18
|
+
import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import {
|
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
ResolvedDocsConfig,
|
|
32
32
|
DocsSurface,
|
|
33
33
|
} from "@metaobjectsdev/codegen-ts";
|
|
34
|
-
import { docsFile, apiDocsFile } from "@metaobjectsdev/codegen-ts/generators";
|
|
34
|
+
import { docsFile, apiDocsFile, requirementsFile } from "@metaobjectsdev/codegen-ts/generators";
|
|
35
35
|
import { composeRegistry, coreProviders, renderCoreMetamodelDocs } from "@metaobjectsdev/metadata";
|
|
36
36
|
import type { MetaDataTypeProvider } from "@metaobjectsdev/metadata";
|
|
37
37
|
import { generateSite, SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } from "@metaobjectsdev/docs-site";
|
|
@@ -39,8 +39,12 @@ import { generateSite, SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } fro
|
|
|
39
39
|
type DocsLayout = "flat" | "package";
|
|
40
40
|
|
|
41
41
|
interface DocsFlags {
|
|
42
|
-
/**
|
|
43
|
-
metadata
|
|
42
|
+
/** The PROJECT ROOT to resolve metadata from — the directory that CONTAINS
|
|
43
|
+
* the metadata, never the metadata directory itself (#344). Named for what
|
|
44
|
+
* it is: while it was called `metadata` the help text spelled the positional
|
|
45
|
+
* `<metadata>`, which is what the Python and C# `docs` positionals actually
|
|
46
|
+
* mean, and pointing this one at `metaobjects/` fails. */
|
|
47
|
+
projectRoot: string;
|
|
44
48
|
/** Output directory for the rendered pages. */
|
|
45
49
|
out: string;
|
|
46
50
|
/** Page-placement layout. `flat` (default) writes `<Name>.md` at the out
|
|
@@ -48,7 +52,7 @@ interface DocsFlags {
|
|
|
48
52
|
* for multi-package models with repeated short names). */
|
|
49
53
|
layout: DocsLayout;
|
|
50
54
|
/** Optional override for the project root used to resolve adopter
|
|
51
|
-
* `templates/` overrides. Defaults to the
|
|
55
|
+
* `templates/` overrides. Defaults to the project root. */
|
|
52
56
|
templates?: string;
|
|
53
57
|
/** Optional directory holding the prompt `.mustache` sources, for a project
|
|
54
58
|
* whose templates live outside the conventional `metaobjects/` or `templates/`
|
|
@@ -66,6 +70,12 @@ interface DocsFlags {
|
|
|
66
70
|
outProvided: boolean;
|
|
67
71
|
/** Whether `--layout` was explicitly passed (same override semantics). */
|
|
68
72
|
layoutProvided: boolean;
|
|
73
|
+
/** Whether the `<project-root>` positional was explicitly passed. An explicit path
|
|
74
|
+
* DEFINES the source set (`resolveCollection`'s `explicitDir` pin); the default
|
|
75
|
+
* cwd discovers one by walking up. Without this distinction the two are
|
|
76
|
+
* indistinguishable at the call site, which is how an explicitly-scoped run came
|
|
77
|
+
* to have an ancestor config's sources unioned into it (#327). */
|
|
78
|
+
projectRootProvided: boolean;
|
|
69
79
|
/** FR-033 S3 — document the METAMODEL ITSELF (the built-in type/subtype/attr
|
|
70
80
|
* vocabulary) instead of a user's entities. Needs NO metadata + NO config. */
|
|
71
81
|
metamodel: boolean;
|
|
@@ -87,7 +97,7 @@ function parseLayout(v: string | undefined, flag: string): DocsLayout {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
90
|
-
let
|
|
100
|
+
let projectRoot: string | undefined;
|
|
91
101
|
let out: string | undefined;
|
|
92
102
|
let templates: string | undefined;
|
|
93
103
|
let prompts: string | undefined;
|
|
@@ -95,6 +105,7 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
95
105
|
let baseUrl: string | undefined;
|
|
96
106
|
let wantModel = false;
|
|
97
107
|
let wantApi = false;
|
|
108
|
+
let wantRequirements = false;
|
|
98
109
|
let wantMetamodel = false;
|
|
99
110
|
let wantSite = false;
|
|
100
111
|
let wantScaffoldSite = false;
|
|
@@ -120,6 +131,8 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
120
131
|
wantModel = true;
|
|
121
132
|
} else if (a === "--api") {
|
|
122
133
|
wantApi = true;
|
|
134
|
+
} else if (a === "--requirements") {
|
|
135
|
+
wantRequirements = true;
|
|
123
136
|
} else if (a === "--metamodel") {
|
|
124
137
|
wantMetamodel = true;
|
|
125
138
|
} else if (a === "--site") {
|
|
@@ -146,8 +159,8 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
146
159
|
prompts = a.slice("--prompts=".length);
|
|
147
160
|
} else if (a.startsWith("-")) {
|
|
148
161
|
throw new Error(`unknown flag: ${a}`);
|
|
149
|
-
} else if (
|
|
150
|
-
|
|
162
|
+
} else if (projectRoot === undefined) {
|
|
163
|
+
projectRoot = a;
|
|
151
164
|
} else {
|
|
152
165
|
throw new Error(`unexpected argument: ${a}`);
|
|
153
166
|
}
|
|
@@ -161,11 +174,13 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
161
174
|
const surfaces: DocsSurface[] = [];
|
|
162
175
|
if (wantModel) surfaces.push("model");
|
|
163
176
|
if (wantApi) surfaces.push("api");
|
|
177
|
+
if (wantRequirements) surfaces.push("requirements");
|
|
164
178
|
return {
|
|
165
|
-
// `<
|
|
166
|
-
// (mirrors how migrate/gen treat the working directory as the root).
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
// `<project-root>` is the project root that CONTAINS the metadata; default
|
|
180
|
+
// cwd (mirrors how migrate/gen treat the working directory as the root).
|
|
181
|
+
projectRoot: projectRoot ?? cwd,
|
|
182
|
+
projectRootProvided: projectRoot !== undefined,
|
|
183
|
+
// Default out dir, resolved against the project root below. In --metamodel
|
|
169
184
|
// mode the renderer writes under <out>/metamodel/, default ./docs/metamodel.
|
|
170
185
|
out: out ?? (wantMetamodel ? "./docs/metamodel" : "./docs"),
|
|
171
186
|
// Default flat preserves today's single-package output (+ existing goldens).
|
|
@@ -199,23 +214,75 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
199
214
|
return metamodelDocsCommand(cwd, flags.out);
|
|
200
215
|
}
|
|
201
216
|
|
|
202
|
-
const metaRoot = resolvePath(cwd, flags.
|
|
217
|
+
const metaRoot = resolvePath(cwd, flags.projectRoot);
|
|
203
218
|
// Absolute prompt-source dir for the site (--prompts), for a project whose
|
|
204
219
|
// templates live outside metaobjects/ or templates/ (e.g. data/templates/).
|
|
205
220
|
const promptsDir = flags.prompts !== undefined ? resolvePath(cwd, flags.prompts) : undefined;
|
|
206
221
|
|
|
207
222
|
// `--scaffold-site`: copy the docs-site templates + assets into codegen/docs-site/
|
|
208
223
|
// so the consumer owns them (ADR-0034 scaffold-and-own). Scaffold and return —
|
|
209
|
-
// it does not also generate.
|
|
224
|
+
// it does not also generate. `resolveConfigDir` rather than `resolveCollection`:
|
|
225
|
+
// scaffolding needs no metadata, but it must write where `emitSite` will READ
|
|
226
|
+
// (below, under the resolved project root), and that is the same walk.
|
|
210
227
|
if (flags.scaffoldSite) {
|
|
211
|
-
|
|
228
|
+
// `resolveGenConfigDir` over the bare walk: `emitSite` reads the owned theme
|
|
229
|
+
// from the directory holding `metaobjects.config.ts`, so scaffolding must write
|
|
230
|
+
// to the same one or the theme is scaffolded where nothing will ever read it.
|
|
231
|
+
return scaffoldSiteCommand(resolveGenConfigDir(metaRoot, await resolveConfigDir(metaRoot)));
|
|
212
232
|
}
|
|
213
233
|
|
|
234
|
+
// Discovery and load are two separate failure modes, kept in separate try
|
|
235
|
+
// blocks deliberately — same reasoning as `meta gen` (gen.ts): a broad
|
|
236
|
+
// catch around both would swallow a genuine ParseError as "no metaobjects/
|
|
237
|
+
// found", masking the real failure.
|
|
238
|
+
//
|
|
239
|
+
// Discovery runs BEFORE the config read, deliberately, and `meta gen` calls
|
|
240
|
+
// out the same ordering as the thing it fixed: the project root is whichever
|
|
241
|
+
// directory `resolveCollection` decided the metadata belongs to, so
|
|
242
|
+
// everything project-relative — `metaobjects.config.ts` and its providers,
|
|
243
|
+
// the `docs.outDir` it names, the adopter `templates/` overrides, the owned
|
|
244
|
+
// `codegen/docs-site/` theme — has to come from that same directory. Reading
|
|
245
|
+
// the config from the ambient `<project-root>` argument while the metadata came
|
|
246
|
+
// from an ancestor renders the ancestor's model with the subdirectory's
|
|
247
|
+
// (absent) providers. For a run at the project root the two are the same path.
|
|
248
|
+
//
|
|
249
|
+
// An EXPLICIT `<project-root>` positional pins the collection rather than seeding a
|
|
250
|
+
// walk (#327). The argument has always meant "document this": before sources were
|
|
251
|
+
// resolvable it read `<path>/metaobjects/` and nothing else, and passing it to
|
|
252
|
+
// discovery turned it into a starting point, so the nearest ancestor
|
|
253
|
+
// `.metaobjects/config.json` was found and ITS sources were unioned in — a command
|
|
254
|
+
// whose entire purpose is documenting one subset silently documenting the whole
|
|
255
|
+
// repo, at exit 0. Pinned, `<path>` governs: its own config if it has one, the
|
|
256
|
+
// default `<path>/metaobjects` if not. A bare `meta docs` still discovers, which is
|
|
257
|
+
// the right default for "document the project I am standing in".
|
|
258
|
+
let collection: Awaited<ReturnType<typeof resolveCollection>>;
|
|
259
|
+
try {
|
|
260
|
+
collection = await resolveCollection(
|
|
261
|
+
metaRoot,
|
|
262
|
+
flags.projectRootProvided ? { explicitDir: metaRoot } : undefined,
|
|
263
|
+
);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log.error(`docs: ${(err as Error).message}`);
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Which directory's `metaobjects.config.ts` governs is its OWN nearest-ancestor
|
|
270
|
+
// walk, not the collection's: the two files answer different questions (design
|
|
271
|
+
// §4.6), and in a Maven- or pip-rooted monorepo the collection is declared at the
|
|
272
|
+
// repo root while the TS config sits in the app. Reading the second from the
|
|
273
|
+
// first made docs silently drop that app's providers and skip its api surface
|
|
274
|
+
// entirely (#326). Everything that config names follows it — the `docs.outDir` it
|
|
275
|
+
// carries, the adopter `templates/` overrides and the owned `codegen/docs-site/`
|
|
276
|
+
// theme beside it. Same directory whenever the two files sit together, which is
|
|
277
|
+
// every `meta init` project.
|
|
278
|
+
const genConfigDir = resolveGenConfigDir(metaRoot, collection.configDir);
|
|
279
|
+
|
|
214
280
|
// The project root used to resolve adopter `templates/` overrides; the
|
|
215
|
-
// framework defaults sit underneath via projectProvider's chain.
|
|
281
|
+
// framework defaults sit underneath via projectProvider's chain. `--templates`
|
|
282
|
+
// is the one explicit override.
|
|
216
283
|
const projectRoot = flags.templates !== undefined
|
|
217
284
|
? resolvePath(cwd, flags.templates)
|
|
218
|
-
:
|
|
285
|
+
: genConfigDir;
|
|
219
286
|
|
|
220
287
|
// Best-effort load of metaobjects.config.ts to pick up consumer-supplied
|
|
221
288
|
// providers (e.g. a project's custom field/object subtypes). Unlike `gen`,
|
|
@@ -225,12 +292,14 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
225
292
|
// if the metadata genuinely uses an unregistered type.
|
|
226
293
|
let loadedConfig: Awaited<ReturnType<typeof loadMetaobjectsConfig>> | undefined;
|
|
227
294
|
let configProviders: NonNullable<Awaited<ReturnType<typeof loadMetaobjectsConfig>>["providers"]> | undefined;
|
|
295
|
+
// The same config's contributions to the metadata LOAD — providers plus the shipped
|
|
296
|
+
// `libraries` a project opts into (#333). `configProviders` stays separate because the
|
|
297
|
+
// site surface has its own loader and takes providers alone.
|
|
298
|
+
let configLoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
228
299
|
// hasConfig gates the api surface: api docs describe the GENERATED REST
|
|
229
300
|
// surface, which only exists when there is a (loadable) gen config. A config
|
|
230
301
|
// that EXISTS but fails to load degrades to model-only with a warning.
|
|
231
|
-
const hasConfig = existsSync(join(
|
|
232
|
-
// The config lives alongside metaobjects/ at the metadata root (metaRoot);
|
|
233
|
-
// projectRoot only diverges when --templates overrides the template lookup.
|
|
302
|
+
const hasConfig = existsSync(join(genConfigDir, "metaobjects.config.ts"));
|
|
234
303
|
// Only attempt the load when the file is actually present: absence is the
|
|
235
304
|
// expected config-less case (stay silent), but a config that EXISTS yet fails
|
|
236
305
|
// to load is surfaced as a warning rather than silently degrading to
|
|
@@ -238,8 +307,9 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
238
307
|
// cryptic unknown-subtype error instead of the real config error.
|
|
239
308
|
if (hasConfig) {
|
|
240
309
|
try {
|
|
241
|
-
loadedConfig = await loadMetaobjectsConfig(
|
|
310
|
+
loadedConfig = await loadMetaobjectsConfig(genConfigDir);
|
|
242
311
|
configProviders = loadedConfig.providers;
|
|
312
|
+
configLoadOptions = loadMemoryOptionsFrom(loadedConfig);
|
|
243
313
|
} catch (err) {
|
|
244
314
|
log.warn(
|
|
245
315
|
`docs: metaobjects.config.ts failed to load (${(err as Error).message}); ` +
|
|
@@ -247,6 +317,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
247
317
|
);
|
|
248
318
|
loadedConfig = undefined;
|
|
249
319
|
configProviders = undefined;
|
|
320
|
+
configLoadOptions = {};
|
|
250
321
|
}
|
|
251
322
|
}
|
|
252
323
|
|
|
@@ -269,7 +340,11 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
269
340
|
cliOverrides,
|
|
270
341
|
loadedConfig?.outputLayout ?? "flat",
|
|
271
342
|
);
|
|
272
|
-
|
|
343
|
+
// Against `genConfigDir`, not the collection's: `docs.outDir` is declared in
|
|
344
|
+
// `metaobjects.config.ts`, so it resolves against the directory that config lives
|
|
345
|
+
// in — the same rule `meta gen` applies to its own `outDir`. Identical whenever
|
|
346
|
+
// the two files sit together.
|
|
347
|
+
const outDir = resolvePath(genConfigDir, docsCfg.outDir);
|
|
273
348
|
|
|
274
349
|
// SITE surface has its OWN model loader (docs-site's loadModel — NOT the sdk
|
|
275
350
|
// loadMemory below) and needs no gen config. When the site is the ONLY
|
|
@@ -277,23 +352,19 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
277
352
|
// WITHOUT building the markdown GenContext — decoupled and one fewer failure
|
|
278
353
|
// surface. Combined with --model/--api it is emitted after them (below).
|
|
279
354
|
if (flags.site && docsCfg.surfaces.length === 0) {
|
|
280
|
-
return emitSite(
|
|
355
|
+
return emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
|
|
281
356
|
}
|
|
282
357
|
|
|
283
358
|
// Load metadata standalone — same loader path as migrate/gen. Threads any
|
|
284
359
|
// consumer providers from the config so custom types resolve.
|
|
285
360
|
let root;
|
|
286
361
|
try {
|
|
287
|
-
root = await loadMemory(
|
|
288
|
-
|
|
362
|
+
root = await loadMemory(collection.configDir, {
|
|
363
|
+
files: collection.files,
|
|
364
|
+
...configLoadOptions,
|
|
289
365
|
});
|
|
290
366
|
} catch (err) {
|
|
291
|
-
|
|
292
|
-
if (!existsSync(join(metaRoot, DEFAULT_METADATA_DIR))) {
|
|
293
|
-
log.error(`docs: no metaobjects/ found in ${metaRoot}; run 'meta init' to scaffold`);
|
|
294
|
-
} else {
|
|
295
|
-
log.error(`docs: failed to load metadata: ${msg}`);
|
|
296
|
-
}
|
|
367
|
+
log.error(`docs: failed to load metadata: ${(err as Error).message}`);
|
|
297
368
|
return 2;
|
|
298
369
|
}
|
|
299
370
|
|
|
@@ -385,6 +456,16 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
385
456
|
emit.push(...modelFiles);
|
|
386
457
|
}
|
|
387
458
|
|
|
459
|
+
// REQUIREMENTS surface — the declared ledger as documentation (requirements.md +
|
|
460
|
+
// requirements.toon). Metadata-alone like the model surface: it reads ctx.loadedRoot
|
|
461
|
+
// and nothing else, so it is NOT gated on a loadable gen config the way `api` is.
|
|
462
|
+
//
|
|
463
|
+
// Emits ZERO files when the project declares no `requirement.*` node, which is what
|
|
464
|
+
// makes this surface safe to default ON — a project without a ledger sees no change.
|
|
465
|
+
if (docsCfg.surfaces.includes("requirements")) {
|
|
466
|
+
emit.push(...(await requirementsFile().generate(ctx)));
|
|
467
|
+
}
|
|
468
|
+
|
|
388
469
|
// API surface — the SDK reference for the GENERATED REST surface, side by side
|
|
389
470
|
// under each surface's subDir. THIS command only OWNS the surfaces it can
|
|
390
471
|
// generate — i.e. its own port (lang "ts"). Surfaces owned by other ports are
|
|
@@ -444,7 +525,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
444
525
|
|
|
445
526
|
// SITE surface (additive) — emit after the markdown surfaces so both coexist.
|
|
446
527
|
if (flags.site) {
|
|
447
|
-
const siteRc = await emitSite(
|
|
528
|
+
const siteRc = await emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
|
|
448
529
|
if (siteRc !== 0) return siteRc;
|
|
449
530
|
}
|
|
450
531
|
|
|
@@ -462,7 +543,12 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
462
543
|
const apiSummary = apiFiles.length > 0
|
|
463
544
|
? `${apiFiles.length} api page(s)`
|
|
464
545
|
: "no api pages";
|
|
465
|
-
|
|
546
|
+
// Reported only when it actually wrote something. A project with no ledger emits no
|
|
547
|
+
// requirements file, and saying "0 requirement pages" would advertise a surface that
|
|
548
|
+
// did not run — the opposite of the silence the empty-ledger guard exists to produce.
|
|
549
|
+
const requirementFiles = emit.filter((f) => f.path.startsWith("requirements.")).length;
|
|
550
|
+
const reqSummary = requirementFiles > 0 ? `; ${requirementFiles} requirement file(s)` : "";
|
|
551
|
+
log.info(`meta docs — wrote ${modelSummary}; ${apiSummary}${reqSummary} → ${outDir}`);
|
|
466
552
|
return 0;
|
|
467
553
|
}
|
|
468
554
|
|
|
@@ -471,9 +557,9 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
471
557
|
* into `<root>/codegen/docs-site/{templates,assets}`, writing each file ONLY if
|
|
472
558
|
* absent so a re-run never clobbers a hand-edited file.
|
|
473
559
|
*/
|
|
474
|
-
async function scaffoldSiteCommand(
|
|
475
|
-
const tplDir = join(
|
|
476
|
-
const astDir = join(
|
|
560
|
+
async function scaffoldSiteCommand(projectRoot: string): Promise<number> {
|
|
561
|
+
const tplDir = join(projectRoot, "codegen/docs-site/templates");
|
|
562
|
+
const astDir = join(projectRoot, "codegen/docs-site/assets");
|
|
477
563
|
const created: string[] = [];
|
|
478
564
|
const preserved: string[] = [];
|
|
479
565
|
try {
|
|
@@ -499,53 +585,80 @@ async function scaffoldSiteCommand(metaRoot: string): Promise<number> {
|
|
|
499
585
|
}
|
|
500
586
|
log.info(
|
|
501
587
|
`meta docs --scaffold-site — ${created.length} created, ${preserved.length} preserved ` +
|
|
502
|
-
`→ ${join(
|
|
588
|
+
`→ ${join(projectRoot, "codegen/docs-site")} (edit these to own your theme)`,
|
|
503
589
|
);
|
|
504
590
|
return 0;
|
|
505
591
|
}
|
|
506
592
|
|
|
507
593
|
/**
|
|
508
594
|
* Emit the browsable HTML documentation site via `@metaobjectsdev/docs-site`.
|
|
509
|
-
* The site loads the model with its OWN loader from the
|
|
510
|
-
* (
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
595
|
+
* The site loads the model with its OWN loader from the collection's declared
|
|
596
|
+
* source ROOTS (whole directories, one page group each) rather than from the
|
|
597
|
+
* per-file list the sdk `loadMemory` path takes, so this is independent of the
|
|
598
|
+
* markdown surfaces. Writes under `<outDir>/site` so it can coexist with the markdown
|
|
599
|
+
* output. Scaffold-and-own: when the consumer has copied templates/assets
|
|
600
|
+
* into `<projectRoot>/codegen/docs-site/` (via `--scaffold-site`), those win
|
|
601
|
+
* over the bundled defaults.
|
|
602
|
+
*
|
|
603
|
+
* Takes the ALREADY-RESOLVED collection: `docsCommand` resolved it to read the
|
|
604
|
+
* config from the right directory, and resolving a second time here made the
|
|
605
|
+
* combined `--model --site` path do the whole discovery-and-config walk twice.
|
|
515
606
|
*/
|
|
516
607
|
async function emitSite(
|
|
517
|
-
|
|
608
|
+
collection: Collection,
|
|
609
|
+
projectRoot: string,
|
|
610
|
+
/** Directory holding `metaobjects.config.ts` — where `--scaffold-site` writes the
|
|
611
|
+
* owned theme, so where this must read it from. Distinct from `collection.configDir`
|
|
612
|
+
* since #326: in a polyglot monorepo the collection is declared at the repo root
|
|
613
|
+
* while the TS package (and its owned theme) sits in the app. */
|
|
614
|
+
genConfigDir: string,
|
|
518
615
|
outDir: string,
|
|
519
616
|
configProviders?: readonly MetaDataTypeProvider[],
|
|
520
617
|
promptsDir?: string,
|
|
521
618
|
): Promise<number> {
|
|
522
619
|
const siteOutDir = resolvePath(outDir, "site");
|
|
523
|
-
//
|
|
524
|
-
// Prompt `.mustache` source is
|
|
525
|
-
// <root>/templates/ and any
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
620
|
+
// The resolved metadata source dir(s) are REQUIRED (the site loads the
|
|
621
|
+
// model from them) and always first. Prompt `.mustache` source is
|
|
622
|
+
// additionally searched in the conventional <root>/templates/ and any
|
|
623
|
+
// explicit --prompts dir (for a project whose templates live elsewhere,
|
|
624
|
+
// e.g. data/templates/) — else the site can't show the prompt TEXT and
|
|
625
|
+
// prints a "source missing" note. Only existing dirs are added.
|
|
626
|
+
//
|
|
627
|
+
// Deduped by resolved PATH, not by basename. Two DIFFERENT directories that
|
|
628
|
+
// happen to share a basename are a legitimate multi-source project (`metaobjects`
|
|
629
|
+
// plus `../shared-model/metaobjects`); `loadModel` disambiguates their site
|
|
630
|
+
// group names, so refusing the pair here — which a basename key did, by
|
|
631
|
+
// dropping the second — would break the feature this branch exists to ship.
|
|
632
|
+
// The same directory named twice is the real hazard: it would be symlinked
|
|
633
|
+
// and loaded twice.
|
|
634
|
+
// The DECLARED source roots, not directories re-derived from the resolved
|
|
635
|
+
// files: a declared source directory holding no metadata yet would otherwise
|
|
636
|
+
// vanish from the site's group list entirely, and `sourceDirs` could come back
|
|
637
|
+
// empty where the pre-branch code always passed `<root>/metaobjects`.
|
|
638
|
+
const sourceDirs = [...collection.sourceRoots];
|
|
639
|
+
const seenDirs = new Set(sourceDirs);
|
|
531
640
|
if (promptsDir !== undefined && !existsSync(promptsDir)) {
|
|
532
641
|
log.warn(`docs: --prompts dir does not exist: ${promptsDir}`);
|
|
533
642
|
}
|
|
534
|
-
for (const d of [join(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
643
|
+
for (const d of [join(projectRoot, "templates"), ...(promptsDir !== undefined ? [promptsDir] : [])]) {
|
|
644
|
+
const abs = resolvePath(d);
|
|
645
|
+
if (existsSync(abs) && !seenDirs.has(abs)) {
|
|
646
|
+
sourceDirs.push(abs);
|
|
647
|
+
seenDirs.add(abs);
|
|
538
648
|
}
|
|
539
649
|
}
|
|
540
650
|
// Scaffold-and-own: when the consumer has copied templates/assets into
|
|
541
651
|
// codegen/docs-site/ (via --scaffold-site), use those; else the bundled defaults.
|
|
542
|
-
|
|
543
|
-
|
|
652
|
+
// Keyed on `genConfigDir`, NOT `projectRoot`: `--templates` redirects the adopter
|
|
653
|
+
// RENDER template chain (the `templates/` above), and letting it also move the
|
|
654
|
+
// docs-site theme would read it from somewhere `--scaffold-site` never writes.
|
|
655
|
+
const ownedTemplates = join(genConfigDir, "codegen/docs-site/templates");
|
|
656
|
+
const ownedAssets = join(genConfigDir, "codegen/docs-site/assets");
|
|
544
657
|
try {
|
|
545
658
|
const r = await generateSite({
|
|
546
659
|
sourceDirs,
|
|
547
660
|
outDir: siteOutDir,
|
|
548
|
-
title: basename(
|
|
661
|
+
title: basename(collection.configDir) || "Metadata",
|
|
549
662
|
stamp: new Date().toISOString().slice(0, 10),
|
|
550
663
|
commit: "",
|
|
551
664
|
core: { n: 15 },
|
package/src/commands/export.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { resolve
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
3
|
import { parseExportArgs } from "../lib/args.js";
|
|
4
4
|
import { log } from "../lib/log.js";
|
|
5
|
-
import {
|
|
6
|
-
import { TypeRegistry, registerCoreTypes } from "@metaobjectsdev/metadata";
|
|
7
|
-
import {
|
|
5
|
+
import { FileSource } from "@metaobjectsdev/metadata/core";
|
|
6
|
+
import { TypeRegistry, registerCoreTypes, MetaDataLoader, canonicalSerialize } from "@metaobjectsdev/metadata";
|
|
7
|
+
import { registerForgeTypes, resolveCollection } from "@metaobjectsdev/sdk";
|
|
8
8
|
|
|
9
9
|
export async function exportCommand(args: string[], cwd: string): Promise<number> {
|
|
10
10
|
let flags;
|
|
@@ -16,7 +16,6 @@ export async function exportCommand(args: string[], cwd: string): Promise<number
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const projectRoot = cwd;
|
|
19
|
-
const metadataDir = join(projectRoot, DEFAULT_METADATA_DIR);
|
|
20
19
|
|
|
21
20
|
// Build a registry with core + forge types so metadata that includes
|
|
22
21
|
// descriptive types (decision, principle, etc.) loads without errors.
|
|
@@ -24,7 +23,32 @@ export async function exportCommand(args: string[], cwd: string): Promise<number
|
|
|
24
23
|
registerCoreTypes(registry);
|
|
25
24
|
registerForgeTypes(registry);
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
// `export` has never used exit 2 for a metadata problem — only for a bad CLI
|
|
27
|
+
// flag (see parseExportArgs above). Previously any directory-load failure
|
|
28
|
+
// surfaced through loadAndExportJson's collected result.errors (exit 1); a
|
|
29
|
+
// resolveCollection failure (no declared sources, no default metaobjects/,
|
|
30
|
+
// or a malformed config.json) is the same class of problem and is reported
|
|
31
|
+
// the same way, to keep that contract exactly as it was.
|
|
32
|
+
let files: readonly string[];
|
|
33
|
+
try {
|
|
34
|
+
files = (await resolveCollection(projectRoot)).files;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
log.error((err as Error).message);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `loadAndExportJson` only accepts a scanned directory (`MetaDataLoader.fromDirectory`);
|
|
41
|
+
// `resolveCollection` already resolved the file SET (declared `sources`, or the
|
|
42
|
+
// `metaobjects/` default), so load that list directly via the same loader +
|
|
43
|
+
// serializer `loadAndExportJson` composes, rather than re-deriving a directory.
|
|
44
|
+
const loadResult = await new MetaDataLoader({ registry }).load(
|
|
45
|
+
files.map((f) => new FileSource(f)),
|
|
46
|
+
);
|
|
47
|
+
const result = {
|
|
48
|
+
json: canonicalSerialize(loadResult.root),
|
|
49
|
+
errors: loadResult.errors,
|
|
50
|
+
warnings: loadResult.warnings.map((w) => w.message),
|
|
51
|
+
};
|
|
28
52
|
|
|
29
53
|
for (const w of result.warnings) {
|
|
30
54
|
log.warn(w);
|
package/src/commands/gen.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { relative
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { relative } from "node:path";
|
|
3
2
|
import { parseGenArgs } from "../lib/args.js";
|
|
4
3
|
import { resolveGenConfig } from "../lib/config.js";
|
|
5
|
-
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
4
|
+
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
6
5
|
import { formatGenResult, formatGenResultToon, type GenFileEntry, type GenFileStatus } from "../lib/output.js";
|
|
7
6
|
import { formatGenResultJson } from "../lib/output-json.js";
|
|
8
7
|
import type { OutputFormat } from "../lib/format.js";
|
|
9
8
|
import { log } from "../lib/log.js";
|
|
10
9
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
10
|
+
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
11
11
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
12
|
-
import { loadMemory,
|
|
12
|
+
import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
|
|
13
13
|
import { runGen, listGenerators } from "@metaobjectsdev/codegen-ts";
|
|
14
14
|
import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
|
|
15
15
|
|
|
@@ -22,6 +22,10 @@ function mapStatus(s: WriteStatus): GenFileStatus {
|
|
|
22
22
|
case "unchanged":
|
|
23
23
|
case "skipped": return "unchanged";
|
|
24
24
|
case "refused": return "refused";
|
|
25
|
+
// FR-038 §8 — a generated file deleted because it is no longer generated.
|
|
26
|
+
// Shown as its own outcome, not folded into "unchanged": a run summary that
|
|
27
|
+
// lists writes but hides deletions is how a silent deletion happens.
|
|
28
|
+
case "removed": return "removed";
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -36,12 +40,59 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
36
40
|
return listGeneratorsCommand();
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
// Advisory: nudge to refresh the .claude/skills docs if they predate this CLI.
|
|
40
|
-
warnIfAgentContextStale(cwd);
|
|
41
|
-
|
|
42
|
-
const projectRoot = cwd;
|
|
43
43
|
const cliConfig = resolveGenConfig(flags);
|
|
44
44
|
|
|
45
|
+
// Discovery and load are two separate failure modes, kept in separate try
|
|
46
|
+
// blocks deliberately: a broad catch around both previously swallowed
|
|
47
|
+
// genuine ParseErrors (e.g. `origin.@via "X.y" ...: no such relationship
|
|
48
|
+
// "y" on X`) as "no metaobjects/ found", masking the real failure.
|
|
49
|
+
// `resolveCollection` raises `ERR_COLLECTION_NOT_FOUND` with its own
|
|
50
|
+
// message when nothing is discovered and no default directory exists.
|
|
51
|
+
//
|
|
52
|
+
// Discovery runs BEFORE the config read, deliberately: everything named BY the
|
|
53
|
+
// metadata resolves against the directory `resolveCollection` decided the
|
|
54
|
+
// metadata belongs to. Reading THAT from ambient cwd while the metadata came
|
|
55
|
+
// from an ancestor is the divergence this design exists to remove (design
|
|
56
|
+
// §4.6.1).
|
|
57
|
+
let collection;
|
|
58
|
+
try {
|
|
59
|
+
collection = await resolveCollection(cwd);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error((err as Error).message);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
// ...but `metaobjects.config.ts` is not named by the metadata: it is this
|
|
65
|
+
// package's own answer to a different question (design §4.6 — "how is code
|
|
66
|
+
// generated here?", per-port, versus the port-neutral "where does metadata come
|
|
67
|
+
// from?"). It gets its own nearest-ancestor walk, so a JS app under a Maven- or
|
|
68
|
+
// pip-rooted repo generates with the config sitting beside it rather than
|
|
69
|
+
// demanding one at the repo root that no such repo has (#326). Everything the
|
|
70
|
+
// TS config names follows it — `outDir`, `targets`, and the
|
|
71
|
+
// `.metaobjects/.gen-state/` merge base that mirrors that output, which must be
|
|
72
|
+
// per-package or two apps sharing one collection would clobber each other's.
|
|
73
|
+
// Nearest wins, so a subdirectory declaring nothing still walks up to the
|
|
74
|
+
// project root's config exactly as before.
|
|
75
|
+
const projectRoot = resolveGenConfigDir(cwd, collection.configDir);
|
|
76
|
+
|
|
77
|
+
// ...and a package that declares its own sources GENERATES from them (#340). An
|
|
78
|
+
// ancestor collection is the default for a package that declares none, never an
|
|
79
|
+
// addition to one that does — otherwise a sub-project's output silently absorbs
|
|
80
|
+
// metadata from unrelated trees. Identical object when the two configs sit together.
|
|
81
|
+
const genCollection = await resolveGenCollection(collection, projectRoot);
|
|
82
|
+
|
|
83
|
+
// Advisory: nudge to refresh the .claude/skills docs if they predate this CLI.
|
|
84
|
+
// Rooted at `projectRoot`, not ambient cwd — the scaffolded agent context sits
|
|
85
|
+
// with the project that declares the metadata, so a run from a subdirectory
|
|
86
|
+
// would find no manifest there and silently skip the nudge. `meta verify` makes
|
|
87
|
+
// the same call for the same reason; the two commands describe this and the
|
|
88
|
+
// anti-pattern scan below as one advisory pass, so they must scan one tree.
|
|
89
|
+
warnIfAgentContextStale(projectRoot);
|
|
90
|
+
// Advisory: the committed hash manifest is what makes hand-edit detection work on a
|
|
91
|
+
// machine that did not generate the output. Silent unless it is ignored. Keyed on
|
|
92
|
+
// projectRoot, not cwd, for the same reason its neighbour is — the manifest belongs to
|
|
93
|
+
// whichever directory `resolveCollection` decided the metadata lives in.
|
|
94
|
+
warnIfManifestIgnored(projectRoot);
|
|
95
|
+
|
|
45
96
|
let forgeConfig;
|
|
46
97
|
try {
|
|
47
98
|
forgeConfig = await loadMetaobjectsConfig(projectRoot);
|
|
@@ -52,22 +103,12 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
52
103
|
|
|
53
104
|
let metadata;
|
|
54
105
|
try {
|
|
55
|
-
metadata = await loadMemory(
|
|
56
|
-
|
|
106
|
+
metadata = await loadMemory(genCollection.configDir, {
|
|
107
|
+
files: genCollection.files,
|
|
108
|
+
...loadMemoryOptionsFrom(forgeConfig),
|
|
57
109
|
});
|
|
58
110
|
} catch (err) {
|
|
59
|
-
|
|
60
|
-
// Only emit the scaffold hint for the ACTUAL missing-metadata-dir
|
|
61
|
-
// condition — checked explicitly here. A broad substring match on
|
|
62
|
-
// "no such" / "cannot read" wrongly swallowed genuine ParseErrors (e.g.
|
|
63
|
-
// `origin.@via "X.y" ...: no such relationship "y" on X`) as "no
|
|
64
|
-
// metaobjects/ found", masking the real failure. Real parse/validation
|
|
65
|
-
// errors propagate with their actual message.
|
|
66
|
-
if (!existsSync(join(projectRoot, DEFAULT_METADATA_DIR))) {
|
|
67
|
-
log.error(`no metaobjects/ found in ${projectRoot}; run 'meta init' to scaffold`);
|
|
68
|
-
} else {
|
|
69
|
-
log.error(`failed to load metadata: ${msg}`);
|
|
70
|
-
}
|
|
111
|
+
log.error(`failed to load metadata: ${(err as Error).message}`);
|
|
71
112
|
return 2;
|
|
72
113
|
}
|
|
73
114
|
|
|
@@ -81,6 +122,11 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
81
122
|
// --dry-run must actually preview. This was previously passed only to the
|
|
82
123
|
// display object below, so a "preview" run wrote every file.
|
|
83
124
|
dryRun: cliConfig.dryRun,
|
|
125
|
+
// Collection-level `scope` (Task 12b) — the output filter over
|
|
126
|
+
// GENERATED entities, never over what the collection loads. Always
|
|
127
|
+
// passed: an unconfigured project's predicate admits everything, so this
|
|
128
|
+
// is a no-op for the common case, not a behavior change.
|
|
129
|
+
scope: genCollection.inScope,
|
|
84
130
|
...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
|
|
85
131
|
});
|
|
86
132
|
} catch (err) {
|