@metaobjectsdev/cli 0.24.3 → 0.24.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/eject.d.ts +38 -0
- package/dist/src/commands/eject.d.ts.map +1 -0
- package/dist/src/commands/eject.js +233 -0
- package/dist/src/commands/eject.js.map +1 -0
- package/dist/src/commands/init.d.ts +11 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +182 -16
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +26 -5
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +8 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -0
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +4 -13
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/package-manifest.d.ts +27 -0
- package/dist/src/lib/package-manifest.d.ts.map +1 -0
- package/dist/src/lib/package-manifest.js +52 -0
- package/dist/src/lib/package-manifest.js.map +1 -0
- package/package.json +11 -11
- package/src/commands/eject.ts +282 -0
- package/src/commands/init.ts +187 -15
- package/src/commands/verify.ts +25 -5
- package/src/index.ts +24 -0
- package/src/lib/args.ts +34 -0
- package/src/lib/detect-stack.ts +4 -11
- package/src/lib/package-manifest.ts +58 -0
package/src/commands/init.ts
CHANGED
|
@@ -11,13 +11,34 @@ import { resolveStack } from "../lib/detect-stack.js";
|
|
|
11
11
|
import { parseInitArgs } from "../lib/args.js";
|
|
12
12
|
import { log } from "../lib/log.js";
|
|
13
13
|
import { cliVersion } from "../lib/version.js";
|
|
14
|
+
import { declaredDependencyNames, type PackageManifest } from "../lib/package-manifest.js";
|
|
14
15
|
import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
|
|
15
|
-
import { readReferenceTemplate,
|
|
16
|
+
import { readReferenceTemplate, type ReferenceGeneratorName } from "@metaobjectsdev/codegen-ts";
|
|
16
17
|
|
|
17
18
|
// ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
|
|
18
19
|
// the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
|
|
19
20
|
const OWNED_GENERATORS_DIR = "codegen/generators";
|
|
20
21
|
|
|
22
|
+
// The FOUR reference generators `meta init` copies EAGERLY — deliberately an explicit
|
|
23
|
+
// literal, not derived from @metaobjectsdev/codegen-ts's REFERENCE_GENERATOR_NAMES (the
|
|
24
|
+
// full list of everything `meta eject` can copy). Looping over that array unconditionally
|
|
25
|
+
// used to mean init scaffolded whatever it contained: when a later task registered
|
|
26
|
+
// "routes-hono" there, init started writing an unwired Hono generator nothing in the
|
|
27
|
+
// scaffolded config imports, on every fresh project, silently. This constant is the
|
|
28
|
+
// scaffolded metaobjects.config.ts's import list (buildMetaobjectsConfigBody below) made
|
|
29
|
+
// explicit and checkable — anything else is eject-on-demand via `meta eject <name>`, which
|
|
30
|
+
// exists exactly so eager copying isn't the only way to take ownership of a template.
|
|
31
|
+
const SCAFFOLDED_GENERATOR_NAMES: readonly ReferenceGeneratorName[] = ["entity", "queries", "routes", "barrel"];
|
|
32
|
+
|
|
33
|
+
// The scaffolded config's outDir + dbImport, as named constants so the throwing-stub
|
|
34
|
+
// path below is DERIVED from the same values the config template embeds rather than
|
|
35
|
+
// duplicated as a second literal that could drift from it.
|
|
36
|
+
const SCAFFOLD_OUT_DIR = "src/generated";
|
|
37
|
+
const SCAFFOLD_DB_IMPORT = "../db";
|
|
38
|
+
// "src/generated" + "../db" -> "src/db" -> "src/db.ts" (dbImport resolves relative
|
|
39
|
+
// to outDir, same as the module specifier a generated route file emits).
|
|
40
|
+
const DB_STUB_REL_PATH = `${join(SCAFFOLD_OUT_DIR, SCAFFOLD_DB_IMPORT)}.ts`;
|
|
41
|
+
|
|
21
42
|
const META_COMMON_JSON = JSON.stringify(
|
|
22
43
|
{
|
|
23
44
|
metadata: {
|
|
@@ -87,9 +108,18 @@ import { routesFile } from "./codegen/generators/routes.js";
|
|
|
87
108
|
import { barrel } from "./codegen/generators/barrel.js";
|
|
88
109
|
|
|
89
110
|
export default defineConfig({
|
|
90
|
-
outDir: "
|
|
91
|
-
extStyle: "js", // ".js"-extensioned relative imports —
|
|
92
|
-
|
|
111
|
+
outDir: "${SCAFFOLD_OUT_DIR}",
|
|
112
|
+
extStyle: "js", // ".js"-extensioned relative imports — correct for Node ESM and \`tsc\` with
|
|
113
|
+
// nodenext, which is what a fresh project has. BUNDLERS DISAGREE: this fails
|
|
114
|
+
// outright under Turbopack (even between two generated files, so the whole
|
|
115
|
+
// generated tree goes unresolvable), while Vite and esbuild accept it and
|
|
116
|
+
// webpack needs \`resolve.extensionAlias\`. If a generated import fails to
|
|
117
|
+
// resolve, set "none" and retest — do not assume this line covers your bundler.
|
|
118
|
+
dbImport: "${SCAFFOLD_DB_IMPORT}", // routesFile() below emits \`import { db } from …\` — meta init
|
|
119
|
+
// scaffolded ${DB_STUB_REL_PATH} as a THROWING STUB (types clean, no
|
|
120
|
+
// driver chosen) so meta gen and tsc pass; replace it with your real
|
|
121
|
+
// Drizzle connection before running the app.
|
|
122
|
+
// (queriesFile() takes db as a parameter and never reads this.)
|
|
93
123
|
dialect: "${dialect}",
|
|
94
124
|
apiPrefix: "", // set to "/api" if your routes mount under /api
|
|
95
125
|
generators: [
|
|
@@ -107,10 +137,76 @@ export default defineConfig({
|
|
|
107
137
|
`;
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
|
|
140
|
+
// The throwing-stub scaffolded at `dbImport`'s resolved path (DB_STUB_REL_PATH,
|
|
141
|
+
// "src/db.ts" by default). It exists so `meta gen` and a fresh project's FIRST
|
|
142
|
+
// `tsc` both succeed with no driver chosen and no dependency added — deliberately
|
|
143
|
+
// NOT a real connection. Every generated route only ever passes `db` straight
|
|
144
|
+
// through to `mountCrudRoutes(...)`; nothing reads a property off it at import
|
|
145
|
+
// time, so a value typed `unknown` (not `any`) satisfies every call site while
|
|
146
|
+
// making a genuine runtime use (mountCrudRoutes calling `db.select()` etc.) throw
|
|
147
|
+
// immediately with an actionable message instead of failing to resolve at all.
|
|
148
|
+
// Built as an array of plain single-quoted lines (not a template literal) so the
|
|
149
|
+
// backticks and quotes inside the comment/message need no escaping.
|
|
150
|
+
const DB_STUB_BODY = [
|
|
151
|
+
"// `meta init` scaffolded this file because the generated Fastify routes",
|
|
152
|
+
'// `import { db } from "../db.js"` (see `dbImport` in metaobjects.config.ts) —',
|
|
153
|
+
"// a module that has to exist for `meta gen` and `tsc` to succeed. MetaObjects",
|
|
154
|
+
"// cannot fill it in for real without choosing a database driver on your",
|
|
155
|
+
"// behalf (better-sqlite3 vs @libsql/client vs pg vs postgres.js) and adding a",
|
|
156
|
+
"// dependency you may not want, so this is a STUB, not a connection.",
|
|
157
|
+
"//",
|
|
158
|
+
"// It type-checks and satisfies every generated import, but throws the first",
|
|
159
|
+
"// time anything actually touches `db` at runtime. Replace the export below",
|
|
160
|
+
"// with your real Drizzle connection, e.g.:",
|
|
161
|
+
"//",
|
|
162
|
+
'// import { drizzle } from "drizzle-orm/better-sqlite3";',
|
|
163
|
+
'// import Database from "better-sqlite3";',
|
|
164
|
+
'// export const db = drizzle(new Database("dev.sqlite"));',
|
|
165
|
+
"//",
|
|
166
|
+
"// (swap the driver import for your dialect — see",
|
|
167
|
+
"// https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/wiring-generated-queries.md",
|
|
168
|
+
"// for SQLite/libsql, Cloudflare D1, Postgres and multi-tenant setups.)",
|
|
169
|
+
"",
|
|
170
|
+
"const UNWIRED_MESSAGE =",
|
|
171
|
+
' "src/db.ts is still the scaffolded stub meta init wrote — it cannot choose " +',
|
|
172
|
+
" \"a database driver for you. Replace 'export const db = ...' below with \" +",
|
|
173
|
+
' "your real Drizzle connection, e.g.:\\n\\n" +',
|
|
174
|
+
" \" import { drizzle } from 'drizzle-orm/better-sqlite3';\\n\" +",
|
|
175
|
+
" \" import Database from 'better-sqlite3';\\n\" +",
|
|
176
|
+
" \" export const db = drizzle(new Database('dev.sqlite'));\\n\";",
|
|
177
|
+
"",
|
|
178
|
+
"function unwired(): never {",
|
|
179
|
+
" throw new Error(UNWIRED_MESSAGE);",
|
|
180
|
+
"}",
|
|
181
|
+
"",
|
|
182
|
+
"/**",
|
|
183
|
+
" * Stand-in for your real Drizzle database connection. Generated code only",
|
|
184
|
+
" * ever passes `db` straight through to `mountCrudRoutes(...)` — it never",
|
|
185
|
+
" * reads a property off it at import time — so this typechecks everywhere",
|
|
186
|
+
" * `db` is used, and throws the message above the first time anything really",
|
|
187
|
+
" * touches it.",
|
|
188
|
+
" */",
|
|
189
|
+
"export const db: unknown = new Proxy({}, { get: unwired });",
|
|
190
|
+
"",
|
|
191
|
+
].join("\n");
|
|
192
|
+
|
|
193
|
+
// Printed only when the stub was ACTUALLY written this run. It is gated on having just
|
|
194
|
+
// written the scaffolded config (see the db-stub block in `init`), so a re-run in a
|
|
195
|
+
// project that keeps its own config writes nothing — and a block claiming otherwise is
|
|
196
|
+
// the same "asserting things about its own scaffold that aren't true" defect the rest of
|
|
197
|
+
// this file was corrected for.
|
|
198
|
+
const DB_STUB_NOTE = `Also scaffolded ${DB_STUB_REL_PATH}: a THROWING STUB standing in for your database
|
|
199
|
+
connection, so the generated routes' \`db\` import resolves and the first tsc is clean.
|
|
200
|
+
Replace it with a real connection before running the app — until you do, the first
|
|
201
|
+
request that touches \`db\` throws with instructions.
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
const SCAFFOLD_SUMMARY = `
|
|
111
205
|
Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
|
|
112
206
|
Codegen generators copied to codegen/generators/ — they're YOURS to edit (ADR-0034 scaffold-and-own).
|
|
207
|
+
`;
|
|
113
208
|
|
|
209
|
+
const NEXT_STEPS = `
|
|
114
210
|
Next steps:
|
|
115
211
|
0. Everything here is ESM — package.json needs "type": "module" (init sets it
|
|
116
212
|
unless the project has CommonJS sources; without it the first tsc fails).
|
|
@@ -329,11 +425,16 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false):
|
|
|
329
425
|
* `codegen/generators/<name>.ts` so they own them. Each file is written only if absent,
|
|
330
426
|
* so a re-run with --force never clobbers a hand-edited generator. The scaffolded
|
|
331
427
|
* metaobjects.config.ts imports these local copies (not the package `/generators` export).
|
|
428
|
+
*
|
|
429
|
+
* Copies SCAFFOLDED_GENERATOR_NAMES only — the four the scaffolded config actually
|
|
430
|
+
* wires — not every name @metaobjectsdev/codegen-ts happens to register. Anything else
|
|
431
|
+
* (routes-hono, and any UI-tier template from codegen-ts-react/-tanstack) is reached with
|
|
432
|
+
* `meta eject <name>`, not by eager copying.
|
|
332
433
|
*/
|
|
333
434
|
async function writeOwnedGenerators(opts: InitOptions, result: InitResult): Promise<void> {
|
|
334
435
|
const dir = join(opts.cwd, OWNED_GENERATORS_DIR);
|
|
335
436
|
await mkdir(dir, { recursive: true });
|
|
336
|
-
for (const name of
|
|
437
|
+
for (const name of SCAFFOLDED_GENERATOR_NAMES) {
|
|
337
438
|
const rel = `${OWNED_GENERATORS_DIR}/${name}.ts`;
|
|
338
439
|
const abs = join(dir, `${name}.ts`);
|
|
339
440
|
if (await fileExists(abs)) {
|
|
@@ -475,8 +576,8 @@ export async function init(opts: InitOptions): Promise<InitResult> {
|
|
|
475
576
|
`.metaobjects/${PACKAGE_MANIFEST_FILE}`,
|
|
476
577
|
);
|
|
477
578
|
result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
|
|
478
|
-
for (const name of
|
|
479
|
-
result.created.push("metaobjects.config.ts", ".gitignore");
|
|
579
|
+
for (const name of SCAFFOLDED_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
|
|
580
|
+
result.created.push("metaobjects.config.ts", DB_STUB_REL_PATH, ".gitignore");
|
|
480
581
|
return result;
|
|
481
582
|
}
|
|
482
583
|
|
|
@@ -524,11 +625,54 @@ export async function init(opts: InitOptions): Promise<InitResult> {
|
|
|
524
625
|
|
|
525
626
|
// Scaffold metaobjects.config.ts at the project root. Never overwrite if it exists.
|
|
526
627
|
const forgeConfigPath = join(opts.cwd, "metaobjects.config.ts");
|
|
527
|
-
|
|
628
|
+
const wroteScaffoldedConfig = !(await fileExists(forgeConfigPath));
|
|
629
|
+
if (wroteScaffoldedConfig) {
|
|
528
630
|
await writeFile(forgeConfigPath, buildMetaobjectsConfigBody(opts.d1 ? "d1" : "sqlite"), "utf8");
|
|
529
631
|
result.created.push("metaobjects.config.ts");
|
|
530
632
|
}
|
|
531
633
|
|
|
634
|
+
// Scaffold the `dbImport` throwing stub at DB_STUB_REL_PATH ("src/db.ts" by
|
|
635
|
+
// default) — ONLY if absent, so a re-run never clobbers a user's real db module
|
|
636
|
+
// (same "write once" precedent as writeOwnedGenerators above). Without this, the
|
|
637
|
+
// scaffolded config declares `dbImport: "../db"` pointing at a module `meta init`
|
|
638
|
+
// never creates, and a fresh project's FIRST `tsc` fails to resolve it.
|
|
639
|
+
//
|
|
640
|
+
// Gated on having just WRITTEN that config, not merely on the stub being absent.
|
|
641
|
+
// DB_STUB_REL_PATH is derived from SCAFFOLD_OUT_DIR + SCAFFOLD_DB_IMPORT — the
|
|
642
|
+
// scaffold's own constants — so it describes where the SCAFFOLDED config points and
|
|
643
|
+
// nowhere else. Re-running `meta init` in a project that already has a config with
|
|
644
|
+
// its own `outDir`/`dbImport` preserves that config (above) and would otherwise still
|
|
645
|
+
// drop a src/db.ts that nothing in the project references: a stray file, in the
|
|
646
|
+
// adopter's application source, answering a question they had already answered.
|
|
647
|
+
const dbStubPath = join(opts.cwd, DB_STUB_REL_PATH);
|
|
648
|
+
const dbStubExists = await fileExists(dbStubPath);
|
|
649
|
+
if (wroteScaffoldedConfig && !dbStubExists) {
|
|
650
|
+
await mkdir(dirname(dbStubPath), { recursive: true });
|
|
651
|
+
await writeFile(dbStubPath, DB_STUB_BODY, "utf8");
|
|
652
|
+
result.created.push(DB_STUB_REL_PATH);
|
|
653
|
+
} else if (dbStubExists) {
|
|
654
|
+
// "preserved" means a file we would have written was left alone. Skipping because
|
|
655
|
+
// this project keeps its own config is not preservation — there is nothing there.
|
|
656
|
+
result.preserved.push(DB_STUB_REL_PATH);
|
|
657
|
+
} else {
|
|
658
|
+
// Neither written nor preserved: this project keeps its own config AND has no stub.
|
|
659
|
+
// Usually correct — its `dbImport` points at a real module somewhere else. But it is
|
|
660
|
+
// ALSO what a scaffolded project looks like after someone deletes or moves src/db.ts,
|
|
661
|
+
// and `meta init --force` cannot tell those apart: `wroteScaffoldedConfig` is only
|
|
662
|
+
// "no config existed", so it is false for the config init itself wrote. Silently
|
|
663
|
+
// doing nothing there leaves the scaffolded `dbImport: "../db"` and the generated
|
|
664
|
+
// routes' `import { db } from "../db.js"` pointing at nothing, and the adopter meets
|
|
665
|
+
// it as a TS2307 from `tsc` with no word from the command that could have said so.
|
|
666
|
+
// Say it here rather than writing: dropping a file into a project that owns its
|
|
667
|
+
// config is what the branch above deliberately refuses.
|
|
668
|
+
result.warnings.push(
|
|
669
|
+
`${DB_STUB_REL_PATH} was not scaffolded — this project has its own ` +
|
|
670
|
+
"metaobjects.config.ts, so init leaves the database module to it. If that config's " +
|
|
671
|
+
`\`dbImport\` resolves to ${DB_STUB_REL_PATH}, create it or the generated routes will ` +
|
|
672
|
+
"not resolve.",
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
532
676
|
// Scaffold a minimal root .gitignore ONLY when the project has none — never
|
|
533
677
|
// clobber a user's existing one (they may have their own rules).
|
|
534
678
|
const rootGitignorePath = join(opts.cwd, ".gitignore");
|
|
@@ -598,13 +742,24 @@ async function ensureEsmPackageType(cwd: string, result: InitResult): Promise<vo
|
|
|
598
742
|
return;
|
|
599
743
|
}
|
|
600
744
|
|
|
745
|
+
const declaredType = pkg.type; // read BEFORE the mutation below overwrites it
|
|
601
746
|
pkg.type = "module";
|
|
602
747
|
const added = addScaffoldDevDependencies(pkg);
|
|
603
748
|
// Preserve the file's existing indentation rather than reformatting someone's manifest.
|
|
604
749
|
const indent = /\n(\s+)"/.exec(raw)?.[1] ?? " ";
|
|
605
750
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`, "utf8");
|
|
751
|
+
// Past tense, deliberately: this reports an edit already made. The imperative
|
|
752
|
+
// ("set `\"type\": \"module\"`") read as a TODO on the one line a newcomer sees
|
|
753
|
+
// last, so a scaffold that had just done the right thing looked like it had failed.
|
|
754
|
+
//
|
|
755
|
+
// And it must not claim the manifest was SILENT on the point: `npm init -y` writes
|
|
756
|
+
// `"type": "commonjs"` explicitly (npm 11.x), which is the dominant first-touch path,
|
|
757
|
+
// so "declared no module system" was false exactly where it is read most. Report what
|
|
758
|
+
// was actually there.
|
|
759
|
+
const previous = typeof declaredType === "string" ? declaredType : undefined;
|
|
606
760
|
result.warnings.push(
|
|
607
|
-
|
|
761
|
+
`package.json ${previous === undefined ? "declared no module system" : `declared "type": "${previous}"`} — ` +
|
|
762
|
+
'set `"type": "module"` for you, because MetaObjects scaffolds and generates ESM, ' +
|
|
608
763
|
"which a CommonJS project cannot compile.",
|
|
609
764
|
);
|
|
610
765
|
if (added.length > 0) {
|
|
@@ -640,10 +795,17 @@ function addScaffoldDevDependencies(pkg: Record<string, unknown>): string[] {
|
|
|
640
795
|
"@metaobjectsdev/metadata": `^${version}`,
|
|
641
796
|
};
|
|
642
797
|
const dev = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
643
|
-
|
|
798
|
+
// "Already declared" spans all four dependency fields — the shared rule, so this
|
|
799
|
+
// cannot drift from what `meta eject` means by the same words. It used to ask only
|
|
800
|
+
// dependencies + devDependencies, which meant a project declaring codegen-ts as a
|
|
801
|
+
// PEER dependency (correct for a library whose consumer supplies the version) got it
|
|
802
|
+
// added to devDependencies as well: the same package pinned twice in one manifest,
|
|
803
|
+
// and a second physical copy is the class-identity split this repo has been bitten
|
|
804
|
+
// by twice.
|
|
805
|
+
const declared = declaredDependencyNames(pkg as PackageManifest);
|
|
644
806
|
const added: string[] = [];
|
|
645
807
|
for (const [name, range] of Object.entries(wanted)) {
|
|
646
|
-
if (
|
|
808
|
+
if (declared.has(name)) continue;
|
|
647
809
|
dev[name] = range;
|
|
648
810
|
added.push(name);
|
|
649
811
|
}
|
|
@@ -694,8 +856,18 @@ function buildD1MigrateBlock(cwd: string): Record<string, unknown> {
|
|
|
694
856
|
return block;
|
|
695
857
|
}
|
|
696
858
|
|
|
697
|
-
|
|
698
|
-
|
|
859
|
+
/**
|
|
860
|
+
* The post-init message.
|
|
861
|
+
*
|
|
862
|
+
* @param dbStubWritten whether THIS run wrote {@link DB_STUB_REL_PATH}. Required rather
|
|
863
|
+
* than defaulted, because a default would silently restore the bug this parameter
|
|
864
|
+
* exists to close: the note used to be part of one static string and so claimed the
|
|
865
|
+
* stub on every run, including the `meta init --force` in a project keeping its own
|
|
866
|
+
* `metaobjects.config.ts`, where the stub is deliberately not written. Callers pass
|
|
867
|
+
* `result.created.includes(DB_STUB_REL_PATH)` — the same list the message describes.
|
|
868
|
+
*/
|
|
869
|
+
export function nextStepsBlock(dbStubWritten: boolean): string {
|
|
870
|
+
return SCAFFOLD_SUMMARY + (dbStubWritten ? DB_STUB_NOTE : "") + NEXT_STEPS;
|
|
699
871
|
}
|
|
700
872
|
|
|
701
873
|
async function dirExists(p: string): Promise<boolean> {
|
|
@@ -760,7 +932,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
|
|
|
760
932
|
}
|
|
761
933
|
for (const w of result.warnings) log.warn(w);
|
|
762
934
|
} else {
|
|
763
|
-
log.info(nextStepsBlock());
|
|
935
|
+
log.info(nextStepsBlock(result.created.includes(DB_STUB_REL_PATH)));
|
|
764
936
|
// Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
|
|
765
937
|
// discovery warning) — these are otherwise dropped on the normal init path.
|
|
766
938
|
for (const w of result.warnings) log.warn(w);
|
package/src/commands/verify.ts
CHANGED
|
@@ -581,7 +581,18 @@ export async function verifyCommand(
|
|
|
581
581
|
|
|
582
582
|
let errorCount = 0;
|
|
583
583
|
let warnCount = 0;
|
|
584
|
-
|
|
584
|
+
// Both report lines below must divide by the SAME thing, and it must be
|
|
585
|
+
// something that was actually examined. They used to disagree: the failure line
|
|
586
|
+
// divided by `templates.length` — every node found, INCLUDING every one the loop
|
|
587
|
+
// `continue`s past (unknown subtype, no renderable body ref) — while the pass
|
|
588
|
+
// line divided by a count of bodies verified. On a real project the same run read
|
|
589
|
+
// "11 drift error(s) across 29 template(s)" while failing and "22 template(s)
|
|
590
|
+
// clean" once fixed: seven templates apparently vanishing on the way to green,
|
|
591
|
+
// and a failure line claiming a denominator of work that had not been done.
|
|
592
|
+
// `checkedTemplates` is now the single unit — templates at least one body of
|
|
593
|
+
// which was verified. (An @kind=email template has up to three bodies and still
|
|
594
|
+
// counts once; the line says "template(s)", so it counts templates.)
|
|
595
|
+
let checkedTemplates = 0;
|
|
585
596
|
|
|
586
597
|
for (const tmpl of templates) {
|
|
587
598
|
// ADR-0039: effective attrs — @payloadRef may be inherited via an abstract template.
|
|
@@ -645,6 +656,7 @@ export async function verifyCommand(
|
|
|
645
656
|
const requiredSlots = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_SLOTS)) : [];
|
|
646
657
|
const requiredTags = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_TAGS)) : [];
|
|
647
658
|
|
|
659
|
+
let anyBodyChecked = false;
|
|
648
660
|
for (const { label, ref } of refs) {
|
|
649
661
|
// Render-engine drift check: mustache variables ↔ payload field names.
|
|
650
662
|
const text = provider.resolve(ref);
|
|
@@ -656,7 +668,7 @@ export async function verifyCommand(
|
|
|
656
668
|
continue;
|
|
657
669
|
}
|
|
658
670
|
const drift = verify(text, fieldTree, { provider, requiredSlots, requiredTags });
|
|
659
|
-
|
|
671
|
+
anyBodyChecked = true;
|
|
660
672
|
for (const e of drift) {
|
|
661
673
|
if (e.code === ERR_REQUIRED_SLOT_UNUSED) {
|
|
662
674
|
log.warn(`[${tmpl.name}] (${label}) ${e.code}: ${e.path}`);
|
|
@@ -667,16 +679,17 @@ export async function verifyCommand(
|
|
|
667
679
|
}
|
|
668
680
|
}
|
|
669
681
|
}
|
|
682
|
+
if (anyBodyChecked) checkedTemplates++;
|
|
670
683
|
}
|
|
671
684
|
|
|
672
685
|
if (errorCount > 0) {
|
|
673
686
|
log.error(
|
|
674
|
-
`meta verify — ${errorCount} drift error(s) across ${
|
|
687
|
+
`meta verify — ${errorCount} drift error(s) across ${checkedTemplates} template(s).`,
|
|
675
688
|
);
|
|
676
689
|
return 1;
|
|
677
690
|
}
|
|
678
691
|
log.info(
|
|
679
|
-
`meta verify — ${
|
|
692
|
+
`meta verify — ${checkedTemplates} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`,
|
|
680
693
|
);
|
|
681
694
|
return 0;
|
|
682
695
|
}
|
|
@@ -936,9 +949,16 @@ export async function verifyCommand(
|
|
|
936
949
|
if (result.changes.length === 0) return [];
|
|
937
950
|
|
|
938
951
|
return [
|
|
952
|
+
// `meta migrate --from-db` is NOT the repair: it writes a snapshot only when it has
|
|
953
|
+
// changes to EMIT, so on a database that already matches the metadata it reports
|
|
954
|
+
// "no schema changes / nothing to do", writes nothing, and leaves the stale snapshot
|
|
955
|
+
// exactly as it was — so this gate fails again, identically, with the user having
|
|
956
|
+
// been told everything is in sync. `baseline --from-db` rewrites it unconditionally,
|
|
957
|
+
// which is the whole point of the subcommand.
|
|
939
958
|
`the committed schema snapshot disagrees with ${displayUrl} ` +
|
|
940
959
|
`(${result.changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
|
|
941
|
-
`and fail at apply. Re-derive it with
|
|
960
|
+
`and fail at apply. Re-derive it with ` +
|
|
961
|
+
`'meta migrate baseline --from-db --db <url> --dialect ${dialect}'.`,
|
|
942
962
|
...summarizeDrift(result.changes),
|
|
943
963
|
];
|
|
944
964
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ COMMANDS:
|
|
|
19
19
|
init --config-only Write only .metaobjects/config.json — for a Maven- or pip-rooted project
|
|
20
20
|
agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports
|
|
21
21
|
gen [<entity>...] Codegen TS targets from your declared metadata
|
|
22
|
+
eject <generator> Copy a reference generator into codegen/generators/ to own it (any time after init)
|
|
23
|
+
eject --list List every ejectable generator name, grouped by package
|
|
22
24
|
types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
|
|
23
25
|
export Flatten loaded metadata to one canonical JSON artifact
|
|
24
26
|
docs [<project-root>] --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
|
|
@@ -113,6 +115,24 @@ field.enum). Warnings only — it never fails the build. Opt out with --no-antip
|
|
|
113
115
|
or META_NO_ANTIPATTERNS=1.
|
|
114
116
|
|
|
115
117
|
NOTE: outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts
|
|
118
|
+
`,
|
|
119
|
+
eject: `meta eject — copy a reference generator into your repo so you own it
|
|
120
|
+
|
|
121
|
+
USAGE:
|
|
122
|
+
meta eject <name> Copy generator <name> into codegen/generators/<name>.ts
|
|
123
|
+
meta eject --list List every ejectable generator name, grouped by package
|
|
124
|
+
|
|
125
|
+
FLAGS:
|
|
126
|
+
--list List ejectable generators instead of copying one
|
|
127
|
+
--force Overwrite an already-ejected file (default: never clobber)
|
|
128
|
+
--help, -h Print this help
|
|
129
|
+
|
|
130
|
+
\`meta init\` copies four generators (entity, queries, routes, barrel) into
|
|
131
|
+
codegen/generators/ automatically (ADR-0034 scaffold-and-own). \`meta eject\` is
|
|
132
|
+
the same operation for ANY generator — one you skipped at init time, a UI-tier
|
|
133
|
+
generator like form/hooks/grid, or one a package gains later. It prints the
|
|
134
|
+
import line to paste into metaobjects.config.ts, and it never overwrites a
|
|
135
|
+
file you already own unless you pass --force.
|
|
116
136
|
`,
|
|
117
137
|
verify: `meta verify — drift gate (templates / DB schema / codegen / migration replay)
|
|
118
138
|
|
|
@@ -374,6 +394,10 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
374
394
|
const { genCommand } = await import("./commands/gen.js");
|
|
375
395
|
return genCommand(rest, cwd, fmt);
|
|
376
396
|
}
|
|
397
|
+
case "eject": {
|
|
398
|
+
const { ejectCommand } = await import("./commands/eject.js");
|
|
399
|
+
return ejectCommand(rest, cwd);
|
|
400
|
+
}
|
|
377
401
|
case "export": {
|
|
378
402
|
const { exportCommand } = await import("./commands/export.js");
|
|
379
403
|
return exportCommand(rest, cwd);
|
package/src/lib/args.ts
CHANGED
|
@@ -490,3 +490,37 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
490
490
|
applyPending,
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
|
+
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// eject flags — FR-040 §4.2(a)
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
export interface EjectFlags {
|
|
499
|
+
/** The generator name to eject; undefined when only --list was given. */
|
|
500
|
+
name: string | undefined;
|
|
501
|
+
list: boolean;
|
|
502
|
+
/** Overwrite an already-ejected file; default false — eject never clobbers. */
|
|
503
|
+
force: boolean;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function parseEjectArgs(argv: string[]): EjectFlags {
|
|
507
|
+
const { values, positionals } = parseArgs({
|
|
508
|
+
args: argv,
|
|
509
|
+
options: {
|
|
510
|
+
"list": { type: "boolean", default: false },
|
|
511
|
+
"force": { type: "boolean", default: false },
|
|
512
|
+
},
|
|
513
|
+
strict: true,
|
|
514
|
+
allowPositionals: true,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (positionals.length > 1) {
|
|
518
|
+
throw new Error(`meta eject takes at most one generator name; got: ${positionals.join(", ")}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
name: positionals[0],
|
|
523
|
+
list: !!values.list,
|
|
524
|
+
force: !!values.force,
|
|
525
|
+
};
|
|
526
|
+
}
|
package/src/lib/detect-stack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveCollection } from "@metaobjectsdev/sdk";
|
|
4
|
+
import { declaredDependencyNames, readPackageManifest } from "./package-manifest.js";
|
|
4
5
|
import {
|
|
5
6
|
detectStack, detectConcerns, makeStack,
|
|
6
7
|
type ServerLang, type ClientFramework, type Stack, type ProjectProbe,
|
|
@@ -8,17 +9,9 @@ import {
|
|
|
8
9
|
} from "@metaobjectsdev/sdk/agent-context";
|
|
9
10
|
|
|
10
11
|
function depNames(cwd: string): Set<string> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, Record<string, string>>;
|
|
16
|
-
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
17
|
-
for (const name of Object.keys(pkg[key] ?? {})) out.add(name);
|
|
18
|
-
}
|
|
19
|
-
} catch { /* unreadable manifest — treat as no deps */ }
|
|
20
|
-
}
|
|
21
|
-
return out;
|
|
12
|
+
// An unreadable or absent manifest reads as no deps, which is what stack detection
|
|
13
|
+
// wants: it probes, it does not require.
|
|
14
|
+
return declaredDependencyNames(readPackageManifest(cwd) ?? {});
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
// Cheap substring probe, not a metamodel load: matches both canonical JSON's
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// One answer to "what does this project declare as a dependency".
|
|
2
|
+
//
|
|
3
|
+
// Three commands ask it — `meta init` (is this package already declared, or must the
|
|
4
|
+
// scaffold add it?), `meta eject` (will the ejected file's imports resolve, or does the
|
|
5
|
+
// adopter's tsc report TS2307?), and stack detection (which frameworks is this project
|
|
6
|
+
// on?). They had three hand-rolled JSON-parse-and-union blocks covering DIFFERENT field
|
|
7
|
+
// sets, and the asymmetry was the cost: `eject` reported a peer-declared package as
|
|
8
|
+
// missing and told the adopter to install what they already had — advice that, followed,
|
|
9
|
+
// adds a second physical copy of a package whose class identity is load-bearing (the
|
|
10
|
+
// ts-poet split in 0.21.6 and the metadata node-guard split are both that bug).
|
|
11
|
+
//
|
|
12
|
+
// A fix to one had no way to find the other two: nothing referenced anything, and no
|
|
13
|
+
// shared name connected them to a grep.
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** The dependency-bearing fields of a package.json, all optional. */
|
|
18
|
+
export interface PackageManifest {
|
|
19
|
+
dependencies?: Record<string, string>;
|
|
20
|
+
devDependencies?: Record<string, string>;
|
|
21
|
+
peerDependencies?: Record<string, string>;
|
|
22
|
+
optionalDependencies?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every package name the manifest declares, across ALL FOUR dependency fields.
|
|
27
|
+
*
|
|
28
|
+
* All four, because the question every caller is really asking is "will this resolve,
|
|
29
|
+
* and will their typecheck be happy" — not "is it in one particular field". A library
|
|
30
|
+
* consuming MetaObjects through `peerDependencies` (the correct declaration for a
|
|
31
|
+
* package whose consumer supplies the version) has it declared, and so does one using
|
|
32
|
+
* `optionalDependencies`.
|
|
33
|
+
*/
|
|
34
|
+
export function declaredDependencyNames(pkg: PackageManifest): Set<string> {
|
|
35
|
+
const out = new Set<string>();
|
|
36
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const) {
|
|
37
|
+
for (const name of Object.keys(pkg[field] ?? {})) out.add(name);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read and parse `<cwd>/package.json`.
|
|
44
|
+
*
|
|
45
|
+
* `undefined` distinguishes "no readable manifest" from "a manifest declaring nothing",
|
|
46
|
+
* which callers report differently: with no manifest there is nothing to compare
|
|
47
|
+
* against, so the honest message names what a file needs rather than claiming it is
|
|
48
|
+
* missing from a list that was never read.
|
|
49
|
+
*/
|
|
50
|
+
export function readPackageManifest(cwd: string): PackageManifest | undefined {
|
|
51
|
+
const path = join(cwd, "package.json");
|
|
52
|
+
if (!existsSync(path)) return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(path, "utf8")) as PackageManifest;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|