@metaobjectsdev/cli 1.0.0-rc.3 → 1.0.0-rc.5
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/dist/src/commands/gen.d.ts +3 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +5 -3
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +237 -29
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +1 -0
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +1 -1
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +39 -9
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +30 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +228 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +88 -72
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts +12 -1
- package/dist/src/lib/docs-drift.d.ts.map +1 -1
- package/dist/src/lib/docs-drift.js +71 -11
- package/dist/src/lib/docs-drift.js.map +1 -1
- package/dist/src/lib/git-ignore.d.ts +40 -0
- package/dist/src/lib/git-ignore.d.ts.map +1 -0
- package/dist/src/lib/git-ignore.js +77 -0
- package/dist/src/lib/git-ignore.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +65 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
- package/dist/src/lib/manifest-ignored-check.js +5 -31
- package/dist/src/lib/manifest-ignored-check.js.map +1 -1
- package/dist/src/lib/output.d.ts +10 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +15 -6
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-lint.d.ts.map +1 -1
- package/dist/src/lib/requirement-lint.js +25 -5
- package/dist/src/lib/requirement-lint.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/gen.ts +5 -3
- package/src/commands/init.ts +247 -33
- package/src/commands/migrate.ts +1 -1
- package/src/commands/verify.ts +46 -9
- package/src/index.ts +30 -2
- package/src/lib/args.ts +96 -72
- package/src/lib/docs-drift.ts +92 -12
- package/src/lib/git-ignore.ts +115 -0
- package/src/lib/load-metaobjects-config.ts +58 -1
- package/src/lib/manifest-ignored-check.ts +5 -28
- package/src/lib/output.ts +30 -5
- package/src/lib/requirement-lint.ts +26 -5
package/src/index.ts
CHANGED
|
@@ -55,6 +55,12 @@ GLOBAL OPTIONS:
|
|
|
55
55
|
|
|
56
56
|
GEN FLAGS:
|
|
57
57
|
--dry-run Compute and print, don't write
|
|
58
|
+
--list Print the generator registry (name, tier, options) and exit —
|
|
59
|
+
no config or metadata required
|
|
60
|
+
--baseline <default|fresh>
|
|
61
|
+
First-time-on-existing-file behavior. Default: write-if-different,
|
|
62
|
+
adopting existing content as the canonical baseline. "fresh"
|
|
63
|
+
overwrites and re-baselines.
|
|
58
64
|
--no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
|
|
59
65
|
--limit <n|all> How many advisory lines TEXT output prints before it truncates
|
|
60
66
|
(default 20). Never applies to --format toon/json, which
|
|
@@ -127,7 +133,7 @@ ship in later sub-projects. See https://metaobjects.com for docs.
|
|
|
127
133
|
`;
|
|
128
134
|
|
|
129
135
|
/** Focused per-subcommand usage slices shown by `<cmd> --help`. */
|
|
130
|
-
const COMMAND_HELP: Record<string, string> = {
|
|
136
|
+
export const COMMAND_HELP: Record<string, string> = {
|
|
131
137
|
gen: `meta gen — codegen TS targets from your declared metadata
|
|
132
138
|
|
|
133
139
|
USAGE:
|
|
@@ -135,6 +141,12 @@ USAGE:
|
|
|
135
141
|
|
|
136
142
|
FLAGS:
|
|
137
143
|
--dry-run Compute and print, don't write
|
|
144
|
+
--list Print the generator registry (name, tier, options) and exit —
|
|
145
|
+
no config or metadata required
|
|
146
|
+
--baseline <default|fresh>
|
|
147
|
+
First-time-on-existing-file behavior. Default: write-if-different,
|
|
148
|
+
adopting existing content as the canonical baseline. "fresh"
|
|
149
|
+
overwrites and re-baselines.
|
|
138
150
|
--no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
|
|
139
151
|
--limit <n|all> Advisory lines TEXT output prints before truncating (default 20)
|
|
140
152
|
--format <toon|json|text> Output format (global flag; default toon off-TTY)
|
|
@@ -182,7 +194,11 @@ FLAGS:
|
|
|
182
194
|
--codegen Codegen drift — regenerate to temp dir and diff committed output
|
|
183
195
|
Needs metaobjects.config.ts; exit 2 if absent.
|
|
184
196
|
--docs Docs drift — run 'meta docs' to a temp dir and diff docs.outDir
|
|
185
|
-
Needs metaobjects.config.ts; exit 2 if absent.
|
|
197
|
+
Needs metaobjects.config.ts; exit 2 if absent. A page the
|
|
198
|
+
project GIT-IGNORES is exempt (docs.outDir is a directory, not
|
|
199
|
+
a namespace MetaObjects owns); the count of exempt pages is
|
|
200
|
+
reported either way, and a run where every page is ignored is
|
|
201
|
+
refused rather than reported clean.
|
|
186
202
|
--db <url> Schema drift — live DB URL enables the schema-drift gate.
|
|
187
203
|
Supports: file:, libsql:, postgres:, postgresql:
|
|
188
204
|
D1 has no URL — use --dialect d1 / --d1 <binding> instead.
|
|
@@ -204,6 +220,10 @@ FLAGS:
|
|
|
204
220
|
--d1 <binding> D1 binding name from wrangler.toml (only with --dialect d1)
|
|
205
221
|
--remote Target remote D1 instead of local (only with --dialect d1) —
|
|
206
222
|
the ONLY way to verify the actual deployed D1 database
|
|
223
|
+
--lax Load with the legacy OPEN attribute set instead of strict
|
|
224
|
+
(ADR-0023). Strict is the default: an undeclared or typo'd own
|
|
225
|
+
@attr fails verify with ERR_UNKNOWN_ATTR. Use only while
|
|
226
|
+
migrating a model onto a registered provider.
|
|
207
227
|
--no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
|
|
208
228
|
--no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
|
|
209
229
|
--limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
|
|
@@ -274,6 +294,14 @@ USAGE:
|
|
|
274
294
|
|
|
275
295
|
FLAGS:
|
|
276
296
|
--refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades
|
|
297
|
+
--docs-only Write only the agent-context files — no metadata sources, no
|
|
298
|
+
codegen scaffold, no config. Pair with --refresh-docs to update
|
|
299
|
+
an existing project after upgrading.
|
|
300
|
+
--server <lang> Declare a server language for the agent context (repeatable;
|
|
301
|
+
e.g. csharp, kotlin, python, node)
|
|
302
|
+
--client <fw> Declare a client framework for the agent context (repeatable;
|
|
303
|
+
e.g. react, vue)
|
|
304
|
+
--no-skills Skip the .claude/skills/ scaffold
|
|
277
305
|
--force Overwrite existing files
|
|
278
306
|
--quiet Suppress output
|
|
279
307
|
--print-only Print what would be written, don't write
|
package/src/lib/args.ts
CHANGED
|
@@ -19,22 +19,25 @@ export interface InitFlags {
|
|
|
19
19
|
configOnly: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** The flag table `parseInitArgs` parses. Exported so the help text can be gated against it. */
|
|
23
|
+
export const INIT_OPTIONS = {
|
|
24
|
+
force: { type: "boolean", default: false },
|
|
25
|
+
quiet: { type: "boolean", default: false },
|
|
26
|
+
"print-only": { type: "boolean", default: false },
|
|
27
|
+
"refresh-docs": { type: "boolean", default: false },
|
|
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 },
|
|
34
|
+
"config-only": { type: "boolean", default: false },
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
22
37
|
export function parseInitArgs(argv: string[]): InitFlags {
|
|
23
38
|
const { values } = parseArgs({
|
|
24
39
|
args: argv,
|
|
25
|
-
options:
|
|
26
|
-
force: { type: "boolean", default: false },
|
|
27
|
-
quiet: { type: "boolean", default: false },
|
|
28
|
-
"print-only": { type: "boolean", default: false },
|
|
29
|
-
"refresh-docs": { type: "boolean", default: false },
|
|
30
|
-
d1: { type: "boolean", default: false },
|
|
31
|
-
server: { type: "string", multiple: true },
|
|
32
|
-
client: { type: "string", multiple: true },
|
|
33
|
-
"no-skills": { type: "boolean", default: false },
|
|
34
|
-
"no-wire-root": { type: "boolean", default: false },
|
|
35
|
-
"docs-only": { type: "boolean", default: false },
|
|
36
|
-
"config-only": { type: "boolean", default: false },
|
|
37
|
-
},
|
|
40
|
+
options: INIT_OPTIONS,
|
|
38
41
|
strict: true,
|
|
39
42
|
allowPositionals: false,
|
|
40
43
|
});
|
|
@@ -65,16 +68,19 @@ export interface AgentDocsFlags {
|
|
|
65
68
|
wireRoot: boolean;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** The flag table `parseAgentDocsArgs` parses. Exported so the help text can be gated against it. */
|
|
72
|
+
export const AGENT_DOCS_OPTIONS = {
|
|
73
|
+
server: { type: "string", multiple: true },
|
|
74
|
+
client: { type: "string", multiple: true },
|
|
75
|
+
out: { type: "string" },
|
|
76
|
+
"no-skills": { type: "boolean", default: false },
|
|
77
|
+
"no-wire-root": { type: "boolean", default: false },
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
68
80
|
export function parseAgentDocsArgs(argv: string[]): AgentDocsFlags {
|
|
69
81
|
const { values } = parseArgs({
|
|
70
82
|
args: argv,
|
|
71
|
-
options:
|
|
72
|
-
server: { type: "string", multiple: true },
|
|
73
|
-
client: { type: "string", multiple: true },
|
|
74
|
-
out: { type: "string" },
|
|
75
|
-
"no-skills": { type: "boolean", default: false },
|
|
76
|
-
"no-wire-root": { type: "boolean", default: false },
|
|
77
|
-
},
|
|
83
|
+
options: AGENT_DOCS_OPTIONS,
|
|
78
84
|
strict: true,
|
|
79
85
|
allowPositionals: false,
|
|
80
86
|
});
|
|
@@ -111,16 +117,19 @@ export interface GenFlags {
|
|
|
111
117
|
limit: number;
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
/** The flag table `parseGenArgs` parses. Exported so the help text can be gated against it. */
|
|
121
|
+
export const GEN_OPTIONS = {
|
|
122
|
+
"dry-run": { type: "boolean", default: false },
|
|
123
|
+
"baseline": { type: "string" },
|
|
124
|
+
"list": { type: "boolean", default: false },
|
|
125
|
+
"no-antipatterns": { type: "boolean", default: false },
|
|
126
|
+
"limit": { type: "string" },
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
114
129
|
export function parseGenArgs(argv: string[]): GenFlags {
|
|
115
130
|
const { values, positionals } = parseArgs({
|
|
116
131
|
args: argv,
|
|
117
|
-
options:
|
|
118
|
-
"dry-run": { type: "boolean", default: false },
|
|
119
|
-
"baseline": { type: "string" },
|
|
120
|
-
"list": { type: "boolean", default: false },
|
|
121
|
-
"no-antipatterns": { type: "boolean", default: false },
|
|
122
|
-
"limit": { type: "string" },
|
|
123
|
-
},
|
|
132
|
+
options: GEN_OPTIONS,
|
|
124
133
|
strict: true,
|
|
125
134
|
allowPositionals: true,
|
|
126
135
|
});
|
|
@@ -150,12 +159,15 @@ export interface ExportFlags {
|
|
|
150
159
|
out: string | undefined;
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
/** The flag table `parseExportArgs` parses. Exported so the help text can be gated against it. */
|
|
163
|
+
export const EXPORT_OPTIONS = {
|
|
164
|
+
out: { type: "string" },
|
|
165
|
+
} as const;
|
|
166
|
+
|
|
153
167
|
export function parseExportArgs(argv: string[]): ExportFlags {
|
|
154
168
|
const { values } = parseArgs({
|
|
155
169
|
args: argv,
|
|
156
|
-
options:
|
|
157
|
-
out: { type: "string" },
|
|
158
|
-
},
|
|
170
|
+
options: EXPORT_OPTIONS,
|
|
159
171
|
strict: true,
|
|
160
172
|
allowPositionals: false,
|
|
161
173
|
});
|
|
@@ -306,27 +318,30 @@ export interface VerifyFlags {
|
|
|
306
318
|
limit: number;
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
/** The flag table `parseVerifyArgs` parses. Exported so the help text can be gated against it. */
|
|
322
|
+
export const VERIFY_OPTIONS = {
|
|
323
|
+
prompts: { type: "string" },
|
|
324
|
+
db: { type: "string" },
|
|
325
|
+
dialect: { type: "string" },
|
|
326
|
+
allow: { type: "string" },
|
|
327
|
+
"skip-schema": { type: "boolean", default: false },
|
|
328
|
+
templates: { type: "boolean", default: false },
|
|
329
|
+
codegen: { type: "boolean", default: false },
|
|
330
|
+
docs: { type: "boolean", default: false },
|
|
331
|
+
replay: { type: "boolean", default: false },
|
|
332
|
+
"replay-snapshot": { type: "boolean", default: false },
|
|
333
|
+
"no-antipatterns": { type: "boolean", default: false },
|
|
334
|
+
"no-requirement-lint": { type: "boolean", default: false },
|
|
335
|
+
lax: { type: "boolean", default: false },
|
|
336
|
+
"d1": { type: "string" },
|
|
337
|
+
"remote": { type: "boolean", default: false },
|
|
338
|
+
"limit": { type: "string" },
|
|
339
|
+
} as const;
|
|
340
|
+
|
|
309
341
|
export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
310
342
|
const { values } = parseArgs({
|
|
311
343
|
args: argv,
|
|
312
|
-
options:
|
|
313
|
-
prompts: { type: "string" },
|
|
314
|
-
db: { type: "string" },
|
|
315
|
-
dialect: { type: "string" },
|
|
316
|
-
allow: { type: "string" },
|
|
317
|
-
"skip-schema": { type: "boolean", default: false },
|
|
318
|
-
templates: { type: "boolean", default: false },
|
|
319
|
-
codegen: { type: "boolean", default: false },
|
|
320
|
-
docs: { type: "boolean", default: false },
|
|
321
|
-
replay: { type: "boolean", default: false },
|
|
322
|
-
"replay-snapshot": { type: "boolean", default: false },
|
|
323
|
-
"no-antipatterns": { type: "boolean", default: false },
|
|
324
|
-
"no-requirement-lint": { type: "boolean", default: false },
|
|
325
|
-
lax: { type: "boolean", default: false },
|
|
326
|
-
"d1": { type: "string" },
|
|
327
|
-
"remote": { type: "boolean", default: false },
|
|
328
|
-
"limit": { type: "string" },
|
|
329
|
-
},
|
|
344
|
+
options: VERIFY_OPTIONS,
|
|
330
345
|
strict: true,
|
|
331
346
|
allowPositionals: false,
|
|
332
347
|
});
|
|
@@ -395,13 +410,16 @@ export interface PromptSnapshotFlags {
|
|
|
395
410
|
prompts: string | undefined;
|
|
396
411
|
}
|
|
397
412
|
|
|
413
|
+
/** The flag table `parsePromptSnapshotArgs` parses. Exported so the help text can be gated against it. */
|
|
414
|
+
export const PROMPT_SNAPSHOT_OPTIONS = {
|
|
415
|
+
check: { type: "boolean", default: false },
|
|
416
|
+
prompts: { type: "string" },
|
|
417
|
+
} as const;
|
|
418
|
+
|
|
398
419
|
export function parsePromptSnapshotArgs(argv: string[]): PromptSnapshotFlags {
|
|
399
420
|
const { values } = parseArgs({
|
|
400
421
|
args: argv,
|
|
401
|
-
options:
|
|
402
|
-
check: { type: "boolean", default: false },
|
|
403
|
-
prompts: { type: "string" },
|
|
404
|
-
},
|
|
422
|
+
options: PROMPT_SNAPSHOT_OPTIONS,
|
|
405
423
|
strict: true,
|
|
406
424
|
allowPositionals: false,
|
|
407
425
|
});
|
|
@@ -447,25 +465,28 @@ export interface MigrateFlags {
|
|
|
447
465
|
applyPending: boolean;
|
|
448
466
|
}
|
|
449
467
|
|
|
468
|
+
/** The flag table `parseMigrateArgs` parses. Exported so the help text can be gated against it. */
|
|
469
|
+
export const MIGRATE_OPTIONS = {
|
|
470
|
+
"db": { type: "string" },
|
|
471
|
+
"dialect": { type: "string" },
|
|
472
|
+
"migration-format": { type: "string" },
|
|
473
|
+
"out-dir": { type: "string" },
|
|
474
|
+
"slug": { type: "string" },
|
|
475
|
+
"allow": { type: "string" },
|
|
476
|
+
"on-ambiguous": { type: "string" },
|
|
477
|
+
"dry-run": { type: "boolean", default: false },
|
|
478
|
+
"from-db": { type: "boolean", default: false },
|
|
479
|
+
"d1": { type: "string" },
|
|
480
|
+
"remote": { type: "boolean", default: false },
|
|
481
|
+
"apply": { type: "boolean", default: false },
|
|
482
|
+
"rollback": { type: "string" },
|
|
483
|
+
"yes": { type: "boolean", default: false },
|
|
484
|
+
} as const;
|
|
485
|
+
|
|
450
486
|
export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
451
487
|
const { values, positionals } = parseArgs({
|
|
452
488
|
args: argv,
|
|
453
|
-
options:
|
|
454
|
-
"db": { type: "string" },
|
|
455
|
-
"dialect": { type: "string" },
|
|
456
|
-
"migration-format": { type: "string" },
|
|
457
|
-
"out-dir": { type: "string" },
|
|
458
|
-
"slug": { type: "string" },
|
|
459
|
-
"allow": { type: "string" },
|
|
460
|
-
"on-ambiguous": { type: "string" },
|
|
461
|
-
"dry-run": { type: "boolean", default: false },
|
|
462
|
-
"from-db": { type: "boolean", default: false },
|
|
463
|
-
"d1": { type: "string" },
|
|
464
|
-
"remote": { type: "boolean", default: false },
|
|
465
|
-
"apply": { type: "boolean", default: false },
|
|
466
|
-
"rollback": { type: "string" },
|
|
467
|
-
"yes": { type: "boolean", default: false },
|
|
468
|
-
},
|
|
489
|
+
options: MIGRATE_OPTIONS,
|
|
469
490
|
strict: true,
|
|
470
491
|
allowPositionals: true,
|
|
471
492
|
});
|
|
@@ -541,13 +562,16 @@ export interface EjectFlags {
|
|
|
541
562
|
force: boolean;
|
|
542
563
|
}
|
|
543
564
|
|
|
565
|
+
/** The flag table `parseEjectArgs` parses. Exported so the help text can be gated against it. */
|
|
566
|
+
export const EJECT_OPTIONS = {
|
|
567
|
+
"list": { type: "boolean", default: false },
|
|
568
|
+
"force": { type: "boolean", default: false },
|
|
569
|
+
} as const;
|
|
570
|
+
|
|
544
571
|
export function parseEjectArgs(argv: string[]): EjectFlags {
|
|
545
572
|
const { values, positionals } = parseArgs({
|
|
546
573
|
args: argv,
|
|
547
|
-
options:
|
|
548
|
-
"list": { type: "boolean", default: false },
|
|
549
|
-
"force": { type: "boolean", default: false },
|
|
550
|
-
},
|
|
574
|
+
options: EJECT_OPTIONS,
|
|
551
575
|
strict: true,
|
|
552
576
|
allowPositionals: true,
|
|
553
577
|
});
|
package/src/lib/docs-drift.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, lstatSync }
|
|
|
49
49
|
import { tmpdir } from "node:os";
|
|
50
50
|
import { join, relative, resolve, isAbsolute, sep } from "node:path";
|
|
51
51
|
import { docsCommand } from "../commands/docs.js";
|
|
52
|
+
import { gitIgnored } from "./git-ignore.js";
|
|
52
53
|
|
|
53
54
|
export interface DocsDriftResult {
|
|
54
55
|
/** True when every page a fresh `meta docs` would write is committed and identical. */
|
|
@@ -59,8 +60,19 @@ export interface DocsDriftResult {
|
|
|
59
60
|
/** Human-readable, one line per file. */
|
|
60
61
|
lines: string[];
|
|
61
62
|
/** The denominator both the passing and the failing report divide by: the pages a fresh
|
|
62
|
-
* run produced, plus any committed page under `agent/` it no longer emits
|
|
63
|
+
* run produced, plus any committed page under `agent/` it no longer emits, MINUS the
|
|
64
|
+
* git-ignored ones. A pure function of (fresh set, repository ignore rules) — never of
|
|
65
|
+
* which files happen to be on this machine's disk. */
|
|
63
66
|
checked: number;
|
|
67
|
+
/** How many pages a fresh run emits that the project git-ignores, so did not check.
|
|
68
|
+
* Reported on BOTH the passing and the failing line: a gate that quietly checked two
|
|
69
|
+
* of 589 pages and said "no drift" would be indistinguishable from one that checked
|
|
70
|
+
* everything. */
|
|
71
|
+
ignored: number;
|
|
72
|
+
/** Why the ignore rules could not be consulted, when they could not be. Undefined means
|
|
73
|
+
* they were. The gate then falls back to checking EVERY page a fresh run emits — the
|
|
74
|
+
* pre-existing behaviour — and says so rather than degrading silently. */
|
|
75
|
+
ignoreReason?: string | undefined;
|
|
64
76
|
/** Set when the gate could not run at all. */
|
|
65
77
|
error?: string;
|
|
66
78
|
}
|
|
@@ -163,6 +175,7 @@ export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<Docs
|
|
|
163
175
|
driftedFiles: [],
|
|
164
176
|
lines: [],
|
|
165
177
|
checked: 0,
|
|
178
|
+
ignored: 0,
|
|
166
179
|
error:
|
|
167
180
|
`verify --docs: 'meta docs' exited ${exit}, so the committed pages could not be ` +
|
|
168
181
|
`compared against a fresh run. Fix that first — the error is above.`,
|
|
@@ -176,19 +189,89 @@ export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<Docs
|
|
|
176
189
|
driftedFiles: [],
|
|
177
190
|
lines: [],
|
|
178
191
|
checked: 0,
|
|
192
|
+
ignored: 0,
|
|
179
193
|
error:
|
|
180
194
|
"verify --docs: a fresh 'meta docs' produced no pages, so there is nothing to " +
|
|
181
195
|
"compare. Check that this project declares metadata the docs surfaces cover.",
|
|
182
196
|
};
|
|
183
197
|
}
|
|
184
198
|
|
|
199
|
+
// Committed-but-not-regenerated, inside the directories this command owns.
|
|
200
|
+
const freshSet = new Set(fresh);
|
|
201
|
+
const ownedOrphans = listFiles(docsDir)
|
|
202
|
+
.filter((rel) => !freshSet.has(rel) && isOurs(docsDir, rel))
|
|
203
|
+
.sort();
|
|
204
|
+
|
|
205
|
+
// A page the project deliberately does not commit is not drift.
|
|
206
|
+
//
|
|
207
|
+
// `docs.outDir` is a directory, not a namespace MetaObjects owns — the same
|
|
208
|
+
// jurisdiction ruling 0.24.3 made for `verify --codegen`. But `--codegen` could key
|
|
209
|
+
// on `.gen-state/.hashes.json`, a record of what the generator WROTE, and `meta
|
|
210
|
+
// docs` keeps no such manifest. What it can ask instead is what the PROJECT says:
|
|
211
|
+
// a page under `docs.outDir` that git reports ignored is one the project has
|
|
212
|
+
// declared it does not commit.
|
|
213
|
+
//
|
|
214
|
+
// This is not a guess about intent, it is the project's own statement of it. And it
|
|
215
|
+
// cannot be used to hide a file the project actually tracks: git never reports a
|
|
216
|
+
// TRACKED path as ignored, whatever pattern matches it, so a committed page is
|
|
217
|
+
// always compared.
|
|
218
|
+
//
|
|
219
|
+
// The exemption is computed over the FRESH set by NAME, present on disk or not.
|
|
220
|
+
// That is what makes the verdict machine-independent: a developer box that has run
|
|
221
|
+
// `meta docs` locally has all 589 pages on disk and a CI runner has two, and both
|
|
222
|
+
// must report the same denominator. Deciding per-file-existence would have made
|
|
223
|
+
// `checked` a property of the machine.
|
|
224
|
+
//
|
|
225
|
+
// Without it: an estate generating three surfaces and committing 2 files of 589 —
|
|
226
|
+
// gitignoring the rest with a comment explaining they are a derived view of
|
|
227
|
+
// metadata that is already the source of truth — was told it had 588 drifted pages,
|
|
228
|
+
// of which exactly ONE was real.
|
|
229
|
+
const ignoreCandidates = [...fresh, ...ownedOrphans];
|
|
230
|
+
// honourGlobalExcludes: false — this verdict must be a property of the repository.
|
|
231
|
+
// A gate that passes for the author and fails in CI because of a personal ignore
|
|
232
|
+
// file is worse than no gate.
|
|
233
|
+
const ignoreLookup = gitIgnored(docsDir, ignoreCandidates, {
|
|
234
|
+
honourGlobalExcludes: false,
|
|
235
|
+
});
|
|
236
|
+
const ignored: ReadonlySet<string> =
|
|
237
|
+
"ignored" in ignoreLookup ? ignoreLookup.ignored : new Set<string>();
|
|
238
|
+
const ignoreReason: string | undefined =
|
|
239
|
+
"unavailable" in ignoreLookup ? ignoreLookup.unavailable : undefined;
|
|
240
|
+
const isIgnored = (rel: string): boolean => ignored.has(rel.split(sep).join("/"));
|
|
241
|
+
|
|
242
|
+
const toCheck = fresh.filter((rel) => !isIgnored(rel));
|
|
243
|
+
const orphans = ownedOrphans.filter((rel) => !isIgnored(rel));
|
|
244
|
+
const ignoredCount = fresh.length - toCheck.length;
|
|
245
|
+
|
|
246
|
+
// A gate asked to check pages that could check NONE must not answer "no drift".
|
|
247
|
+
if (toCheck.length === 0 && orphans.length === 0) {
|
|
248
|
+
return {
|
|
249
|
+
clean: false,
|
|
250
|
+
driftedFiles: [],
|
|
251
|
+
lines: [],
|
|
252
|
+
checked: 0,
|
|
253
|
+
ignored: ignoredCount,
|
|
254
|
+
ignoreReason,
|
|
255
|
+
error:
|
|
256
|
+
`verify --docs: a fresh 'meta docs' produced ${fresh.length} page(s) and every ` +
|
|
257
|
+
`one of them is git-ignored, so there is nothing to compare. Commit the pages ` +
|
|
258
|
+
`you want checked, or run without --docs.`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
185
262
|
const driftedFiles: string[] = [];
|
|
186
263
|
const lines: string[] = [];
|
|
187
|
-
for (const rel of
|
|
264
|
+
for (const rel of toCheck) {
|
|
188
265
|
const committedPath = join(docsDir, rel);
|
|
189
266
|
if (!existsSync(committedPath)) {
|
|
190
267
|
driftedFiles.push(rel);
|
|
191
|
-
|
|
268
|
+
// When the rules could not be consulted the last clause would be a claim we
|
|
269
|
+
// have not checked, so it is dropped rather than asserted.
|
|
270
|
+
lines.push(
|
|
271
|
+
ignoreReason === undefined
|
|
272
|
+
? `+ ${rel} (a fresh 'meta docs' emits it; not committed, not git-ignored)`
|
|
273
|
+
: `+ ${rel} (a fresh 'meta docs' emits it; not committed)`,
|
|
274
|
+
);
|
|
192
275
|
continue;
|
|
193
276
|
}
|
|
194
277
|
const a = readFileSync(committedPath, "utf8");
|
|
@@ -198,14 +281,9 @@ export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<Docs
|
|
|
198
281
|
lines.push(`~ ${rel} (committed content differs from a fresh 'meta docs')`);
|
|
199
282
|
}
|
|
200
283
|
}
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// and a green run reported different denominators for the same project.
|
|
205
|
-
const freshSet = new Set(fresh);
|
|
206
|
-
const orphans = listFiles(docsDir)
|
|
207
|
-
.filter((rel) => !freshSet.has(rel) && isOurs(docsDir, rel))
|
|
208
|
-
.sort();
|
|
284
|
+
// Counted into `checked` as well as `driftedFiles` so the failing line and the
|
|
285
|
+
// passing line keep dividing by the same set — the `verify --templates` mistake,
|
|
286
|
+
// where a red run and a green run reported different denominators for one project.
|
|
209
287
|
for (const rel of orphans) {
|
|
210
288
|
driftedFiles.push(rel);
|
|
211
289
|
lines.push(`- ${rel} (committed; a fresh 'meta docs' no longer emits it)`);
|
|
@@ -214,7 +292,9 @@ export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<Docs
|
|
|
214
292
|
clean: driftedFiles.length === 0,
|
|
215
293
|
driftedFiles: driftedFiles.sort(),
|
|
216
294
|
lines,
|
|
217
|
-
checked:
|
|
295
|
+
checked: toCheck.length + orphans.length,
|
|
296
|
+
ignored: ignoredCount,
|
|
297
|
+
ignoreReason,
|
|
218
298
|
};
|
|
219
299
|
} finally {
|
|
220
300
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// "Does this project deliberately not commit these files?" — asked of git, once.
|
|
2
|
+
//
|
|
3
|
+
// `check-ignore` is the only reliable answer. Parsing `.gitignore` by hand would have
|
|
4
|
+
// to reimplement precedence, negation and nested ignore files, and the traps are
|
|
5
|
+
// exactly the rules a hand-rolled parser gets wrong: a directory pattern (`docs/`)
|
|
6
|
+
// makes a `!` negation inside it unreachable, and a TRACKED file is never reported
|
|
7
|
+
// ignored no matter what pattern matches it.
|
|
8
|
+
//
|
|
9
|
+
// That last property is what makes this usable as an exemption: a page the project
|
|
10
|
+
// has committed is always compared, whatever its `.gitignore` says. So a rule built
|
|
11
|
+
// on this cannot be used to hide a file the project actually tracks.
|
|
12
|
+
//
|
|
13
|
+
// Paths are evaluated BY NAME — a path need not exist on disk for git to answer. That
|
|
14
|
+
// is deliberate and load-bearing for the docs gate: the exemption is computed over
|
|
15
|
+
// the set a fresh run WOULD emit, so the verdict is identical on a developer box
|
|
16
|
+
// where those files are present and on a CI runner where they are not.
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { sep } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** Ignored paths, or the reason the question could not be asked. */
|
|
22
|
+
export type GitIgnoreResult =
|
|
23
|
+
| { readonly ignored: ReadonlySet<string> }
|
|
24
|
+
| { readonly unavailable: string };
|
|
25
|
+
|
|
26
|
+
export interface GitIgnoreOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Whether a per-user ignore file (`core.excludesFile`) may contribute to the
|
|
29
|
+
* verdict.
|
|
30
|
+
*
|
|
31
|
+
* `false` for anything whose answer must be a property of the REPOSITORY — a gate
|
|
32
|
+
* that passes on the author's machine and fails in CI because of a personal ignore
|
|
33
|
+
* file is worse than no gate. Implemented by pointing `core.excludesFile` at a path
|
|
34
|
+
* that does not exist, which is the only way to neutralise it.
|
|
35
|
+
*
|
|
36
|
+
* `true` where the question is genuinely about this machine — "will this file reach
|
|
37
|
+
* anyone else?" is answered by every rule that applies here, personal ones included.
|
|
38
|
+
*
|
|
39
|
+
* Note `.git/info/exclude` is per-clone and CANNOT be neutralised; it is reported
|
|
40
|
+
* like any other rule, and the caller's count line is what exposes it.
|
|
41
|
+
*/
|
|
42
|
+
readonly honourGlobalExcludes: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A path that cannot exist, used to neutralise a per-user excludes file. */
|
|
46
|
+
const NO_SUCH_EXCLUDES_FILE = "/dev/null/metaobjects-no-global-excludes";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Ask git which of `rels` are ignored, relative to `dir`.
|
|
50
|
+
*
|
|
51
|
+
* Returns `unavailable` — never throws, never guesses — when git is not on PATH or
|
|
52
|
+
* `dir` is not inside a repository. A caller must decide what that means for it;
|
|
53
|
+
* silently treating "cannot say" as "nothing is ignored" is a fallback that lies, and
|
|
54
|
+
* the callers here say so out loud instead.
|
|
55
|
+
*/
|
|
56
|
+
export function gitIgnored(
|
|
57
|
+
dir: string,
|
|
58
|
+
rels: readonly string[],
|
|
59
|
+
opts: GitIgnoreOptions,
|
|
60
|
+
): GitIgnoreResult {
|
|
61
|
+
if (rels.length === 0) return { ignored: new Set() };
|
|
62
|
+
|
|
63
|
+
const gitBin = process.env.META_GEN_GIT ?? "git";
|
|
64
|
+
const args: string[] = [];
|
|
65
|
+
if (!opts.honourGlobalExcludes) {
|
|
66
|
+
args.push("-c", `core.excludesFile=${NO_SUCH_EXCLUDES_FILE}`);
|
|
67
|
+
}
|
|
68
|
+
args.push("-C", dir, "check-ignore", "--stdin", "-z");
|
|
69
|
+
|
|
70
|
+
// Forward slashes: git speaks them on every platform, and `listFiles` hands us
|
|
71
|
+
// platform separators.
|
|
72
|
+
const input = rels.map((r) => r.split(sep).join("/")).join("\0");
|
|
73
|
+
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = spawnSync(gitBin, args, { encoding: "utf-8", input });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { unavailable: `git could not be run (${(err as Error).message})` };
|
|
79
|
+
}
|
|
80
|
+
if (res.error !== undefined) {
|
|
81
|
+
return { unavailable: "git is not on PATH" };
|
|
82
|
+
}
|
|
83
|
+
// 0 = at least one ignored, 1 = none ignored, anything else = not a repository
|
|
84
|
+
// (or a git error), which is a different fact from "nothing is ignored".
|
|
85
|
+
if (res.status === 1) return { ignored: new Set() };
|
|
86
|
+
if (res.status !== 0) {
|
|
87
|
+
const stderr = (res.stderr ?? "").split("\n")[0]?.trim();
|
|
88
|
+
return {
|
|
89
|
+
unavailable:
|
|
90
|
+
stderr !== undefined && stderr !== ""
|
|
91
|
+
? stderr
|
|
92
|
+
: `'${dir}' is not inside a git repository`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ignored = new Set(
|
|
97
|
+
(res.stdout ?? "").split("\0").map((p) => p.trim()).filter((p) => p !== ""),
|
|
98
|
+
);
|
|
99
|
+
return { ignored };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Single-path convenience: true / false / undefined when git cannot say.
|
|
104
|
+
*
|
|
105
|
+
* `honourGlobalExcludes` is the caller's decision — see the option's doc.
|
|
106
|
+
*/
|
|
107
|
+
export function isGitIgnored(
|
|
108
|
+
dir: string,
|
|
109
|
+
rel: string,
|
|
110
|
+
opts: GitIgnoreOptions,
|
|
111
|
+
): boolean | undefined {
|
|
112
|
+
const res = gitIgnored(dir, [rel], opts);
|
|
113
|
+
if ("unavailable" in res) return undefined;
|
|
114
|
+
return res.ignored.has(rel.split(sep).join("/"));
|
|
115
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -351,6 +351,63 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
351
351
|
} catch { /* no ts-poet adjacent to codegen-ts — leave project resolution in place */ }
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
// EVERY DECLARED SUBPATH EXPORT NEEDS ITS OWN ENTRY, because jiti's `alias` matches by
|
|
355
|
+
// PREFIX and then concatenates the remainder onto the mapped value. The mapped value is
|
|
356
|
+
// a FILE, so `@metaobjectsdev/codegen-ts/templates/inferred-types` became
|
|
357
|
+
// `…/codegen-ts/dist/index.js/templates/inferred-types` — a path that has never existed,
|
|
358
|
+
// named in an error message that mentions neither jiti nor the alias map:
|
|
359
|
+
//
|
|
360
|
+
// Cannot find module '…/@metaobjectsdev/codegen-ts/dist/index.js/templates/inferred-types'
|
|
361
|
+
//
|
|
362
|
+
// So every subpath the package deliberately exports was unloadable from an owned
|
|
363
|
+
// generator, and the documented escape (a dynamic `import()`, which this file's own
|
|
364
|
+
// comment offers) failed identically, because it falls back to the same broken matcher.
|
|
365
|
+
// A cold adoption probe hit this retargeting the entity tier off Drizzle and got past it
|
|
366
|
+
// only by reading this file's compiled output — the subpath holds `fieldTsTypeString`,
|
|
367
|
+
// the one neutral MetaField→TypeScript mapper, which anyone replacing `renderDrizzleSchema`
|
|
368
|
+
// needs. `rewriteImportSpecifiers` above was always correct here (it anchors on the
|
|
369
|
+
// closing quote, so a subpath is left alone); only the alias fallback was wrong, and the
|
|
370
|
+
// fallback is what an OWNED GENERATOR gets, since only the config file is pre-processed.
|
|
371
|
+
//
|
|
372
|
+
// DERIVED from each package's own `exports` map rather than listed. A hand-kept list of
|
|
373
|
+
// subpaths is the same defect one level up: the package adds an export, nobody edits this
|
|
374
|
+
// file, and it is unloadable again with the same unrecognisable error. Resolution goes
|
|
375
|
+
// through `createRequire().resolve`, so the exports map — including its conditions — is
|
|
376
|
+
// the authority, and a subpath that does not resolve is skipped rather than guessed at.
|
|
377
|
+
for (const [specifier, resolvedPath] of [...Object.entries(aliasMap)]) {
|
|
378
|
+
if (!specifier.startsWith("@metaobjectsdev/")) continue;
|
|
379
|
+
// The package root is the specifier itself; a specifier that is already a subpath
|
|
380
|
+
// (e.g. ".../generators") contributes nothing further.
|
|
381
|
+
if (specifier.slice("@metaobjectsdev/".length).includes("/")) continue;
|
|
382
|
+
// Read the manifest off the FILE SYSTEM by walking up from the resolved entry, not by
|
|
383
|
+
// requiring `<specifier>/package.json` — these packages do not list `./package.json` in
|
|
384
|
+
// their own exports map, so requiring it throws ERR_PACKAGE_PATH_NOT_EXPORTED and the
|
|
385
|
+
// derivation silently produces nothing. (Which it did, on the first cut of this fix:
|
|
386
|
+
// the gate in test/owned-generator-subpath-import.test.ts stayed red and named the
|
|
387
|
+
// same impossible path, which is exactly what that assertion is for.)
|
|
388
|
+
let manifest: { exports?: Record<string, unknown> } | undefined;
|
|
389
|
+
for (let dir = dirname(resolvedPath); ; dir = dirname(dir)) {
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")) as {
|
|
392
|
+
name?: string;
|
|
393
|
+
exports?: Record<string, unknown>;
|
|
394
|
+
};
|
|
395
|
+
if (parsed.name === specifier) { manifest = parsed; break; }
|
|
396
|
+
} catch { /* not this directory — keep walking */ }
|
|
397
|
+
const parent = dirname(dir);
|
|
398
|
+
if (parent === dir) break; // filesystem root
|
|
399
|
+
}
|
|
400
|
+
if (manifest === undefined) continue; // no readable manifest — leave normal resolution
|
|
401
|
+
for (const key of Object.keys(manifest?.exports ?? {})) {
|
|
402
|
+
if (!key.startsWith("./") || key.includes("*")) continue;
|
|
403
|
+
const sub = `${specifier}/${key.slice(2)}`;
|
|
404
|
+
if (aliasMap[sub] !== undefined) continue; // an explicit CLI_PKG_PATHS entry wins
|
|
405
|
+
try {
|
|
406
|
+
aliasMap[sub] = createRequire(resolvedPath).resolve(sub);
|
|
407
|
+
} catch { /* not resolvable under these conditions — skip rather than invent a path */ }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
354
411
|
// Pre-process the config file content to rewrite @metaobjectsdev/* import
|
|
355
412
|
// specifiers to resolved absolute paths. This is the primary mechanism that
|
|
356
413
|
// ensures the CLI's own copies are used: jiti's `alias` map is kept below as
|