@metaobjectsdev/cli 1.0.3 → 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.
Files changed (79) hide show
  1. package/README.md +13 -5
  2. package/dist/src/commands/eject.d.ts +2 -1
  3. package/dist/src/commands/eject.d.ts.map +1 -1
  4. package/dist/src/commands/eject.js +235 -75
  5. package/dist/src/commands/eject.js.map +1 -1
  6. package/dist/src/commands/gen.d.ts.map +1 -1
  7. package/dist/src/commands/gen.js +119 -29
  8. package/dist/src/commands/gen.js.map +1 -1
  9. package/dist/src/commands/init.d.ts +5 -9
  10. package/dist/src/commands/init.d.ts.map +1 -1
  11. package/dist/src/commands/init.js +80 -384
  12. package/dist/src/commands/init.js.map +1 -1
  13. package/dist/src/commands/verify.d.ts.map +1 -1
  14. package/dist/src/commands/verify.js +40 -14
  15. package/dist/src/commands/verify.js.map +1 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +36 -14
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lib/advisory.d.ts +6 -0
  20. package/dist/src/lib/advisory.d.ts.map +1 -1
  21. package/dist/src/lib/advisory.js +10 -0
  22. package/dist/src/lib/advisory.js.map +1 -1
  23. package/dist/src/lib/args.d.ts +18 -2
  24. package/dist/src/lib/args.d.ts.map +1 -1
  25. package/dist/src/lib/args.js +15 -3
  26. package/dist/src/lib/args.js.map +1 -1
  27. package/dist/src/lib/catalog-listing.d.ts +92 -0
  28. package/dist/src/lib/catalog-listing.d.ts.map +1 -0
  29. package/dist/src/lib/catalog-listing.js +268 -0
  30. package/dist/src/lib/catalog-listing.js.map +1 -0
  31. package/dist/src/lib/catalog.d.ts +41 -0
  32. package/dist/src/lib/catalog.d.ts.map +1 -0
  33. package/dist/src/lib/catalog.js +89 -0
  34. package/dist/src/lib/catalog.js.map +1 -0
  35. package/dist/src/lib/collection-load-options.d.ts +1 -1
  36. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  37. package/dist/src/lib/collection-load-options.js +6 -0
  38. package/dist/src/lib/collection-load-options.js.map +1 -1
  39. package/dist/src/lib/install-set.d.ts +31 -0
  40. package/dist/src/lib/install-set.d.ts.map +1 -0
  41. package/dist/src/lib/install-set.js +103 -0
  42. package/dist/src/lib/install-set.js.map +1 -0
  43. package/dist/src/lib/library-eject.d.ts +79 -0
  44. package/dist/src/lib/library-eject.d.ts.map +1 -0
  45. package/dist/src/lib/library-eject.js +241 -0
  46. package/dist/src/lib/library-eject.js.map +1 -0
  47. package/dist/src/lib/library-listing.d.ts +63 -0
  48. package/dist/src/lib/library-listing.d.ts.map +1 -0
  49. package/dist/src/lib/library-listing.js +178 -0
  50. package/dist/src/lib/library-listing.js.map +1 -0
  51. package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
  52. package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
  53. package/dist/src/lib/library-prefix-advisory.js +67 -0
  54. package/dist/src/lib/library-prefix-advisory.js.map +1 -0
  55. package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
  56. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  57. package/dist/src/lib/load-metaobjects-config.js +11 -17
  58. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  59. package/dist/src/lib/requirement-check.d.ts +17 -2
  60. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  61. package/dist/src/lib/requirement-check.js +51 -9
  62. package/dist/src/lib/requirement-check.js.map +1 -1
  63. package/package.json +10 -10
  64. package/src/commands/eject.ts +304 -90
  65. package/src/commands/gen.ts +128 -28
  66. package/src/commands/init.ts +82 -390
  67. package/src/commands/verify.ts +47 -15
  68. package/src/index.ts +36 -14
  69. package/src/lib/advisory.ts +13 -0
  70. package/src/lib/args.ts +29 -5
  71. package/src/lib/catalog-listing.ts +369 -0
  72. package/src/lib/catalog.ts +107 -0
  73. package/src/lib/collection-load-options.ts +7 -1
  74. package/src/lib/install-set.ts +116 -0
  75. package/src/lib/library-eject.ts +298 -0
  76. package/src/lib/library-listing.ts +258 -0
  77. package/src/lib/library-prefix-advisory.ts +78 -0
  78. package/src/lib/load-metaobjects-config.ts +15 -21
  79. package/src/lib/requirement-check.ts +71 -11
@@ -2,7 +2,6 @@ import { mkdir, writeFile, readFile, readdir, stat, rm } from "node:fs/promises"
2
2
  import { join } from "node:path";
3
3
  import { dirname } from "node:path";
4
4
  import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
5
- import { createRequire } from "node:module";
6
5
  import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, LOCK_FILE } from "@metaobjectsdev/sdk";
7
6
  import {
8
7
  assemble, resolveAgentContextRoot, planScaffold,
@@ -13,39 +12,23 @@ import { reportIgnoredScaffold } from "../lib/ignored-scaffold-check.js";
13
12
  import { parseInitArgs } from "../lib/args.js";
14
13
  import { log } from "../lib/log.js";
15
14
  import { cliVersion } from "../lib/version.js";
16
- import { declaredDependencyNames, type PackageManifest } from "../lib/package-manifest.js";
17
15
  import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
18
- import { DEFAULT_DOCS_DIR, readReferenceTemplate, type ReferenceGeneratorName } from "@metaobjectsdev/codegen-ts";
16
+ import { DEFAULT_DOCS_DIR } from "@metaobjectsdev/codegen-ts";
19
17
 
20
18
  // ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
21
19
  // the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
22
20
  const OWNED_GENERATORS_DIR = "codegen/generators";
23
21
 
24
- // The FIVE reference generators `meta init` copies EAGERLY — deliberately an explicit
25
- // literal, not derived from @metaobjectsdev/codegen-ts's REFERENCE_GENERATOR_NAMES (the
26
- // full list of everything `meta eject` can copy). Looping over that array unconditionally
27
- // used to mean init scaffolded whatever it contained: when a later task registered
28
- // "routes-hono" there, init started writing an unwired Hono generator nothing in the
29
- // scaffolded config imports, on every fresh project, silently. This constant is the
30
- // scaffolded metaobjects.config.ts's import list (buildMetaobjectsConfigBody below) made
31
- // explicit and checkable — anything else is eject-on-demand via `meta eject <name>`, which
32
- // exists exactly so eager copying isn't the only way to take ownership of a template.
22
+
23
+ // The scaffolded config's outDir, as a named constant so the config template and
24
+ // anything derived from it cannot drift.
33
25
  //
34
- // "names" joined this list by spec §A5's ruling, not by falling out of the general rule
35
- // above: TypeScript is opt-in by construction under ADR-0034 (meta gen runs the adopter's
36
- // copy, so a packaged change to a generator can never reach anyone who has ejected), so
37
- // the honest maximum for the names artifact is every NEW `meta init` getting it and every
38
- // existing project adding one config line by hand.
39
- export const SCAFFOLDED_GENERATOR_NAMES: readonly ReferenceGeneratorName[] = ["entity", "queries", "routes", "barrel", "names"];
40
-
41
- // The scaffolded config's outDir + dbImport, as named constants so the throwing-stub
42
- // path below is DERIVED from the same values the config template embeds rather than
43
- // duplicated as a second literal that could drift from it.
26
+ // `dbImport` is deliberately NOT here any more. It existed only because the scaffold
27
+ // wired `routesFile()`, whose output emits `import { db } from …`; with nothing wired
28
+ // there is no such import, so the throwing `src/db.ts` stub that made it resolve has
29
+ // gone too. `dbImport` is now a `configKey` on the `routes` catalog entry — reported by
30
+ // `meta gen --list` and by `meta eject routes`, to the adopter who actually chose it.
44
31
  const SCAFFOLD_OUT_DIR = "src/generated";
45
- const SCAFFOLD_DB_IMPORT = "../db";
46
- // "src/generated" + "../db" -> "src/db" -> "src/db.ts" (dbImport resolves relative
47
- // to outDir, same as the module specifier a generated route file emits).
48
- const DB_STUB_REL_PATH = `${join(SCAFFOLD_OUT_DIR, SCAFFOLD_DB_IMPORT)}.ts`;
49
32
 
50
33
  const META_COMMON_JSON = JSON.stringify(
51
34
  {
@@ -107,122 +90,49 @@ dist/
107
90
 
108
91
  function buildMetaobjectsConfigBody(dialect: "sqlite" | "postgres" | "d1" = "sqlite"): string {
109
92
  return `import { defineConfig } from "@metaobjectsdev/cli";
110
- // Owned codegen generators (ADR-0034 scaffold-and-own). \`meta init\` copied these
111
- // reference templates into ./codegen/generators/ — they are YOURS to edit, and
112
- // \`meta gen\` runs from these local copies, not from the package. Read each file's
113
- // header doc-block for what it emits and how to customize it.
114
- import { entityFile } from "./codegen/generators/entity.js";
115
- import { queriesFile } from "./codegen/generators/queries.js";
116
- import { routesFile } from "./codegen/generators/routes.js";
117
- import { namesFile } from "./codegen/generators/names.js";
118
- import { barrel } from "./codegen/generators/barrel.js";
119
93
 
120
94
  export default defineConfig({
121
- outDir: "${SCAFFOLD_OUT_DIR}",
122
- extStyle: "js", // ".js"-extensioned relative imports — correct for Node ESM and \`tsc\` with
123
- // nodenext, which is what a fresh project has. BUNDLERS DISAGREE: this fails
124
- // outright under Turbopack (even between two generated files, so the whole
125
- // generated tree goes unresolvable), while Vite and esbuild accept it and
126
- // webpack needs \`resolve.extensionAlias\`. If a generated import fails to
127
- // resolve, set "none" and retest — do not assume this line covers your bundler.
128
- dbImport: "${SCAFFOLD_DB_IMPORT}", // routesFile() below emits \`import { db } from …\` — meta init
129
- // scaffolded ${DB_STUB_REL_PATH} as a THROWING STUB (types clean, no
130
- // driver chosen) so meta gen and tsc pass; replace it with your real
131
- // Drizzle connection before running the app.
132
- // (queriesFile() takes db as a parameter and never reads this.)
133
- dialect: "${dialect}",
134
- apiPrefix: "", // set to "/api" if your routes mount under /api
135
- generators: [
136
- entityFile(),
137
- queriesFile(),
138
- routesFile(),
139
- namesFile(), // <Entity>Names — physical table/column constants (spec §A1/§A5)
140
- barrel(),
141
- ],
95
+ outDir: "${SCAFFOLD_OUT_DIR}",
96
+ dialect: "${dialect}",
97
+
98
+ // NOTHING IS GENERATED UNTIL YOU CHOOSE IT.
99
+ //
100
+ // MetaObjects does not decide which code your application needs — you do, or the
101
+ // agent working in this repo does. The catalog is:
102
+ //
103
+ // meta gen --list --format json --probe
104
+ //
105
+ // \`--probe\` runs every generator against YOUR model and reports how many files each
106
+ // would emit, so you can see what your metadata is already asking for. Group by
107
+ // \`layer\`: model / persistence / api / client / docs / capability.
108
+ //
109
+ // Then take the ones you want:
110
+ //
111
+ // meta eject entity queries routes barrel
112
+ //
113
+ // That copies each generator into ./codegen/generators/ — YOURS to edit, and what
114
+ // \`meta gen\` runs — and prints the import line to add here, the entry to add below,
115
+ // what to install, and any config keys those generators read (\`dbImport\`,
116
+ // \`apiPrefix\`, \`extStyle\`, …). Add them here as you go.
117
+ generators: [],
118
+
142
119
  docs: {
143
- outDir: "${DEFAULT_DOCS_DIR}", // every surface lands here (run: meta docs).
120
+ outDir: "${DEFAULT_DOCS_DIR}", // every surface lands here (run: meta docs).
144
121
  // A SUB-directory on purpose: these pages are regenerated
145
122
  // and overwritten, and docs/ itself is usually yours.
146
- layout: "flat", // or "package" for multi-package models
123
+ layout: "flat", // or "package" for multi-package models
147
124
  // surfaces defaults to ["model", "api", "requirements", "agent"] — all four.
148
- // Deliberately NOT narrowed here: the requirements and agent surfaces emit ZERO
149
- // files for a project that has nothing for them to describe, and the always-on
150
- // agent context points at the agent/ pages by name. Scaffolding a narrower list
151
- // turned that pointer into a pointer at nothing. Narrow it yourself if you want
152
- // fewer docs — but note it says what your docs ARE, not which of them you commit
153
- // (a page you do not commit is exempt from verify --docs once it is git-ignored).
125
+ // Documentation is the one thing that IS on by default, because the requirements
126
+ // and agent surfaces emit ZERO files for a project with nothing to describe, and
127
+ // the always-on agent context points at the agent/ pages by name.
154
128
  },
155
129
  });
156
130
  `;
157
131
  }
158
132
 
159
- // The throwing-stub scaffolded at `dbImport`'s resolved path (DB_STUB_REL_PATH,
160
- // "src/db.ts" by default). It exists so `meta gen` and a fresh project's FIRST
161
- // `tsc` both succeed with no driver chosen and no dependency added — deliberately
162
- // NOT a real connection. Every generated route only ever passes `db` straight
163
- // through to `mountCrudRoutes(...)`; nothing reads a property off it at import
164
- // time, so a value typed `unknown` (not `any`) satisfies every call site while
165
- // making a genuine runtime use (mountCrudRoutes calling `db.select()` etc.) throw
166
- // immediately with an actionable message instead of failing to resolve at all.
167
- // Built as an array of plain single-quoted lines (not a template literal) so the
168
- // backticks and quotes inside the comment/message need no escaping.
169
- const DB_STUB_BODY = [
170
- "// `meta init` scaffolded this file because the generated Fastify routes",
171
- '// `import { db } from "../db.js"` (see `dbImport` in metaobjects.config.ts) —',
172
- "// a module that has to exist for `meta gen` and `tsc` to succeed. MetaObjects",
173
- "// cannot fill it in for real without choosing a database driver on your",
174
- "// behalf (better-sqlite3 vs @libsql/client vs pg vs postgres.js) and adding a",
175
- "// dependency you may not want, so this is a STUB, not a connection.",
176
- "//",
177
- "// It type-checks and satisfies every generated import, but throws the first",
178
- "// time anything actually touches `db` at runtime. Replace the export below",
179
- "// with your real Drizzle connection, e.g.:",
180
- "//",
181
- '// import { drizzle } from "drizzle-orm/better-sqlite3";',
182
- '// import Database from "better-sqlite3";',
183
- '// export const db = drizzle(new Database("dev.sqlite"));',
184
- "//",
185
- "// (swap the driver import for your dialect — see",
186
- "// https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/wiring-generated-queries.md",
187
- "// for SQLite/libsql, Cloudflare D1, Postgres and multi-tenant setups.)",
188
- "",
189
- "const UNWIRED_MESSAGE =",
190
- ' "src/db.ts is still the scaffolded stub meta init wrote — it cannot choose " +',
191
- " \"a database driver for you. Replace 'export const db = ...' below with \" +",
192
- ' "your real Drizzle connection, e.g.:\\n\\n" +',
193
- " \" import { drizzle } from 'drizzle-orm/better-sqlite3';\\n\" +",
194
- " \" import Database from 'better-sqlite3';\\n\" +",
195
- " \" export const db = drizzle(new Database('dev.sqlite'));\\n\";",
196
- "",
197
- "function unwired(): never {",
198
- " throw new Error(UNWIRED_MESSAGE);",
199
- "}",
200
- "",
201
- "/**",
202
- " * Stand-in for your real Drizzle database connection. Generated code only",
203
- " * ever passes `db` straight through to `mountCrudRoutes(...)` — it never",
204
- " * reads a property off it at import time — so this typechecks everywhere",
205
- " * `db` is used, and throws the message above the first time anything really",
206
- " * touches it.",
207
- " */",
208
- "export const db: unknown = new Proxy({}, { get: unwired });",
209
- "",
210
- ].join("\n");
211
-
212
- // Printed only when the stub was ACTUALLY written this run. It is gated on having just
213
- // written the scaffolded config (see the db-stub block in `init`), so a re-run in a
214
- // project that keeps its own config writes nothing — and a block claiming otherwise is
215
- // the same "asserting things about its own scaffold that aren't true" defect the rest of
216
- // this file was corrected for.
217
- const DB_STUB_NOTE = `Also scaffolded ${DB_STUB_REL_PATH}: a THROWING STUB standing in for your database
218
- connection, so the generated routes' \`db\` import resolves and the first tsc is clean.
219
- Replace it with a real connection before running the app — until you do, the first
220
- request that touches \`db\` throws with instructions.
221
- `;
222
-
223
133
  const SCAFFOLD_SUMMARY = `
224
134
  Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
225
- Codegen generators copied to codegen/generators/ — they're YOURS to edit (ADR-0034 scaffold-and-own).
135
+ codegen/generators/ is EMPTY on purpose: no code is generated until you choose it.
226
136
  `;
227
137
 
228
138
  const NEXT_STEPS = `
@@ -230,10 +140,14 @@ Next steps:
230
140
  0. Everything here is ESM — package.json needs "type": "module" (init sets it
231
141
  unless the project has CommonJS sources; without it the first tsc fails).
232
142
  1. Author entities under metaobjects/ (start from the scaffolded meta.common.json)
233
- 2. meta gen # generate idiomatic TypeScript from your entities
234
- meta gen --dry-run # ...preview without writing
235
- 3. meta docs # neutral model + API docs
236
- 4. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
143
+ 2. meta gen --list --probe # the catalog: every generator, grouped by layer, with
144
+ # how many files each would emit for YOUR model
145
+ 3. meta eject <name>... # take the ones you want — copies them into
146
+ # codegen/generators/ (yours to edit) and prints the
147
+ # import, the entry to wire, and what to install
148
+ 4. meta gen # generate from exactly what you wired
149
+ 5. meta docs # neutral model + API docs (on by default)
150
+ 6. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
237
151
 
238
152
  Ship in later sub-projects: meta ingest (propose entities from existing code),
239
153
  meta serve (local viewer), meta install-hooks (MCP server + Claude Code hooks).
@@ -461,29 +375,19 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false):
461
375
  }
462
376
 
463
377
  /**
464
- * ADR-0034 — copy the codegen reference templates into the consumer's repo at
465
- * `codegen/generators/<name>.ts` so they own them. Each file is written only if absent,
466
- * so a re-run with --force never clobbers a hand-edited generator. The scaffolded
467
- * metaobjects.config.ts imports these local copies (not the package `/generators` export).
378
+ * Scaffold the owned-codegen TIER: the directory and its tsconfig, and nothing in it.
379
+ *
380
+ * `meta init` used to copy five reference generators here eagerly and wire all five in
381
+ * the scaffolded config. Codegen is opt-in now, so it copies NONE: the directory exists
382
+ * because it is part of the layout ADR-0034 promises (and `tsconfig.codegen.json`
383
+ * covers it the moment something lands), and `meta eject <name>...` is the door.
468
384
  *
469
- * Copies SCAFFOLDED_GENERATOR_NAMES only — the five the scaffolded config actually
470
- * wires — not every name @metaobjectsdev/codegen-ts happens to register. Anything else
471
- * (routes-hono, and any UI-tier template from codegen-ts-react/-tanstack) is reached with
472
- * `meta eject <name>`, not by eager copying.
385
+ * The empty directory is not a placeholder for a decision deferred — it is the decision.
386
+ * A scaffold that wires a suite pre-empts the one judgement this design exists to leave
387
+ * to whoever is building the app.
473
388
  */
474
389
  async function writeOwnedGenerators(opts: InitOptions, result: InitResult): Promise<void> {
475
- const dir = join(opts.cwd, OWNED_GENERATORS_DIR);
476
- await mkdir(dir, { recursive: true });
477
- for (const name of SCAFFOLDED_GENERATOR_NAMES) {
478
- const rel = `${OWNED_GENERATORS_DIR}/${name}.ts`;
479
- const abs = join(dir, `${name}.ts`);
480
- if (await fileExists(abs)) {
481
- result.preserved.push(rel);
482
- continue;
483
- }
484
- await writeFile(abs, readReferenceTemplate(name), "utf8");
485
- result.created.push(rel);
486
- }
390
+ await mkdir(join(opts.cwd, OWNED_GENERATORS_DIR), { recursive: true });
487
391
  await writeCodegenTsconfig(opts, result);
488
392
  }
489
393
 
@@ -676,9 +580,8 @@ export async function init(opts: InitOptions): Promise<InitResult> {
676
580
  ".metaobjects/.gitignore",
677
581
  );
678
582
  result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
679
- for (const name of SCAFFOLDED_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
680
- result.created.push(CODEGEN_TSCONFIG_REL);
681
- result.created.push("metaobjects.config.ts", DB_STUB_REL_PATH, ".gitignore");
583
+ result.created.push(OWNED_GENERATORS_DIR, CODEGEN_TSCONFIG_REL);
584
+ result.created.push("metaobjects.config.ts", ".gitignore");
682
585
  return result;
683
586
  }
684
587
 
@@ -716,60 +619,17 @@ export async function init(opts: InitOptions): Promise<InitResult> {
716
619
 
717
620
  await writeAgentContext(opts, result);
718
621
 
719
- // ADR-0034 — scaffold the OWNED codegen generators that metaobjects.config.ts imports
720
- // locally. Done before the config so the import targets exist on first `meta gen`.
622
+ // ADR-0034 — the owned-codegen TIER: the directory and its tsconfig. Nothing is
623
+ // copied into it; `meta eject <name>...` is the door (see writeOwnedGenerators).
721
624
  await writeOwnedGenerators(opts, result);
722
625
 
723
626
  // Scaffold metaobjects.config.ts at the project root. Never overwrite if it exists.
724
627
  const forgeConfigPath = join(opts.cwd, "metaobjects.config.ts");
725
- const wroteScaffoldedConfig = !(await fileExists(forgeConfigPath));
726
- if (wroteScaffoldedConfig) {
628
+ if (!(await fileExists(forgeConfigPath))) {
727
629
  await writeFile(forgeConfigPath, buildMetaobjectsConfigBody(opts.d1 ? "d1" : "sqlite"), "utf8");
728
630
  result.created.push("metaobjects.config.ts");
729
631
  }
730
632
 
731
- // Scaffold the `dbImport` throwing stub at DB_STUB_REL_PATH ("src/db.ts" by
732
- // default) — ONLY if absent, so a re-run never clobbers a user's real db module
733
- // (same "write once" precedent as writeOwnedGenerators above). Without this, the
734
- // scaffolded config declares `dbImport: "../db"` pointing at a module `meta init`
735
- // never creates, and a fresh project's FIRST `tsc` fails to resolve it.
736
- //
737
- // Gated on having just WRITTEN that config, not merely on the stub being absent.
738
- // DB_STUB_REL_PATH is derived from SCAFFOLD_OUT_DIR + SCAFFOLD_DB_IMPORT — the
739
- // scaffold's own constants — so it describes where the SCAFFOLDED config points and
740
- // nowhere else. Re-running `meta init` in a project that already has a config with
741
- // its own `outDir`/`dbImport` preserves that config (above) and would otherwise still
742
- // drop a src/db.ts that nothing in the project references: a stray file, in the
743
- // adopter's application source, answering a question they had already answered.
744
- const dbStubPath = join(opts.cwd, DB_STUB_REL_PATH);
745
- const dbStubExists = await fileExists(dbStubPath);
746
- if (wroteScaffoldedConfig && !dbStubExists) {
747
- await mkdir(dirname(dbStubPath), { recursive: true });
748
- await writeFile(dbStubPath, DB_STUB_BODY, "utf8");
749
- result.created.push(DB_STUB_REL_PATH);
750
- } else if (dbStubExists) {
751
- // "preserved" means a file we would have written was left alone. Skipping because
752
- // this project keeps its own config is not preservation — there is nothing there.
753
- result.preserved.push(DB_STUB_REL_PATH);
754
- } else {
755
- // Neither written nor preserved: this project keeps its own config AND has no stub.
756
- // Usually correct — its `dbImport` points at a real module somewhere else. But it is
757
- // ALSO what a scaffolded project looks like after someone deletes or moves src/db.ts,
758
- // and `meta init --force` cannot tell those apart: `wroteScaffoldedConfig` is only
759
- // "no config existed", so it is false for the config init itself wrote. Silently
760
- // doing nothing there leaves the scaffolded `dbImport: "../db"` and the generated
761
- // routes' `import { db } from "../db.js"` pointing at nothing, and the adopter meets
762
- // it as a TS2307 from `tsc` with no word from the command that could have said so.
763
- // Say it here rather than writing: dropping a file into a project that owns its
764
- // config is what the branch above deliberately refuses.
765
- result.warnings.push(
766
- `${DB_STUB_REL_PATH} was not scaffolded — this project has its own ` +
767
- "metaobjects.config.ts, so init leaves the database module to it. If that config's " +
768
- `\`dbImport\` resolves to ${DB_STUB_REL_PATH}, create it or the generated routes will ` +
769
- "not resolve.",
770
- );
771
- }
772
-
773
633
  // Scaffold a minimal root .gitignore ONLY when the project has none — never
774
634
  // clobber a user's existing one (they may have their own rules).
775
635
  const rootGitignorePath = join(opts.cwd, ".gitignore");
@@ -877,195 +737,29 @@ async function prepareManifestForScaffold(cwd: string, result: InitResult): Prom
877
737
  }
878
738
  }
879
739
 
880
- const added = addScaffoldDevDependencies(pkg);
881
- const addedRuntime = addScaffoldRuntimeDependencies(pkg);
882
- if (moduleSystemNote !== undefined || added.length > 0 || addedRuntime.length > 0) {
740
+ // NO DEPENDENCIES ARE ADDED. `meta init` used to declare five packages here —
741
+ // codegen-ts and metadata for the generators it copied, drizzle-orm / zod / fastify
742
+ // for the code those generators would write — and every one of them was a
743
+ // consequence of the scaffold WIRING a suite. It wires nothing now, so declaring
744
+ // anything would be declaring a dependency on code this project may never generate.
745
+ //
746
+ // The need did not vanish, it MOVED to the moment a generator is chosen:
747
+ // `meta eject <name>...` reports the exact install set for what you took, with
748
+ // third-party ranges read from the runtime package's own peerDependencies (see
749
+ // lib/install-set.ts), and `meta gen --list --format json` carries the same set per
750
+ // entry before you commit to anything.
751
+ if (moduleSystemNote !== undefined) {
883
752
  // Preserve the file's existing indentation rather than reformatting someone's manifest.
884
753
  const indent = /\n(\s+)"/.exec(raw)?.[1] ?? " ";
885
754
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`, "utf8");
886
- }
887
- if (moduleSystemNote !== undefined) result.warnings.push(moduleSystemNote);
888
- if (added.length > 0) {
889
- result.warnings.push(
890
- `added ${added.join(" + ")} to devDependencies — the scaffolded ` +
891
- "codegen/generators/ are YOUR source now (ADR-0034) and import them. " +
892
- "Run your package manager's install before `meta gen`.",
893
- );
894
- }
895
- if (addedRuntime.length > 0) {
896
- result.warnings.push(
897
- `added ${addedRuntime.join(" + ")} to dependencies — the code \`meta gen\` writes ` +
898
- "imports them, so `npx tsc` reports TS2307 on the generated files until they are " +
899
- "installed. Run your package manager's install before `meta gen`.",
900
- );
901
- }
902
- // Never degrade in silence. The first version of this returned `{}` on any failure to
903
- // read runtime-ts's peer ranges and skipped the three packages that come from them —
904
- // which is exactly what happened against the published package, and the run looked
905
- // successful while leaving the project with the six TS2307s this code exists to
906
- // prevent. A fallback that cannot be observed is indistinguishable from a bug.
907
- const undeclarable = SCAFFOLD_OUTPUT_PEERS.filter(
908
- (name) => !declaredDependencyNames(pkg as PackageManifest).has(name),
909
- );
910
- if (undeclarable.length > 0) {
911
- result.warnings.push(
912
- `could NOT declare ${undeclarable.join(" + ")} — the version ranges are read from ` +
913
- "@metaobjectsdev/runtime-ts's own peerDependencies and that manifest could not be " +
914
- "read. Generated code imports them, so add them to dependencies by hand or " +
915
- "`npx tsc` will report TS2307 on files `meta gen` writes.",
916
- );
755
+ result.warnings.push(moduleSystemNote);
917
756
  }
918
757
  }
919
758
 
920
- /**
921
- * ADR-0034 scaffold-and-own hands the project real source files under
922
- * `codegen/generators/`, and those files import `@metaobjectsdev/codegen-ts` and
923
- * `@metaobjectsdev/metadata`. Installing `@metaobjectsdev/cli` alone does not put
924
- * either of them where the project can resolve them, so the scaffold arrived
925
- * un-typecheckable: ten TS2307s on files `meta init` had just written.
926
- *
927
- * Declaring them is the honest fix — they are dependencies of code that now lives in
928
- * the adopter's repo. Deliberately NOT declared: `ts-poet`. The scaffolded templates
929
- * import the ts-poet combinators via @metaobjectsdev/codegen-ts (re-exported from its
930
- * own ts-poet instance) precisely so that the Code objects they compose share ONE
931
- * ts-poet copy with the engine's render* primitives — a project-local ts-poet is the
932
- * second physical copy that split the class identity under a globally-installed /
933
- * linked CLI (duplicate imports in generated files, TS2300 on first tsc; see the
934
- * gen-split-tree gate). Only ever ADDS a missing key: an existing pin is the user's.
935
- * Returns what it added so the caller can tell them to install.
936
- */
937
- function addScaffoldDevDependencies(pkg: Record<string, unknown>): string[] {
938
- const version = cliVersion();
939
- const wanted: Record<string, string> = {
940
- "@metaobjectsdev/codegen-ts": `^${version}`,
941
- "@metaobjectsdev/metadata": `^${version}`,
942
- };
943
- const dev = (pkg.devDependencies ?? {}) as Record<string, string>;
944
- // "Already declared" spans all four dependency fields — the shared rule, so this
945
- // cannot drift from what `meta eject` means by the same words. It used to ask only
946
- // dependencies + devDependencies, which meant a project declaring codegen-ts as a
947
- // PEER dependency (correct for a library whose consumer supplies the version) got it
948
- // added to devDependencies as well: the same package pinned twice in one manifest,
949
- // and a second physical copy is the class-identity split this repo has been bitten
950
- // by twice.
951
- const declared = declaredDependencyNames(pkg as PackageManifest);
952
- const added: string[] = [];
953
- for (const [name, range] of Object.entries(wanted)) {
954
- if (declared.has(name)) continue;
955
- dev[name] = range;
956
- added.push(name);
957
- }
958
- if (added.length > 0) {
959
- pkg.devDependencies = Object.fromEntries(Object.entries(dev).sort(([a], [b]) => a.localeCompare(b)));
960
- }
961
- return added;
962
- }
963
759
 
964
- /**
965
- * The third-party packages the scaffolded suite's GENERATED OUTPUT imports, each named
966
- * with the artifact that imports it. This is the set, not the ranges — see
967
- * `scaffoldRuntimeDependencies` for where those come from.
968
- *
969
- * `@metaobjectsdev/runtime-ts` is deliberately not here: its range is the CLI's own
970
- * version, like the two build-time packages above, not a peer range read off itself.
971
- */
972
- const SCAFFOLD_OUTPUT_PEERS = [
973
- "drizzle-orm", // <Entity>.ts (the table + column builders) and <Entity>.queries.ts
974
- "zod", // <Entity>.ts — the Insert/Update schemas
975
- "fastify", // <Entity>.routes.ts — `import type { FastifyInstance }`
976
- ] as const;
977
760
 
978
- /**
979
- * The runtime dependencies of the code `meta gen` will WRITE, for `dependencies`.
980
- *
981
- * `addScaffoldDevDependencies` above fixed this defect one layer in: the generator
982
- * SOURCES under `codegen/generators/` import `@metaobjectsdev/codegen-ts` and
983
- * `@metaobjectsdev/metadata`, nothing declared them, and the scaffold arrived
984
- * un-typecheckable. The same argument reaches one layer further out and was not
985
- * followed there. Generated `<Entity>.ts` / `.queries.ts` / `.routes.ts` import
986
- * drizzle-orm, zod, fastify and `@metaobjectsdev/runtime-ts/drizzle-fastify` — five
987
- * specifiers, none declared — so `npx tsc`, which is the next step `meta gen` itself
988
- * prints, reported NINE TS2307s on a brand-new project that had done nothing wrong.
989
- * npm hides four of the five by hoisting them out of the CLI's own tree; pnpm's strict
990
- * layout, which is the point of testing both, shows all five.
991
- *
992
- * `dependencies`, not `devDependencies`: generated routes and queries are application
993
- * source that runs in production. The two build-time packages stay where they are.
994
- *
995
- * The RANGES are read from `@metaobjectsdev/runtime-ts`'s own `peerDependencies` rather
996
- * than written here. That package already declares the versions its helpers are built
997
- * against, bounded above (the peer-range gate enforces the bound), so a second copy of
998
- * those ranges in the scaffolder is a second thing to keep in step — and the failure
999
- * mode of drift is an adopter installing a major nothing was tested against. If a range
1000
- * cannot be read, the package is SKIPPED rather than guessed at, and `meta gen` still
1001
- * type-checks for anyone whose manifest already declares it.
1002
- */
1003
- function scaffoldRuntimeDependencies(): Record<string, string> {
1004
- const wanted: Record<string, string> = {
1005
- "@metaobjectsdev/runtime-ts": `^${cliVersion()}`,
1006
- };
1007
- const peers = runtimeTsPeerRanges();
1008
- for (const name of SCAFFOLD_OUTPUT_PEERS) {
1009
- const range = peers[name];
1010
- if (range !== undefined) wanted[name] = range;
1011
- }
1012
- return wanted;
1013
- }
1014
761
 
1015
- /**
1016
- * `@metaobjectsdev/runtime-ts`'s declared peer ranges, or `{}` if they cannot be read.
1017
- *
1018
- * Resolves the package's ENTRY and walks up to the nearest package.json, rather than
1019
- * asking for `"@metaobjectsdev/runtime-ts/package.json"` directly. That direct form is
1020
- * the obvious one and it is WRONG: a package's `exports` map gates every subpath, and
1021
- * runtime-ts's exports are `.` / `./drivers` / `./fastify` / `./drizzle-fastify` /
1022
- * `./hono` — no `./package.json`. Under real Node it throws
1023
- * ERR_PACKAGE_PATH_NOT_EXPORTED. It appeared to work here only because the workspace
1024
- * runs this under bun against a symlinked source tree; against the published package it
1025
- * failed on the first try, silently, and `meta init` declared one package instead of
1026
- * four. Resolving the entry is never gated — `.` is the one subpath every package
1027
- * exports — so this form works under npm, pnpm, bun and a linked checkout alike.
1028
- */
1029
- function runtimeTsPeerRanges(): Record<string, string> {
1030
- try {
1031
- const req = createRequire(import.meta.url);
1032
- let dir = dirname(req.resolve("@metaobjectsdev/runtime-ts"));
1033
- // The entry sits under dist/; the manifest is at the package root above it.
1034
- for (let hops = 0; hops < 8; hops++) {
1035
- const candidate = join(dir, "package.json");
1036
- if (existsSyncWrap(candidate)) {
1037
- const manifest = JSON.parse(readFileSyncWrap(candidate, "utf8")) as PackageManifest & { name?: string };
1038
- // Guard against stopping at a nested manifest that is not the package itself.
1039
- if (manifest.name === "@metaobjectsdev/runtime-ts") {
1040
- return (manifest.peerDependencies ?? {}) as Record<string, string>;
1041
- }
1042
- }
1043
- const up = dirname(dir);
1044
- if (up === dir) break;
1045
- dir = up;
1046
- }
1047
- return {};
1048
- } catch {
1049
- return {};
1050
- }
1051
- }
1052
762
 
1053
- /** Adds any missing `scaffoldRuntimeDependencies()` to `dependencies`. Only ever ADDS a
1054
- * missing key — an existing pin, in any of the four dependency fields, is the user's. */
1055
- function addScaffoldRuntimeDependencies(pkg: Record<string, unknown>): string[] {
1056
- const deps = (pkg.dependencies ?? {}) as Record<string, string>;
1057
- const declared = declaredDependencyNames(pkg as PackageManifest);
1058
- const added: string[] = [];
1059
- for (const [name, range] of Object.entries(scaffoldRuntimeDependencies())) {
1060
- if (declared.has(name)) continue;
1061
- deps[name] = range;
1062
- added.push(name);
1063
- }
1064
- if (added.length > 0) {
1065
- pkg.dependencies = Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
1066
- }
1067
- return added;
1068
- }
1069
763
 
1070
764
  /** True when the project has hand-written CommonJS at the root (excluding tooling dirs). */
1071
765
  async function hasCommonJsSources(cwd: string): Promise<boolean> {
@@ -1111,15 +805,13 @@ function buildD1MigrateBlock(cwd: string): Record<string, unknown> {
1111
805
  /**
1112
806
  * The post-init message.
1113
807
  *
1114
- * @param dbStubWritten whether THIS run wrote {@link DB_STUB_REL_PATH}. Required rather
1115
- * than defaulted, because a default would silently restore the bug this parameter
1116
- * exists to close: the note used to be part of one static string and so claimed the
1117
- * stub on every run, including the `meta init --force` in a project keeping its own
1118
- * `metaobjects.config.ts`, where the stub is deliberately not written. Callers pass
1119
- * `result.created.includes(DB_STUB_REL_PATH)` — the same list the message describes.
808
+ * Takes no arguments now. It used to take `dbStubWritten`, because the scaffold wrote a
809
+ * throwing `src/db.ts` on some runs and not others and a static string claimed it on
810
+ * every one. With nothing wired there is no `dbImport` and no stub, so the message is
811
+ * the same on every path again.
1120
812
  */
1121
- export function nextStepsBlock(dbStubWritten: boolean): string {
1122
- return SCAFFOLD_SUMMARY + (dbStubWritten ? DB_STUB_NOTE : "") + NEXT_STEPS;
813
+ export function nextStepsBlock(): string {
814
+ return SCAFFOLD_SUMMARY + NEXT_STEPS;
1123
815
  }
1124
816
 
1125
817
  async function dirExists(p: string): Promise<boolean> {
@@ -1195,7 +887,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
1195
887
  }
1196
888
  for (const w of result.warnings) log.warn(w);
1197
889
  } else {
1198
- log.info(nextStepsBlock(result.created.includes(DB_STUB_REL_PATH)));
890
+ log.info(nextStepsBlock());
1199
891
  // Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
1200
892
  // discovery warning) — these are otherwise dropped on the normal init path.
1201
893
  for (const w of result.warnings) log.warn(w);