@metaobjectsdev/cli 0.9.0 → 0.11.0-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 (52) hide show
  1. package/dist/src/commands/docs.d.ts +2 -0
  2. package/dist/src/commands/docs.d.ts.map +1 -0
  3. package/dist/src/commands/docs.js +395 -0
  4. package/dist/src/commands/docs.js.map +1 -0
  5. package/dist/src/commands/gen.d.ts.map +1 -1
  6. package/dist/src/commands/gen.js +41 -1
  7. package/dist/src/commands/gen.js.map +1 -1
  8. package/dist/src/commands/init.d.ts +6 -0
  9. package/dist/src/commands/init.d.ts.map +1 -1
  10. package/dist/src/commands/init.js +102 -29
  11. package/dist/src/commands/init.js.map +1 -1
  12. package/dist/src/commands/verify.d.ts.map +1 -1
  13. package/dist/src/commands/verify.js +69 -15
  14. package/dist/src/commands/verify.js.map +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +20 -32
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/lib/agent-context-staleness.d.ts +7 -0
  19. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -0
  20. package/dist/src/lib/agent-context-staleness.js +26 -0
  21. package/dist/src/lib/agent-context-staleness.js.map +1 -0
  22. package/dist/src/lib/args.d.ts +14 -0
  23. package/dist/src/lib/args.d.ts.map +1 -1
  24. package/dist/src/lib/args.js +22 -0
  25. package/dist/src/lib/args.js.map +1 -1
  26. package/dist/src/lib/codegen-drift.d.ts +21 -0
  27. package/dist/src/lib/codegen-drift.d.ts.map +1 -0
  28. package/dist/src/lib/codegen-drift.js +142 -0
  29. package/dist/src/lib/codegen-drift.js.map +1 -0
  30. package/dist/src/lib/detect-stack.d.ts +7 -0
  31. package/dist/src/lib/detect-stack.d.ts.map +1 -0
  32. package/dist/src/lib/detect-stack.js +38 -0
  33. package/dist/src/lib/detect-stack.js.map +1 -0
  34. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  35. package/dist/src/lib/load-metaobjects-config.js +8 -0
  36. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  37. package/dist/src/lib/version.d.ts +7 -0
  38. package/dist/src/lib/version.d.ts.map +1 -0
  39. package/dist/src/lib/version.js +30 -0
  40. package/dist/src/lib/version.js.map +1 -0
  41. package/package.json +56 -45
  42. package/src/commands/docs.ts +438 -0
  43. package/src/commands/gen.ts +44 -1
  44. package/src/commands/init.ts +106 -31
  45. package/src/commands/verify.ts +81 -15
  46. package/src/index.ts +20 -30
  47. package/src/lib/agent-context-staleness.ts +24 -0
  48. package/src/lib/args.ts +41 -0
  49. package/src/lib/codegen-drift.ts +173 -0
  50. package/src/lib/detect-stack.ts +41 -0
  51. package/src/lib/load-metaobjects-config.ts +8 -0
  52. package/src/lib/version.ts +27 -0
@@ -1,10 +1,16 @@
1
1
  import { mkdir, writeFile, readFile, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { basename } from "node:path";
3
+ import { basename, dirname } from "node:path";
4
+ import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
4
5
  import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
6
+ import {
7
+ assemble, resolveAgentContextRoot, planScaffold,
8
+ AGENT_CONTEXT_MANIFEST_PATH, type Manifest,
9
+ } from "@metaobjectsdev/sdk/agent-context";
10
+ import { resolveStack } from "../lib/detect-stack.js";
5
11
  import { parseInitArgs } from "../lib/args.js";
6
12
  import { log } from "../lib/log.js";
7
- import { AGENT_DOCS_BODY, withContentHash, isUnmodified } from "@metaobjectsdev/sdk/agent-docs";
13
+ import { cliVersion } from "../lib/version.js";
8
14
  import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
9
15
 
10
16
  const META_COMMON_JSON = JSON.stringify(
@@ -32,7 +38,7 @@ import {
32
38
  } from "@metaobjectsdev/codegen-ts/generators";
33
39
 
34
40
  export default defineConfig({
35
- outDir: "./src/db",
41
+ outDir: "src/generated",
36
42
  extStyle: "none",
37
43
  dbImport: "../db",
38
44
  dialect: "${dialect}",
@@ -43,6 +49,11 @@ export default defineConfig({
43
49
  routesFile(),
44
50
  barrel(),
45
51
  ],
52
+ docs: {
53
+ outDir: "./docs", // model + api surfaces both land here (run: meta docs)
54
+ layout: "flat", // or "package" for multi-package models
55
+ surfaces: ["model", "api"],
56
+ },
46
57
  });
47
58
  `;
48
59
  }
@@ -53,12 +64,11 @@ Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
53
64
  Next steps (when later sub-projects ship):
54
65
  meta ingest # propose entities from your existing TS code
55
66
  meta gen # codegen TS targets from entities
67
+ meta docs # neutral model docs (entity + template pages, incl. linked template source)
56
68
  meta serve # local viewer
57
69
  meta install-hooks # register MCP server + Claude Code hooks
58
70
  `;
59
71
 
60
- const AGENT_DOC_FILES = ["AGENTS.md", "CLAUDE.md"] as const;
61
-
62
72
  export interface InitOptions {
63
73
  cwd: string;
64
74
  force?: boolean;
@@ -66,6 +76,12 @@ export interface InitOptions {
66
76
  printOnly?: boolean;
67
77
  refreshDocs?: boolean;
68
78
  d1?: boolean;
79
+ servers?: string[];
80
+ clients?: string[];
81
+ noSkills?: boolean;
82
+ wireRoot?: boolean;
83
+ /** Scaffold ONLY the agent-context (always-on + skills + root wiring), skipping the metaobjects/ project scaffold — for dropping context into an existing/polyglot repo. */
84
+ docsOnly?: boolean;
69
85
  }
70
86
 
71
87
  export interface InitResult {
@@ -74,29 +90,71 @@ export interface InitResult {
74
90
  warnings: string[];
75
91
  }
76
92
 
77
- async function writeAgentDocs(agentDir: string, result: InitResult): Promise<void> {
78
- const docsBody = withContentHash(AGENT_DOCS_BODY);
79
- for (const filename of AGENT_DOC_FILES) {
80
- const path = join(agentDir, filename);
81
- const exists = await fileExists(path);
93
+ async function readManifest(cwd: string): Promise<Manifest | undefined> {
94
+ const p = join(cwd, AGENT_CONTEXT_MANIFEST_PATH);
95
+ if (!(await fileExists(p))) return undefined;
96
+ try { return JSON.parse(await readFile(p, "utf8")) as Manifest; } catch { return undefined; }
97
+ }
82
98
 
83
- if (!exists) {
84
- await writeFile(path, docsBody, "utf8");
85
- result.created.push(`.metaobjects/${filename}`);
86
- continue;
87
- }
99
+ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise<void> {
100
+ const stack = resolveStack(opts.cwd, { servers: opts.servers ?? [], clients: opts.clients ?? [] });
101
+ let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack });
102
+ if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/"));
103
+
104
+ const prior = await readManifest(opts.cwd);
105
+ const decision = planScaffold({
106
+ stack, assembled, prior,
107
+ readCurrent: (rel) => {
108
+ const abs = join(opts.cwd, rel);
109
+ return existsSyncWrap(abs) ? readFileSyncWrap(abs, "utf8") : undefined;
110
+ },
111
+ generatedBy: cliVersion(),
112
+ });
113
+
114
+ for (const w of decision.writes) {
115
+ const abs = join(opts.cwd, w.path);
116
+ await mkdir(dirname(abs), { recursive: true });
117
+ await writeFile(abs, w.contents, "utf8");
118
+ result.created.push(w.path);
119
+ }
120
+ for (const c of decision.conflicts) {
121
+ const abs = join(opts.cwd, c.newPath);
122
+ await mkdir(dirname(abs), { recursive: true });
123
+ await writeFile(abs, c.contents, "utf8");
124
+ result.created.push(c.newPath);
125
+ result.warnings.push(`${c.path} appears hand-edited; refreshed version written to ${c.newPath}`);
126
+ }
127
+ const manifestAbs = join(opts.cwd, AGENT_CONTEXT_MANIFEST_PATH);
128
+ await mkdir(dirname(manifestAbs), { recursive: true });
129
+ await writeFile(manifestAbs, JSON.stringify(decision.manifest, null, 2) + "\n", "utf8");
88
130
 
89
- const existingBody = await readFile(path, "utf8");
90
- if (isUnmodified(existingBody)) {
91
- await writeFile(path, docsBody, "utf8");
92
- result.created.push(`.metaobjects/${filename}`);
93
- } else {
94
- await writeFile(`${path}.new`, docsBody, "utf8");
95
- result.created.push(`.metaobjects/${filename}.new`);
96
- result.warnings.push(
97
- `${filename} appears to have been hand-edited; refreshed docs written to ${filename}.new`,
98
- );
99
- }
131
+ for (const orphan of decision.removed) {
132
+ result.warnings.push(`${orphan} is no longer part of this stack; orphaned (safe to delete).`);
133
+ }
134
+
135
+ if (opts.wireRoot) await wireRootMemory(opts.cwd, result);
136
+ }
137
+
138
+ const ROOT_IMPORT_LINE = "@.metaobjects/AGENTS.md";
139
+ async function wireRootMemory(cwd: string, result: InitResult): Promise<void> {
140
+ const claudePath = join(cwd, "CLAUDE.md");
141
+ const agentsPath = join(cwd, "AGENTS.md");
142
+ const claudeExists = await fileExists(claudePath);
143
+ const agentsExists = await fileExists(agentsPath);
144
+
145
+ // If neither root memory file exists, create CLAUDE.md (Claude Code's canonical) with the import.
146
+ if (!claudeExists && !agentsExists) {
147
+ await writeFile(claudePath, `# Project memory\n\n${ROOT_IMPORT_LINE}\n`, "utf8");
148
+ result.created.push("CLAUDE.md (created with MetaObjects @import)");
149
+ return;
150
+ }
151
+ // Otherwise append the import to whichever exist (idempotent — never double-add).
152
+ for (const [path, exists] of [[claudePath, claudeExists], [agentsPath, agentsExists]] as const) {
153
+ if (!exists) continue;
154
+ const body = await readFile(path, "utf8");
155
+ if (body.includes(ROOT_IMPORT_LINE)) continue;
156
+ await writeFile(path, `${body.replace(/\n*$/, "\n")}\n${ROOT_IMPORT_LINE}\n`, "utf8");
157
+ result.warnings.push(`wired ${ROOT_IMPORT_LINE} into ${path.endsWith("AGENTS.md") ? "AGENTS.md" : "CLAUDE.md"} so the MetaObjects context loads`);
100
158
  }
101
159
  }
102
160
 
@@ -109,9 +167,15 @@ export async function init(opts: InitOptions): Promise<InitResult> {
109
167
  const metaobjectsExists = await dirExists(metaobjectsDir);
110
168
  const exists = agentDirExists || metaobjectsExists;
111
169
 
170
+ if (opts.docsOnly) {
171
+ // Agent-context only: scaffold the always-on + skills + root wiring, never the metaobjects/ project.
172
+ await writeAgentContext(opts, result);
173
+ return result;
174
+ }
175
+
112
176
  if (opts.refreshDocs && exists && !opts.force) {
113
- // Refresh-only path: scaffold agent docs, leave everything else alone.
114
- await writeAgentDocs(agentDir, result);
177
+ // Refresh-only path: scaffold the agent-context, leave everything else alone.
178
+ await writeAgentContext(opts, result);
115
179
  return result;
116
180
  }
117
181
 
@@ -135,7 +199,7 @@ export async function init(opts: InitOptions): Promise<InitResult> {
135
199
  ".metaobjects/.gitignore",
136
200
  `.metaobjects/${PACKAGE_MANIFEST_FILE}`,
137
201
  );
138
- for (const filename of AGENT_DOC_FILES) result.created.push(`.metaobjects/${filename}`);
202
+ result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
139
203
  result.created.push("metaobjects.config.ts");
140
204
  return result;
141
205
  }
@@ -214,7 +278,7 @@ export async function init(opts: InitOptions): Promise<InitResult> {
214
278
  result.preserved.push(`.metaobjects/${PACKAGE_MANIFEST_FILE}`);
215
279
  }
216
280
 
217
- await writeAgentDocs(agentDir, result);
281
+ await writeAgentContext(opts, result);
218
282
 
219
283
  // Scaffold metaobjects.config.ts at the project root. Never overwrite if it exists.
220
284
  const forgeConfigPath = join(opts.cwd, "metaobjects.config.ts");
@@ -283,6 +347,11 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
283
347
  printOnly: flags.printOnly,
284
348
  refreshDocs: flags.refreshDocs,
285
349
  d1: flags.d1,
350
+ servers: flags.servers,
351
+ clients: flags.clients,
352
+ noSkills: flags.noSkills,
353
+ wireRoot: flags.wireRoot,
354
+ docsOnly: flags.docsOnly,
286
355
  });
287
356
 
288
357
  if (flags.printOnly) {
@@ -292,7 +361,13 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
292
361
  }
293
362
 
294
363
  if (!flags.quiet) {
295
- log.info(nextStepsBlock());
364
+ if (flags.docsOnly) {
365
+ log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.`);
366
+ for (const w of result.warnings) log.info(` ${w}`);
367
+ log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
368
+ } else {
369
+ log.info(nextStepsBlock());
370
+ }
296
371
  }
297
372
  return 0;
298
373
  } catch (err) {
@@ -10,9 +10,12 @@
10
10
  import { join } from "node:path";
11
11
  import { parseVerifyArgs } from "../lib/args.js";
12
12
  import { log } from "../lib/log.js";
13
+ import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
13
14
  import { FileProvider } from "../lib/file-provider.js";
14
15
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
15
16
  import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
17
+ import { computeCodegenDrift } from "../lib/codegen-drift.js";
18
+ import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
16
19
  import { buildKyselyFromUrl, type Dialect } from "../lib/kysely.js";
17
20
  import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
18
21
  import { computeDrift, type Change } from "@metaobjectsdev/migrate-ts";
@@ -46,18 +49,38 @@ export async function verifyCommand(args: string[], cwd: string): Promise<number
46
49
  return 2;
47
50
  }
48
51
 
49
- // Best-effort load of metaobjects.config.ts to pick up consumer-supplied
50
- // providers (e.g. a project's `template.toolcall` subtype). verify doesn't
51
- // require codegen config; if it's absent or invalid, fall back to defaults
52
- // — the loader will surface a stable ERR_UNKNOWN_SUBTYPE if the metadata
53
- // actually uses a non-default subtype.
54
- let configProviders: NonNullable<Awaited<ReturnType<typeof loadMetaobjectsConfig>>["providers"]> | undefined;
52
+ // Advisory: nudge to refresh the .claude/skills docs if they predate this CLI.
53
+ warnIfAgentContextStale(cwd);
54
+
55
+ // ADR-0021 D2 explicit verify subverbs. Each flag selects one drift mode;
56
+ // any combination runs each and the overall exit code is the MAX (non-zero on
57
+ // any drift). A bare `verify` (no explicit subverb) keeps its documented
58
+ // back-compat default: the template/prompt drift gate — plus a one-line note
59
+ // advertising the explicit subverbs.
60
+ const runTemplates = flags.templates || !flags.anyExplicit;
61
+ // The schema (--db) gate is selected by the presence of --db; that check lives
62
+ // inside runSchemaVerify (where `flags.db === undefined` also narrows the type).
63
+ const runCodegen = flags.codegen;
64
+ if (!flags.anyExplicit) {
65
+ log.info(
66
+ "meta verify — running --templates (default). Explicit subverbs: " +
67
+ "--templates (prompt drift), --db (schema drift), --codegen (codegen drift).",
68
+ );
69
+ }
70
+
71
+ // Best-effort load of metaobjects.config.ts. Two consumers:
72
+ // 1) consumer-supplied providers (e.g. a `template.toolcall` subtype) threaded
73
+ // into loadMemory — verify doesn't REQUIRE codegen config for templates/db;
74
+ // 2) the full config object, which `--codegen` needs to locate outDir/targets.
75
+ // If absent/invalid we fall back to defaults; `--codegen` then reports a clear
76
+ // error (it can't diff without knowing where the committed output lives).
77
+ let forgeConfig: MetaobjectsGenConfig | undefined;
55
78
  try {
56
- const forgeConfig = await loadMetaobjectsConfig(cwd);
57
- configProviders = forgeConfig.providers;
79
+ forgeConfig = await loadMetaobjectsConfig(cwd);
58
80
  } catch {
59
- configProviders = undefined;
81
+ forgeConfig = undefined;
60
82
  }
83
+ const configProviders = forgeConfig?.providers;
61
84
 
62
85
  let root: Awaited<ReturnType<typeof loadMemory>>;
63
86
  try {
@@ -77,13 +100,13 @@ export async function verifyCommand(args: string[], cwd: string): Promise<number
77
100
  const promptsDir = join(cwd, flags.prompts ?? DEFAULT_PROMPTS_DIR);
78
101
  const provider = new FileProvider(promptsDir);
79
102
 
80
- // Exit-code composition: the overall result is max(templateExit, schemaExit)
81
- // so either kind of drift fails CI. The schema path only runs when --db is
82
- // present (and not --skip-schema); with no --db it is skipped entirely and
83
- // the exit reflects the template path alone (unchanged behavior).
84
- const templateExit = runTemplateVerify();
103
+ // Exit-code composition: the overall result is the MAX across every selected
104
+ // subverb so ANY kind of drift fails CI. Each gate only runs when its mode is
105
+ // selected; an unselected gate contributes 0.
106
+ const templateExit = runTemplates ? runTemplateVerify() : 0;
85
107
  const schemaExit = await runSchemaVerify();
86
- return Math.max(templateExit, schemaExit);
108
+ const codegenExit = runCodegen ? await runCodegenVerify() : 0;
109
+ return Math.max(templateExit, schemaExit, codegenExit);
87
110
 
88
111
  // -- template (prompt / output) drift --------------------------------------
89
112
  function runTemplateVerify(): number {
@@ -168,6 +191,8 @@ export async function verifyCommand(args: string[], cwd: string): Promise<number
168
191
  // Gated on --db. With no --db (or --skip-schema), this is a no-op returning 0
169
192
  // — the DB-free default behavior is unchanged.
170
193
  async function runSchemaVerify(): Promise<number> {
194
+ // `flags.db === undefined` is exactly `!runDb`; written this way so TS
195
+ // narrows flags.db to `string` for buildKyselyFromUrl below.
171
196
  if (flags.db === undefined || flags.skipSchema) return 0;
172
197
 
173
198
  // d1 has no Kysely-driver introspection path, so the schema-drift gate
@@ -216,6 +241,47 @@ export async function verifyCommand(args: string[], cwd: string): Promise<number
216
241
  }
217
242
  }
218
243
  }
244
+
245
+ // -- codegen drift (ADR-0021 D2) -------------------------------------------
246
+ // Gated on --codegen. Regenerates to a temp dir and diffs against the
247
+ // committed output (config outDir / per-target outDirs). Requires a config:
248
+ // without one, there's no committed-output location to diff against, so it
249
+ // errors clearly (exit 2 — a usage/configuration problem, not a drift result).
250
+ async function runCodegenVerify(): Promise<number> {
251
+ if (forgeConfig === undefined) {
252
+ log.error(
253
+ "verify --codegen: no metaobjects.config.ts found (or it is invalid) — " +
254
+ "cannot locate the committed generated output to diff against. " +
255
+ "Run 'meta init' to scaffold one, or run without --codegen.",
256
+ );
257
+ return 2;
258
+ }
259
+
260
+ let result;
261
+ try {
262
+ result = await computeCodegenDrift(forgeConfig, root, cwd);
263
+ } catch (err) {
264
+ log.error(`verify --codegen: regeneration failed: ${(err as Error).message}`);
265
+ return 1;
266
+ }
267
+
268
+ if (result.error !== undefined) {
269
+ log.error(result.error);
270
+ return 2;
271
+ }
272
+
273
+ if (result.clean) {
274
+ log.info("meta verify — generated output is in sync with the metadata (no codegen drift).");
275
+ return 0;
276
+ }
277
+
278
+ log.error(
279
+ `meta verify — codegen drift (${result.driftedFiles.length} file(s) differ from a fresh regen):`,
280
+ );
281
+ for (const line of result.lines) log.error(` ${line}`);
282
+ log.error("Run 'meta gen' to regenerate, then commit the result.");
283
+ return 1;
284
+ }
219
285
  }
220
286
 
221
287
  /**
package/src/index.ts CHANGED
@@ -1,33 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { resolve } from "node:path";
4
2
  import { log } from "./lib/log.js";
3
+ import { cliVersion } from "./lib/version.js";
5
4
  export { defineConfig } from "@metaobjectsdev/codegen-ts";
6
5
  export type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
7
6
 
8
- // Derive the version from the CLI's own package.json so it never goes stale.
9
- // The compiled entry is dist/src/index.js while package.json sits at the package
10
- // root, so walk up from the module location until @metaobjectsdev/cli's manifest.
11
- function readCliVersion(): string {
12
- let dir = dirname(fileURLToPath(import.meta.url));
13
- for (let i = 0; i < 6; i++) {
14
- const candidate = join(dir, "package.json");
15
- if (existsSync(candidate)) {
16
- try {
17
- const pkg = JSON.parse(readFileSync(candidate, "utf8")) as { name?: string; version?: string };
18
- if (pkg.name === "@metaobjectsdev/cli" && pkg.version) return pkg.version;
19
- } catch {
20
- // not our manifest / unreadable — keep walking up
21
- }
22
- }
23
- const parent = dirname(dir);
24
- if (parent === dir) break;
25
- dir = parent;
26
- }
27
- return "0.0.0";
28
- }
29
-
30
- const VERSION = readCliVersion();
7
+ const VERSION = cliVersion();
31
8
 
32
9
  const HELP_TEXT = `meta — MetaObjects CLI (v${VERSION})
33
10
 
@@ -39,7 +16,8 @@ COMMANDS:
39
16
  init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
40
17
  gen [<entity>...] Codegen TS targets from metaobjects/ entities
41
18
  export Flatten loaded metadata to one canonical JSON artifact
42
- verify Check template.* text against its payload (drift gate)
19
+ docs <metadata> --out <dir> Generate neutral metadata documentation (entity + template pages)
20
+ verify Drift gate — subverbs: --templates / --db / --codegen (bare = --templates)
43
21
  prompt-snapshot Snapshot rendered template.* output; --check gates drift
44
22
  migrate Diff metadata vs live DB; emit migration SQL files
45
23
  --version, -v Print version
@@ -56,10 +34,18 @@ GEN FLAGS:
56
34
  EXPORT FLAGS:
57
35
  --out <file> Write output to a file (default: stdout)
58
36
 
59
- VERIFY FLAGS:
60
- --prompts <dir> Directory of provider-resolved template text (default: prompts)
61
- --db <url> Live DB URL enables the schema-drift gate (exit 1 on drift).
37
+ DOCS FLAGS:
38
+ <metadata> Project root holding metaobjects/ (default: current directory)
39
+ --out <dir>, -o Output directory for the pages (default: ./docs)
40
+ --templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
41
+
42
+ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drift):
43
+ --templates Template/prompt {{field}}↔payload drift (the bare-verify default)
44
+ --codegen Codegen drift — regenerate to a temp dir and diff the committed
45
+ output (config outDir/targets). Needs metaobjects.config.ts; exit 2 if absent.
46
+ --db <url> Schema drift — live DB URL enables the schema-drift gate.
62
47
  Supports: file:, libsql:, postgres:, postgresql:. Omit to skip.
48
+ --prompts <dir> Directory of provider-resolved template text (default: prompts)
63
49
  --dialect sqlite|postgres Optional override (auto-detected from --db URL scheme)
64
50
  --allow <csv> Accepted for parity with 'migrate'; does NOT affect the
65
51
  verify drift gate (the gate fails on ANY detected change)
@@ -140,6 +126,10 @@ export async function run(argv: string[]): Promise<number> {
140
126
  const { exportCommand } = await import("./commands/export.js");
141
127
  return exportCommand(rest, cwd);
142
128
  }
129
+ case "docs": {
130
+ const { docsCommand } = await import("./commands/docs.js");
131
+ return docsCommand(rest, cwd);
132
+ }
143
133
  case "verify": {
144
134
  const { verifyCommand } = await import("./commands/verify.js");
145
135
  return verifyCommand(rest, cwd);
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { AGENT_CONTEXT_MANIFEST_PATH, agentContextStaleness, type Manifest } from "@metaobjectsdev/sdk";
4
+ import { cliVersion } from "./version.js";
5
+ import { log } from "./log.js";
6
+
7
+ /**
8
+ * Advisory: if a scaffolded MetaObjects agent context predates this CLI version,
9
+ * print a one-line nudge to re-scaffold. Never throws, never blocks — an absent or
10
+ * corrupt manifest is silently ignored (this is a reminder, not a gate).
11
+ */
12
+ export function warnIfAgentContextStale(cwd: string): void {
13
+ const p = join(cwd, AGENT_CONTEXT_MANIFEST_PATH);
14
+ let manifest: Manifest | undefined;
15
+ if (existsSync(p)) {
16
+ try {
17
+ manifest = JSON.parse(readFileSync(p, "utf8")) as Manifest;
18
+ } catch {
19
+ return; // unreadable/corrupt — say nothing
20
+ }
21
+ }
22
+ const msg = agentContextStaleness({ manifest, currentVersion: cliVersion() });
23
+ if (msg !== null) log.warn(msg);
24
+ }
package/src/lib/args.ts CHANGED
@@ -10,6 +10,11 @@ export interface InitFlags {
10
10
  printOnly: boolean;
11
11
  refreshDocs: boolean;
12
12
  d1: boolean;
13
+ servers: string[];
14
+ clients: string[];
15
+ noSkills: boolean;
16
+ wireRoot: boolean;
17
+ docsOnly: boolean;
13
18
  }
14
19
 
15
20
  export function parseInitArgs(argv: string[]): InitFlags {
@@ -21,6 +26,11 @@ export function parseInitArgs(argv: string[]): InitFlags {
21
26
  "print-only": { type: "boolean", default: false },
22
27
  "refresh-docs": { type: "boolean", default: false },
23
28
  d1: { type: "boolean", default: false },
29
+ server: { type: "string", multiple: true },
30
+ client: { type: "string", multiple: true },
31
+ "no-skills": { type: "boolean", default: false },
32
+ "no-wire-root": { type: "boolean", default: false },
33
+ "docs-only": { type: "boolean", default: false },
24
34
  },
25
35
  strict: true,
26
36
  allowPositionals: false,
@@ -31,6 +41,11 @@ export function parseInitArgs(argv: string[]): InitFlags {
31
41
  printOnly: !!values["print-only"],
32
42
  refreshDocs: !!values["refresh-docs"],
33
43
  d1: !!values.d1,
44
+ servers: (values.server as string[] | undefined) ?? [],
45
+ clients: (values.client as string[] | undefined) ?? [],
46
+ noSkills: !!values["no-skills"],
47
+ wireRoot: !values["no-wire-root"],
48
+ docsOnly: !!values["docs-only"],
34
49
  };
35
50
  }
36
51
 
@@ -45,6 +60,9 @@ export interface GenFlags {
45
60
  * (existing content becomes the canonical baseline). "fresh" → overwrite
46
61
  * and re-baseline. */
47
62
  baseline: "default" | "fresh";
63
+ /** ADR-0021 D3 — print the stable-name generator registry and exit without
64
+ * running codegen. */
65
+ list: boolean;
48
66
  }
49
67
 
50
68
  export function parseGenArgs(argv: string[]): GenFlags {
@@ -53,6 +71,7 @@ export function parseGenArgs(argv: string[]): GenFlags {
53
71
  options: {
54
72
  "dry-run": { type: "boolean", default: false },
55
73
  "baseline": { type: "string" },
74
+ "list": { type: "boolean", default: false },
56
75
  },
57
76
  strict: true,
58
77
  allowPositionals: true,
@@ -67,6 +86,7 @@ export function parseGenArgs(argv: string[]): GenFlags {
67
86
  dryRun: !!values["dry-run"],
68
87
  entities: positionals,
69
88
  baseline: (baselineRaw as "default" | "fresh" | undefined) ?? "default",
89
+ list: !!values.list,
70
90
  };
71
91
  }
72
92
 
@@ -124,6 +144,16 @@ export interface VerifyFlags {
124
144
  allow: AllowToken[];
125
145
  /** Skip the schema-drift gate even when --db is present. */
126
146
  skipSchema: boolean;
147
+ // ADR-0021 D2 — explicit verify subverbs. Each selects one drift mode; any
148
+ // combination may be passed and the exit code aggregates (non-zero on any
149
+ // drift). The boolean flags record which modes were explicitly requested; the
150
+ // command layer applies the bare-verify default (= --templates) when none are.
151
+ /** Run the template/prompt {{field}}↔payload drift gate. */
152
+ templates: boolean;
153
+ /** Run the codegen-drift gate (regenerate-to-temp and diff committed output). */
154
+ codegen: boolean;
155
+ /** Whether ANY explicit subverb flag (--templates/--db/--codegen) was passed. */
156
+ anyExplicit: boolean;
127
157
  }
128
158
 
129
159
  export function parseVerifyArgs(argv: string[]): VerifyFlags {
@@ -135,6 +165,8 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
135
165
  dialect: { type: "string" },
136
166
  allow: { type: "string" },
137
167
  "skip-schema": { type: "boolean", default: false },
168
+ templates: { type: "boolean", default: false },
169
+ codegen: { type: "boolean", default: false },
138
170
  },
139
171
  strict: true,
140
172
  allowPositionals: false,
@@ -157,12 +189,21 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
157
189
  }
158
190
  }
159
191
 
192
+ const templates = !!values.templates;
193
+ const codegen = !!values.codegen;
194
+ // --db is itself an explicit subverb selector: passing a connection URL means
195
+ // "run the schema-drift mode". So "any explicit subverb" is templates|codegen|db.
196
+ const anyExplicit = templates || codegen || values.db !== undefined;
197
+
160
198
  return {
161
199
  prompts: values.prompts,
162
200
  db: values.db as string | undefined,
163
201
  dialect: dialect as Dialect | undefined,
164
202
  allow: allowTokens as AllowToken[],
165
203
  skipSchema: !!values["skip-schema"],
204
+ templates,
205
+ codegen,
206
+ anyExplicit,
166
207
  };
167
208
  }
168
209