@metaobjectsdev/cli 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -1
  2. package/dist/src/commands/docs.d.ts +14 -1
  3. package/dist/src/commands/docs.d.ts.map +1 -1
  4. package/dist/src/commands/docs.js +153 -8
  5. package/dist/src/commands/docs.js.map +1 -1
  6. package/dist/src/commands/eject.d.ts +38 -0
  7. package/dist/src/commands/eject.d.ts.map +1 -0
  8. package/dist/src/commands/eject.js +233 -0
  9. package/dist/src/commands/eject.js.map +1 -0
  10. package/dist/src/commands/gen.d.ts.map +1 -1
  11. package/dist/src/commands/gen.js +43 -20
  12. package/dist/src/commands/gen.js.map +1 -1
  13. package/dist/src/commands/init.d.ts +15 -1
  14. package/dist/src/commands/init.d.ts.map +1 -1
  15. package/dist/src/commands/init.js +208 -20
  16. package/dist/src/commands/init.js.map +1 -1
  17. package/dist/src/commands/types.d.ts +2 -1
  18. package/dist/src/commands/types.d.ts.map +1 -1
  19. package/dist/src/commands/types.js +165 -28
  20. package/dist/src/commands/types.js.map +1 -1
  21. package/dist/src/commands/verify.d.ts +9 -1
  22. package/dist/src/commands/verify.d.ts.map +1 -1
  23. package/dist/src/commands/verify.js +299 -51
  24. package/dist/src/commands/verify.js.map +1 -1
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +105 -4
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/lib/advisory.d.ts +77 -0
  29. package/dist/src/lib/advisory.d.ts.map +1 -0
  30. package/dist/src/lib/advisory.js +97 -0
  31. package/dist/src/lib/advisory.js.map +1 -0
  32. package/dist/src/lib/anti-patterns.d.ts +27 -3
  33. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  34. package/dist/src/lib/anti-patterns.js +145 -8
  35. package/dist/src/lib/anti-patterns.js.map +1 -1
  36. package/dist/src/lib/args.d.ts +35 -1
  37. package/dist/src/lib/args.d.ts.map +1 -1
  38. package/dist/src/lib/args.js +32 -1
  39. package/dist/src/lib/args.js.map +1 -1
  40. package/dist/src/lib/detect-stack.d.ts.map +1 -1
  41. package/dist/src/lib/detect-stack.js +4 -13
  42. package/dist/src/lib/detect-stack.js.map +1 -1
  43. package/dist/src/lib/docs-drift.d.ts +31 -0
  44. package/dist/src/lib/docs-drift.d.ts.map +1 -0
  45. package/dist/src/lib/docs-drift.js +195 -0
  46. package/dist/src/lib/docs-drift.js.map +1 -0
  47. package/dist/src/lib/format.d.ts +10 -0
  48. package/dist/src/lib/format.d.ts.map +1 -1
  49. package/dist/src/lib/format.js +15 -0
  50. package/dist/src/lib/format.js.map +1 -1
  51. package/dist/src/lib/output.d.ts +13 -0
  52. package/dist/src/lib/output.d.ts.map +1 -1
  53. package/dist/src/lib/output.js +16 -1
  54. package/dist/src/lib/output.js.map +1 -1
  55. package/dist/src/lib/package-manifest.d.ts +27 -0
  56. package/dist/src/lib/package-manifest.d.ts.map +1 -0
  57. package/dist/src/lib/package-manifest.js +52 -0
  58. package/dist/src/lib/package-manifest.js.map +1 -0
  59. package/package.json +11 -11
  60. package/src/commands/docs.ts +194 -8
  61. package/src/commands/eject.ts +282 -0
  62. package/src/commands/gen.ts +54 -19
  63. package/src/commands/init.ts +217 -19
  64. package/src/commands/types.ts +185 -34
  65. package/src/commands/verify.ts +358 -47
  66. package/src/index.ts +114 -5
  67. package/src/lib/advisory.ts +150 -0
  68. package/src/lib/anti-patterns.ts +163 -8
  69. package/src/lib/args.ts +74 -2
  70. package/src/lib/detect-stack.ts +4 -11
  71. package/src/lib/docs-drift.ts +222 -0
  72. package/src/lib/format.ts +14 -0
  73. package/src/lib/output.ts +33 -2
  74. package/src/lib/package-manifest.ts +58 -0
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises";
1
+ import { mkdir, writeFile, readFile, readdir, stat, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { basename, dirname } from "node:path";
4
4
  import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
@@ -11,13 +11,40 @@ 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, REFERENCE_GENERATOR_NAMES } from "@metaobjectsdev/codegen-ts";
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 FIVE 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
+ //
32
+ // "names" joined this list by spec §A5's ruling, not by falling out of the general rule
33
+ // above: TypeScript is opt-in by construction under ADR-0034 (meta gen runs the adopter's
34
+ // copy, so a packaged change to a generator can never reach anyone who has ejected), so
35
+ // the honest maximum for the names artifact is every NEW `meta init` getting it and every
36
+ // existing project adding one config line by hand.
37
+ export const SCAFFOLDED_GENERATOR_NAMES: readonly ReferenceGeneratorName[] = ["entity", "queries", "routes", "barrel", "names"];
38
+
39
+ // The scaffolded config's outDir + dbImport, as named constants so the throwing-stub
40
+ // path below is DERIVED from the same values the config template embeds rather than
41
+ // duplicated as a second literal that could drift from it.
42
+ const SCAFFOLD_OUT_DIR = "src/generated";
43
+ const SCAFFOLD_DB_IMPORT = "../db";
44
+ // "src/generated" + "../db" -> "src/db" -> "src/db.ts" (dbImport resolves relative
45
+ // to outDir, same as the module specifier a generated route file emits).
46
+ const DB_STUB_REL_PATH = `${join(SCAFFOLD_OUT_DIR, SCAFFOLD_DB_IMPORT)}.ts`;
47
+
21
48
  const META_COMMON_JSON = JSON.stringify(
22
49
  {
23
50
  metadata: {
@@ -84,18 +111,29 @@ function buildMetaobjectsConfigBody(dialect: "sqlite" | "postgres" | "d1" = "sql
84
111
  import { entityFile } from "./codegen/generators/entity.js";
85
112
  import { queriesFile } from "./codegen/generators/queries.js";
86
113
  import { routesFile } from "./codegen/generators/routes.js";
114
+ import { namesFile } from "./codegen/generators/names.js";
87
115
  import { barrel } from "./codegen/generators/barrel.js";
88
116
 
89
117
  export default defineConfig({
90
- outDir: "src/generated",
91
- extStyle: "js", // ".js"-extensioned relative imports — safe under Node ESM / tsc nodenext AND bundlers
92
- dbImport: "../db",
118
+ outDir: "${SCAFFOLD_OUT_DIR}",
119
+ extStyle: "js", // ".js"-extensioned relative imports — correct for Node ESM and \`tsc\` with
120
+ // nodenext, which is what a fresh project has. BUNDLERS DISAGREE: this fails
121
+ // outright under Turbopack (even between two generated files, so the whole
122
+ // generated tree goes unresolvable), while Vite and esbuild accept it and
123
+ // webpack needs \`resolve.extensionAlias\`. If a generated import fails to
124
+ // resolve, set "none" and retest — do not assume this line covers your bundler.
125
+ dbImport: "${SCAFFOLD_DB_IMPORT}", // routesFile() below emits \`import { db } from …\` — meta init
126
+ // scaffolded ${DB_STUB_REL_PATH} as a THROWING STUB (types clean, no
127
+ // driver chosen) so meta gen and tsc pass; replace it with your real
128
+ // Drizzle connection before running the app.
129
+ // (queriesFile() takes db as a parameter and never reads this.)
93
130
  dialect: "${dialect}",
94
131
  apiPrefix: "", // set to "/api" if your routes mount under /api
95
132
  generators: [
96
133
  entityFile(),
97
134
  queriesFile(),
98
135
  routesFile(),
136
+ namesFile(), // <Entity>Names — physical table/column constants (spec §A1/§A5)
99
137
  barrel(),
100
138
  ],
101
139
  docs: {
@@ -107,10 +145,76 @@ export default defineConfig({
107
145
  `;
108
146
  }
109
147
 
110
- const NEXT_STEPS = `
148
+ // The throwing-stub scaffolded at `dbImport`'s resolved path (DB_STUB_REL_PATH,
149
+ // "src/db.ts" by default). It exists so `meta gen` and a fresh project's FIRST
150
+ // `tsc` both succeed with no driver chosen and no dependency added — deliberately
151
+ // NOT a real connection. Every generated route only ever passes `db` straight
152
+ // through to `mountCrudRoutes(...)`; nothing reads a property off it at import
153
+ // time, so a value typed `unknown` (not `any`) satisfies every call site while
154
+ // making a genuine runtime use (mountCrudRoutes calling `db.select()` etc.) throw
155
+ // immediately with an actionable message instead of failing to resolve at all.
156
+ // Built as an array of plain single-quoted lines (not a template literal) so the
157
+ // backticks and quotes inside the comment/message need no escaping.
158
+ const DB_STUB_BODY = [
159
+ "// `meta init` scaffolded this file because the generated Fastify routes",
160
+ '// `import { db } from "../db.js"` (see `dbImport` in metaobjects.config.ts) —',
161
+ "// a module that has to exist for `meta gen` and `tsc` to succeed. MetaObjects",
162
+ "// cannot fill it in for real without choosing a database driver on your",
163
+ "// behalf (better-sqlite3 vs @libsql/client vs pg vs postgres.js) and adding a",
164
+ "// dependency you may not want, so this is a STUB, not a connection.",
165
+ "//",
166
+ "// It type-checks and satisfies every generated import, but throws the first",
167
+ "// time anything actually touches `db` at runtime. Replace the export below",
168
+ "// with your real Drizzle connection, e.g.:",
169
+ "//",
170
+ '// import { drizzle } from "drizzle-orm/better-sqlite3";',
171
+ '// import Database from "better-sqlite3";',
172
+ '// export const db = drizzle(new Database("dev.sqlite"));',
173
+ "//",
174
+ "// (swap the driver import for your dialect — see",
175
+ "// https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/wiring-generated-queries.md",
176
+ "// for SQLite/libsql, Cloudflare D1, Postgres and multi-tenant setups.)",
177
+ "",
178
+ "const UNWIRED_MESSAGE =",
179
+ ' "src/db.ts is still the scaffolded stub meta init wrote — it cannot choose " +',
180
+ " \"a database driver for you. Replace 'export const db = ...' below with \" +",
181
+ ' "your real Drizzle connection, e.g.:\\n\\n" +',
182
+ " \" import { drizzle } from 'drizzle-orm/better-sqlite3';\\n\" +",
183
+ " \" import Database from 'better-sqlite3';\\n\" +",
184
+ " \" export const db = drizzle(new Database('dev.sqlite'));\\n\";",
185
+ "",
186
+ "function unwired(): never {",
187
+ " throw new Error(UNWIRED_MESSAGE);",
188
+ "}",
189
+ "",
190
+ "/**",
191
+ " * Stand-in for your real Drizzle database connection. Generated code only",
192
+ " * ever passes `db` straight through to `mountCrudRoutes(...)` — it never",
193
+ " * reads a property off it at import time — so this typechecks everywhere",
194
+ " * `db` is used, and throws the message above the first time anything really",
195
+ " * touches it.",
196
+ " */",
197
+ "export const db: unknown = new Proxy({}, { get: unwired });",
198
+ "",
199
+ ].join("\n");
200
+
201
+ // Printed only when the stub was ACTUALLY written this run. It is gated on having just
202
+ // written the scaffolded config (see the db-stub block in `init`), so a re-run in a
203
+ // project that keeps its own config writes nothing — and a block claiming otherwise is
204
+ // the same "asserting things about its own scaffold that aren't true" defect the rest of
205
+ // this file was corrected for.
206
+ const DB_STUB_NOTE = `Also scaffolded ${DB_STUB_REL_PATH}: a THROWING STUB standing in for your database
207
+ connection, so the generated routes' \`db\` import resolves and the first tsc is clean.
208
+ Replace it with a real connection before running the app — until you do, the first
209
+ request that touches \`db\` throws with instructions.
210
+ `;
211
+
212
+ const SCAFFOLD_SUMMARY = `
111
213
  Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
112
214
  Codegen generators copied to codegen/generators/ — they're YOURS to edit (ADR-0034 scaffold-and-own).
215
+ `;
113
216
 
217
+ const NEXT_STEPS = `
114
218
  Next steps:
115
219
  0. Everything here is ESM — package.json needs "type": "module" (init sets it
116
220
  unless the project has CommonJS sources; without it the first tsc fails).
@@ -150,6 +254,8 @@ export interface InitOptions {
150
254
  export interface InitResult {
151
255
  created: string[];
152
256
  preserved: string[];
257
+ /** agent-context files deleted because this stack no longer assembles them. */
258
+ removed: string[];
153
259
  warnings: string[];
154
260
  }
155
261
 
@@ -228,6 +334,12 @@ async function writeUnlessDryRun(cwd: string, dryRun: boolean, path: string, con
228
334
  await writeFile(abs, contents, "utf8");
229
335
  }
230
336
 
337
+ /** Delete twin of writeUnlessDryRun — a dry run must not remove anything either. */
338
+ async function removeUnlessDryRun(cwd: string, dryRun: boolean, path: string): Promise<void> {
339
+ if (dryRun) return;
340
+ await rm(join(cwd, path), { force: true });
341
+ }
342
+
231
343
  /** "would be VERBED" during a dry run, plain VERBED otherwise — the one tense
232
344
  * marker every reported write shares, so each call site states only its own
233
345
  * past participle instead of writing out both tenses of the whole sentence. */
@@ -291,8 +403,21 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise
291
403
  );
292
404
  result.created.push(AGENT_CONTEXT_MANIFEST_PATH);
293
405
 
406
+ // A fragment this stack no longer assembles. `prunes` are ones we wrote that nobody has
407
+ // touched — deleting is exactly as safe as the overwrite the same hash predicate already
408
+ // authorises above, and leaving them contradicts every SKILL.md footer, which tells the
409
+ // reader to read every references/*.md "one per server language in this project's stack".
410
+ for (const orphan of decision.prunes) {
411
+ await removeUnlessDryRun(opts.cwd, dryRun, orphan);
412
+ result.removed.push(orphan);
413
+ }
414
+ // Hand-edited ones are never deleted — losing an adopter's writing is worse than leaving
415
+ // a stale file behind — so they are named, with the reason and the remedy.
294
416
  for (const orphan of decision.removed) {
295
- result.warnings.push(`${orphan} is no longer part of this stack; orphaned (safe to delete).`);
417
+ result.warnings.push(
418
+ `${orphan} is no longer part of this stack but appears hand-edited, so it was kept; ` +
419
+ "delete it yourself once you have salvaged anything you want from it.",
420
+ );
296
421
  }
297
422
 
298
423
  if (opts.wireRoot) await wireRootMemory(opts.cwd, result, dryRun);
@@ -329,11 +454,16 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false):
329
454
  * `codegen/generators/<name>.ts` so they own them. Each file is written only if absent,
330
455
  * so a re-run with --force never clobbers a hand-edited generator. The scaffolded
331
456
  * metaobjects.config.ts imports these local copies (not the package `/generators` export).
457
+ *
458
+ * Copies SCAFFOLDED_GENERATOR_NAMES only — the five the scaffolded config actually
459
+ * wires — not every name @metaobjectsdev/codegen-ts happens to register. Anything else
460
+ * (routes-hono, and any UI-tier template from codegen-ts-react/-tanstack) is reached with
461
+ * `meta eject <name>`, not by eager copying.
332
462
  */
333
463
  async function writeOwnedGenerators(opts: InitOptions, result: InitResult): Promise<void> {
334
464
  const dir = join(opts.cwd, OWNED_GENERATORS_DIR);
335
465
  await mkdir(dir, { recursive: true });
336
- for (const name of REFERENCE_GENERATOR_NAMES) {
466
+ for (const name of SCAFFOLDED_GENERATOR_NAMES) {
337
467
  const rel = `${OWNED_GENERATORS_DIR}/${name}.ts`;
338
468
  const abs = join(dir, `${name}.ts`);
339
469
  if (await fileExists(abs)) {
@@ -412,7 +542,7 @@ async function writeConfigFile(opts: InitOptions, result: InitResult, agentDir:
412
542
  }
413
543
 
414
544
  export async function init(opts: InitOptions): Promise<InitResult> {
415
- const result: InitResult = { created: [], preserved: [], warnings: [] };
545
+ const result: InitResult = { created: [], preserved: [], removed: [], warnings: [] };
416
546
  const agentDir = join(opts.cwd, DEFAULT_METAOBJECTS_DIR);
417
547
  const metaobjectsDir = join(opts.cwd, DEFAULT_METADATA_DIR);
418
548
 
@@ -475,8 +605,8 @@ export async function init(opts: InitOptions): Promise<InitResult> {
475
605
  `.metaobjects/${PACKAGE_MANIFEST_FILE}`,
476
606
  );
477
607
  result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
478
- for (const name of REFERENCE_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
479
- result.created.push("metaobjects.config.ts", ".gitignore");
608
+ for (const name of SCAFFOLDED_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
609
+ result.created.push("metaobjects.config.ts", DB_STUB_REL_PATH, ".gitignore");
480
610
  return result;
481
611
  }
482
612
 
@@ -524,11 +654,54 @@ export async function init(opts: InitOptions): Promise<InitResult> {
524
654
 
525
655
  // Scaffold metaobjects.config.ts at the project root. Never overwrite if it exists.
526
656
  const forgeConfigPath = join(opts.cwd, "metaobjects.config.ts");
527
- if (!(await fileExists(forgeConfigPath))) {
657
+ const wroteScaffoldedConfig = !(await fileExists(forgeConfigPath));
658
+ if (wroteScaffoldedConfig) {
528
659
  await writeFile(forgeConfigPath, buildMetaobjectsConfigBody(opts.d1 ? "d1" : "sqlite"), "utf8");
529
660
  result.created.push("metaobjects.config.ts");
530
661
  }
531
662
 
663
+ // Scaffold the `dbImport` throwing stub at DB_STUB_REL_PATH ("src/db.ts" by
664
+ // default) — ONLY if absent, so a re-run never clobbers a user's real db module
665
+ // (same "write once" precedent as writeOwnedGenerators above). Without this, the
666
+ // scaffolded config declares `dbImport: "../db"` pointing at a module `meta init`
667
+ // never creates, and a fresh project's FIRST `tsc` fails to resolve it.
668
+ //
669
+ // Gated on having just WRITTEN that config, not merely on the stub being absent.
670
+ // DB_STUB_REL_PATH is derived from SCAFFOLD_OUT_DIR + SCAFFOLD_DB_IMPORT — the
671
+ // scaffold's own constants — so it describes where the SCAFFOLDED config points and
672
+ // nowhere else. Re-running `meta init` in a project that already has a config with
673
+ // its own `outDir`/`dbImport` preserves that config (above) and would otherwise still
674
+ // drop a src/db.ts that nothing in the project references: a stray file, in the
675
+ // adopter's application source, answering a question they had already answered.
676
+ const dbStubPath = join(opts.cwd, DB_STUB_REL_PATH);
677
+ const dbStubExists = await fileExists(dbStubPath);
678
+ if (wroteScaffoldedConfig && !dbStubExists) {
679
+ await mkdir(dirname(dbStubPath), { recursive: true });
680
+ await writeFile(dbStubPath, DB_STUB_BODY, "utf8");
681
+ result.created.push(DB_STUB_REL_PATH);
682
+ } else if (dbStubExists) {
683
+ // "preserved" means a file we would have written was left alone. Skipping because
684
+ // this project keeps its own config is not preservation — there is nothing there.
685
+ result.preserved.push(DB_STUB_REL_PATH);
686
+ } else {
687
+ // Neither written nor preserved: this project keeps its own config AND has no stub.
688
+ // Usually correct — its `dbImport` points at a real module somewhere else. But it is
689
+ // ALSO what a scaffolded project looks like after someone deletes or moves src/db.ts,
690
+ // and `meta init --force` cannot tell those apart: `wroteScaffoldedConfig` is only
691
+ // "no config existed", so it is false for the config init itself wrote. Silently
692
+ // doing nothing there leaves the scaffolded `dbImport: "../db"` and the generated
693
+ // routes' `import { db } from "../db.js"` pointing at nothing, and the adopter meets
694
+ // it as a TS2307 from `tsc` with no word from the command that could have said so.
695
+ // Say it here rather than writing: dropping a file into a project that owns its
696
+ // config is what the branch above deliberately refuses.
697
+ result.warnings.push(
698
+ `${DB_STUB_REL_PATH} was not scaffolded — this project has its own ` +
699
+ "metaobjects.config.ts, so init leaves the database module to it. If that config's " +
700
+ `\`dbImport\` resolves to ${DB_STUB_REL_PATH}, create it or the generated routes will ` +
701
+ "not resolve.",
702
+ );
703
+ }
704
+
532
705
  // Scaffold a minimal root .gitignore ONLY when the project has none — never
533
706
  // clobber a user's existing one (they may have their own rules).
534
707
  const rootGitignorePath = join(opts.cwd, ".gitignore");
@@ -598,6 +771,7 @@ async function ensureEsmPackageType(cwd: string, result: InitResult): Promise<vo
598
771
  return;
599
772
  }
600
773
 
774
+ const declaredType = pkg.type; // read BEFORE the mutation below overwrites it
601
775
  pkg.type = "module";
602
776
  const added = addScaffoldDevDependencies(pkg);
603
777
  // Preserve the file's existing indentation rather than reformatting someone's manifest.
@@ -606,9 +780,16 @@ async function ensureEsmPackageType(cwd: string, result: InitResult): Promise<vo
606
780
  // Past tense, deliberately: this reports an edit already made. The imperative
607
781
  // ("set `\"type\": \"module\"`") read as a TODO on the one line a newcomer sees
608
782
  // last, so a scaffold that had just done the right thing looked like it had failed.
783
+ //
784
+ // And it must not claim the manifest was SILENT on the point: `npm init -y` writes
785
+ // `"type": "commonjs"` explicitly (npm 11.x), which is the dominant first-touch path,
786
+ // so "declared no module system" was false exactly where it is read most. Report what
787
+ // was actually there.
788
+ const previous = typeof declaredType === "string" ? declaredType : undefined;
609
789
  result.warnings.push(
610
- 'package.json declared no module system set `"type": "module"` for you, because ' +
611
- "MetaObjects scaffolds and generates ESM, which a CommonJS project cannot compile.",
790
+ `package.json ${previous === undefined ? "declared no module system" : `declared "type": "${previous}"`} ` +
791
+ 'set `"type": "module"` for you, because MetaObjects scaffolds and generates ESM, ' +
792
+ "which a CommonJS project cannot compile.",
612
793
  );
613
794
  if (added.length > 0) {
614
795
  result.warnings.push(
@@ -643,10 +824,17 @@ function addScaffoldDevDependencies(pkg: Record<string, unknown>): string[] {
643
824
  "@metaobjectsdev/metadata": `^${version}`,
644
825
  };
645
826
  const dev = (pkg.devDependencies ?? {}) as Record<string, string>;
646
- const deps = (pkg.dependencies ?? {}) as Record<string, string>;
827
+ // "Already declared" spans all four dependency fields — the shared rule, so this
828
+ // cannot drift from what `meta eject` means by the same words. It used to ask only
829
+ // dependencies + devDependencies, which meant a project declaring codegen-ts as a
830
+ // PEER dependency (correct for a library whose consumer supplies the version) got it
831
+ // added to devDependencies as well: the same package pinned twice in one manifest,
832
+ // and a second physical copy is the class-identity split this repo has been bitten
833
+ // by twice.
834
+ const declared = declaredDependencyNames(pkg as PackageManifest);
647
835
  const added: string[] = [];
648
836
  for (const [name, range] of Object.entries(wanted)) {
649
- if (dev[name] !== undefined || deps[name] !== undefined) continue;
837
+ if (declared.has(name)) continue;
650
838
  dev[name] = range;
651
839
  added.push(name);
652
840
  }
@@ -697,8 +885,18 @@ function buildD1MigrateBlock(cwd: string): Record<string, unknown> {
697
885
  return block;
698
886
  }
699
887
 
700
- export function nextStepsBlock(): string {
701
- return NEXT_STEPS;
888
+ /**
889
+ * The post-init message.
890
+ *
891
+ * @param dbStubWritten whether THIS run wrote {@link DB_STUB_REL_PATH}. Required rather
892
+ * than defaulted, because a default would silently restore the bug this parameter
893
+ * exists to close: the note used to be part of one static string and so claimed the
894
+ * stub on every run, including the `meta init --force` in a project keeping its own
895
+ * `metaobjects.config.ts`, where the stub is deliberately not written. Callers pass
896
+ * `result.created.includes(DB_STUB_REL_PATH)` — the same list the message describes.
897
+ */
898
+ export function nextStepsBlock(dbStubWritten: boolean): string {
899
+ return SCAFFOLD_SUMMARY + (dbStubWritten ? DB_STUB_NOTE : "") + NEXT_STEPS;
702
900
  }
703
901
 
704
902
  async function dirExists(p: string): Promise<boolean> {
@@ -763,7 +961,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
763
961
  }
764
962
  for (const w of result.warnings) log.warn(w);
765
963
  } else {
766
- log.info(nextStepsBlock());
964
+ log.info(nextStepsBlock(result.created.includes(DB_STUB_REL_PATH)));
767
965
  // Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
768
966
  // discovery warning) — these are otherwise dropped on the normal init path.
769
967
  for (const w of result.warnings) log.warn(w);