@metaobjectsdev/cli 1.0.2 → 1.0.4-rc.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 +13 -5
- package/dist/src/commands/deps.d.ts +3 -0
- package/dist/src/commands/deps.d.ts.map +1 -0
- package/dist/src/commands/deps.js +200 -0
- package/dist/src/commands/deps.js.map +1 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -3
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +2 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +235 -75
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +173 -30
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +5 -9
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +92 -402
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +24 -19
- 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 +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +186 -29
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +97 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +10 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +7 -1
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/args.d.ts +75 -3
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +54 -5
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +92 -0
- package/dist/src/lib/catalog-listing.d.ts.map +1 -0
- package/dist/src/lib/catalog-listing.js +268 -0
- package/dist/src/lib/catalog-listing.js.map +1 -0
- package/dist/src/lib/catalog.d.ts +41 -0
- package/dist/src/lib/catalog.d.ts.map +1 -0
- package/dist/src/lib/catalog.js +89 -0
- package/dist/src/lib/catalog.js.map +1 -0
- package/dist/src/lib/collection-load-options.d.ts +13 -0
- package/dist/src/lib/collection-load-options.d.ts.map +1 -0
- package/dist/src/lib/collection-load-options.js +25 -0
- package/dist/src/lib/collection-load-options.js.map +1 -0
- package/dist/src/lib/dependency-sync.d.ts +170 -0
- package/dist/src/lib/dependency-sync.d.ts.map +1 -0
- package/dist/src/lib/dependency-sync.js +457 -0
- package/dist/src/lib/dependency-sync.js.map +1 -0
- package/dist/src/lib/install-set.d.ts +31 -0
- package/dist/src/lib/install-set.d.ts.map +1 -0
- package/dist/src/lib/install-set.js +103 -0
- package/dist/src/lib/install-set.js.map +1 -0
- package/dist/src/lib/library-eject.d.ts +79 -0
- package/dist/src/lib/library-eject.d.ts.map +1 -0
- package/dist/src/lib/library-eject.js +241 -0
- package/dist/src/lib/library-eject.js.map +1 -0
- package/dist/src/lib/library-listing.d.ts +63 -0
- package/dist/src/lib/library-listing.d.ts.map +1 -0
- package/dist/src/lib/library-listing.js +178 -0
- package/dist/src/lib/library-listing.js.map +1 -0
- package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
- package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
- package/dist/src/lib/library-prefix-advisory.js +67 -0
- package/dist/src/lib/library-prefix-advisory.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +11 -17
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/migrate-scope.d.ts +32 -1
- package/dist/src/lib/migrate-scope.d.ts.map +1 -1
- package/dist/src/lib/migrate-scope.js +51 -3
- package/dist/src/lib/migrate-scope.js.map +1 -1
- package/dist/src/lib/overlay-lint.d.ts +37 -0
- package/dist/src/lib/overlay-lint.d.ts.map +1 -0
- package/dist/src/lib/overlay-lint.js +102 -0
- package/dist/src/lib/overlay-lint.js.map +1 -0
- package/dist/src/lib/requirement-check.d.ts +26 -3
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +71 -13
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/deps.ts +231 -0
- package/src/commands/docs.ts +19 -3
- package/src/commands/eject.ts +304 -90
- package/src/commands/gen.ts +189 -29
- package/src/commands/init.ts +95 -407
- package/src/commands/migrate.ts +26 -16
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/verify.ts +213 -30
- package/src/index.ts +97 -15
- package/src/lib/advisory.ts +13 -0
- package/src/lib/agent-context-staleness.ts +7 -1
- package/src/lib/args.ts +118 -8
- package/src/lib/catalog-listing.ts +369 -0
- package/src/lib/catalog.ts +107 -0
- package/src/lib/collection-load-options.ts +42 -0
- package/src/lib/dependency-sync.ts +656 -0
- package/src/lib/install-set.ts +116 -0
- package/src/lib/library-eject.ts +298 -0
- package/src/lib/library-listing.ts +258 -0
- package/src/lib/library-prefix-advisory.ts +78 -0
- package/src/lib/load-metaobjects-config.ts +15 -21
- package/src/lib/migrate-scope.ts +59 -4
- package/src/lib/overlay-lint.ts +129 -0
- package/src/lib/requirement-check.ts +100 -14
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// `meta gen --list` — the catalog as one machine-readable document.
|
|
2
|
+
//
|
|
3
|
+
// The governing rule (opt-in-codegen design §1): stop hard-coding selection decisions
|
|
4
|
+
// into the CLI. The tool's job is to describe what it can do, accurately, and to make
|
|
5
|
+
// each choice cheap to act on. Deciding WHICH generators an application needs belongs
|
|
6
|
+
// to whoever is building it — increasingly an LLM working in the repo, which is well
|
|
7
|
+
// able to make that call given a truthful catalog and is badly served by a default that
|
|
8
|
+
// pre-empts it.
|
|
9
|
+
//
|
|
10
|
+
// Two questions this answers, kept apart on purpose (§D5):
|
|
11
|
+
//
|
|
12
|
+
// APPLICABILITY — "would this emit anything for MY model?" — is `--probe`, which
|
|
13
|
+
// cannot drift, because it does not describe the generators, it RUNS them.
|
|
14
|
+
// COMPATIBILITY — "what does this need in order to work?" — is declared
|
|
15
|
+
// (`requires`, `runtimePeers`) and therefore has to be gated separately.
|
|
16
|
+
//
|
|
17
|
+
// Conflating the two is how a catalog goes quietly wrong as frameworks are added.
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
22
|
+
import {
|
|
23
|
+
runGen,
|
|
24
|
+
type GeneratorRegistryEntry,
|
|
25
|
+
type Layer,
|
|
26
|
+
type MetaobjectsGenConfig,
|
|
27
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
28
|
+
import * as coreTpl from "@metaobjectsdev/codegen-ts";
|
|
29
|
+
import * as reactTpl from "@metaobjectsdev/codegen-ts-react";
|
|
30
|
+
import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
|
|
31
|
+
import { listCatalog, packageOf } from "./catalog.js";
|
|
32
|
+
import {
|
|
33
|
+
buildLibraryRows, renderLibraryText,
|
|
34
|
+
type LibraryCatalogRow, type LibraryProjectContext,
|
|
35
|
+
} from "./library-listing.js";
|
|
36
|
+
import { installSetFor } from "./install-set.js";
|
|
37
|
+
import { readPackageManifest, declaredDependencyNames } from "./package-manifest.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One `--list` row. The cross-port subset is name / layer / tier / description.
|
|
41
|
+
*
|
|
42
|
+
* `kind` is the discriminator: FR-043 adds `kind: "library"` rows to THIS table rather
|
|
43
|
+
* than a parallel one, because an agent about to model a capability should meet the
|
|
44
|
+
* library that already declares it in the list it was already reading.
|
|
45
|
+
*/
|
|
46
|
+
export type CatalogRow = GeneratorCatalogRow | LibraryCatalogRow;
|
|
47
|
+
|
|
48
|
+
export type { LibraryCatalogRow };
|
|
49
|
+
|
|
50
|
+
export interface GeneratorCatalogRow {
|
|
51
|
+
name: string;
|
|
52
|
+
kind: "generator";
|
|
53
|
+
layer: Layer;
|
|
54
|
+
framework?: string;
|
|
55
|
+
tier: "native" | "neutral";
|
|
56
|
+
package: string;
|
|
57
|
+
description: string;
|
|
58
|
+
/** From the reference template's `use-when:` header. Absent for a non-ejectable entry. */
|
|
59
|
+
useWhen?: string;
|
|
60
|
+
/** From the reference template's `emits:` header. Absent for a non-ejectable entry. */
|
|
61
|
+
emits?: string;
|
|
62
|
+
requires?: readonly string[];
|
|
63
|
+
configKeys?: readonly string[];
|
|
64
|
+
install: { dev: string[]; runtime: string[] };
|
|
65
|
+
source: {
|
|
66
|
+
kind: "reference-template" | "package-only";
|
|
67
|
+
ejectable: boolean;
|
|
68
|
+
/** Whether this project already owns a copy. `null` with no project. */
|
|
69
|
+
owned: boolean | null;
|
|
70
|
+
};
|
|
71
|
+
project?: {
|
|
72
|
+
/** Wired in this project's `generators: [...]`. */
|
|
73
|
+
wired: boolean;
|
|
74
|
+
/** This generator's framework is a declared dependency. `null` when neutral or unknown. */
|
|
75
|
+
frameworkDetected: boolean | null;
|
|
76
|
+
/** Files this generator WOULD emit for this model. `null` unless --probe. */
|
|
77
|
+
wouldEmit: number | null;
|
|
78
|
+
/** Why `wouldEmit` is null despite --probe. Absent when it is a number. */
|
|
79
|
+
probeError?: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Where an ejected copy lands — mirrors `eject.ts`'s constant of the same name. */
|
|
84
|
+
const OWNED_GENERATORS_DIR = "codegen/generators";
|
|
85
|
+
|
|
86
|
+
/** The npm package a framework token names, for `frameworkDetected`. */
|
|
87
|
+
const FRAMEWORK_PACKAGE: Record<string, string> = {
|
|
88
|
+
fastify: "fastify",
|
|
89
|
+
hono: "hono",
|
|
90
|
+
react: "react",
|
|
91
|
+
tanstack: "@tanstack/react-query",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// reference-template headers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** The three packages that ship reference templates, and their roots. */
|
|
99
|
+
const TEMPLATE_ROOTS: ReadonlyArray<readonly [readonly string[], () => string]> = [
|
|
100
|
+
[coreTpl.REFERENCE_GENERATOR_NAMES, coreTpl.resolveReferenceRoot],
|
|
101
|
+
[reactTpl.REFERENCE_GENERATOR_NAMES, reactTpl.resolveReferenceRoot],
|
|
102
|
+
[tanstackTpl.REFERENCE_GENERATOR_NAMES, tanstackTpl.resolveReferenceRoot],
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A `// <facet>: <text>` header line and its indented continuations, unwrapped.
|
|
107
|
+
*
|
|
108
|
+
* Read from the template rather than restated in the registry entry, for the same
|
|
109
|
+
* reason `eject` reads its import line from the header: the template is the thing an
|
|
110
|
+
* adopter opens, so a second copy of its own summary in the registry is a second thing
|
|
111
|
+
* to keep in step. A test asserts every ejectable entry's row actually carries these.
|
|
112
|
+
*/
|
|
113
|
+
function headerFacet(source: string, facet: string): string | undefined {
|
|
114
|
+
const lines = source.split("\n");
|
|
115
|
+
const start = lines.findIndex((l) => l.startsWith(`// ${facet}:`));
|
|
116
|
+
if (start === -1) return undefined;
|
|
117
|
+
const parts = [lines[start]!.slice(`// ${facet}:`.length).trim()];
|
|
118
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
119
|
+
const line = lines[i]!;
|
|
120
|
+
// A continuation is a comment line indented past the facet column and NOT itself a
|
|
121
|
+
// new `facet:` line. Anything else ends the block.
|
|
122
|
+
if (!/^\/\/ {3,}\S/.test(line) || /^\/\/ {3,}[a-z-]+: /.test(line)) break;
|
|
123
|
+
parts.push(line.replace(/^\/\/\s+/, "").trim());
|
|
124
|
+
}
|
|
125
|
+
return parts.join(" ").trim() || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readTemplate(name: string): string | undefined {
|
|
129
|
+
for (const [names, root] of TEMPLATE_ROOTS) {
|
|
130
|
+
if (!names.includes(name)) continue;
|
|
131
|
+
try {
|
|
132
|
+
return readFileSync(join(root(), `${name}.ts`), "utf8");
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// the probe
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The project facts a `--list` row can carry. All optional: `--list` with no project at
|
|
146
|
+
* all is the documented shape (`meta types` behaves the same way), because "what CAN
|
|
147
|
+
* this engine do" is a question about the installed engine, not about a repo.
|
|
148
|
+
*/
|
|
149
|
+
export interface CatalogProject {
|
|
150
|
+
projectRoot: string;
|
|
151
|
+
config: MetaobjectsGenConfig;
|
|
152
|
+
/** Stable names of the wired generators, as far as they can be identified. */
|
|
153
|
+
wiredNames: ReadonlySet<string>;
|
|
154
|
+
/** Owned copies present under `codegen/generators/`. */
|
|
155
|
+
ownedNames: ReadonlySet<string>;
|
|
156
|
+
/** Declared dependency names from package.json, for `frameworkDetected`. */
|
|
157
|
+
declaredDeps: ReadonlySet<string> | undefined;
|
|
158
|
+
/** FR-043 — the project's `libraries` selection, for the `kind: "library"` rows.
|
|
159
|
+
* Defaults to none, so a caller that predates libraries reports "not opted in"
|
|
160
|
+
* rather than crashing. */
|
|
161
|
+
libraries?: readonly string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface CatalogListingOpts {
|
|
165
|
+
project?: CatalogProject;
|
|
166
|
+
/** Run every generator in memory and report a real file count. Needs `project` + `metadata`. */
|
|
167
|
+
probe?: { metadata: MetaData; scope?: (fqn: string) => boolean };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Dry-run ONE generator and count what it would emit.
|
|
172
|
+
*
|
|
173
|
+
* One `runGen` per generator rather than one run over all of them, for isolation: a
|
|
174
|
+
* generator that throws (or one whose real use requires options `--list` cannot supply,
|
|
175
|
+
* like `template` and `shared-model`) must report itself and leave every other row
|
|
176
|
+
* intact. `dryRun` touches nothing on disk.
|
|
177
|
+
*/
|
|
178
|
+
async function probeOne(
|
|
179
|
+
entry: GeneratorRegistryEntry,
|
|
180
|
+
project: CatalogProject,
|
|
181
|
+
metadata: MetaData,
|
|
182
|
+
scope: ((fqn: string) => boolean) | undefined,
|
|
183
|
+
): Promise<{ count: number } | { error: string }> {
|
|
184
|
+
try {
|
|
185
|
+
const result = await runGen({
|
|
186
|
+
config: { ...project.config, generators: [entry.factory()] },
|
|
187
|
+
metadata,
|
|
188
|
+
projectRoot: project.projectRoot,
|
|
189
|
+
dryRun: true,
|
|
190
|
+
...(scope !== undefined ? { scope } : {}),
|
|
191
|
+
});
|
|
192
|
+
return { count: result.files.length };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return { error: (err as Error).message };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// the listing
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promise<CatalogRow[]> {
|
|
203
|
+
const rows: CatalogRow[] = [];
|
|
204
|
+
|
|
205
|
+
for (const entry of listCatalog()) {
|
|
206
|
+
const template = entry.ejectable ? readTemplate(entry.name) : undefined;
|
|
207
|
+
const install = installSetFor([entry]);
|
|
208
|
+
const pkg = packageOf(entry.name) ?? "";
|
|
209
|
+
|
|
210
|
+
const row: GeneratorCatalogRow = {
|
|
211
|
+
name: entry.name,
|
|
212
|
+
kind: entry.kind,
|
|
213
|
+
layer: entry.layer,
|
|
214
|
+
...(entry.framework !== undefined ? { framework: entry.framework } : {}),
|
|
215
|
+
tier: entry.tier,
|
|
216
|
+
package: pkg,
|
|
217
|
+
description: entry.description,
|
|
218
|
+
...(template !== undefined && headerFacet(template, "use-when") !== undefined
|
|
219
|
+
? { useWhen: headerFacet(template, "use-when")! }
|
|
220
|
+
: {}),
|
|
221
|
+
...(template !== undefined && headerFacet(template, "emits") !== undefined
|
|
222
|
+
? { emits: headerFacet(template, "emits")! }
|
|
223
|
+
: {}),
|
|
224
|
+
...(entry.requires !== undefined ? { requires: entry.requires } : {}),
|
|
225
|
+
...(entry.configKeys !== undefined ? { configKeys: entry.configKeys } : {}),
|
|
226
|
+
install: { dev: install.dev, runtime: install.runtime },
|
|
227
|
+
source: {
|
|
228
|
+
kind: entry.ejectable ? "reference-template" : "package-only",
|
|
229
|
+
ejectable: entry.ejectable,
|
|
230
|
+
owned: opts.project === undefined ? null : opts.project.ownedNames.has(entry.name),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (opts.project !== undefined) {
|
|
235
|
+
const project = opts.project;
|
|
236
|
+
const fw = entry.framework;
|
|
237
|
+
const frameworkDetected =
|
|
238
|
+
fw === undefined || project.declaredDeps === undefined
|
|
239
|
+
? null
|
|
240
|
+
: project.declaredDeps.has(FRAMEWORK_PACKAGE[fw] ?? fw);
|
|
241
|
+
|
|
242
|
+
let wouldEmit: number | null = null;
|
|
243
|
+
let probeError: string | undefined;
|
|
244
|
+
if (opts.probe !== undefined) {
|
|
245
|
+
const outcome = await probeOne(entry, project, opts.probe.metadata, opts.probe.scope);
|
|
246
|
+
if ("count" in outcome) wouldEmit = outcome.count;
|
|
247
|
+
else probeError = outcome.error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
row.project = {
|
|
251
|
+
wired: project.wiredNames.has(entry.name),
|
|
252
|
+
frameworkDetected,
|
|
253
|
+
wouldEmit,
|
|
254
|
+
...(probeError !== undefined ? { probeError } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
rows.push(row);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// FR-043 — the library rows, after the generators. Same table, same `--probe`.
|
|
262
|
+
const libraryCtx: LibraryProjectContext | undefined =
|
|
263
|
+
opts.project === undefined
|
|
264
|
+
? undefined
|
|
265
|
+
: {
|
|
266
|
+
libraries: opts.project.libraries ?? [],
|
|
267
|
+
wiredNames: opts.project.wiredNames,
|
|
268
|
+
...(opts.probe !== undefined ? { metadata: opts.probe.metadata } : {}),
|
|
269
|
+
};
|
|
270
|
+
rows.push(...(await buildLibraryRows(libraryCtx)));
|
|
271
|
+
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The stable names wired in a config.
|
|
277
|
+
*
|
|
278
|
+
* `generators: [...]` holds constructed Generator objects (and, since ADR-0021 #1,
|
|
279
|
+
* bare stable-name strings). A constructed generator carries its own kebab-case `name`,
|
|
280
|
+
* which IS the stable name for every catalog entry — the registry's factory is what
|
|
281
|
+
* produced it. A generator whose name is not a catalog key is an owned or third-party
|
|
282
|
+
* one and simply contributes nothing here.
|
|
283
|
+
*/
|
|
284
|
+
export function wiredGeneratorNames(config: MetaobjectsGenConfig): Set<string> {
|
|
285
|
+
const names = new Set<string>();
|
|
286
|
+
for (const spec of config.generators ?? []) {
|
|
287
|
+
if (typeof spec === "string") names.add(spec);
|
|
288
|
+
else if (typeof spec?.name === "string") names.add(spec.name);
|
|
289
|
+
}
|
|
290
|
+
return names;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Owned copies present under `codegen/generators/` in this project. */
|
|
294
|
+
export function ownedGeneratorNames(projectRoot: string): Set<string> {
|
|
295
|
+
const owned = new Set<string>();
|
|
296
|
+
for (const entry of listCatalog()) {
|
|
297
|
+
if (!entry.ejectable) continue;
|
|
298
|
+
try {
|
|
299
|
+
readFileSync(join(projectRoot, OWNED_GENERATORS_DIR, `${entry.name}.ts`), "utf8");
|
|
300
|
+
owned.add(entry.name);
|
|
301
|
+
} catch {
|
|
302
|
+
// absent — not owned
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return owned;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Declared dependency names, or undefined when there is no readable manifest. */
|
|
309
|
+
export function declaredDepsOf(projectRoot: string): Set<string> | undefined {
|
|
310
|
+
const pkg = readPackageManifest(projectRoot);
|
|
311
|
+
return pkg === undefined ? undefined : declaredDependencyNames(pkg);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// text rendering
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const LAYER_BLURB: Record<Layer, string> = {
|
|
319
|
+
model: "the entity modules and the constants beside them",
|
|
320
|
+
persistence: "how rows are read and written",
|
|
321
|
+
api: "the HTTP surface — pick ONE framework",
|
|
322
|
+
client: "the browser tier — form + hooks + grid compose, they do not conflict",
|
|
323
|
+
docs: "on by default; the canonical door is `meta docs`",
|
|
324
|
+
capability: "chosen by your MODEL, not by browsing — run --probe",
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** The human rendering: grouped by layer, because layer is the axis you select by. */
|
|
328
|
+
export function renderCatalogText(rows: CatalogRow[], probed: boolean): string {
|
|
329
|
+
const lines: string[] = [];
|
|
330
|
+
lines.push("Generator catalog — nothing runs until you wire it in `generators: [...]`.");
|
|
331
|
+
lines.push("");
|
|
332
|
+
|
|
333
|
+
const generators = rows.filter((r): r is GeneratorCatalogRow => r.kind === "generator");
|
|
334
|
+
const width = Math.max(...generators.map((r) => r.name.length));
|
|
335
|
+
let layer: string | undefined;
|
|
336
|
+
for (const r of generators) {
|
|
337
|
+
if (r.layer !== layer) {
|
|
338
|
+
layer = r.layer;
|
|
339
|
+
lines.push(`${layer} — ${LAYER_BLURB[r.layer]}`);
|
|
340
|
+
}
|
|
341
|
+
const marks: string[] = [];
|
|
342
|
+
if (r.framework !== undefined) marks.push(r.framework);
|
|
343
|
+
if (r.tier === "neutral") marks.push("neutral");
|
|
344
|
+
if (r.project?.wired) marks.push("WIRED");
|
|
345
|
+
if (r.source.owned) marks.push("owned");
|
|
346
|
+
if (probed && r.project?.wouldEmit !== null && r.project?.wouldEmit !== undefined) {
|
|
347
|
+
marks.push(`would emit ${r.project.wouldEmit}`);
|
|
348
|
+
}
|
|
349
|
+
const suffix = marks.length > 0 ? ` [${marks.join(", ")}]` : "";
|
|
350
|
+
lines.push(` ${r.name.padEnd(width)} — ${r.description}${suffix}`);
|
|
351
|
+
if (r.requires !== undefined && r.requires.length > 0) {
|
|
352
|
+
lines.push(` ${" ".repeat(width)} requires: ${r.requires.join(", ")}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The library section sits between the generators and the footer: the footer's
|
|
357
|
+
// `meta eject` line applies to both kinds, and putting libraries after it buries
|
|
358
|
+
// the one row an agent modelling a capability most needs to see.
|
|
359
|
+
lines.push(...renderLibraryText(rows.filter((r): r is LibraryCatalogRow => r.kind === "library")));
|
|
360
|
+
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("`meta eject <name...>` copies a generator into codegen/generators/ and prints");
|
|
363
|
+
lines.push("the import to add, the entry to wire, and what to install.");
|
|
364
|
+
if (!probed) {
|
|
365
|
+
lines.push("Add --probe to see how many files each would emit for YOUR model.");
|
|
366
|
+
}
|
|
367
|
+
lines.push("`meta gen --list --format json` is the same catalog, machine-readable.");
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The TypeScript port's generator CATALOG — the one place all three registry slices
|
|
2
|
+
// are visible at once (ADR-0021 D3; opt-in-codegen design §D2a).
|
|
3
|
+
//
|
|
4
|
+
// `codegen-ts` owns the entry type and its own entries, but cannot import
|
|
5
|
+
// `codegen-ts-react` / `codegen-ts-tanstack` (dependency direction), so each of those
|
|
6
|
+
// exports its own slice and the CLI unions them here — exactly how `eject.ts` already
|
|
7
|
+
// composes its `SOURCES` table over the same three packages.
|
|
8
|
+
//
|
|
9
|
+
// Before this existed, `meta gen --list` read the codegen-ts registry and
|
|
10
|
+
// `meta eject --list` read the reference-template lists, so an agent asking "what can I
|
|
11
|
+
// turn on" got two different answers. After it, they are one table with two views.
|
|
12
|
+
//
|
|
13
|
+
// (The Angular generators stay out until ADR-0048's promotion bar is met — a
|
|
14
|
+
// source-only package is not a catalog entry.)
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
generatorRegistry,
|
|
18
|
+
GENERATOR_LAYERS,
|
|
19
|
+
type GeneratorRegistryEntry,
|
|
20
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
21
|
+
import { reactGeneratorRegistry } from "@metaobjectsdev/codegen-ts-react";
|
|
22
|
+
import { tanstackGeneratorRegistry } from "@metaobjectsdev/codegen-ts-tanstack";
|
|
23
|
+
|
|
24
|
+
/** The slices, in install-boundary order: the engine, then its framework packages. */
|
|
25
|
+
const SLICES: ReadonlyArray<readonly [string, Record<string, GeneratorRegistryEntry>]> = [
|
|
26
|
+
["@metaobjectsdev/codegen-ts", generatorRegistry],
|
|
27
|
+
["@metaobjectsdev/codegen-ts-react", reactGeneratorRegistry],
|
|
28
|
+
["@metaobjectsdev/codegen-ts-tanstack", tanstackGeneratorRegistry],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which npm package a catalog entry comes from.
|
|
33
|
+
*
|
|
34
|
+
* This is the fact composition ADDS: a slice cannot know its own package name without
|
|
35
|
+
* hard-coding it in every entry, and the package is precisely what `meta eject` has to
|
|
36
|
+
* name in an install command. Undefined for a name no slice registers.
|
|
37
|
+
*/
|
|
38
|
+
export function packageOf(name: string): string | undefined {
|
|
39
|
+
return SLICES.find(([, slice]) => name in slice)?.[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every `@metaobjectsdev` package that contributes catalog entries. */
|
|
43
|
+
export function catalogPackages(): string[] {
|
|
44
|
+
return SLICES.map(([pkg]) => pkg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The composed catalog, keyed by stable name.
|
|
49
|
+
*
|
|
50
|
+
* Throws on a name registered by two packages. A stable name identifies ONE generator
|
|
51
|
+
* across the whole catalog — that is the entire content of ADR-0021 D3 — so a collision
|
|
52
|
+
* is a build-time defect, not something to resolve by precedence. Silently letting the
|
|
53
|
+
* last slice win would make `meta gen --list` describe one generator and `meta gen` run
|
|
54
|
+
* another.
|
|
55
|
+
*/
|
|
56
|
+
export function composeCatalog(): Record<string, GeneratorRegistryEntry> {
|
|
57
|
+
return composeSlices(SLICES);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The composition itself, over any slice list.
|
|
62
|
+
*
|
|
63
|
+
* Separate from {@link composeCatalog} so the REFUSAL is reachable from a test. With the
|
|
64
|
+
* live `SLICES` closed over, the only thing a test could assert is that today's three
|
|
65
|
+
* packages happen not to collide — which stays true if the check above is deleted, so
|
|
66
|
+
* the guard that makes a collision a build failure was never covered by anything.
|
|
67
|
+
*/
|
|
68
|
+
export function composeSlices(
|
|
69
|
+
slices: ReadonlyArray<readonly [string, Record<string, GeneratorRegistryEntry>]>,
|
|
70
|
+
): Record<string, GeneratorRegistryEntry> {
|
|
71
|
+
const out: Record<string, GeneratorRegistryEntry> = {};
|
|
72
|
+
const from: Record<string, string> = {};
|
|
73
|
+
for (const [pkg, slice] of slices) {
|
|
74
|
+
for (const [name, entry] of Object.entries(slice)) {
|
|
75
|
+
if (name in out) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`generator "${name}" is registered by both ${from[name]} and ${pkg} — a stable ` +
|
|
78
|
+
"name identifies ONE generator across the whole catalog (ADR-0021 D3). Rename " +
|
|
79
|
+
"one of them, or delete the duplicate registration.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
out[name] = entry;
|
|
83
|
+
from[name] = pkg;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Every catalog entry, grouped by layer in {@link GENERATOR_LAYERS} order and
|
|
91
|
+
* alphabetical within a layer.
|
|
92
|
+
*
|
|
93
|
+
* Layer order, not tier order: `layer` is the axis an adopter selects by, so it is the
|
|
94
|
+
* axis the listing is organized along. `tier` still travels on each row.
|
|
95
|
+
*/
|
|
96
|
+
export function listCatalog(): GeneratorRegistryEntry[] {
|
|
97
|
+
return Object.values(composeCatalog()).sort(
|
|
98
|
+
(a, b) =>
|
|
99
|
+
GENERATOR_LAYERS.indexOf(a.layer) - GENERATOR_LAYERS.indexOf(b.layer) ||
|
|
100
|
+
a.name.localeCompare(b.name),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve one catalog entry by stable name, or undefined. */
|
|
105
|
+
export function catalogEntry(name: string): GeneratorRegistryEntry | undefined {
|
|
106
|
+
return composeCatalog()[name];
|
|
107
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// server/typescript/packages/cli/src/lib/collection-load-options.ts
|
|
2
|
+
//
|
|
3
|
+
// FR-023 — everything a resolved `Collection` contributes to a metadata LOAD,
|
|
4
|
+
// in one helper.
|
|
5
|
+
//
|
|
6
|
+
// One helper rather than four properties spread by hand at each of the nine
|
|
7
|
+
// `loadMemory` call sites, for the reason `loadMemoryOptionsFrom` exists next
|
|
8
|
+
// door (#333): threading one and forgetting another is how a capability reaches
|
|
9
|
+
// some commands and not others. Here the stakes are higher than a missing
|
|
10
|
+
// provider — a site that passes `files` alone loads a dependency's artifact
|
|
11
|
+
// under a local-looking source id AND loses the
|
|
12
|
+
// `ERR_DEPENDENCY_PACKAGE_NOT_OWNED` refusal, so a node declared into a
|
|
13
|
+
// dependency's package is silently excluded from that command's output instead
|
|
14
|
+
// of being refused by name.
|
|
15
|
+
import type { Collection, LoadMemoryOptions } from "@metaobjectsdev/sdk";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The load half of a `Collection`: the resolved file list (dependency artifacts
|
|
19
|
+
* first, then the project's own files), the `dep:<name>/<artifact>` source ids
|
|
20
|
+
* those artifacts load under, and the two imported sets the ownership refusal
|
|
21
|
+
* reads.
|
|
22
|
+
*
|
|
23
|
+
* The return type is a `Pick` of {@link LoadMemoryOptions} rather than a
|
|
24
|
+
* hand-written shape, so renaming or retyping an option there is a compile
|
|
25
|
+
* error here rather than a silently-dropped key.
|
|
26
|
+
*/
|
|
27
|
+
export function collectionLoadOptions(
|
|
28
|
+
collection: Collection,
|
|
29
|
+
): Required<Pick<LoadMemoryOptions, "files" | "fileIds" | "importedPackages" | "importedNodes" | "libraries">> {
|
|
30
|
+
return {
|
|
31
|
+
files: collection.files,
|
|
32
|
+
fileIds: collection.fileIds,
|
|
33
|
+
importedPackages: collection.importedPackages,
|
|
34
|
+
importedNodes: collection.importedNodes,
|
|
35
|
+
// FR-043 — the shipped-library selection moved here from metaobjects.config.ts, so
|
|
36
|
+
// it now rides the same helper as everything else the collection contributes. That
|
|
37
|
+
// is the point of the move as much as the neutrality is: `libraries` used to be
|
|
38
|
+
// threaded by `loadMemoryOptionsFrom`, a SECOND helper, and #333 is on record as the
|
|
39
|
+
// bug where one of the two reached every command and the other reached none.
|
|
40
|
+
libraries: collection.libraries,
|
|
41
|
+
};
|
|
42
|
+
}
|