@metaobjectsdev/cli 0.9.0-rc.1 → 0.10.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 (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
@@ -0,0 +1,173 @@
1
+ // `meta verify --codegen` — the codegen-drift gate (ADR-0021 D2).
2
+ //
3
+ // Regenerates the configured codegen into a throwaway temp directory and DIFFs
4
+ // the freshly-generated file tree against the committed output (the config's
5
+ // outDir / per-target outDirs). Any difference — a file present in one tree but
6
+ // not the other, or differing content — is drift: either "metadata changed but
7
+ // `meta gen` wasn't re-run" or "a generated file was hand-edited". Reuses the
8
+ // exact same `runGen` pipeline `meta gen` uses, so the comparison is faithful.
9
+ //
10
+ // Faithfulness note: generated content can embed import paths computed RELATIVE
11
+ // to a target's outDir (and between targets). To keep the regen byte-identical
12
+ // to the committed output, each outDir is remapped into the temp tree at the
13
+ // SAME path it has relative to the project root — preserving the inter-target
14
+ // layout. `importBase` (a stable string, not a path) is untouched.
15
+
16
+ import {
17
+ mkdtempSync,
18
+ rmSync,
19
+ existsSync,
20
+ readFileSync,
21
+ readdirSync,
22
+ statSync,
23
+ } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join, relative, resolve, isAbsolute } from "node:path";
26
+ import { runGen } from "@metaobjectsdev/codegen-ts";
27
+ import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
28
+ import type { MetaData } from "@metaobjectsdev/metadata";
29
+
30
+ /** Per-target config as carried on MetaobjectsGenConfig (TargetConfig isn't
31
+ * re-exported from the package index, so derive it from the config type). */
32
+ type TargetConfig = NonNullable<MetaobjectsGenConfig["targets"]>[string];
33
+
34
+ export interface CodegenDriftResult {
35
+ /** True when the committed output matches a fresh regen exactly. */
36
+ clean: boolean;
37
+ /** Project-relative paths that differ (changed / missing / extra), sorted. */
38
+ driftedFiles: string[];
39
+ /** Human-readable, one-line-per-file drift summary. */
40
+ lines: string[];
41
+ /** Set when the gate could not run (e.g. no outDir to compare against). */
42
+ error?: string;
43
+ }
44
+
45
+ /** Collect the committed outDirs declared by the config (default + per-target). */
46
+ function committedOutDirs(config: MetaobjectsGenConfig): string[] {
47
+ const dirs = new Set<string>();
48
+ if (typeof config.outDir === "string" && config.outDir.length > 0) {
49
+ dirs.add(resolve(config.outDir));
50
+ }
51
+ for (const t of Object.values(config.targets ?? {})) {
52
+ if (t.outDir && t.outDir.length > 0) dirs.add(resolve(t.outDir));
53
+ }
54
+ return [...dirs];
55
+ }
56
+
57
+ /** Recursively list files under `dir` as paths relative to `dir` (POSIX-ish). */
58
+ function listFiles(dir: string): string[] {
59
+ if (!existsSync(dir)) return [];
60
+ const out: string[] = [];
61
+ const walk = (d: string): void => {
62
+ for (const entry of readdirSync(d)) {
63
+ const full = join(d, entry);
64
+ if (statSync(full).isDirectory()) walk(full);
65
+ else out.push(relative(dir, full));
66
+ }
67
+ };
68
+ walk(dir);
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Run codegen into a temp tree and diff it against the committed output.
74
+ *
75
+ * @param config the loaded metaobjects config (provides outDir/targets).
76
+ * @param metadata the loaded MetaRoot (same object `meta gen` would use).
77
+ * @param projectRoot absolute project root (committed outDirs are keyed off it).
78
+ */
79
+ export async function computeCodegenDrift(
80
+ config: MetaobjectsGenConfig,
81
+ metadata: MetaData,
82
+ projectRoot: string,
83
+ ): Promise<CodegenDriftResult> {
84
+ const root = isAbsolute(projectRoot) ? projectRoot : resolve(projectRoot);
85
+
86
+ const committedDirs = committedOutDirs(config);
87
+ if (committedDirs.length === 0) {
88
+ return {
89
+ clean: false,
90
+ driftedFiles: [],
91
+ lines: [],
92
+ error:
93
+ "verify --codegen: no outDir configured — cannot locate the committed " +
94
+ "generated output to diff against. Set 'outDir' (and/or per-target " +
95
+ "outDir) in metaobjects.config.ts.",
96
+ };
97
+ }
98
+
99
+ // Build a temp tree that mirrors each outDir at its project-relative path, so
100
+ // relative import paths in generated content come out identical.
101
+ const tempRoot = mkdtempSync(join(tmpdir(), "meta-verify-codegen-"));
102
+ try {
103
+ const tempFor = (committed: string): string => {
104
+ const rel = relative(root, committed);
105
+ // If a committed outDir lives outside the project root, fall back to a
106
+ // flattened, collision-safe slot under the temp root. (Relative-import
107
+ // fidelity for out-of-tree targets isn't guaranteed, but the diff is
108
+ // still meaningful per-file.)
109
+ const safeRel = rel.startsWith("..") || isAbsolute(rel)
110
+ ? committed.replace(/[^A-Za-z0-9]+/g, "_")
111
+ : rel;
112
+ return join(tempRoot, safeRel);
113
+ };
114
+
115
+ // Remap the config so runGen writes into the temp mirror instead of the
116
+ // committed output. Default outDir + each named target's outDir are rewritten.
117
+ const remappedTargets: Record<string, TargetConfig> = {};
118
+ for (const [name, t] of Object.entries(config.targets ?? {})) {
119
+ remappedTargets[name] = { ...t, outDir: tempFor(resolve(t.outDir)) };
120
+ }
121
+ const tempConfig: MetaobjectsGenConfig = {
122
+ ...config,
123
+ outDir: tempFor(resolve(config.outDir)),
124
+ ...(config.targets !== undefined ? { targets: remappedTargets } : {}),
125
+ };
126
+
127
+ // Generate fresh into the temp tree. "overwrite" + "fresh" + a temp
128
+ // gen-state dir guarantees every file is written (no merge/skip), so the
129
+ // temp tree is the canonical "what gen would produce right now".
130
+ await runGen({
131
+ config: tempConfig,
132
+ metadata,
133
+ projectRoot: tempRoot,
134
+ genStateDir: join(tempRoot, ".gen-state"),
135
+ mergeStrategy: "overwrite",
136
+ baseline: "fresh",
137
+ });
138
+
139
+ // Diff each committed outDir against its temp mirror.
140
+ const driftedFiles = new Set<string>();
141
+ const lines: string[] = [];
142
+ for (const committed of committedDirs) {
143
+ const fresh = tempFor(committed);
144
+ const committedFiles = new Set(listFiles(committed));
145
+ const freshFiles = new Set(listFiles(fresh));
146
+ const all = new Set([...committedFiles, ...freshFiles]);
147
+ for (const rel of [...all].sort()) {
148
+ const relKey = relative(root, join(committed, rel));
149
+ const inCommitted = committedFiles.has(rel);
150
+ const inFresh = freshFiles.has(rel);
151
+ if (inCommitted && !inFresh) {
152
+ driftedFiles.add(relKey);
153
+ lines.push(`- ${relKey} (committed but regen would not emit it)`);
154
+ } else if (!inCommitted && inFresh) {
155
+ driftedFiles.add(relKey);
156
+ lines.push(`+ ${relKey} (regen would emit it; not committed — run 'meta gen')`);
157
+ } else {
158
+ const a = readFileSync(join(committed, rel), "utf8");
159
+ const b = readFileSync(join(fresh, rel), "utf8");
160
+ if (a !== b) {
161
+ driftedFiles.add(relKey);
162
+ lines.push(`~ ${relKey} (committed content differs from a fresh regen)`);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ const sorted = [...driftedFiles].sort();
169
+ return { clean: sorted.length === 0, driftedFiles: sorted, lines };
170
+ } finally {
171
+ rmSync(tempRoot, { recursive: true, force: true });
172
+ }
173
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ detectStack, makeStack,
5
+ type ServerLang, type ClientFramework, type Stack, type ProjectProbe,
6
+ SERVER_LANGS, CLIENT_FRAMEWORKS,
7
+ } from "@metaobjectsdev/sdk/agent-context";
8
+
9
+ function depNames(cwd: string): Set<string> {
10
+ const out = new Set<string>();
11
+ const pkgPath = join(cwd, "package.json");
12
+ if (existsSync(pkgPath)) {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, Record<string, string>>;
15
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
16
+ for (const name of Object.keys(pkg[key] ?? {})) out.add(name);
17
+ }
18
+ } catch { /* unreadable manifest — treat as no deps */ }
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function probe(cwd: string): ProjectProbe {
24
+ const deps = depNames(cwd);
25
+ const names = existsSync(cwd) ? readdirSync(cwd) : [];
26
+ return {
27
+ hasDep: (name) => deps.has(name),
28
+ hasFileMatching: (re) => names.some((n) => re.test(n)),
29
+ };
30
+ }
31
+
32
+ /** Resolve the stack: explicit --server/--client overrides take precedence; otherwise detect. */
33
+ export function resolveStack(cwd: string, overrides: { servers: string[]; clients: string[] }): Stack {
34
+ const validServers = SERVER_LANGS as readonly string[];
35
+ const validClients = CLIENT_FRAMEWORKS as readonly string[];
36
+ const oServers = overrides.servers.filter((s): s is ServerLang => validServers.includes(s));
37
+ const oClients = overrides.clients.filter((c): c is ClientFramework => validClients.includes(c));
38
+ if (oServers.length > 0 || oClients.length > 0) return makeStack(oServers, oClients);
39
+ const detected = detectStack(probe(cwd));
40
+ return makeStack(detected.servers, detected.clients);
41
+ }
@@ -32,6 +32,13 @@ const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
32
32
  dist: "node_modules/@metaobjectsdev/codegen-ts/dist/index.js",
33
33
  src: "node_modules/@metaobjectsdev/codegen-ts/src/index.ts",
34
34
  },
35
+ // Consumer configs that ship custom MetaDataTypeProviders import the type
36
+ // primitives (TypeId, MetaField, TYPE_* …) from here. Aliased to the CLI's
37
+ // own copy so the user's project needn't declare it as a direct dependency.
38
+ "@metaobjectsdev/metadata": {
39
+ dist: "node_modules/@metaobjectsdev/metadata/dist/index.js",
40
+ src: "node_modules/@metaobjectsdev/metadata/src/index.ts",
41
+ },
35
42
  "@metaobjectsdev/codegen-ts/generators": {
36
43
  dist: "node_modules/@metaobjectsdev/codegen-ts/dist/generators/index.js",
37
44
  src: "node_modules/@metaobjectsdev/codegen-ts/src/generators/index.ts",
@@ -94,6 +101,7 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
94
101
  interopDefault: true,
95
102
  alias: {
96
103
  "@metaobjectsdev/codegen-ts": resolveCliPkg("@metaobjectsdev/codegen-ts"),
104
+ "@metaobjectsdev/metadata": resolveCliPkg("@metaobjectsdev/metadata"),
97
105
  "@metaobjectsdev/codegen-ts/generators": resolveCliPkg("@metaobjectsdev/codegen-ts/generators"),
98
106
  "@metaobjectsdev/codegen-ts-react": resolveCliPkg("@metaobjectsdev/codegen-ts-react"),
99
107
  "@metaobjectsdev/codegen-ts-tanstack": resolveCliPkg("@metaobjectsdev/codegen-ts-tanstack"),
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /**
6
+ * The installed `@metaobjectsdev/cli` version, read from its own package.json so it
7
+ * never goes stale. Walks up from this module to the cli package root; returns
8
+ * "0.0.0" if not found.
9
+ */
10
+ export function cliVersion(): string {
11
+ let dir = dirname(fileURLToPath(import.meta.url));
12
+ for (let i = 0; i < 6; i++) {
13
+ const candidate = join(dir, "package.json");
14
+ if (existsSync(candidate)) {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(candidate, "utf8")) as { name?: string; version?: string };
17
+ if (pkg.name === "@metaobjectsdev/cli" && pkg.version) return pkg.version;
18
+ } catch {
19
+ // not our manifest / unreadable — keep walking up
20
+ }
21
+ }
22
+ const parent = dirname(dir);
23
+ if (parent === dir) break;
24
+ dir = parent;
25
+ }
26
+ return "0.0.0";
27
+ }