@metaobjectsdev/cli 0.23.2 → 0.24.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 (89) hide show
  1. package/README.md +78 -0
  2. package/dist/src/commands/docs.d.ts.map +1 -1
  3. package/dist/src/commands/docs.js +157 -59
  4. package/dist/src/commands/docs.js.map +1 -1
  5. package/dist/src/commands/export.d.ts.map +1 -1
  6. package/dist/src/commands/export.js +28 -6
  7. package/dist/src/commands/export.js.map +1 -1
  8. package/dist/src/commands/gen.d.ts.map +1 -1
  9. package/dist/src/commands/gen.js +66 -22
  10. package/dist/src/commands/gen.js.map +1 -1
  11. package/dist/src/commands/init.d.ts +8 -0
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/init.js +142 -52
  14. package/dist/src/commands/init.js.map +1 -1
  15. package/dist/src/commands/migrate.d.ts +9 -2
  16. package/dist/src/commands/migrate.d.ts.map +1 -1
  17. package/dist/src/commands/migrate.js +399 -73
  18. package/dist/src/commands/migrate.js.map +1 -1
  19. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  20. package/dist/src/commands/prompt-snapshot.js +40 -18
  21. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  22. package/dist/src/commands/upgrade.d.ts +2 -0
  23. package/dist/src/commands/upgrade.d.ts.map +1 -0
  24. package/dist/src/commands/upgrade.js +163 -0
  25. package/dist/src/commands/upgrade.js.map +1 -0
  26. package/dist/src/commands/verify.d.ts.map +1 -1
  27. package/dist/src/commands/verify.js +360 -48
  28. package/dist/src/commands/verify.js.map +1 -1
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +55 -19
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/lib/allow.d.ts.map +1 -1
  33. package/dist/src/lib/allow.js +4 -0
  34. package/dist/src/lib/allow.js.map +1 -1
  35. package/dist/src/lib/args.d.ts +16 -2
  36. package/dist/src/lib/args.d.ts.map +1 -1
  37. package/dist/src/lib/args.js +19 -3
  38. package/dist/src/lib/args.js.map +1 -1
  39. package/dist/src/lib/codegen-drift.d.ts +8 -1
  40. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  41. package/dist/src/lib/codegen-drift.js +9 -1
  42. package/dist/src/lib/codegen-drift.js.map +1 -1
  43. package/dist/src/lib/detect-stack.d.ts +1 -1
  44. package/dist/src/lib/detect-stack.d.ts.map +1 -1
  45. package/dist/src/lib/detect-stack.js +22 -37
  46. package/dist/src/lib/detect-stack.js.map +1 -1
  47. package/dist/src/lib/load-metaobjects-config.d.ts +79 -1
  48. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  49. package/dist/src/lib/load-metaobjects-config.js +123 -1
  50. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  51. package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
  52. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
  53. package/dist/src/lib/manifest-ignored-check.js +74 -0
  54. package/dist/src/lib/manifest-ignored-check.js.map +1 -0
  55. package/dist/src/lib/migrate-scope.d.ts +49 -0
  56. package/dist/src/lib/migrate-scope.d.ts.map +1 -0
  57. package/dist/src/lib/migrate-scope.js +92 -0
  58. package/dist/src/lib/migrate-scope.js.map +1 -0
  59. package/dist/src/lib/output.d.ts +1 -1
  60. package/dist/src/lib/output.d.ts.map +1 -1
  61. package/dist/src/lib/output.js +9 -3
  62. package/dist/src/lib/output.js.map +1 -1
  63. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  64. package/dist/src/lib/requirement-check.js +10 -57
  65. package/dist/src/lib/requirement-check.js.map +1 -1
  66. package/package.json +11 -11
  67. package/src/commands/docs.ts +174 -61
  68. package/src/commands/export.ts +30 -6
  69. package/src/commands/gen.ts +68 -22
  70. package/src/commands/init.ts +161 -58
  71. package/src/commands/migrate.ts +423 -75
  72. package/src/commands/prompt-snapshot.ts +43 -18
  73. package/src/commands/upgrade.ts +179 -0
  74. package/src/commands/verify.ts +397 -48
  75. package/src/index.ts +55 -19
  76. package/src/lib/allow.ts +4 -0
  77. package/src/lib/args.ts +35 -4
  78. package/src/lib/codegen-drift.ts +9 -0
  79. package/src/lib/detect-stack.ts +20 -33
  80. package/src/lib/load-metaobjects-config.ts +129 -2
  81. package/src/lib/manifest-ignored-check.ts +75 -0
  82. package/src/lib/migrate-scope.ts +102 -0
  83. package/src/lib/output.ts +8 -4
  84. package/src/lib/requirement-check.ts +10 -58
  85. package/dist/src/lib/verified-by-scan.d.ts +0 -20
  86. package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
  87. package/dist/src/lib/verified-by-scan.js +0 -344
  88. package/dist/src/lib/verified-by-scan.js.map +0 -1
  89. package/src/lib/verified-by-scan.ts +0 -375
@@ -36,7 +36,17 @@ const META_COMMON_JSON = JSON.stringify(
36
36
  // ignore the per-target generated shadow with a narrow `*/src/generated/`
37
37
  // pattern, then explicitly re-include `migrations/` and `config.json` so the
38
38
  // tracked artifacts are never swept up even if a future broad pattern were added.
39
- const METAOBJECTS_GITIGNORE_BODY = `.gen-state/
39
+ const METAOBJECTS_GITIGNORE_BODY = `# The codegen merge base. The snapshot BODIES are a second full copy of all
40
+ # generated output — never commit those. \`.hashes.json\` is different: it is one
41
+ # hash per generated path, and it is the only thing that lets \`meta gen\` tell "this
42
+ # file is exactly what I wrote" from "somebody edited this" on a machine that did
43
+ # not generate it. Without it committed, every fresh clone and every CI runner has
44
+ # no merge base, and a hand-edited generated file cannot be recognised as such.
45
+ #
46
+ # The glob form matters: \`.gen-state/\` (a directory) would stop git descending, and
47
+ # the negation below could never take effect.
48
+ .gen-state/*
49
+ !.gen-state/.hashes.json
40
50
 
41
51
  # Per-target codegen output routed under .metaobjects/<target>/ is regenerable
42
52
  # (re-run \`meta gen\`); never commit it. The canonical output is your configured
@@ -127,6 +137,14 @@ export interface InitOptions {
127
137
  wireRoot?: boolean;
128
138
  /** Scaffold ONLY the agent-context (always-on + skills + root wiring), skipping the metaobjects/ project scaffold — for dropping context into an existing/polyglot repo. */
129
139
  docsOnly?: boolean;
140
+ /**
141
+ * Write ONLY `.metaobjects/config.json` — no TypeScript scaffold (metaobjects.config.ts,
142
+ * codegen/generators/, package.json edits, .gitignore, agent-context files, or a
143
+ * metaobjects/ directory). For a Maven- or pip-rooted project that needs the Node CLI
144
+ * (which owns `migrate` and `verify --db` under ADR-0015) to discover its metadata
145
+ * without acquiring a TypeScript project it will never use.
146
+ */
147
+ configOnly?: boolean;
130
148
  }
131
149
 
132
150
  export interface InitResult {
@@ -192,7 +210,7 @@ function warnIfMonorepoSubdir(opts: InitOptions, result: InitResult): void {
192
210
  * string[]s (or nothing) straight through is safe — no need to special-case an empty
193
211
  * or absent prior.
194
212
  */
195
- function stackForAgentContext(opts: InitOptions, prior: Manifest | undefined): Stack {
213
+ async function stackForAgentContext(opts: InitOptions, prior: Manifest | undefined): Promise<Stack> {
196
214
  const hasOverride = (opts.servers?.length ?? 0) > 0 || (opts.clients?.length ?? 0) > 0;
197
215
  const overrides = hasOverride
198
216
  ? { servers: opts.servers ?? [], clients: opts.clients ?? [] }
@@ -200,10 +218,27 @@ function stackForAgentContext(opts: InitOptions, prior: Manifest | undefined): S
200
218
  return resolveStack(opts.cwd, overrides);
201
219
  }
202
220
 
221
+ /** Writes `contents` to `path` (relative to `cwd`), unless `dryRun` — in which case
222
+ * the write is skipped entirely and the caller still records what WOULD have
223
+ * landed. Factors the mkdir+writeFile pair shared by every write site below. */
224
+ async function writeUnlessDryRun(cwd: string, dryRun: boolean, path: string, contents: string): Promise<void> {
225
+ if (dryRun) return;
226
+ const abs = join(cwd, path);
227
+ await mkdir(dirname(abs), { recursive: true });
228
+ await writeFile(abs, contents, "utf8");
229
+ }
230
+
231
+ /** "would be VERBED" during a dry run, plain VERBED otherwise — the one tense
232
+ * marker every reported write shares, so each call site states only its own
233
+ * past participle instead of writing out both tenses of the whole sentence. */
234
+ function verbed(dryRun: boolean, pastParticiple: string): string {
235
+ return dryRun ? `would be ${pastParticiple}` : pastParticiple;
236
+ }
237
+
203
238
  async function writeAgentContext(opts: InitOptions, result: InitResult): Promise<void> {
204
239
  warnIfMonorepoSubdir(opts, result);
205
240
  const prior = await readManifest(opts.cwd);
206
- const stack = stackForAgentContext(opts, prior);
241
+ const stack = await stackForAgentContext(opts, prior);
207
242
  let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack });
208
243
  if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/"));
209
244
 
@@ -226,32 +261,45 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise
226
261
  : decision.writes;
227
262
  const conflicts = opts.force ? [] : decision.conflicts;
228
263
 
264
+ // --print-only must win outright: a documented dry run must never write. Both
265
+ // callers of this function (`--docs-only` and `--refresh-docs`) return from
266
+ // `init()` ABOVE the full-scaffold path's own printOnly guard, so without this
267
+ // the dry run silently scaffolded for real — the same defect `--config-only`
268
+ // carried. The guard lives HERE rather than as a path list beside that one
269
+ // because this write set is dynamic (it depends on the resolved stack), and
270
+ // `decision` is already the complete plan: suppressing just the I/O reports
271
+ // exactly the paths a real run would touch, with no second list to drift.
272
+ const dryRun = opts.printOnly === true;
273
+
229
274
  for (const w of writes) {
230
- const abs = join(opts.cwd, w.path);
231
- await mkdir(dirname(abs), { recursive: true });
232
- await writeFile(abs, w.contents, "utf8");
275
+ await writeUnlessDryRun(opts.cwd, dryRun, w.path, w.contents);
233
276
  result.created.push(w.path);
234
277
  }
235
278
  for (const c of conflicts) {
236
- const abs = join(opts.cwd, c.newPath);
237
- await mkdir(dirname(abs), { recursive: true });
238
- await writeFile(abs, c.contents, "utf8");
279
+ await writeUnlessDryRun(opts.cwd, dryRun, c.newPath, c.contents);
239
280
  result.created.push(c.newPath);
240
- result.warnings.push(`${c.path} appears hand-edited; refreshed version written to ${c.newPath}`);
281
+ // Past tense only when it actually happened — a dry run that reports "written
282
+ // to <path>.new" is claiming an edit-preserving side effect the user can go
283
+ // look for and will not find.
284
+ result.warnings.push(
285
+ `${c.path} appears hand-edited; refreshed version ${verbed(dryRun, "written")} to ${c.newPath}`,
286
+ );
241
287
  }
242
- const manifestAbs = join(opts.cwd, AGENT_CONTEXT_MANIFEST_PATH);
243
- await mkdir(dirname(manifestAbs), { recursive: true });
244
- await writeFile(manifestAbs, JSON.stringify(decision.manifest, null, 2) + "\n", "utf8");
288
+ await writeUnlessDryRun(
289
+ opts.cwd, dryRun, AGENT_CONTEXT_MANIFEST_PATH,
290
+ JSON.stringify(decision.manifest, null, 2) + "\n",
291
+ );
292
+ result.created.push(AGENT_CONTEXT_MANIFEST_PATH);
245
293
 
246
294
  for (const orphan of decision.removed) {
247
295
  result.warnings.push(`${orphan} is no longer part of this stack; orphaned (safe to delete).`);
248
296
  }
249
297
 
250
- if (opts.wireRoot) await wireRootMemory(opts.cwd, result);
298
+ if (opts.wireRoot) await wireRootMemory(opts.cwd, result, dryRun);
251
299
  }
252
300
 
253
301
  const ROOT_IMPORT_LINE = "@.metaobjects/AGENTS.md";
254
- async function wireRootMemory(cwd: string, result: InitResult): Promise<void> {
302
+ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false): Promise<void> {
255
303
  const claudePath = join(cwd, "CLAUDE.md");
256
304
  const agentsPath = join(cwd, "AGENTS.md");
257
305
  const claudeExists = await fileExists(claudePath);
@@ -259,8 +307,8 @@ async function wireRootMemory(cwd: string, result: InitResult): Promise<void> {
259
307
 
260
308
  // If neither root memory file exists, create CLAUDE.md (Claude Code's canonical) with the import.
261
309
  if (!claudeExists && !agentsExists) {
262
- await writeFile(claudePath, `# Project memory\n\n${ROOT_IMPORT_LINE}\n`, "utf8");
263
- result.created.push("CLAUDE.md (created with MetaObjects @import)");
310
+ await writeUnlessDryRun(cwd, dryRun, "CLAUDE.md", `# Project memory\n\n${ROOT_IMPORT_LINE}\n`);
311
+ result.created.push(`CLAUDE.md (${verbed(dryRun, "created")} with MetaObjects @import)`);
264
312
  return;
265
313
  }
266
314
  // Otherwise append the import to whichever exist (idempotent — never double-add).
@@ -268,8 +316,11 @@ async function wireRootMemory(cwd: string, result: InitResult): Promise<void> {
268
316
  if (!exists) continue;
269
317
  const body = await readFile(path, "utf8");
270
318
  if (body.includes(ROOT_IMPORT_LINE)) continue;
271
- await writeFile(path, `${body.replace(/\n*$/, "\n")}\n${ROOT_IMPORT_LINE}\n`, "utf8");
272
- result.warnings.push(`wired ${ROOT_IMPORT_LINE} into ${path.endsWith("AGENTS.md") ? "AGENTS.md" : "CLAUDE.md"} so the MetaObjects context loads`);
319
+ const target = path.endsWith("AGENTS.md") ? "AGENTS.md" : "CLAUDE.md";
320
+ await writeUnlessDryRun(cwd, dryRun, target, `${body.replace(/\n*$/, "\n")}\n${ROOT_IMPORT_LINE}\n`);
321
+ // Past tense only when it actually happened — this one mutates a file the user
322
+ // owns, so a dry run reporting it as done is the most misleading of the three.
323
+ result.warnings.push(`${verbed(dryRun, "wired")} ${ROOT_IMPORT_LINE} into ${target} so the MetaObjects context loads`);
273
324
  }
274
325
  }
275
326
 
@@ -294,6 +345,72 @@ async function writeOwnedGenerators(opts: InitOptions, result: InitResult): Prom
294
345
  }
295
346
  }
296
347
 
348
+ /**
349
+ * .metaobjects/config.json — write fresh defaults, or preserve+merge an existing
350
+ * valid config. Shared by the full scaffold and `--config-only` so the two paths
351
+ * cannot drift on the config's default content.
352
+ */
353
+ async function writeConfigFile(opts: InitOptions, result: InitResult, agentDir: string, agentDirExists: boolean): Promise<void> {
354
+ const freshConfig = opts.d1
355
+ ? ConfigSchema.parse({ ...DEFAULT_CONFIG, migrate: buildD1MigrateBlock(opts.cwd) })
356
+ : DEFAULT_CONFIG;
357
+ const writeFresh = (): Promise<void> =>
358
+ writeFile(join(agentDir, "config.json"), JSON.stringify(freshConfig, null, 2) + "\n", "utf8");
359
+
360
+ if (!agentDirExists) {
361
+ await writeFresh();
362
+ result.created.push(".metaobjects/config.json");
363
+ return;
364
+ }
365
+
366
+ const configPath = join(agentDir, "config.json");
367
+ let priorContent: string | undefined;
368
+ try {
369
+ priorContent = await readFile(configPath, "utf8");
370
+ const parsed = ConfigSchema.parse(JSON.parse(priorContent));
371
+ const merged = ConfigSchema.parse({ ...DEFAULT_CONFIG, ...parsed });
372
+ // When a valid .metaobjects/config.json already exists and the user passes --force,
373
+ // we preserve the existing config and only re-scaffold support files. The --d1 flag
374
+ // only takes effect on fresh inits — retro-fitting D1 onto an existing project is
375
+ // the user's job (edit migrate.dialect and migrate.d1 in config.json directly).
376
+ await saveConfig(agentDir, merged);
377
+ result.preserved.push(".metaobjects/config.json");
378
+ return;
379
+ } catch {
380
+ if (priorContent === undefined) {
381
+ // The .metaobjects/ dir existed but config.json itself did not — a fresh write.
382
+ await writeFresh();
383
+ result.created.push(".metaobjects/config.json");
384
+ return;
385
+ }
386
+
387
+ // In the full-scaffold path this is only reachable once the caller has
388
+ // already required --force (the exists-guard at the top of `init()`
389
+ // throws before writeConfigFile runs otherwise), so opts.force is always
390
+ // true there. `--config-only` calls this function directly with no such
391
+ // guard, so without this check it would silently destroy an existing,
392
+ // merely-unparseable config on every run — the one thing `--force` is
393
+ // supposed to gate.
394
+ if (!opts.force) {
395
+ throw new Error(
396
+ `existing .metaobjects/config.json exists but could not be parsed; refusing to overwrite it. ` +
397
+ `Use --force to replace it with defaults. Prior content:\n${priorContent}`,
398
+ );
399
+ }
400
+ log.warn("existing .metaobjects/config.json was invalid — writing fresh defaults. Prior content:");
401
+ log.warn(priorContent);
402
+ result.warnings.push("invalid .metaobjects/config.json replaced with defaults");
403
+ await writeFresh();
404
+ // F11 — matches the OTHER two `writeFresh()` call sites above: this IS a
405
+ // fresh write (a destructive one, replacing content that could not be
406
+ // parsed), not a no-op. Omitting this left it in neither `created` nor
407
+ // `preserved`, so the `--config-only` CLI summary (which keys on
408
+ // `result.created.includes(...)` alone) reported "already exists — left
409
+ // untouched" for a config it had just overwritten with defaults.
410
+ result.created.push(".metaobjects/config.json");
411
+ }
412
+ }
413
+
297
414
  export async function init(opts: InitOptions): Promise<InitResult> {
298
415
  const result: InitResult = { created: [], preserved: [], warnings: [] };
299
416
  const agentDir = join(opts.cwd, DEFAULT_METAOBJECTS_DIR);
@@ -309,6 +426,22 @@ export async function init(opts: InitOptions): Promise<InitResult> {
309
426
  return result;
310
427
  }
311
428
 
429
+ if (opts.configOnly) {
430
+ // --print-only must win outright: a documented dry run must never write, and
431
+ // this branch used to return ABOVE the printOnly guard the full-scaffold path
432
+ // uses below, so `--config-only --print-only` silently wrote the real file.
433
+ if (opts.printOnly) {
434
+ result.created.push(".metaobjects/config.json");
435
+ return result;
436
+ }
437
+ // Config only: write/preserve .metaobjects/config.json and nothing else — no
438
+ // metaobjects/ dir, no agent-context, no TypeScript scaffold. `agentDirExists` is
439
+ // captured before the mkdir below so an existing valid config is still preserved.
440
+ await mkdir(agentDir, { recursive: true });
441
+ await writeConfigFile(opts, result, agentDir, agentDirExists);
442
+ return result;
443
+ }
444
+
312
445
  if (opts.refreshDocs && exists) {
313
446
  // Refresh-only path: (re)write the agent-context docs and NOTHING else — never
314
447
  // the project scaffold (metaobjects/, config.json, codegen/generators/,
@@ -362,45 +495,7 @@ export async function init(opts: InitOptions): Promise<InitResult> {
362
495
  }
363
496
 
364
497
  // .metaobjects/config.json
365
- const freshConfig = opts.d1
366
- ? ConfigSchema.parse({ ...DEFAULT_CONFIG, migrate: buildD1MigrateBlock(opts.cwd) })
367
- : DEFAULT_CONFIG;
368
- if (agentDirExists) {
369
- const configPath = join(agentDir, "config.json");
370
- let priorContent: string | undefined;
371
- try {
372
- priorContent = await readFile(configPath, "utf8");
373
- const parsed = ConfigSchema.parse(JSON.parse(priorContent));
374
- const merged = ConfigSchema.parse({ ...DEFAULT_CONFIG, ...parsed });
375
- // When a valid .metaobjects/config.json already exists and the user passes --force,
376
- // we preserve the existing config and only re-scaffold support files. The --d1 flag
377
- // only takes effect on fresh inits — retro-fitting D1 onto an existing project is
378
- // the user's job (edit migrate.dialect and migrate.d1 in config.json directly).
379
- await saveConfig(agentDir, merged);
380
- result.preserved.push(".metaobjects/config.json");
381
- } catch {
382
- if (priorContent !== undefined) {
383
- log.warn("existing .metaobjects/config.json was invalid — writing fresh defaults. Prior content:");
384
- log.warn(priorContent);
385
- result.warnings.push("invalid .metaobjects/config.json replaced with defaults");
386
- }
387
- await writeFile(
388
- join(agentDir, "config.json"),
389
- JSON.stringify(freshConfig, null, 2) + "\n",
390
- "utf8",
391
- );
392
- if (priorContent === undefined) {
393
- result.created.push(".metaobjects/config.json");
394
- }
395
- }
396
- } else {
397
- await writeFile(
398
- join(agentDir, "config.json"),
399
- JSON.stringify(freshConfig, null, 2) + "\n",
400
- "utf8",
401
- );
402
- result.created.push(".metaobjects/config.json");
403
- }
498
+ await writeConfigFile(opts, result, agentDir, agentDirExists);
404
499
 
405
500
  // .metaobjects/.gitignore
406
501
  await writeFile(join(agentDir, ".gitignore"), METAOBJECTS_GITIGNORE_BODY, "utf8");
@@ -643,6 +738,7 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
643
738
  noSkills: flags.noSkills,
644
739
  wireRoot: flags.wireRoot,
645
740
  docsOnly: flags.docsOnly,
741
+ configOnly: flags.configOnly,
646
742
  });
647
743
 
648
744
  if (flags.printOnly) {
@@ -656,6 +752,13 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
656
752
  log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.`);
657
753
  for (const w of result.warnings) log.info(` ${w}`);
658
754
  log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
755
+ } else if (flags.configOnly) {
756
+ if (result.created.includes(".metaobjects/config.json")) {
757
+ log.info("Wrote .metaobjects/config.json — declare your metadata sources there for the Node CLI (migrate, verify --db).");
758
+ } else {
759
+ log.info(".metaobjects/config.json already exists — left untouched.");
760
+ }
761
+ for (const w of result.warnings) log.warn(w);
659
762
  } else {
660
763
  log.info(nextStepsBlock());
661
764
  // Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context