@metaobjectsdev/cli 1.0.13 → 1.1.1-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 (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -0,0 +1,52 @@
1
+ import { parseArgs, type ParseArgsConfig } from "node:util";
2
+
3
+ /**
4
+ * The flags every command accepts beyond its own table. `--cwd` and `--format` are
5
+ * stripped by the dispatcher before a command parses anything, and `--help` is
6
+ * answered there too, so a command's own table never lists them — but an agent told
7
+ * "these are the valid flags" must not then be refused for using one of them.
8
+ */
9
+ export const GLOBAL_FLAGS: readonly string[] = ["--help", "--cwd", "--format"];
10
+
11
+ /**
12
+ * The one wording of an unknown-flag refusal, for every command.
13
+ *
14
+ * It names the command and lists that command's valid flags inline, so the refusal
15
+ * corrects itself in one step: the caller's deterministic next move after "unknown
16
+ * option" is `meta <command> --help`, and this folds that lookup into the error. It
17
+ * used to come in three spellings (`Unknown option '--x'. To specify a positional
18
+ * argument starting with a '-', place it at the end of the command after '--' …`,
19
+ * `unknown flag: --x`, `unknown option: --x`), the first being Node's own parser text,
20
+ * and none of them said which flags would have worked.
21
+ */
22
+ export function unknownFlagMessage(command: string, flag: string, valid: readonly string[]): string {
23
+ return `unknown flag ${flag} for \`meta ${command}\`. Valid flags: ${valid.join(", ")} ` +
24
+ `(also accepted everywhere: ${GLOBAL_FLAGS.join(", ")})`;
25
+ }
26
+
27
+ /** The `--long` (and `-s`) spellings an option table accepts, in table order. */
28
+ export function flagNames(options: ParseArgsConfig["options"]): string[] {
29
+ const names: string[] = [];
30
+ for (const [long, opt] of Object.entries(options ?? {})) {
31
+ if (long === "help") continue; // a global flag — listed once, in GLOBAL_FLAGS
32
+ names.push(opt.short !== undefined ? `--${long}, -${opt.short}` : `--${long}`);
33
+ }
34
+ return names;
35
+ }
36
+
37
+ /**
38
+ * `node:util` `parseArgs` with the unknown-flag refusal translated into
39
+ * {@link unknownFlagMessage}. Every other parse error passes through unchanged.
40
+ */
41
+ export function parseCommandArgs<T extends ParseArgsConfig>(
42
+ command: string,
43
+ config: T,
44
+ ): ReturnType<typeof parseArgs<T>> {
45
+ try {
46
+ return parseArgs(config);
47
+ } catch (err) {
48
+ if ((err as { code?: unknown }).code !== "ERR_PARSE_ARGS_UNKNOWN_OPTION") throw err;
49
+ const flag = /Unknown option '([^']+)'/.exec((err as Error).message)?.[1] ?? "(unrecognized)";
50
+ throw new Error(unknownFlagMessage(command, flag, flagNames(config.options)));
51
+ }
52
+ }
@@ -0,0 +1,526 @@
1
+ // ADR-0034 Amendment 8 — ejecting a generator hands over ALL of the code that writes its output.
2
+ //
3
+ // A reference generator (`src/reference/<name>.ts`) is the part an adopter decides things in:
4
+ // which nodes get a file, where it lands, in what order. The text of each file is written by
5
+ // the template modules it calls (`src/templates/*.ts`, `src/cube/*`, `src/generators/*`, in the
6
+ // three codegen packages), and those used to stay in the package: an owned `entity` was 223
7
+ // lines over 3,484 packaged ones, and a defect in what it emitted still waited on an upstream
8
+ // release. So eject also copies the TRANSITIVE CLOSURE of those modules into
9
+ // `codegen/templates/`, the way it copies the HTTP adapter into `codegen/runtime/`
10
+ // (runtime-eject.ts), and the owned generator imports them from there.
11
+ //
12
+ // What stays packaged is the ENGINE (`ENGINE_MODULES`): the generator base, the model walks,
13
+ // and the naming and type-mapping tables. A copied module reaches those through the engine
14
+ // entry, `@metaobjectsdev/codegen-ts/engine` (a framework package's own engine module through
15
+ // that package's root). That is the one edit a copy makes: every module specifier is rewritten, and
16
+ // nothing else in the file is touched, so "what did upstream change?" is a diff of two files
17
+ // that differ in their import lines only.
18
+ //
19
+ // ./names.js, ../column-mapper.js an ENGINE module -> "@metaobjectsdev/codegen-ts/engine"
20
+ // ./zod-validators.js, ../cube/x.js an EMIT module -> the copy beside it
21
+ // @metaobjectsdev/codegen-ts/templates/x another package's emit -> the copy beside it
22
+ // ts-poet the package's own Code -> "@metaobjectsdev/codegen-ts/engine"
23
+ //
24
+ // The last row is the 0.21.6 split-tree defect: `Code` objects are recognised by `instanceof`,
25
+ // so a copy must build them with the same ts-poet the engine uses.
26
+ //
27
+ // Imports are found by scanning the leading import block of each module — the only place these
28
+ // files declare a module specifier. A scanner and not a regex over the whole file because the
29
+ // template modules hold text that LOOKS like an import inside their template literals. The
30
+ // scanner is held to the TypeScript compiler's own reading of every closure module in
31
+ // test/template-eject.test.ts.
32
+
33
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
34
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
35
+ import { dirname, join, posix, relative, sep } from "node:path";
36
+ import * as coreTpl from "@metaobjectsdev/codegen-ts";
37
+ import { ENGINE_MODULES } from "@metaobjectsdev/codegen-ts/engine";
38
+ import * as reactTpl from "@metaobjectsdev/codegen-ts-react";
39
+ import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
40
+ import { compareOwnedCopy, type OwnedComparison } from "./owned-copy.js";
41
+
42
+ /** A codegen package's shipped source: `<root>/src`, with its `reference/` directory inside. */
43
+ export interface PackageSource {
44
+ /** `@metaobjectsdev/codegen-ts`, `@metaobjectsdev/codegen-ts-react`, … */
45
+ name: string;
46
+ /** `<package root>/src` — what the copies are taken from. */
47
+ srcRoot: string;
48
+ }
49
+
50
+ const SCOPE = "@metaobjectsdev/";
51
+ /** Where the copied template modules land, relative to the project root. */
52
+ export const OWNED_TEMPLATES_DIR = "codegen/templates";
53
+ const OWNED_GENERATORS_DIR = "codegen/generators";
54
+ /** The package that holds the engine. */
55
+ const CORE_PACKAGE = `${SCOPE}codegen-ts`;
56
+ /** The entry an owned copy imports the core engine from, and the ts-poet it shares. */
57
+ const ENGINE_ENTRY = `${CORE_PACKAGE}/engine`;
58
+
59
+ /** `@metaobjectsdev/codegen-ts-react` → `codegen-ts-react`, the directory spelling `ENGINE_MODULES` uses. */
60
+ function shortName(pkg: string): string {
61
+ return pkg.startsWith(SCOPE) ? pkg.slice(SCOPE.length) : pkg;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // reading a module's import block
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /** One module specifier in a source file: the text between the quotes, and where it sits. */
69
+ export interface ImportRef {
70
+ spec: string;
71
+ /** Offsets of the specifier text, excluding the quotes. */
72
+ start: number;
73
+ end: number;
74
+ }
75
+
76
+ const IDENT = /[A-Za-z0-9_$]/;
77
+
78
+ /**
79
+ * Every module specifier in the leading import block of `source`: `import … from "x"`,
80
+ * `import "x"` and `export … from "x"`, up to the first statement that is none of those.
81
+ *
82
+ * Within that block there are only comments, string literals, braces and identifiers, which
83
+ * is why a scanner this small is sound: no template literal and no regular expression can
84
+ * occur before the first real statement.
85
+ */
86
+ export function importRefs(source: string): ImportRef[] {
87
+ const refs: ImportRef[] = [];
88
+ const n = source.length;
89
+ let i = 0;
90
+
91
+ const skipTrivia = (): void => {
92
+ for (;;) {
93
+ const c = source[i];
94
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") i++;
95
+ else if (c === "/" && source[i + 1] === "/") {
96
+ while (i < n && source[i] !== "\n") i++;
97
+ } else if (c === "/" && source[i + 1] === "*") {
98
+ const close = source.indexOf("*/", i + 2);
99
+ i = close === -1 ? n : close + 2;
100
+ } else return;
101
+ }
102
+ };
103
+ const word = (): string => {
104
+ const from = i;
105
+ while (i < n && IDENT.test(source[i]!)) i++;
106
+ return source.slice(from, i);
107
+ };
108
+ const peekWord = (): string => {
109
+ const save = i;
110
+ const w = word();
111
+ i = save;
112
+ return w;
113
+ };
114
+ /** Read a quoted string at `i`; record it as a specifier. */
115
+ const specifier = (): void => {
116
+ const quote = source[i]!;
117
+ const close = source.indexOf(quote, i + 1);
118
+ if (close === -1) throw new Error("unterminated module specifier");
119
+ refs.push({ spec: source.slice(i + 1, close), start: i + 1, end: close });
120
+ i = close + 1;
121
+ };
122
+ const endStatement = (): void => {
123
+ skipTrivia();
124
+ if (source[i] === ";") i++;
125
+ };
126
+
127
+ /**
128
+ * After `import` / `export`: skip the clause to its `from` and read the specifier.
129
+ * Returns false when the statement has none (`export const …`, `export { a };`).
130
+ */
131
+ const clause = (): boolean => {
132
+ let depth = 0;
133
+ for (;;) {
134
+ skipTrivia();
135
+ if (i >= n) return false;
136
+ const c = source[i]!;
137
+ if (c === '"' || c === "'") {
138
+ specifier(); // `import "x"` — a bare side-effect import
139
+ return true;
140
+ }
141
+ if (c === "{") { depth++; i++; continue; }
142
+ if (c === "}") { depth--; i++; continue; }
143
+ if (c === "," || c === "*" || c === "=") { i++; continue; }
144
+ if (c === ";") return false;
145
+ if (IDENT.test(c)) {
146
+ const w = word();
147
+ if (w === "from" && depth === 0) {
148
+ skipTrivia();
149
+ if (source[i] === '"' || source[i] === "'") { specifier(); return true; }
150
+ return false;
151
+ }
152
+ continue;
153
+ }
154
+ return false;
155
+ }
156
+ };
157
+
158
+ for (;;) {
159
+ skipTrivia();
160
+ const w = peekWord();
161
+ if (w === "import") {
162
+ i += w.length;
163
+ // `import("x")` and `import.meta` are expressions, which is the end of the block.
164
+ skipTrivia();
165
+ if (source[i] === "(" || source[i] === ".") return refs;
166
+ if (!clause()) return refs;
167
+ endStatement();
168
+ } else if (w === "export") {
169
+ i += w.length;
170
+ skipTrivia();
171
+ const next = peekWord();
172
+ if (next === "type") i += next.length;
173
+ skipTrivia();
174
+ if (source[i] !== "*" && source[i] !== "{") return refs;
175
+ const save = refs.length;
176
+ if (!clause() || refs.length === save) return refs;
177
+ endStatement();
178
+ } else {
179
+ return refs;
180
+ }
181
+ }
182
+ }
183
+
184
+ /** `source` with each specifier in `refs` replaced by `replace(spec)` — nothing else is touched. */
185
+ function rewriteImports(source: string, refs: readonly ImportRef[], replace: (spec: string) => string): string {
186
+ let out = source;
187
+ for (const r of [...refs].sort((a, b) => b.start - a.start)) {
188
+ out = out.slice(0, r.start) + replace(r.spec) + out.slice(r.end);
189
+ }
190
+ return out;
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // the closure
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /** One module of a codegen package, by package and path under its `src/`. */
198
+ interface SourceModule {
199
+ pkg: PackageSource;
200
+ /** POSIX path under `src/`: `templates/entity-file.ts`. */
201
+ rel: string;
202
+ }
203
+
204
+ /** The spelling `ENGINE_MODULES` uses: `codegen-ts/src/templates/find-inbound.ts`. */
205
+ function keyOf(m: SourceModule): string {
206
+ return `${shortName(m.pkg.name)}/src/${m.rel}`;
207
+ }
208
+
209
+ const ENGINE: ReadonlySet<string> = new Set(ENGINE_MODULES);
210
+
211
+ /** Where a template module's copy lands: `templates/x.ts` → `codegen/templates/x.ts`, `cube/y.ts` → `codegen/templates/cube/y.ts`. */
212
+ function destinationOf(m: SourceModule): string {
213
+ const rel = m.rel.startsWith("templates/") ? m.rel.slice("templates/".length) : m.rel;
214
+ return `${OWNED_TEMPLATES_DIR}/${rel}`;
215
+ }
216
+
217
+ function sourceOf(m: SourceModule): string {
218
+ return readFileSync(posix.join(m.pkg.srcRoot.split("\\").join("/"), m.rel), "utf8");
219
+ }
220
+
221
+ /** The module `spec` names, imported from `from`, or undefined when it is not one of ours. */
222
+ function resolveModule(packages: readonly PackageSource[], from: SourceModule, spec: string): SourceModule | undefined {
223
+ const find = (pkg: PackageSource, rel: string): SourceModule => {
224
+ for (const candidate of [rel.replace(/\.js$/, ".ts"), `${rel}.ts`, `${rel}/index.ts`]) {
225
+ if (existsSync(posix.join(pkg.srcRoot.split("\\").join("/"), candidate))) return { pkg, rel: candidate };
226
+ }
227
+ throw new Error(
228
+ `${keyOf(from)} imports "${spec}", which is not a module of ${pkg.name}. ` +
229
+ "Only TypeScript modules can be handed over with a generator.",
230
+ );
231
+ };
232
+ if (spec.startsWith("./") || spec.startsWith("../")) {
233
+ return find(from.pkg, posix.normalize(posix.join(posix.dirname(from.rel), spec)));
234
+ }
235
+ // `@metaobjectsdev/codegen-ts/templates/x` — another codegen package's emit module, by path.
236
+ const sub = /^(@metaobjectsdev\/[\w-]+)\/(templates\/[\w-]+)$/.exec(spec);
237
+ if (sub !== null) {
238
+ const pkg = packages.find((p) => p.name === sub[1]);
239
+ if (pkg !== undefined) return find(pkg, `${sub[2]}.ts`);
240
+ }
241
+ return undefined;
242
+ }
243
+
244
+ /** One file an eject writes. */
245
+ export interface OwnedFile {
246
+ /** Project-relative POSIX path. */
247
+ path: string;
248
+ /** What is written: the package's source with its module specifiers rewritten. */
249
+ content: string;
250
+ /** The packaged module it comes from, in the manifest's spelling. */
251
+ origin: string;
252
+ }
253
+
254
+ export interface TemplateClosure {
255
+ /** The owned generator itself. */
256
+ generator: OwnedFile;
257
+ /** The template modules it calls, transitively, sorted by path. */
258
+ templates: OwnedFile[];
259
+ /** Every package the written files import — what the adopter's project must be able to resolve. */
260
+ packages: string[];
261
+ }
262
+
263
+ /**
264
+ * The owned form of one reference generator: the generator file and every template module it
265
+ * reaches, each with its module specifiers pointed at the project.
266
+ *
267
+ * `pkg` is the package the reference ships in; `packages` is every codegen package, since a
268
+ * client-tier reference reaches template modules in the core one.
269
+ */
270
+ export function templateClosure(
271
+ packages: readonly PackageSource[],
272
+ pkg: PackageSource,
273
+ name: string,
274
+ /** The modules that stay packaged. The tests hand in a smaller set than the shipped one. */
275
+ engine: ReadonlySet<string> = ENGINE,
276
+ ): TemplateClosure {
277
+ const reference: SourceModule = { pkg, rel: `reference/${name}.ts` };
278
+ const destination = (m: SourceModule): string =>
279
+ m.rel === reference.rel && m.pkg === reference.pkg ? `${OWNED_GENERATORS_DIR}/${name}.ts` : destinationOf(m);
280
+
281
+ // Breadth-first over the specifiers: an engine module ends a path, anything else is copied.
282
+ const members = new Map<string, SourceModule>([[keyOf(reference), reference]]);
283
+ const queue = [reference];
284
+ while (queue.length > 0) {
285
+ const module = queue.shift()!;
286
+ for (const ref of importRefs(sourceOf(module))) {
287
+ const target = resolveModule(packages, module, ref.spec);
288
+ if (target === undefined || engine.has(keyOf(target)) || members.has(keyOf(target))) continue;
289
+ members.set(keyOf(target), target);
290
+ queue.push(target);
291
+ }
292
+ }
293
+
294
+ // Two packaged modules must not land on one path; the layout drops a directory level.
295
+ const owners = new Map<string, string>();
296
+ for (const [key, m] of members) {
297
+ const dest = destination(m);
298
+ const other = owners.get(dest);
299
+ if (other !== undefined) throw new Error(`${other} and ${key} would both be copied to ${dest}.`);
300
+ owners.set(dest, key);
301
+ }
302
+
303
+ const imported = new Set<string>();
304
+ const render = (m: SourceModule): OwnedFile => {
305
+ const source = sourceOf(m);
306
+ const here = destination(m);
307
+ const content = rewriteImports(source, importRefs(source), (spec) => {
308
+ const target = resolveModule(packages, m, spec);
309
+ let next = spec;
310
+ if (target !== undefined) {
311
+ next = engine.has(keyOf(target))
312
+ ? engineEntryOf(target.pkg.name)
313
+ : relativeSpecifier(here, destination(target));
314
+ } else if (spec === "ts-poet") {
315
+ next = ENGINE_ENTRY;
316
+ }
317
+ if (!next.startsWith(".") && !next.startsWith("node:")) imported.add(packageOfSpecifier(next));
318
+ return next;
319
+ });
320
+ return { path: here, content, origin: keyOf(m) };
321
+ };
322
+
323
+ const rendered = [...members.values()].map(render);
324
+ const generator = rendered.find((f) => f.path === `${OWNED_GENERATORS_DIR}/${name}.ts`)!;
325
+ const templates = rendered.filter((f) => f !== generator).sort((a, b) => a.path.localeCompare(b.path));
326
+ return { generator, templates, packages: [...imported].sort() };
327
+ }
328
+
329
+ /** Where a copy imports an engine module of `pkg` from: the core engine entry, or a framework package's root. */
330
+ function engineEntryOf(pkg: string): string {
331
+ return pkg === CORE_PACKAGE ? ENGINE_ENTRY : pkg;
332
+ }
333
+
334
+ /** `./x.js` / `../y/z.js` from the file at `from` to the one at `to`, both project-relative. */
335
+ function relativeSpecifier(from: string, to: string): string {
336
+ const rel = posix.relative(posix.dirname(from), to).replace(/\.ts$/, ".js");
337
+ return rel.startsWith(".") ? rel : `./${rel}`;
338
+ }
339
+
340
+ /** `@scope/name/sub` → `@scope/name`; `name/sub` → `name`. */
341
+ function packageOfSpecifier(spec: string): string {
342
+ const parts = spec.split("/");
343
+ return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]!;
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // handing the copies over
348
+ // ---------------------------------------------------------------------------
349
+
350
+ export type TemplateFileStatus = "created" | "preserved" | "replaced";
351
+
352
+ export interface TemplateFileResult {
353
+ /** Project-relative path of the copy. */
354
+ path: string;
355
+ status: TemplateFileStatus;
356
+ /** How the copy ALREADY on disk compared to what the package would write. Absent when there was none. */
357
+ comparison?: OwnedComparison | undefined;
358
+ }
359
+
360
+ export interface TemplateEjectResult {
361
+ files: TemplateFileResult[];
362
+ }
363
+
364
+ /**
365
+ * Write the template modules of `closures` into `<cwd>/codegen/templates/`. Never overwrites a
366
+ * file without `force`, the rule the generator copy keeps: two generators that call the same
367
+ * module share one copy, and re-running eject fills in what is missing without touching what
368
+ * the adopter has changed.
369
+ */
370
+ export async function ejectTemplates(
371
+ cwd: string,
372
+ closures: readonly TemplateClosure[],
373
+ force: boolean,
374
+ ): Promise<TemplateEjectResult> {
375
+ const wanted = new Map<string, OwnedFile>();
376
+ for (const closure of closures) {
377
+ for (const file of closure.templates) {
378
+ const seen = wanted.get(file.path);
379
+ // One packaged module, one copy: the rewrite depends on the module alone.
380
+ if (seen !== undefined && seen.content !== file.content) {
381
+ throw new Error(`${file.path} would be written with two different contents (${seen.origin}, ${file.origin}).`);
382
+ }
383
+ wanted.set(file.path, file);
384
+ }
385
+ }
386
+ const files: TemplateFileResult[] = [];
387
+ for (const file of [...wanted.values()].sort((a, b) => a.path.localeCompare(b.path))) {
388
+ const abs = join(cwd, file.path);
389
+ const existing = existsSync(abs) ? await readFile(abs, "utf8") : undefined;
390
+ const comparison = existing === undefined ? undefined : await compareOwnedCopy(existing, file.content);
391
+ if (existing !== undefined && !force) {
392
+ files.push({ path: file.path, status: "preserved", comparison });
393
+ continue;
394
+ }
395
+ await mkdir(dirname(abs), { recursive: true });
396
+ await writeFile(abs, file.content, "utf8");
397
+ files.push({ path: file.path, status: existing === undefined ? "created" : "replaced", comparison });
398
+ }
399
+ return { files };
400
+ }
401
+
402
+ /**
403
+ * True when a kept generator file calls none of the template modules its reference now calls.
404
+ * Such a copy predates the hand-over (1.1): it still imports the emit code from the package,
405
+ * so copying the modules beside it would be dead code. Re-ejecting it with `--force` is the
406
+ * way to take the new shape.
407
+ */
408
+ export function predatesTemplateHandOver(existing: string, closure: TemplateClosure): boolean {
409
+ if (closure.templates.length === 0) return false;
410
+ const called = importRefs(closure.generator.content)
411
+ .map((r) => r.spec)
412
+ .filter((spec) => spec.startsWith("."));
413
+ return called.length > 0 && !called.some((spec) => existing.includes(`"${spec}"`));
414
+ }
415
+
416
+ /** One owned template file, compared to the package it came from. */
417
+ export interface TemplateCopyRow {
418
+ path: string;
419
+ verdict: OwnedComparison["verdict"] | "not-in-package";
420
+ localOnly: number;
421
+ referenceOnly: number;
422
+ /** The packaged module it was copied from, when there is one. */
423
+ origin?: string | undefined;
424
+ }
425
+
426
+ function listTs(dir: string): string[] {
427
+ const out: string[] = [];
428
+ for (const entry of readdirSync(dir)) {
429
+ const abs = join(dir, entry);
430
+ if (statSync(abs).isDirectory()) out.push(...listTs(abs));
431
+ else if (entry.endsWith(".ts")) out.push(abs);
432
+ }
433
+ return out;
434
+ }
435
+
436
+ /**
437
+ * Every file under `<cwd>/codegen/templates/`, compared to what the installed package would
438
+ * write there today. Empty when the project owns no template copy. `not-in-package` is a file
439
+ * the package would not write: your own addition, or one upstream deleted or renamed.
440
+ */
441
+ export async function templateCopyStatus(cwd: string, upstream: ReadonlyMap<string, OwnedFile>): Promise<TemplateCopyRow[]> {
442
+ const root = join(cwd, OWNED_TEMPLATES_DIR);
443
+ if (!existsSync(root)) return [];
444
+ const rows: TemplateCopyRow[] = [];
445
+ for (const abs of listTs(root).sort()) {
446
+ const path = relative(cwd, abs).split(sep).join("/");
447
+ const packaged = upstream.get(path);
448
+ if (packaged === undefined) {
449
+ rows.push({ path, verdict: "not-in-package", localOnly: 0, referenceOnly: 0 });
450
+ continue;
451
+ }
452
+ const cmp = await compareOwnedCopy(readFileSync(abs, "utf8"), packaged.content);
453
+ rows.push({ path, verdict: cmp.verdict, localOnly: cmp.localOnly, referenceOnly: cmp.referenceOnly, origin: packaged.origin });
454
+ }
455
+ return rows;
456
+ }
457
+
458
+ /** The command that shows exactly how one owned template file differs from the installed package's. */
459
+ export function templateDiffCommand(row: TemplateCopyRow): string | undefined {
460
+ if (row.origin === undefined) return undefined;
461
+ const [short, ...rest] = row.origin.split("/");
462
+ return `diff -u node_modules/${SCOPE}${short}/${rest.join("/")} ${row.path}`;
463
+ }
464
+
465
+ // ---------------------------------------------------------------------------
466
+ // the installed packages
467
+ // ---------------------------------------------------------------------------
468
+
469
+ interface ReferenceSource {
470
+ /** The codegen package whose `src/reference/` holds the generator. */
471
+ pkg: PackageSource;
472
+ names: readonly string[];
473
+ }
474
+
475
+ let installed: ReferenceSource[] | undefined;
476
+
477
+ /** The three codegen packages, as installed: their shipped `src/` and the generators each ships a reference for. */
478
+ function installedSources(): ReferenceSource[] {
479
+ installed ??= [
480
+ [`${SCOPE}codegen-ts`, coreTpl],
481
+ [`${SCOPE}codegen-ts-react`, reactTpl],
482
+ [`${SCOPE}codegen-ts-tanstack`, tanstackTpl],
483
+ ].map(([name, tpl]) => {
484
+ const t = tpl as typeof coreTpl;
485
+ return { pkg: { name: name as string, srcRoot: dirname(t.resolveReferenceRoot()) }, names: t.REFERENCE_GENERATOR_NAMES };
486
+ });
487
+ return installed;
488
+ }
489
+
490
+ /** The three codegen packages' shipped source — where the template modules a reference calls live. */
491
+ export function codegenPackages(): PackageSource[] {
492
+ return installedSources().map((s) => s.pkg);
493
+ }
494
+
495
+ const closures = new Map<string, TemplateClosure>();
496
+
497
+ /**
498
+ * The owned form of one reference generator, from the installed packages: the generator file
499
+ * and the template modules it calls, with their module specifiers pointed at the project.
500
+ * Memoized — the installed source does not change under a running command.
501
+ */
502
+ export function closureOf(name: string): TemplateClosure {
503
+ let closure = closures.get(name);
504
+ if (closure === undefined) {
505
+ const sources = installedSources();
506
+ const source = sources.find((s) => s.names.includes(name));
507
+ if (source === undefined) throw new Error(`no reference generator named "${name}".`);
508
+ closure = templateClosure(sources.map((s) => s.pkg), source.pkg, name);
509
+ closures.set(name, closure);
510
+ }
511
+ return closure;
512
+ }
513
+
514
+ /** Every ejectable name, across the three packages, in registry order. */
515
+ export function referenceNames(): string[] {
516
+ return installedSources().flatMap((s) => s.names);
517
+ }
518
+
519
+ /** Every file `meta eject` would write for a template module, across every ejectable generator. */
520
+ export function packagedTemplates(): Map<string, OwnedFile> {
521
+ const all = new Map<string, OwnedFile>();
522
+ for (const name of referenceNames()) {
523
+ for (const file of closureOf(name).templates) all.set(file.path, file);
524
+ }
525
+ return all;
526
+ }
@@ -25,3 +25,21 @@ export function cliVersion(): string {
25
25
  }
26
26
  return "0.0.0";
27
27
  }
28
+
29
+ /**
30
+ * True when `version` carries a semver prerelease identifier (`-rc.4`, `-alpha.1`,
31
+ * `-beta`, `-SNAPSHOT`, …) — the same shape `tools/prerelease/detect-prerelease-pins.sh`'s
32
+ * `PRERELEASE_RE` matches. Used to warn when a pre-release CLI is about to stamp its own
33
+ * version into committed state (`.metaobjects/.agent-context.json`'s `generatedBy`),
34
+ * which that detector now also flags on the tree side.
35
+ */
36
+ export function isPrereleaseVersion(version: string): boolean {
37
+ return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/.test(version.trim());
38
+ }
39
+
40
+ /** The three spellings that print the bare version: `--version`, `-v` and `-V`. */
41
+ const VERSION_FLAGS: readonly string[] = ["--version", "-v", "-V"];
42
+
43
+ export function isVersionFlag(arg: string | undefined): boolean {
44
+ return arg !== undefined && VERSION_FLAGS.includes(arg);
45
+ }
@@ -1,42 +0,0 @@
1
- export interface DocsDriftResult {
2
- /** True when every page a fresh `meta docs` would write is committed and identical. */
3
- clean: boolean;
4
- /** Docs-dir-relative paths that differ — changed, missing, or (under `agent/`) committed
5
- * when a fresh run no longer emits them. Sorted. */
6
- driftedFiles: string[];
7
- /** Human-readable, one line per file. */
8
- lines: string[];
9
- /** The denominator both the passing and the failing report divide by: the pages a fresh
10
- * run produced, plus any committed page under `agent/` it no longer emits, MINUS the
11
- * git-ignored ones. A pure function of (fresh set, repository ignore rules) — never of
12
- * which files happen to be on this machine's disk. */
13
- checked: number;
14
- /** How many pages a fresh run emits that the project git-ignores, so did not check.
15
- * Reported on BOTH the passing and the failing line: a gate that quietly checked two
16
- * of 589 pages and said "no drift" would be indistinguishable from one that checked
17
- * everything. */
18
- ignored: number;
19
- /** Why the ignore rules could not be consulted, when they could not be. Undefined means
20
- * they were. The gate then falls back to checking EVERY page a fresh run emits — the
21
- * pre-existing behaviour — and says so rather than degrading silently. */
22
- ignoreReason?: string | undefined;
23
- /** Set when the gate could not run at all. */
24
- error?: string;
25
- }
26
- export interface ComputeDocsDriftArgs {
27
- /** Absolute project root — what `meta docs` would be pointed at. */
28
- projectRoot: string;
29
- /** The committed docs directory, already resolved (the project's `docs.outDir`). */
30
- docsDir: string;
31
- /** Process cwd to hand the docs command, for parity with a direct invocation. */
32
- cwd: string;
33
- }
34
- /**
35
- * Regenerate the docs into a temp tree and diff it against the committed one.
36
- *
37
- * A non-zero exit from the docs command is returned as `error` rather than as drift: a run
38
- * that could not produce the pages has not shown that the committed ones are wrong, and
39
- * reporting it as drift would send a reader to edit files that are probably fine.
40
- */
41
- export declare function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<DocsDriftResult>;
42
- //# sourceMappingURL=docs-drift.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"docs-drift.d.ts","sourceRoot":"","sources":["../../../src/lib/docs-drift.ts"],"names":[],"mappings":"AAwDA,MAAM,WAAW,eAAe;IAC9B,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;IACf;yDACqD;IACrD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yCAAyC;IACzC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;2DAGuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB;;;sBAGkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;;+EAE2E;IAC3E,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4DD,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2J3F"}