@metaobjectsdev/cli 0.23.2 → 0.24.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/README.md +78 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +148 -50
- 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 +61 -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 +139 -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 +336 -48
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +46 -15
- 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 +48 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +80 -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 +158 -49
- package/src/commands/export.ts +30 -6
- package/src/commands/gen.ts +62 -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 +156 -0
- package/src/commands/verify.ts +373 -48
- package/src/index.ts +46 -15
- 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 +84 -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
|
@@ -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";
|
|
@@ -66,6 +66,12 @@ interface DocsFlags {
|
|
|
66
66
|
outProvided: boolean;
|
|
67
67
|
/** Whether `--layout` was explicitly passed (same override semantics). */
|
|
68
68
|
layoutProvided: boolean;
|
|
69
|
+
/** Whether the `<metadata>` positional was explicitly passed. An explicit path
|
|
70
|
+
* DEFINES the source set (`resolveCollection`'s `explicitDir` pin); the default
|
|
71
|
+
* cwd discovers one by walking up. Without this distinction the two are
|
|
72
|
+
* indistinguishable at the call site, which is how an explicitly-scoped run came
|
|
73
|
+
* to have an ancestor config's sources unioned into it (#327). */
|
|
74
|
+
metadataProvided: boolean;
|
|
69
75
|
/** FR-033 S3 — document the METAMODEL ITSELF (the built-in type/subtype/attr
|
|
70
76
|
* vocabulary) instead of a user's entities. Needs NO metadata + NO config. */
|
|
71
77
|
metamodel: boolean;
|
|
@@ -95,6 +101,7 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
95
101
|
let baseUrl: string | undefined;
|
|
96
102
|
let wantModel = false;
|
|
97
103
|
let wantApi = false;
|
|
104
|
+
let wantRequirements = false;
|
|
98
105
|
let wantMetamodel = false;
|
|
99
106
|
let wantSite = false;
|
|
100
107
|
let wantScaffoldSite = false;
|
|
@@ -120,6 +127,8 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
120
127
|
wantModel = true;
|
|
121
128
|
} else if (a === "--api") {
|
|
122
129
|
wantApi = true;
|
|
130
|
+
} else if (a === "--requirements") {
|
|
131
|
+
wantRequirements = true;
|
|
123
132
|
} else if (a === "--metamodel") {
|
|
124
133
|
wantMetamodel = true;
|
|
125
134
|
} else if (a === "--site") {
|
|
@@ -161,10 +170,12 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
|
|
|
161
170
|
const surfaces: DocsSurface[] = [];
|
|
162
171
|
if (wantModel) surfaces.push("model");
|
|
163
172
|
if (wantApi) surfaces.push("api");
|
|
173
|
+
if (wantRequirements) surfaces.push("requirements");
|
|
164
174
|
return {
|
|
165
175
|
// `<metadata>` is the project root that contains metaobjects/; default cwd
|
|
166
176
|
// (mirrors how migrate/gen treat the working directory as the root).
|
|
167
177
|
metadata: metadata ?? cwd,
|
|
178
|
+
metadataProvided: metadata !== undefined,
|
|
168
179
|
// Default out dir, resolved against the metadata root below. In --metamodel
|
|
169
180
|
// mode the renderer writes under <out>/metamodel/, default ./docs/metamodel.
|
|
170
181
|
out: out ?? (wantMetamodel ? "./docs/metamodel" : "./docs"),
|
|
@@ -206,16 +217,68 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
206
217
|
|
|
207
218
|
// `--scaffold-site`: copy the docs-site templates + assets into codegen/docs-site/
|
|
208
219
|
// so the consumer owns them (ADR-0034 scaffold-and-own). Scaffold and return —
|
|
209
|
-
// it does not also generate.
|
|
220
|
+
// it does not also generate. `resolveConfigDir` rather than `resolveCollection`:
|
|
221
|
+
// scaffolding needs no metadata, but it must write where `emitSite` will READ
|
|
222
|
+
// (below, under the resolved project root), and that is the same walk.
|
|
210
223
|
if (flags.scaffoldSite) {
|
|
211
|
-
|
|
224
|
+
// `resolveGenConfigDir` over the bare walk: `emitSite` reads the owned theme
|
|
225
|
+
// from the directory holding `metaobjects.config.ts`, so scaffolding must write
|
|
226
|
+
// to the same one or the theme is scaffolded where nothing will ever read it.
|
|
227
|
+
return scaffoldSiteCommand(resolveGenConfigDir(metaRoot, await resolveConfigDir(metaRoot)));
|
|
212
228
|
}
|
|
213
229
|
|
|
230
|
+
// Discovery and load are two separate failure modes, kept in separate try
|
|
231
|
+
// blocks deliberately — same reasoning as `meta gen` (gen.ts): a broad
|
|
232
|
+
// catch around both would swallow a genuine ParseError as "no metaobjects/
|
|
233
|
+
// found", masking the real failure.
|
|
234
|
+
//
|
|
235
|
+
// Discovery runs BEFORE the config read, deliberately, and `meta gen` calls
|
|
236
|
+
// out the same ordering as the thing it fixed: the project root is whichever
|
|
237
|
+
// directory `resolveCollection` decided the metadata belongs to, so
|
|
238
|
+
// everything project-relative — `metaobjects.config.ts` and its providers,
|
|
239
|
+
// the `docs.outDir` it names, the adopter `templates/` overrides, the owned
|
|
240
|
+
// `codegen/docs-site/` theme — has to come from that same directory. Reading
|
|
241
|
+
// the config from the ambient `<metadata>` argument while the metadata came
|
|
242
|
+
// from an ancestor renders the ancestor's model with the subdirectory's
|
|
243
|
+
// (absent) providers. For a run at the project root the two are the same path.
|
|
244
|
+
//
|
|
245
|
+
// An EXPLICIT `<metadata>` positional pins the collection rather than seeding a
|
|
246
|
+
// walk (#327). The argument has always meant "document this": before sources were
|
|
247
|
+
// resolvable it read `<path>/metaobjects/` and nothing else, and passing it to
|
|
248
|
+
// discovery turned it into a starting point, so the nearest ancestor
|
|
249
|
+
// `.metaobjects/config.json` was found and ITS sources were unioned in — a command
|
|
250
|
+
// whose entire purpose is documenting one subset silently documenting the whole
|
|
251
|
+
// repo, at exit 0. Pinned, `<path>` governs: its own config if it has one, the
|
|
252
|
+
// default `<path>/metaobjects` if not. A bare `meta docs` still discovers, which is
|
|
253
|
+
// the right default for "document the project I am standing in".
|
|
254
|
+
let collection: Awaited<ReturnType<typeof resolveCollection>>;
|
|
255
|
+
try {
|
|
256
|
+
collection = await resolveCollection(
|
|
257
|
+
metaRoot,
|
|
258
|
+
flags.metadataProvided ? { explicitDir: metaRoot } : undefined,
|
|
259
|
+
);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.error(`docs: ${(err as Error).message}`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Which directory's `metaobjects.config.ts` governs is its OWN nearest-ancestor
|
|
266
|
+
// walk, not the collection's: the two files answer different questions (design
|
|
267
|
+
// §4.6), and in a Maven- or pip-rooted monorepo the collection is declared at the
|
|
268
|
+
// repo root while the TS config sits in the app. Reading the second from the
|
|
269
|
+
// first made docs silently drop that app's providers and skip its api surface
|
|
270
|
+
// entirely (#326). Everything that config names follows it — the `docs.outDir` it
|
|
271
|
+
// carries, the adopter `templates/` overrides and the owned `codegen/docs-site/`
|
|
272
|
+
// theme beside it. Same directory whenever the two files sit together, which is
|
|
273
|
+
// every `meta init` project.
|
|
274
|
+
const genConfigDir = resolveGenConfigDir(metaRoot, collection.configDir);
|
|
275
|
+
|
|
214
276
|
// The project root used to resolve adopter `templates/` overrides; the
|
|
215
|
-
// framework defaults sit underneath via projectProvider's chain.
|
|
277
|
+
// framework defaults sit underneath via projectProvider's chain. `--templates`
|
|
278
|
+
// is the one explicit override.
|
|
216
279
|
const projectRoot = flags.templates !== undefined
|
|
217
280
|
? resolvePath(cwd, flags.templates)
|
|
218
|
-
:
|
|
281
|
+
: genConfigDir;
|
|
219
282
|
|
|
220
283
|
// Best-effort load of metaobjects.config.ts to pick up consumer-supplied
|
|
221
284
|
// providers (e.g. a project's custom field/object subtypes). Unlike `gen`,
|
|
@@ -225,12 +288,14 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
225
288
|
// if the metadata genuinely uses an unregistered type.
|
|
226
289
|
let loadedConfig: Awaited<ReturnType<typeof loadMetaobjectsConfig>> | undefined;
|
|
227
290
|
let configProviders: NonNullable<Awaited<ReturnType<typeof loadMetaobjectsConfig>>["providers"]> | undefined;
|
|
291
|
+
// The same config's contributions to the metadata LOAD — providers plus the shipped
|
|
292
|
+
// `libraries` a project opts into (#333). `configProviders` stays separate because the
|
|
293
|
+
// site surface has its own loader and takes providers alone.
|
|
294
|
+
let configLoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
228
295
|
// hasConfig gates the api surface: api docs describe the GENERATED REST
|
|
229
296
|
// surface, which only exists when there is a (loadable) gen config. A config
|
|
230
297
|
// 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.
|
|
298
|
+
const hasConfig = existsSync(join(genConfigDir, "metaobjects.config.ts"));
|
|
234
299
|
// Only attempt the load when the file is actually present: absence is the
|
|
235
300
|
// expected config-less case (stay silent), but a config that EXISTS yet fails
|
|
236
301
|
// to load is surfaced as a warning rather than silently degrading to
|
|
@@ -238,8 +303,9 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
238
303
|
// cryptic unknown-subtype error instead of the real config error.
|
|
239
304
|
if (hasConfig) {
|
|
240
305
|
try {
|
|
241
|
-
loadedConfig = await loadMetaobjectsConfig(
|
|
306
|
+
loadedConfig = await loadMetaobjectsConfig(genConfigDir);
|
|
242
307
|
configProviders = loadedConfig.providers;
|
|
308
|
+
configLoadOptions = loadMemoryOptionsFrom(loadedConfig);
|
|
243
309
|
} catch (err) {
|
|
244
310
|
log.warn(
|
|
245
311
|
`docs: metaobjects.config.ts failed to load (${(err as Error).message}); ` +
|
|
@@ -247,6 +313,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
247
313
|
);
|
|
248
314
|
loadedConfig = undefined;
|
|
249
315
|
configProviders = undefined;
|
|
316
|
+
configLoadOptions = {};
|
|
250
317
|
}
|
|
251
318
|
}
|
|
252
319
|
|
|
@@ -269,7 +336,11 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
269
336
|
cliOverrides,
|
|
270
337
|
loadedConfig?.outputLayout ?? "flat",
|
|
271
338
|
);
|
|
272
|
-
|
|
339
|
+
// Against `genConfigDir`, not the collection's: `docs.outDir` is declared in
|
|
340
|
+
// `metaobjects.config.ts`, so it resolves against the directory that config lives
|
|
341
|
+
// in — the same rule `meta gen` applies to its own `outDir`. Identical whenever
|
|
342
|
+
// the two files sit together.
|
|
343
|
+
const outDir = resolvePath(genConfigDir, docsCfg.outDir);
|
|
273
344
|
|
|
274
345
|
// SITE surface has its OWN model loader (docs-site's loadModel — NOT the sdk
|
|
275
346
|
// loadMemory below) and needs no gen config. When the site is the ONLY
|
|
@@ -277,23 +348,19 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
277
348
|
// WITHOUT building the markdown GenContext — decoupled and one fewer failure
|
|
278
349
|
// surface. Combined with --model/--api it is emitted after them (below).
|
|
279
350
|
if (flags.site && docsCfg.surfaces.length === 0) {
|
|
280
|
-
return emitSite(
|
|
351
|
+
return emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
|
|
281
352
|
}
|
|
282
353
|
|
|
283
354
|
// Load metadata standalone — same loader path as migrate/gen. Threads any
|
|
284
355
|
// consumer providers from the config so custom types resolve.
|
|
285
356
|
let root;
|
|
286
357
|
try {
|
|
287
|
-
root = await loadMemory(
|
|
288
|
-
|
|
358
|
+
root = await loadMemory(collection.configDir, {
|
|
359
|
+
files: collection.files,
|
|
360
|
+
...configLoadOptions,
|
|
289
361
|
});
|
|
290
362
|
} 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
|
-
}
|
|
363
|
+
log.error(`docs: failed to load metadata: ${(err as Error).message}`);
|
|
297
364
|
return 2;
|
|
298
365
|
}
|
|
299
366
|
|
|
@@ -385,6 +452,16 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
385
452
|
emit.push(...modelFiles);
|
|
386
453
|
}
|
|
387
454
|
|
|
455
|
+
// REQUIREMENTS surface — the declared ledger as documentation (requirements.md +
|
|
456
|
+
// requirements.toon). Metadata-alone like the model surface: it reads ctx.loadedRoot
|
|
457
|
+
// and nothing else, so it is NOT gated on a loadable gen config the way `api` is.
|
|
458
|
+
//
|
|
459
|
+
// Emits ZERO files when the project declares no `requirement.*` node, which is what
|
|
460
|
+
// makes this surface safe to default ON — a project without a ledger sees no change.
|
|
461
|
+
if (docsCfg.surfaces.includes("requirements")) {
|
|
462
|
+
emit.push(...(await requirementsFile().generate(ctx)));
|
|
463
|
+
}
|
|
464
|
+
|
|
388
465
|
// API surface — the SDK reference for the GENERATED REST surface, side by side
|
|
389
466
|
// under each surface's subDir. THIS command only OWNS the surfaces it can
|
|
390
467
|
// generate — i.e. its own port (lang "ts"). Surfaces owned by other ports are
|
|
@@ -444,7 +521,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
444
521
|
|
|
445
522
|
// SITE surface (additive) — emit after the markdown surfaces so both coexist.
|
|
446
523
|
if (flags.site) {
|
|
447
|
-
const siteRc = await emitSite(
|
|
524
|
+
const siteRc = await emitSite(collection, projectRoot, genConfigDir, outDir, configProviders, promptsDir);
|
|
448
525
|
if (siteRc !== 0) return siteRc;
|
|
449
526
|
}
|
|
450
527
|
|
|
@@ -462,7 +539,12 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
462
539
|
const apiSummary = apiFiles.length > 0
|
|
463
540
|
? `${apiFiles.length} api page(s)`
|
|
464
541
|
: "no api pages";
|
|
465
|
-
|
|
542
|
+
// Reported only when it actually wrote something. A project with no ledger emits no
|
|
543
|
+
// requirements file, and saying "0 requirement pages" would advertise a surface that
|
|
544
|
+
// did not run — the opposite of the silence the empty-ledger guard exists to produce.
|
|
545
|
+
const requirementFiles = emit.filter((f) => f.path.startsWith("requirements.")).length;
|
|
546
|
+
const reqSummary = requirementFiles > 0 ? `; ${requirementFiles} requirement file(s)` : "";
|
|
547
|
+
log.info(`meta docs — wrote ${modelSummary}; ${apiSummary}${reqSummary} → ${outDir}`);
|
|
466
548
|
return 0;
|
|
467
549
|
}
|
|
468
550
|
|
|
@@ -471,9 +553,9 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
|
|
|
471
553
|
* into `<root>/codegen/docs-site/{templates,assets}`, writing each file ONLY if
|
|
472
554
|
* absent so a re-run never clobbers a hand-edited file.
|
|
473
555
|
*/
|
|
474
|
-
async function scaffoldSiteCommand(
|
|
475
|
-
const tplDir = join(
|
|
476
|
-
const astDir = join(
|
|
556
|
+
async function scaffoldSiteCommand(projectRoot: string): Promise<number> {
|
|
557
|
+
const tplDir = join(projectRoot, "codegen/docs-site/templates");
|
|
558
|
+
const astDir = join(projectRoot, "codegen/docs-site/assets");
|
|
477
559
|
const created: string[] = [];
|
|
478
560
|
const preserved: string[] = [];
|
|
479
561
|
try {
|
|
@@ -499,53 +581,80 @@ async function scaffoldSiteCommand(metaRoot: string): Promise<number> {
|
|
|
499
581
|
}
|
|
500
582
|
log.info(
|
|
501
583
|
`meta docs --scaffold-site — ${created.length} created, ${preserved.length} preserved ` +
|
|
502
|
-
`→ ${join(
|
|
584
|
+
`→ ${join(projectRoot, "codegen/docs-site")} (edit these to own your theme)`,
|
|
503
585
|
);
|
|
504
586
|
return 0;
|
|
505
587
|
}
|
|
506
588
|
|
|
507
589
|
/**
|
|
508
590
|
* 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
|
-
*
|
|
591
|
+
* The site loads the model with its OWN loader from the collection's declared
|
|
592
|
+
* source ROOTS (whole directories, one page group each) rather than from the
|
|
593
|
+
* per-file list the sdk `loadMemory` path takes, so this is independent of the
|
|
594
|
+
* markdown surfaces. Writes under `<outDir>/site` so it can coexist with the markdown
|
|
595
|
+
* output. Scaffold-and-own: when the consumer has copied templates/assets
|
|
596
|
+
* into `<projectRoot>/codegen/docs-site/` (via `--scaffold-site`), those win
|
|
597
|
+
* over the bundled defaults.
|
|
598
|
+
*
|
|
599
|
+
* Takes the ALREADY-RESOLVED collection: `docsCommand` resolved it to read the
|
|
600
|
+
* config from the right directory, and resolving a second time here made the
|
|
601
|
+
* combined `--model --site` path do the whole discovery-and-config walk twice.
|
|
515
602
|
*/
|
|
516
603
|
async function emitSite(
|
|
517
|
-
|
|
604
|
+
collection: Collection,
|
|
605
|
+
projectRoot: string,
|
|
606
|
+
/** Directory holding `metaobjects.config.ts` — where `--scaffold-site` writes the
|
|
607
|
+
* owned theme, so where this must read it from. Distinct from `collection.configDir`
|
|
608
|
+
* since #326: in a polyglot monorepo the collection is declared at the repo root
|
|
609
|
+
* while the TS package (and its owned theme) sits in the app. */
|
|
610
|
+
genConfigDir: string,
|
|
518
611
|
outDir: string,
|
|
519
612
|
configProviders?: readonly MetaDataTypeProvider[],
|
|
520
613
|
promptsDir?: string,
|
|
521
614
|
): Promise<number> {
|
|
522
615
|
const siteOutDir = resolvePath(outDir, "site");
|
|
523
|
-
//
|
|
524
|
-
// Prompt `.mustache` source is
|
|
525
|
-
// <root>/templates/ and any
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
616
|
+
// The resolved metadata source dir(s) are REQUIRED (the site loads the
|
|
617
|
+
// model from them) and always first. Prompt `.mustache` source is
|
|
618
|
+
// additionally searched in the conventional <root>/templates/ and any
|
|
619
|
+
// explicit --prompts dir (for a project whose templates live elsewhere,
|
|
620
|
+
// e.g. data/templates/) — else the site can't show the prompt TEXT and
|
|
621
|
+
// prints a "source missing" note. Only existing dirs are added.
|
|
622
|
+
//
|
|
623
|
+
// Deduped by resolved PATH, not by basename. Two DIFFERENT directories that
|
|
624
|
+
// happen to share a basename are a legitimate multi-source project (`metaobjects`
|
|
625
|
+
// plus `../shared-model/metaobjects`); `loadModel` disambiguates their site
|
|
626
|
+
// group names, so refusing the pair here — which a basename key did, by
|
|
627
|
+
// dropping the second — would break the feature this branch exists to ship.
|
|
628
|
+
// The same directory named twice is the real hazard: it would be symlinked
|
|
629
|
+
// and loaded twice.
|
|
630
|
+
// The DECLARED source roots, not directories re-derived from the resolved
|
|
631
|
+
// files: a declared source directory holding no metadata yet would otherwise
|
|
632
|
+
// vanish from the site's group list entirely, and `sourceDirs` could come back
|
|
633
|
+
// empty where the pre-branch code always passed `<root>/metaobjects`.
|
|
634
|
+
const sourceDirs = [...collection.sourceRoots];
|
|
635
|
+
const seenDirs = new Set(sourceDirs);
|
|
531
636
|
if (promptsDir !== undefined && !existsSync(promptsDir)) {
|
|
532
637
|
log.warn(`docs: --prompts dir does not exist: ${promptsDir}`);
|
|
533
638
|
}
|
|
534
|
-
for (const d of [join(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
639
|
+
for (const d of [join(projectRoot, "templates"), ...(promptsDir !== undefined ? [promptsDir] : [])]) {
|
|
640
|
+
const abs = resolvePath(d);
|
|
641
|
+
if (existsSync(abs) && !seenDirs.has(abs)) {
|
|
642
|
+
sourceDirs.push(abs);
|
|
643
|
+
seenDirs.add(abs);
|
|
538
644
|
}
|
|
539
645
|
}
|
|
540
646
|
// Scaffold-and-own: when the consumer has copied templates/assets into
|
|
541
647
|
// codegen/docs-site/ (via --scaffold-site), use those; else the bundled defaults.
|
|
542
|
-
|
|
543
|
-
|
|
648
|
+
// Keyed on `genConfigDir`, NOT `projectRoot`: `--templates` redirects the adopter
|
|
649
|
+
// RENDER template chain (the `templates/` above), and letting it also move the
|
|
650
|
+
// docs-site theme would read it from somewhere `--scaffold-site` never writes.
|
|
651
|
+
const ownedTemplates = join(genConfigDir, "codegen/docs-site/templates");
|
|
652
|
+
const ownedAssets = join(genConfigDir, "codegen/docs-site/assets");
|
|
544
653
|
try {
|
|
545
654
|
const r = await generateSite({
|
|
546
655
|
sourceDirs,
|
|
547
656
|
outDir: siteOutDir,
|
|
548
|
-
title: basename(
|
|
657
|
+
title: basename(collection.configDir) || "Metadata",
|
|
549
658
|
stamp: new Date().toISOString().slice(0, 10),
|
|
550
659
|
commit: "",
|
|
551
660
|
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, 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,53 @@ 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
|
+
// Advisory: nudge to refresh the .claude/skills docs if they predate this CLI.
|
|
78
|
+
// Rooted at `projectRoot`, not ambient cwd — the scaffolded agent context sits
|
|
79
|
+
// with the project that declares the metadata, so a run from a subdirectory
|
|
80
|
+
// would find no manifest there and silently skip the nudge. `meta verify` makes
|
|
81
|
+
// the same call for the same reason; the two commands describe this and the
|
|
82
|
+
// anti-pattern scan below as one advisory pass, so they must scan one tree.
|
|
83
|
+
warnIfAgentContextStale(projectRoot);
|
|
84
|
+
// Advisory: the committed hash manifest is what makes hand-edit detection work on a
|
|
85
|
+
// machine that did not generate the output. Silent unless it is ignored. Keyed on
|
|
86
|
+
// projectRoot, not cwd, for the same reason its neighbour is — the manifest belongs to
|
|
87
|
+
// whichever directory `resolveCollection` decided the metadata lives in.
|
|
88
|
+
warnIfManifestIgnored(projectRoot);
|
|
89
|
+
|
|
45
90
|
let forgeConfig;
|
|
46
91
|
try {
|
|
47
92
|
forgeConfig = await loadMetaobjectsConfig(projectRoot);
|
|
@@ -52,22 +97,12 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
52
97
|
|
|
53
98
|
let metadata;
|
|
54
99
|
try {
|
|
55
|
-
metadata = await loadMemory(
|
|
56
|
-
|
|
100
|
+
metadata = await loadMemory(collection.configDir, {
|
|
101
|
+
files: collection.files,
|
|
102
|
+
...loadMemoryOptionsFrom(forgeConfig),
|
|
57
103
|
});
|
|
58
104
|
} 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
|
-
}
|
|
105
|
+
log.error(`failed to load metadata: ${(err as Error).message}`);
|
|
71
106
|
return 2;
|
|
72
107
|
}
|
|
73
108
|
|
|
@@ -81,6 +116,11 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
81
116
|
// --dry-run must actually preview. This was previously passed only to the
|
|
82
117
|
// display object below, so a "preview" run wrote every file.
|
|
83
118
|
dryRun: cliConfig.dryRun,
|
|
119
|
+
// Collection-level `scope` (Task 12b) — the output filter over
|
|
120
|
+
// GENERATED entities, never over what the collection loads. Always
|
|
121
|
+
// passed: an unconfigured project's predicate admits everything, so this
|
|
122
|
+
// is a no-op for the common case, not a behavior change.
|
|
123
|
+
scope: collection.inScope,
|
|
84
124
|
...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
|
|
85
125
|
});
|
|
86
126
|
} catch (err) {
|