@metaobjectsdev/cli 0.23.2 → 0.24.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.
- package/README.md +78 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +148 -50
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +28 -6
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +61 -22
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +8 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +142 -52
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +9 -2
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +399 -73
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +40 -18
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts +2 -0
- package/dist/src/commands/upgrade.d.ts.map +1 -0
- package/dist/src/commands/upgrade.js +139 -0
- package/dist/src/commands/upgrade.js.map +1 -0
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +336 -48
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +46 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +4 -0
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/args.d.ts +16 -2
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -3
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +8 -1
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +9 -1
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +22 -37
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +48 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +80 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
- package/dist/src/lib/manifest-ignored-check.js +74 -0
- package/dist/src/lib/manifest-ignored-check.js.map +1 -0
- package/dist/src/lib/migrate-scope.d.ts +49 -0
- package/dist/src/lib/migrate-scope.d.ts.map +1 -0
- package/dist/src/lib/migrate-scope.js +92 -0
- package/dist/src/lib/migrate-scope.js.map +1 -0
- package/dist/src/lib/output.d.ts +1 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -3
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +10 -57
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +11 -11
- package/src/commands/docs.ts +158 -49
- package/src/commands/export.ts +30 -6
- package/src/commands/gen.ts +62 -22
- package/src/commands/init.ts +161 -58
- package/src/commands/migrate.ts +423 -75
- package/src/commands/prompt-snapshot.ts +43 -18
- package/src/commands/upgrade.ts +156 -0
- package/src/commands/verify.ts +373 -48
- package/src/index.ts +46 -15
- package/src/lib/allow.ts +4 -0
- package/src/lib/args.ts +35 -4
- package/src/lib/codegen-drift.ts +9 -0
- package/src/lib/detect-stack.ts +20 -33
- package/src/lib/load-metaobjects-config.ts +84 -2
- package/src/lib/manifest-ignored-check.ts +75 -0
- package/src/lib/migrate-scope.ts +102 -0
- package/src/lib/output.ts +8 -4
- package/src/lib/requirement-check.ts +10 -58
- package/dist/src/lib/verified-by-scan.d.ts +0 -20
- package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
- package/dist/src/lib/verified-by-scan.js +0 -344
- package/dist/src/lib/verified-by-scan.js.map +0 -1
- package/src/lib/verified-by-scan.ts +0 -375
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { log } from "./lib/log.js";
|
|
3
3
|
import { cliVersion } from "./lib/version.js";
|
|
4
4
|
import { resolveFormat, isValidFormat, VALID_FORMATS } from "./lib/format.js";
|
|
5
|
+
import { resolveCollection } from "@metaobjectsdev/sdk";
|
|
5
6
|
export { defineConfig } from "@metaobjectsdev/codegen-ts";
|
|
6
7
|
export type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
7
8
|
|
|
@@ -13,14 +14,16 @@ USAGE:
|
|
|
13
14
|
meta <command> [flags]
|
|
14
15
|
|
|
15
16
|
COMMANDS:
|
|
16
|
-
init Scaffold
|
|
17
|
+
init Scaffold a MetaObjects project in the current repo
|
|
17
18
|
init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
|
|
19
|
+
init --config-only Write only .metaobjects/config.json — for a Maven- or pip-rooted project
|
|
18
20
|
agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports
|
|
19
|
-
gen [<entity>...] Codegen TS targets from
|
|
21
|
+
gen [<entity>...] Codegen TS targets from your declared metadata
|
|
20
22
|
types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
|
|
21
23
|
export Flatten loaded metadata to one canonical JSON artifact
|
|
22
24
|
docs <metadata> --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
|
|
23
25
|
verify Drift gate — subverbs: --templates / --db / --codegen (bare = --templates)
|
|
26
|
+
upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
|
|
24
27
|
prompt-snapshot Snapshot rendered template.* output; --check gates drift
|
|
25
28
|
migrate Diff metadata vs live DB; emit migration SQL files
|
|
26
29
|
--version, -v Print version
|
|
@@ -39,7 +42,7 @@ EXPORT FLAGS:
|
|
|
39
42
|
--out <file> Write output to a file (default: stdout)
|
|
40
43
|
|
|
41
44
|
DOCS FLAGS:
|
|
42
|
-
<metadata> Project root
|
|
45
|
+
<metadata> Project root to resolve metadata from; passing it SCOPES the run (default: cwd)
|
|
43
46
|
--out <dir>, -o Output directory for the pages (default: ./docs)
|
|
44
47
|
--templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
|
|
45
48
|
--prompts <dir> Extra dir holding prompt .mustache sources for --site (e.g. data/templates/)
|
|
@@ -90,7 +93,7 @@ ship in later sub-projects. See https://metaobjects.com for docs.
|
|
|
90
93
|
|
|
91
94
|
/** Focused per-subcommand usage slices shown by `<cmd> --help`. */
|
|
92
95
|
const COMMAND_HELP: Record<string, string> = {
|
|
93
|
-
gen: `meta gen — codegen TS targets from
|
|
96
|
+
gen: `meta gen — codegen TS targets from your declared metadata
|
|
94
97
|
|
|
95
98
|
USAGE:
|
|
96
99
|
meta gen [<entity>...] [flags]
|
|
@@ -109,7 +112,7 @@ or META_NO_ANTIPATTERNS=1.
|
|
|
109
112
|
|
|
110
113
|
NOTE: outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts
|
|
111
114
|
`,
|
|
112
|
-
verify: `meta verify — drift gate (templates / DB schema / codegen)
|
|
115
|
+
verify: `meta verify — drift gate (templates / DB schema / codegen / migration replay)
|
|
113
116
|
|
|
114
117
|
USAGE:
|
|
115
118
|
meta verify [flags]
|
|
@@ -121,6 +124,17 @@ FLAGS:
|
|
|
121
124
|
--db <url> Schema drift — live DB URL enables the schema-drift gate.
|
|
122
125
|
Supports: file:, libsql:, postgres:, postgresql:
|
|
123
126
|
D1 has no URL — use --dialect d1 / --d1 <binding> instead.
|
|
127
|
+
--replay Migration-chain drift — replay the committed chain into an
|
|
128
|
+
EMPTY throwaway database and assert it applies. Needs no --db:
|
|
129
|
+
the engine is in-process (PGlite for postgres, a temp file for
|
|
130
|
+
sqlite) and provisions nothing. Dialect comes from --dialect,
|
|
131
|
+
else migrate.dialect. flyway and d1 are refused.
|
|
132
|
+
Postgres needs the optional peer '@electric-sql/pglite'.
|
|
133
|
+
--replay-snapshot ...and assert the replayed schema EQUALS the committed
|
|
134
|
+
snapshot — catches a hand-edited up.sql that still applies but
|
|
135
|
+
no longer builds the recorded schema. Does NOT apply to a
|
|
136
|
+
project adopted with 'migrate baseline --from-db' (its chain
|
|
137
|
+
does not build the schema); use --replay there.
|
|
124
138
|
--prompts <dir> Directory of provider-resolved template text (default: prompts)
|
|
125
139
|
--dialect sqlite|postgres|d1 Optional override (auto-detected from --db URL scheme)
|
|
126
140
|
--allow <csv> Accepted for parity with 'migrate'; does NOT affect the drift gate
|
|
@@ -152,19 +166,23 @@ USAGE:
|
|
|
152
166
|
meta docs [<metadata>] [flags]
|
|
153
167
|
|
|
154
168
|
FLAGS:
|
|
155
|
-
<metadata> Project root
|
|
169
|
+
<metadata> Project root to resolve metadata from. Passing it SCOPES the run to
|
|
170
|
+
that directory's own sources; no ancestor .metaobjects/config.json is
|
|
171
|
+
consulted. Omitted (default), the project is discovered by walking up.
|
|
156
172
|
--out <dir>, -o Output directory for the pages (default: ./docs)
|
|
157
173
|
--model Emit the markdown model surface (entity + template pages)
|
|
158
174
|
--api Emit the markdown api surface (generated SDK reference)
|
|
175
|
+
--requirements Emit the declared requirement ledger (requirements.md + .toon).
|
|
176
|
+
Emits nothing when the project declares no requirement.* node.
|
|
159
177
|
--metamodel Document the built-in metamodel vocabulary (no metadata needed)
|
|
160
178
|
--site Generate the browsable HTML documentation site (<out>/site/)
|
|
161
179
|
--scaffold-site Copy the site's templates + assets into codegen/docs-site/ to own (theme) them
|
|
162
180
|
--templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
|
|
163
181
|
--prompts <dir> Extra dir holding prompt .mustache sources (for --site) when they
|
|
164
|
-
live outside
|
|
182
|
+
live outside the metadata sources or templates/ (e.g. data/templates/)
|
|
165
183
|
--help, -h Print this help
|
|
166
184
|
`,
|
|
167
|
-
init: `meta init — scaffold
|
|
185
|
+
init: `meta init — scaffold a MetaObjects project in the current repo
|
|
168
186
|
|
|
169
187
|
USAGE:
|
|
170
188
|
meta init [flags]
|
|
@@ -176,6 +194,8 @@ FLAGS:
|
|
|
176
194
|
--print-only Print what would be written, don't write
|
|
177
195
|
--d1 Include D1 (Cloudflare) migration config
|
|
178
196
|
--no-wire-root Skip wiring root metaobjects.config.ts
|
|
197
|
+
--config-only Write only .metaobjects/config.json (no TS scaffold) — declares
|
|
198
|
+
metadata sources for the Node CLI from a Maven- or pip-rooted project
|
|
179
199
|
--help, -h Print this help
|
|
180
200
|
`,
|
|
181
201
|
"agent-docs": `meta agent-docs — scaffold the agent-context (.metaobjects/ always-on files + .claude/skills/)
|
|
@@ -271,12 +291,19 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
271
291
|
case undefined: {
|
|
272
292
|
// Content-first no-args view: concise status + next-step help[] rather than
|
|
273
293
|
// dumping the full manual (full manual is still available via `meta --help`).
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
// "Is this a MetaObjects project?" routes through resolveCollection — the
|
|
295
|
+
// single authority on where metadata lives — rather than assuming the
|
|
296
|
+
// default `metaobjects/` directory name. The status line must not assert
|
|
297
|
+
// it either: a project whose config points `sources` at
|
|
298
|
+
// `../shared-model/metadata` would be told "metaobjects/ found", which is
|
|
299
|
+
// false, and one that resolves nothing would be told there is no
|
|
300
|
+
// `metaobjects/` here when the real problem may be a declared source that
|
|
301
|
+
// failed to resolve.
|
|
302
|
+
const metadataResolves = await resolveCollection(cwd).then(() => true).catch(() => false);
|
|
303
|
+
const statusLine = metadataResolves
|
|
304
|
+
? `meta — MetaObjects CLI (v${VERSION}) · metadata found`
|
|
305
|
+
: `meta — MetaObjects CLI (v${VERSION}) · no MetaObjects project here`;
|
|
306
|
+
const nextSteps = metadataResolves
|
|
280
307
|
? [
|
|
281
308
|
" meta gen Run codegen",
|
|
282
309
|
" meta verify Check for drift",
|
|
@@ -284,7 +311,7 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
284
311
|
" meta --help Full command reference",
|
|
285
312
|
]
|
|
286
313
|
: [
|
|
287
|
-
" meta init Scaffold
|
|
314
|
+
" meta init Scaffold a MetaObjects project in this directory",
|
|
288
315
|
" meta --help Full command reference",
|
|
289
316
|
];
|
|
290
317
|
log.info(`${statusLine}\n\n${nextSteps.join("\n")}\n`);
|
|
@@ -351,6 +378,10 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
351
378
|
const { verifyCommand } = await import("./commands/verify.js");
|
|
352
379
|
return verifyCommand(rest, cwd);
|
|
353
380
|
}
|
|
381
|
+
case "upgrade": {
|
|
382
|
+
const { upgradeCommand } = await import("./commands/upgrade.js");
|
|
383
|
+
return upgradeCommand(rest, cwd);
|
|
384
|
+
}
|
|
354
385
|
case "prompt-snapshot": {
|
|
355
386
|
const { promptSnapshotCommand } = await import("./commands/prompt-snapshot.js");
|
|
356
387
|
return promptSnapshotCommand(rest, cwd);
|
package/src/lib/allow.ts
CHANGED
|
@@ -33,6 +33,10 @@ export const ALLOW_TOKEN_MAP: Record<string, keyof AllowOptions> = {
|
|
|
33
33
|
// @generation at all — ambiguous between "never declared it" and
|
|
34
34
|
// "deliberately removing auto-increment", so migrate refuses without it.
|
|
35
35
|
"drop-identity-default": "dropIdentityDefault",
|
|
36
|
+
// Gates dropping an object the committed snapshot never contained (#313). Read by
|
|
37
|
+
// migrate's generation-time provenance guard, not by diff()'s status pass — see
|
|
38
|
+
// AllowOptions.dropUnmanaged for why it still belongs in that shape.
|
|
39
|
+
"drop-unmanaged": "dropUnmanaged",
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
/** Translate parsed `--allow` tokens into the migrate-ts `AllowOptions` shape. */
|
package/src/lib/args.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface InitFlags {
|
|
|
15
15
|
noSkills: boolean;
|
|
16
16
|
wireRoot: boolean;
|
|
17
17
|
docsOnly: boolean;
|
|
18
|
+
configOnly: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function parseInitArgs(argv: string[]): InitFlags {
|
|
@@ -31,6 +32,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
|
|
|
31
32
|
"no-skills": { type: "boolean", default: false },
|
|
32
33
|
"no-wire-root": { type: "boolean", default: false },
|
|
33
34
|
"docs-only": { type: "boolean", default: false },
|
|
35
|
+
"config-only": { type: "boolean", default: false },
|
|
34
36
|
},
|
|
35
37
|
strict: true,
|
|
36
38
|
allowPositionals: false,
|
|
@@ -46,6 +48,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
|
|
|
46
48
|
noSkills: !!values["no-skills"],
|
|
47
49
|
wireRoot: !values["no-wire-root"],
|
|
48
50
|
docsOnly: !!values["docs-only"],
|
|
51
|
+
configOnly: !!values["config-only"],
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -197,6 +200,13 @@ export const ALLOW_TOKENS = [
|
|
|
197
200
|
// metadata declares no @generation at all — ambiguous between "never
|
|
198
201
|
// declared it" and "deliberately removing auto-increment".
|
|
199
202
|
"drop-identity-default",
|
|
203
|
+
// drop-unmanaged permits dropping an object the COMMITTED SNAPSHOT never
|
|
204
|
+
// contained — i.e. one this toolchain never managed, typically a table another
|
|
205
|
+
// tool owns. Without it such a drop is refused at generation time, because the
|
|
206
|
+
// migration it writes cannot replay against a database where that object never
|
|
207
|
+
// existed (#313). Unlike its neighbours this one is enforced by `migrate` itself
|
|
208
|
+
// rather than by `diff()`'s status pass; see AllowOptions.dropUnmanaged.
|
|
209
|
+
"drop-unmanaged",
|
|
200
210
|
] as const;
|
|
201
211
|
type AllowToken = (typeof ALLOW_TOKENS)[number];
|
|
202
212
|
|
|
@@ -232,7 +242,20 @@ export interface VerifyFlags {
|
|
|
232
242
|
templates: boolean;
|
|
233
243
|
/** Run the codegen-drift gate (regenerate-to-temp and diff committed output). */
|
|
234
244
|
codegen: boolean;
|
|
235
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Replay the committed migration chain into an empty throwaway database and assert
|
|
247
|
+
* it applies (#313). Needs no `--db`: the engine is local and disposable (PGlite
|
|
248
|
+
* for postgres, a temp sqlite file), so the gate provisions nothing.
|
|
249
|
+
*/
|
|
250
|
+
replay: boolean;
|
|
251
|
+
/**
|
|
252
|
+
* `--replay` plus: assert the replayed schema EQUALS the committed snapshot. A
|
|
253
|
+
* separate subverb rather than a `--strict` modifier, because `--lax` below is a
|
|
254
|
+
* different axis (ADR-0023 attribute strictness) and `--strict` beside it would
|
|
255
|
+
* read as that flag's opposite rather than as a replay depth.
|
|
256
|
+
*/
|
|
257
|
+
replaySnapshot: boolean;
|
|
258
|
+
/** Whether ANY explicit subverb flag (--templates/--db/--codegen/--replay*) was passed. */
|
|
236
259
|
anyExplicit: boolean;
|
|
237
260
|
/** Suppress the advisory anti-pattern (verify-as-teacher) pass. */
|
|
238
261
|
noAntipatterns: boolean;
|
|
@@ -255,6 +278,8 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
255
278
|
"skip-schema": { type: "boolean", default: false },
|
|
256
279
|
templates: { type: "boolean", default: false },
|
|
257
280
|
codegen: { type: "boolean", default: false },
|
|
281
|
+
replay: { type: "boolean", default: false },
|
|
282
|
+
"replay-snapshot": { type: "boolean", default: false },
|
|
258
283
|
"no-antipatterns": { type: "boolean", default: false },
|
|
259
284
|
lax: { type: "boolean", default: false },
|
|
260
285
|
"d1": { type: "string" },
|
|
@@ -283,11 +308,15 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
283
308
|
|
|
284
309
|
const templates = !!values.templates;
|
|
285
310
|
const codegen = !!values.codegen;
|
|
311
|
+
const replay = !!values.replay;
|
|
312
|
+
const replaySnapshot = !!values["replay-snapshot"];
|
|
286
313
|
// --db is itself an explicit subverb selector: passing a connection URL means
|
|
287
314
|
// "run the schema-drift mode". So is `--dialect d1` (D1 has no --db connection
|
|
288
|
-
// URL — see the `d1` field doc above).
|
|
289
|
-
//
|
|
290
|
-
|
|
315
|
+
// URL — see the `d1` field doc above). The replay flags are subverbs too, and
|
|
316
|
+
// must be listed here or `meta verify --replay` would ALSO run the template gate
|
|
317
|
+
// as the bare-verify default.
|
|
318
|
+
const anyExplicit =
|
|
319
|
+
templates || codegen || values.db !== undefined || dialect === "d1" || replay || replaySnapshot;
|
|
291
320
|
|
|
292
321
|
return {
|
|
293
322
|
prompts: values.prompts,
|
|
@@ -297,6 +326,8 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
297
326
|
skipSchema: !!values["skip-schema"],
|
|
298
327
|
templates,
|
|
299
328
|
codegen,
|
|
329
|
+
replay,
|
|
330
|
+
replaySnapshot,
|
|
300
331
|
anyExplicit,
|
|
301
332
|
noAntipatterns: !!values["no-antipatterns"],
|
|
302
333
|
lax: !!values.lax,
|
package/src/lib/codegen-drift.ts
CHANGED
|
@@ -81,11 +81,19 @@ function listFiles(dir: string): string[] {
|
|
|
81
81
|
* @param config the loaded metaobjects config (provides outDir/targets).
|
|
82
82
|
* @param metadata the loaded MetaRoot (same object `meta gen` would use).
|
|
83
83
|
* @param projectRoot absolute project root (committed outDirs are keyed off it).
|
|
84
|
+
* @param scope the SAME output-scope predicate `meta gen` used to produce the
|
|
85
|
+
* committed output (Task 12b / design §7 open question 3). A `verify --codegen`
|
|
86
|
+
* that regenerates unscoped while the committed output was scoped would read
|
|
87
|
+
* every out-of-scope entity as drift — regen would try to emit it, but it was
|
|
88
|
+
* never committed because the `meta gen` that produced the committed tree
|
|
89
|
+
* never emitted it either. Undefined ⇒ everything is in scope (byte-identical
|
|
90
|
+
* to a project with no `scope` declared).
|
|
84
91
|
*/
|
|
85
92
|
export async function computeCodegenDrift(
|
|
86
93
|
config: MetaobjectsGenConfig,
|
|
87
94
|
metadata: MetaData,
|
|
88
95
|
projectRoot: string,
|
|
96
|
+
scope?: (fqn: string) => boolean,
|
|
89
97
|
): Promise<CodegenDriftResult> {
|
|
90
98
|
const root = isAbsolute(projectRoot) ? projectRoot : resolve(projectRoot);
|
|
91
99
|
|
|
@@ -157,6 +165,7 @@ export async function computeCodegenDrift(
|
|
|
157
165
|
genStateDir: join(tempRoot, ".gen-state"),
|
|
158
166
|
mergeStrategy: "overwrite",
|
|
159
167
|
baseline: "fresh",
|
|
168
|
+
...(scope !== undefined ? { scope } : {}),
|
|
160
169
|
});
|
|
161
170
|
|
|
162
171
|
// Diff each committed outDir against its temp mirror.
|
package/src/lib/detect-stack.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
-
import type { Dirent } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
3
|
+
import { resolveCollection } from "@metaobjectsdev/sdk";
|
|
4
4
|
import {
|
|
5
5
|
detectStack, detectConcerns, makeStack,
|
|
6
6
|
type ServerLang, type ClientFramework, type Stack, type ProjectProbe,
|
|
@@ -21,62 +21,49 @@ function depNames(cwd: string): Set<string> {
|
|
|
21
21
|
return out;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const METADATA_DIR = "metaobjects";
|
|
25
|
-
const METADATA_FILE_PATTERN = /\.(json|ya?ml)$/i;
|
|
26
24
|
// Cheap substring probe, not a metamodel load: matches both canonical JSON's
|
|
27
25
|
// quoted `"requirement.functional"` key and sigil-free YAML's bare
|
|
28
26
|
// `requirement.functional:` authoring form.
|
|
29
27
|
const REQUIREMENT_NODE_MARKER = "requirement.";
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
44
|
-
} catch {
|
|
45
|
-
continue; // unreadable directory — skip it, keep scanning siblings
|
|
46
|
-
}
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
const full = join(dir, entry.name);
|
|
49
|
-
if (entry.isDirectory()) {
|
|
50
|
-
pending.push(full);
|
|
51
|
-
} else if (METADATA_FILE_PATTERN.test(entry.name)) {
|
|
52
|
-
try {
|
|
53
|
-
if (readFileSync(full, "utf8").includes(REQUIREMENT_NODE_MARKER)) return true;
|
|
54
|
-
} catch { /* unreadable file — treat as no match */ }
|
|
55
|
-
}
|
|
29
|
+
/** Scans the project's resolved metadata collection (`resolveCollection` — the
|
|
30
|
+
* single authority on where metadata lives, honouring declared `sources` rather
|
|
31
|
+
* than assuming `metaobjects/`) for any file containing a `requirement.*` node
|
|
32
|
+
* marker. Defensive throughout: no declared sources and no default directory, an
|
|
33
|
+
* unresolvable source, or an unreadable file are all treated as "not found",
|
|
34
|
+
* never thrown — this is a cheap heuristic, not a metamodel load. */
|
|
35
|
+
async function hasRequirementNodes(cwd: string): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
const { files } = await resolveCollection(cwd);
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (readFileSync(file, "utf8").includes(REQUIREMENT_NODE_MARKER)) return true;
|
|
56
40
|
}
|
|
41
|
+
return false;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
57
44
|
}
|
|
58
|
-
return false;
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
function probe(cwd: string): ProjectProbe {
|
|
47
|
+
async function probe(cwd: string): Promise<ProjectProbe> {
|
|
62
48
|
const deps = depNames(cwd);
|
|
63
49
|
const names = existsSync(cwd) ? readdirSync(cwd) : [];
|
|
50
|
+
const requirementNodes = await hasRequirementNodes(cwd);
|
|
64
51
|
return {
|
|
65
52
|
hasDep: (name) => deps.has(name),
|
|
66
53
|
hasFileMatching: (re) => names.some((n) => re.test(n)),
|
|
67
|
-
hasRequirementNodes: () =>
|
|
54
|
+
hasRequirementNodes: () => requirementNodes,
|
|
68
55
|
};
|
|
69
56
|
}
|
|
70
57
|
|
|
71
58
|
/** Resolve the stack: explicit --server/--client overrides take precedence; otherwise detect.
|
|
72
59
|
* Concern tokens (e.g. requirements) are always OBSERVED from project state, independent of
|
|
73
60
|
* any --server/--client override — a concern is not a stack axis. */
|
|
74
|
-
export function resolveStack(cwd: string, overrides: { servers: string[]; clients: string[] }): Stack {
|
|
61
|
+
export async function resolveStack(cwd: string, overrides: { servers: string[]; clients: string[] }): Promise<Stack> {
|
|
75
62
|
const validServers = SERVER_LANGS as readonly string[];
|
|
76
63
|
const validClients = CLIENT_FRAMEWORKS as readonly string[];
|
|
77
64
|
const oServers = overrides.servers.filter((s): s is ServerLang => validServers.includes(s));
|
|
78
65
|
const oClients = overrides.clients.filter((c): c is ClientFramework => validClients.includes(c));
|
|
79
|
-
const p = probe(cwd);
|
|
66
|
+
const p = await probe(cwd);
|
|
80
67
|
const concerns = detectConcerns(p);
|
|
81
68
|
if (oServers.length > 0 || oClients.length > 0) return makeStack(oServers, oClients, concerns);
|
|
82
69
|
const detected = detectStack(p);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { createJiti } from "jiti";
|
|
8
|
-
import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
8
|
+
import type { MetaDataTypeProvider, MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
9
9
|
|
|
10
10
|
const CONFIG_FILE = "metaobjects.config.ts";
|
|
11
11
|
|
|
@@ -149,6 +149,69 @@ function rewriteImportSpecifiers(source: string, aliasMap: Record<string, string
|
|
|
149
149
|
return result;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The `loadMemory` options a project's gen config contributes.
|
|
154
|
+
*
|
|
155
|
+
* One helper rather than a spread pair repeated at each of the eight load sites,
|
|
156
|
+
* because threading one of these and forgetting the other is exactly how #333
|
|
157
|
+
* happened: `providers` reached every command while `libraries` reached none, so a
|
|
158
|
+
* generator was registered FOR the CLI with its input unreachable THROUGH it. Adding a
|
|
159
|
+
* third contribution later should not mean finding eight call sites again.
|
|
160
|
+
*
|
|
161
|
+
* Conditional spreads honour `exactOptionalPropertyTypes` — a key is omitted rather
|
|
162
|
+
* than set to `undefined`, which is what lets `loadMemory` apply its own defaults.
|
|
163
|
+
*/
|
|
164
|
+
export function loadMemoryOptionsFrom(
|
|
165
|
+
cfg: Pick<MetaobjectsGenConfig, "providers" | "libraries"> | undefined,
|
|
166
|
+
): { providers?: readonly MetaDataTypeProvider[]; libraries?: readonly string[] } {
|
|
167
|
+
return {
|
|
168
|
+
...(cfg?.providers !== undefined ? { providers: cfg.providers } : {}),
|
|
169
|
+
...(cfg?.libraries !== undefined ? { libraries: cfg.libraries } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The directory whose `metaobjects.config.ts` governs a run started in `startDir` —
|
|
175
|
+
* the nearest ancestor carrying that file, `fallback` when there is none.
|
|
176
|
+
*
|
|
177
|
+
* This is a SECOND walk, deliberately separate from `resolveCollection`'s. The two
|
|
178
|
+
* files answer different questions and design §4.6 already says so: `.metaobjects/
|
|
179
|
+
* config.json` declares where metadata comes from — port-neutral, read by all five
|
|
180
|
+
* CLIs, and reasonably repo-global in a polyglot monorepo — while
|
|
181
|
+
* `metaobjects.config.ts` declares how THIS TypeScript package generates code. Reading
|
|
182
|
+
* the second from the directory that carried the first (#326) meant a Maven- or
|
|
183
|
+
* pip-rooted repo with a JS app underneath could not run `meta gen` at all: the
|
|
184
|
+
* collection resolved to the repo root, and the app's config, sitting in the very
|
|
185
|
+
* directory the command was invoked from, was never looked at.
|
|
186
|
+
*
|
|
187
|
+
* Nearest wins, which is what keeps the opposite arm — the divergence commit
|
|
188
|
+
* 0c8fd136e fixed — closed: a run from a subdirectory that declares NO config of its
|
|
189
|
+
* own walks up to the project root's, exactly as before, rather than silently
|
|
190
|
+
* defaulting `columnNamingStrategy` and emitting a migration that renames every
|
|
191
|
+
* column. When the two files sit together, as they do in every `meta init` project,
|
|
192
|
+
* this walk and the collection walk return the same directory by construction.
|
|
193
|
+
*
|
|
194
|
+
* The `fallback` is the collection's directory, so a project with no
|
|
195
|
+
* `metaobjects.config.ts` anywhere keeps today's diagnostics unchanged: this walk can
|
|
196
|
+
* only ever move the answer CLOSER to the invocation, never further away.
|
|
197
|
+
*
|
|
198
|
+
* Boundaries mirror `discoverCollectionRoot` (sdk `discovery.ts`): the marker is
|
|
199
|
+
* checked before `.git`, so a repo-root project sharing its directory with `.git` is
|
|
200
|
+
* still reachable from any subdirectory, and the walk stops there so a monorepo can
|
|
201
|
+
* never adopt a parent checkout's codegen config.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveGenConfigDir(startDir: string, fallback: string): string {
|
|
204
|
+
let dir = resolve(startDir);
|
|
205
|
+
for (;;) {
|
|
206
|
+
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
207
|
+
if (existsSync(join(dir, ".git"))) break;
|
|
208
|
+
const parent = dirname(dir);
|
|
209
|
+
if (parent === dir) break;
|
|
210
|
+
dir = parent;
|
|
211
|
+
}
|
|
212
|
+
return fallback;
|
|
213
|
+
}
|
|
214
|
+
|
|
152
215
|
export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
|
|
153
216
|
const fullPath = resolve(projectRoot, CONFIG_FILE);
|
|
154
217
|
if (!existsSync(fullPath)) {
|
|
@@ -254,6 +317,25 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
254
317
|
if (!cfg || typeof cfg !== "object" || !Array.isArray(cfg.generators)) {
|
|
255
318
|
throw new Error(`metaobjects.config.ts at ${fullPath} did not export a valid MetaobjectsGenConfig (missing 'generators' array).`);
|
|
256
319
|
}
|
|
320
|
+
// An unknown `libraries` name is a hard config error naming the valid ones, while
|
|
321
|
+
// `librarySources` keeps skipping one silently for a programmatic caller. The two
|
|
322
|
+
// are deliberately different: an API caller asking for a package this version does
|
|
323
|
+
// not ship should still be able to load its own metadata, but a name a human typed
|
|
324
|
+
// into a config file is a mistake worth failing on — skipped, it resurfaces later as
|
|
325
|
+
// ERR_UNRESOLVED_SUPER pointing at the adopter's own metadata, which is the wrong
|
|
326
|
+
// place to send someone looking. Python's `project_config` draws the same line in
|
|
327
|
+
// the same place, and the two ports agreeing here is the point.
|
|
328
|
+
if (cfg.libraries !== undefined && cfg.libraries.length > 0) {
|
|
329
|
+
const { knownLibraryPackages } = await import("@metaobjectsdev/metadata/library");
|
|
330
|
+
const available = knownLibraryPackages();
|
|
331
|
+
const unknown = cfg.libraries.filter((n) => !available.includes(n));
|
|
332
|
+
if (unknown.length > 0) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`metaobjects.config.ts at ${fullPath}: 'libraries' names unknown package(s) ` +
|
|
335
|
+
`${JSON.stringify(unknown)}; available: ${JSON.stringify(available)}.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
257
339
|
return cfg;
|
|
258
340
|
} finally {
|
|
259
341
|
// Restoring is safe: Babel's installer self-neuters after the first call, so
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Advisory: is the codegen hash manifest git-ignored?
|
|
2
|
+
//
|
|
3
|
+
// `.gen-state/.hashes.json` is what lets `meta gen` tell "this file is exactly what
|
|
4
|
+
// I wrote" from "somebody edited this" on a machine that did not generate it. If it
|
|
5
|
+
// is ignored, that knowledge never leaves the generating machine, and on every other
|
|
6
|
+
// machine a hand-edited generated file cannot be recognised as such.
|
|
7
|
+
//
|
|
8
|
+
// The reason this check exists rather than only a migration doc: the failure is
|
|
9
|
+
// INVISIBLE. A project with the manifest ignored looks completely normal — `meta gen`
|
|
10
|
+
// succeeds, output is correct — right up until a fresh clone refuses a wall of files
|
|
11
|
+
// or (before the fix) silently ate an edit. This repo's own 0.23.1 lesson was that a
|
|
12
|
+
// fix ships with the check that would have caught it, or it survives as folklore.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately advisory. It cannot be an error: a project that genuinely does not
|
|
15
|
+
// commit generated output has no use for the manifest, and failing its build over an
|
|
16
|
+
// unused artifact would be wrong.
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { log } from "./log.js";
|
|
22
|
+
|
|
23
|
+
/** Project-relative path of the manifest — the one artifact of `.gen-state/` that is
|
|
24
|
+
* meant to be committed. */
|
|
25
|
+
export const HASH_MANIFEST_REL = join(".metaobjects", ".gen-state", ".hashes.json");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ask git whether `relPath` is ignored, or undefined when the question cannot be
|
|
29
|
+
* answered (not a repository, git missing).
|
|
30
|
+
*
|
|
31
|
+
* `check-ignore` is the only reliable answer: parsing `.gitignore` by hand would have
|
|
32
|
+
* to reimplement precedence, negation and nested ignore files — and the specific trap
|
|
33
|
+
* this check exists to catch (`.gen-state/` excluding a directory so a `!` negation
|
|
34
|
+
* inside it can never apply) is exactly the rule a hand-rolled parser gets wrong.
|
|
35
|
+
*/
|
|
36
|
+
function isGitIgnored(cwd: string, relPath: string): boolean | undefined {
|
|
37
|
+
const gitBin = process.env.META_GEN_GIT ?? "git";
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = spawnSync(gitBin, ["-C", cwd, "check-ignore", "-q", "--", relPath], {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (res.error !== undefined) return undefined;
|
|
47
|
+
// 0 = ignored, 1 = not ignored, 128 = not a git repo / other git error.
|
|
48
|
+
if (res.status === 0) return true;
|
|
49
|
+
if (res.status === 1) return false;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Warn once when the hash manifest is git-ignored, naming the fix.
|
|
55
|
+
*
|
|
56
|
+
* Silent when: it is tracked correctly, the project is not a git repository, git is
|
|
57
|
+
* unavailable, or no generated output has ever been produced (nothing to protect yet).
|
|
58
|
+
* Never throws, never blocks.
|
|
59
|
+
*/
|
|
60
|
+
export function warnIfManifestIgnored(cwd: string): void {
|
|
61
|
+
// Nothing generated yet ⇒ nothing at risk ⇒ nothing worth saying.
|
|
62
|
+
if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return;
|
|
63
|
+
|
|
64
|
+
if (isGitIgnored(cwd, HASH_MANIFEST_REL) !== true) return;
|
|
65
|
+
|
|
66
|
+
log.warn(
|
|
67
|
+
`${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
|
|
68
|
+
`on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
|
|
69
|
+
`own stale output, and will refuse to overwrite rather than guess. Fix it in ` +
|
|
70
|
+
`.metaobjects/.gitignore by replacing '.gen-state/' with '.gen-state/*' plus ` +
|
|
71
|
+
`'!.gen-state/.hashes.json' (the glob matters — git will not descend into an ` +
|
|
72
|
+
`excluded directory, so a negation under '.gen-state/' can never apply), then ` +
|
|
73
|
+
`commit the manifest. If you deliberately do not commit generated output, ignore this.`,
|
|
74
|
+
);
|
|
75
|
+
}
|