@metaobjectsdev/cli 0.24.4 → 0.25.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 +2 -1
- package/dist/src/commands/docs.d.ts +14 -1
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +153 -8
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +38 -0
- package/dist/src/commands/eject.d.ts.map +1 -0
- package/dist/src/commands/eject.js +233 -0
- package/dist/src/commands/eject.js.map +1 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +43 -20
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +15 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +208 -20
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/types.d.ts +2 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +165 -28
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/verify.d.ts +9 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +299 -51
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +105 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +77 -0
- package/dist/src/lib/advisory.d.ts.map +1 -0
- package/dist/src/lib/advisory.js +97 -0
- package/dist/src/lib/advisory.js.map +1 -0
- package/dist/src/lib/anti-patterns.d.ts +27 -3
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +145 -8
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +35 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +32 -1
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +4 -13
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts +31 -0
- package/dist/src/lib/docs-drift.d.ts.map +1 -0
- package/dist/src/lib/docs-drift.js +195 -0
- package/dist/src/lib/docs-drift.js.map +1 -0
- package/dist/src/lib/format.d.ts +10 -0
- package/dist/src/lib/format.d.ts.map +1 -1
- package/dist/src/lib/format.js +15 -0
- package/dist/src/lib/format.js.map +1 -1
- package/dist/src/lib/output.d.ts +13 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +16 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manifest.d.ts +27 -0
- package/dist/src/lib/package-manifest.d.ts.map +1 -0
- package/dist/src/lib/package-manifest.js +52 -0
- package/dist/src/lib/package-manifest.js.map +1 -0
- package/package.json +11 -11
- package/src/commands/docs.ts +194 -8
- package/src/commands/eject.ts +282 -0
- package/src/commands/gen.ts +54 -19
- package/src/commands/init.ts +217 -19
- package/src/commands/types.ts +185 -34
- package/src/commands/verify.ts +358 -47
- package/src/index.ts +114 -5
- package/src/lib/advisory.ts +150 -0
- package/src/lib/anti-patterns.ts +163 -8
- package/src/lib/args.ts +74 -2
- package/src/lib/detect-stack.ts +4 -11
- package/src/lib/docs-drift.ts +222 -0
- package/src/lib/format.ts +14 -0
- package/src/lib/output.ts +33 -2
- package/src/lib/package-manifest.ts +58 -0
package/src/commands/types.ts
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
// `meta types [QUERY]` — search the metadata vocabulary (types, subtypes, @attrs)
|
|
2
2
|
// without loading it all into context. apropos + `kubectl explain` over the live
|
|
3
3
|
// registry, tuned for an agent's token budget: terse names-first default, opt-in
|
|
4
|
-
// description search, drill-in `--detail
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// description search, drill-in `--detail`.
|
|
5
|
+
//
|
|
6
|
+
// It honors the global `--format`, and its default is TEXT in every case — deliberately
|
|
7
|
+
// NOT the TTY-aware default (`resolveFormat`: TOON off a TTY) that gen/verify/migrate take.
|
|
8
|
+
// Two reasons, and the second is the binding one: this command's text output is already the
|
|
9
|
+
// agent-tuned rendering the whole design is for, and every existing non-interactive caller
|
|
10
|
+
// pipes exactly that — a TTY-aware default would silently change what all of them read, with
|
|
11
|
+
// no flag passed. So index.ts hands this command the RAW --format flag rather than the
|
|
12
|
+
// resolved `fmt`, and absent means text. (`gen` already declares its own `fmt = "text"`
|
|
13
|
+
// default, so a command-local default is not a new idea.)
|
|
14
|
+
//
|
|
15
|
+
// In a structured format the rule is stdout PURITY: exactly one document, nothing else. The
|
|
16
|
+
// legend, the "N of M shown" footer and the no-match hint are all TEXT rendering — a `| jq`
|
|
17
|
+
// dies on any of them — so the structured branch emits none of them and carries what they
|
|
18
|
+
// said as FIELDS instead.
|
|
19
|
+
//
|
|
20
|
+
// The `--json` flag this once advertised in its own --help, twice, is not coming back: the
|
|
21
|
+
// CLI rejects a bare `--json` before a command ever sees its args (index.ts, `formatAlias`),
|
|
22
|
+
// on purpose — one global spelling for all three formats.
|
|
23
|
+
import { composeRegistry, coreProviders, buildVocabularyCatalog } from "@metaobjectsdev/metadata";
|
|
24
|
+
import { forgeTypesProvider } from "@metaobjectsdev/sdk";
|
|
11
25
|
import { log } from "../lib/log.js";
|
|
26
|
+
import { emitStructured, type OutputFormat } from "../lib/format.js";
|
|
12
27
|
|
|
13
28
|
interface TypesFlags {
|
|
14
29
|
query: string | null;
|
|
@@ -16,7 +31,6 @@ interface TypesFlags {
|
|
|
16
31
|
kind: Set<"type" | "subtype" | "attr">;
|
|
17
32
|
type: string | null; // scope to one top-level type
|
|
18
33
|
detail: boolean;
|
|
19
|
-
json: boolean;
|
|
20
34
|
noHeaders: boolean;
|
|
21
35
|
limit: number; // 0 = unlimited
|
|
22
36
|
help: boolean;
|
|
@@ -28,7 +42,7 @@ const HELP = `meta types [QUERY] — search the metadata vocabulary without load
|
|
|
28
42
|
meta types --all money # search names AND descriptions ("find by what it does")
|
|
29
43
|
meta types --type field --kind subtype # all field subtypes (terse)
|
|
30
44
|
meta types field.enum --detail # one construct: description + when-to-use + valid @attrs
|
|
31
|
-
meta types --type
|
|
45
|
+
meta types --type view --kind subtype # every registered view control
|
|
32
46
|
|
|
33
47
|
QUERY is a case-insensitive substring, matched on the name (type.subType / @attr).
|
|
34
48
|
Add --desc (or --all) to also match descriptions + when-to-use guidance.
|
|
@@ -38,26 +52,39 @@ Add --desc (or --all) to also match descriptions + when-to-use guidance.
|
|
|
38
52
|
--kind <k> filter by category: type | subtype | attr (comma-list ok)
|
|
39
53
|
--type <name> scope to one top-level type (e.g. --type field)
|
|
40
54
|
--detail drill in: full description, when-to-use, and valid @attrs
|
|
41
|
-
--json emit the matching registry subtree verbatim (stable-sorted)
|
|
42
55
|
--limit <N> cap results (default 20; 0 = unlimited)
|
|
43
56
|
--no-headers omit headers (parse-friendly)
|
|
57
|
+
--format <toon|json|text> Output format (global flag). Defaults to TEXT here, even
|
|
58
|
+
off a TTY — unlike gen/verify/migrate, whose default is TOON off a TTY.
|
|
44
59
|
|
|
45
60
|
Default output is one terse line per match. Reach for metaobjects metadata
|
|
46
61
|
(declare it, regenerate) instead of hand-writing data logic — this finds the
|
|
47
|
-
construct
|
|
62
|
+
construct.
|
|
63
|
+
|
|
64
|
+
--format toon / --format json emit ONE machine-readable document and nothing else. It
|
|
65
|
+
carries every match with its full record, so --limit, --detail and --no-headers — all
|
|
66
|
+
three TEXT display controls — do not change it. The terse line's [base] / [ts-only]
|
|
67
|
+
markers are the sharedRoot / tsOnly fields there, a closed-enum attr carries its
|
|
68
|
+
allowedValues, and no match is an empty matches list rather than a prose hint.`;
|
|
48
69
|
|
|
49
70
|
function parse(args: string[]): TypesFlags {
|
|
50
71
|
const f: TypesFlags = {
|
|
51
72
|
query: null, desc: false, kind: new Set(), type: null,
|
|
52
|
-
detail: false,
|
|
73
|
+
detail: false, noHeaders: false, limit: 20, help: false,
|
|
53
74
|
};
|
|
54
75
|
for (let i = 0; i < args.length; i++) {
|
|
55
76
|
const a = args[i] as string;
|
|
56
77
|
if (a === "--help" || a === "-h") f.help = true;
|
|
57
78
|
else if (a === "--desc" || a === "--all") f.desc = true;
|
|
58
79
|
else if (a === "--detail") f.detail = true;
|
|
59
|
-
else if (a === "--json") f.json = true;
|
|
60
80
|
else if (a === "--no-headers") f.noHeaders = true;
|
|
81
|
+
// `--format` is a GLOBAL flag: index.ts strips it (and its value) from argv before
|
|
82
|
+
// dispatch and passes the result in as `fmt`, so it never reaches here through the
|
|
83
|
+
// CLI. Accepted and ignored anyway, so that the flag this command's --help lists is
|
|
84
|
+
// one the parser accepts — the invariant types-command.test.ts holds it to, and the
|
|
85
|
+
// one the removed `--json` broke.
|
|
86
|
+
else if (a === "--format") i++;
|
|
87
|
+
else if (a.startsWith("--format=")) { /* value is inline; nothing to consume */ }
|
|
61
88
|
else if (a === "--limit") f.limit = Math.max(0, Number(args[++i] ?? "20") || 0);
|
|
62
89
|
else if (a === "--type") f.type = (args[++i] ?? "").toLowerCase() || null;
|
|
63
90
|
else if (a === "--kind") {
|
|
@@ -78,43 +105,160 @@ interface Entry {
|
|
|
78
105
|
description: string;
|
|
79
106
|
whenToUse: string | undefined;
|
|
80
107
|
attrNames: string[]; // attr-name hints (for subtype terse lines)
|
|
81
|
-
|
|
108
|
+
/** Accepted on EVERY node, so no `--type` scope excludes it (#357). */
|
|
109
|
+
common: boolean;
|
|
110
|
+
/** Registered here but carved out of the cross-port manifest — TS-only vocabulary. */
|
|
111
|
+
tsOnly: boolean;
|
|
112
|
+
/** A type's shared root (`<type>.base`): its attrs apply to every subtype. */
|
|
113
|
+
sharedRoot: boolean;
|
|
114
|
+
raw: unknown; // the catalog node
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Compact markers appended to a terse line, and the legend that explains them. */
|
|
118
|
+
const MARK_TS_ONLY = "[ts-only]";
|
|
119
|
+
const MARK_BASE = "[base]";
|
|
120
|
+
const LEGEND = ` ${MARK_BASE} the type's shared root — its @attrs apply to every subtype of that type\n` +
|
|
121
|
+
` ${MARK_TS_ONLY} registered in TypeScript only; the cross-port metamodel contract does not carry it`;
|
|
122
|
+
|
|
123
|
+
function marks(e: Entry): string {
|
|
124
|
+
const m = [e.sharedRoot ? MARK_BASE : "", e.tsOnly ? MARK_TS_ONLY : ""].filter(Boolean);
|
|
125
|
+
return m.length > 0 ? ` ${m.join(" ")}` : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A catalog attr, as far as the payload needs to know. */
|
|
129
|
+
interface RawAttr {
|
|
130
|
+
name: string;
|
|
131
|
+
valueType: string | null;
|
|
132
|
+
isArray: boolean;
|
|
133
|
+
required: boolean;
|
|
134
|
+
allowedValues?: readonly string[];
|
|
135
|
+
description: string;
|
|
136
|
+
whenToUse?: string;
|
|
82
137
|
}
|
|
83
138
|
|
|
84
|
-
|
|
139
|
+
function attrRecord(at: RawAttr): Record<string, unknown> {
|
|
140
|
+
return {
|
|
141
|
+
name: at.name,
|
|
142
|
+
valueType: at.valueType,
|
|
143
|
+
isArray: at.isArray,
|
|
144
|
+
required: at.required,
|
|
145
|
+
...(at.allowedValues !== undefined ? { allowedValues: at.allowedValues } : {}),
|
|
146
|
+
description: at.description,
|
|
147
|
+
...(at.whenToUse !== undefined ? { whenToUse: at.whenToUse } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function payloadMatch(e: Entry): Record<string, unknown> {
|
|
152
|
+
const out: Record<string, unknown> = {
|
|
153
|
+
kind: e.kind,
|
|
154
|
+
name: e.name,
|
|
155
|
+
// A common attr belongs to no type and no subtype; `common: true` is what says so,
|
|
156
|
+
// and null is the honest answer for both. The text rendering's "(any node)" owner is
|
|
157
|
+
// a label for a human reading a column, not a value to hand a program.
|
|
158
|
+
type: e.common || e.type === "" ? null : e.type,
|
|
159
|
+
owner: e.common ? null : e.owner,
|
|
160
|
+
common: e.common,
|
|
161
|
+
tsOnly: e.tsOnly,
|
|
162
|
+
sharedRoot: e.sharedRoot,
|
|
163
|
+
description: e.description,
|
|
164
|
+
...(e.whenToUse !== undefined ? { whenToUse: e.whenToUse } : {}),
|
|
165
|
+
};
|
|
166
|
+
if (e.kind === "subtype") {
|
|
167
|
+
out.attrs = (e.raw as { attrs: RawAttr[] }).attrs.map(attrRecord);
|
|
168
|
+
} else {
|
|
169
|
+
const at = e.raw as RawAttr;
|
|
170
|
+
out.valueType = at.valueType;
|
|
171
|
+
out.isArray = at.isArray;
|
|
172
|
+
out.required = at.required;
|
|
173
|
+
if (at.allowedValues !== undefined) out.allowedValues = at.allowedValues;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The machine-readable answer: ONE document, every match, the whole record.
|
|
180
|
+
*
|
|
181
|
+
* Not a serialization of the text rendering. `--limit`, `--detail` and `--no-headers` are
|
|
182
|
+
* TEXT display controls — the global --help already promises a structured payload is never
|
|
183
|
+
* truncated — so none of them reaches here and the payload always carries everything.
|
|
184
|
+
*
|
|
185
|
+
* `metamodelVersion` is included because it is the number that says whether this answer
|
|
186
|
+
* applies to a given model at all: the vocabulary is a contract with a version, and a
|
|
187
|
+
* consumer caching this document needs to know which one it captured.
|
|
188
|
+
*/
|
|
189
|
+
function buildPayload(f: TypesFlags, matches: Entry[], metamodelVersion: string): unknown {
|
|
190
|
+
return {
|
|
191
|
+
metamodelVersion,
|
|
192
|
+
query: f.query,
|
|
193
|
+
filters: {
|
|
194
|
+
type: f.type,
|
|
195
|
+
kind: [...f.kind].sort(),
|
|
196
|
+
searchDescriptions: f.desc,
|
|
197
|
+
},
|
|
198
|
+
total: matches.length,
|
|
199
|
+
matches: matches.map(payloadMatch),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function typesCommand(args: string[], fmt: OutputFormat = "text"): Promise<number> {
|
|
85
204
|
let f: TypesFlags;
|
|
86
205
|
try {
|
|
87
206
|
f = parse(args);
|
|
88
207
|
} catch (err) {
|
|
89
|
-
|
|
208
|
+
const msg = (err as Error).message;
|
|
209
|
+
log.error(msg);
|
|
210
|
+
// A structured caller gets a structured refusal — exiting 2 with an EMPTY stdout is
|
|
211
|
+
// the same silence a `| jq` cannot tell from "no results". Mirrors verify.ts.
|
|
212
|
+
emitStructured({ error: msg, hint: "run `meta types --help` for the accepted flags" }, fmt);
|
|
90
213
|
return 2;
|
|
91
214
|
}
|
|
92
215
|
if (f.help) {
|
|
216
|
+
// Help is prose by definition — rendering it as a JSON string helps nobody, and a
|
|
217
|
+
// consumer asking for help is a human either way.
|
|
93
218
|
log.info(HELP);
|
|
94
219
|
return 0;
|
|
95
220
|
}
|
|
96
221
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
222
|
+
// #357 — COMPOSE the registry, never `registerCoreTypes` alone. The db, ui-web and
|
|
223
|
+
// documentation providers register attrs onto types the core provider declares, so a
|
|
224
|
+
// partially-composed registry reports a type that exists with most of its attributes
|
|
225
|
+
// missing: `field.string` came back with 6 attrs instead of 16 (no @column, @filterable,
|
|
226
|
+
// @sortable, @dbColumnType), `view.textarea` with none (no @rows), and the eight
|
|
227
|
+
// documentation commonAttrs — @title among them — were absent entirely. This is the same
|
|
228
|
+
// provider set the loader composes, so what this prints is what the loader accepts.
|
|
229
|
+
const registry = composeRegistry([...coreProviders, forgeTypesProvider], { validate: true });
|
|
230
|
+
const catalog = buildVocabularyCatalog(registry);
|
|
101
231
|
|
|
102
|
-
// Flatten the
|
|
232
|
+
// Flatten the catalog into searchable entries.
|
|
103
233
|
const entries: Entry[] = [];
|
|
104
|
-
for (const mt of
|
|
234
|
+
for (const mt of catalog.types) {
|
|
105
235
|
const tsName = `${mt.type}.${mt.subType}`;
|
|
106
236
|
entries.push({
|
|
107
237
|
kind: "subtype", name: tsName, owner: tsName, type: mt.type,
|
|
108
238
|
description: mt.description, whenToUse: mt.whenToUse,
|
|
109
|
-
attrNames: mt.attrs.map((a) => `@${a.name}`),
|
|
239
|
+
attrNames: mt.attrs.map((a) => `@${a.name}`),
|
|
240
|
+
common: false, tsOnly: !mt.crossPort, sharedRoot: mt.sharedRoot, raw: mt,
|
|
110
241
|
});
|
|
111
242
|
for (const at of mt.attrs) {
|
|
112
243
|
entries.push({
|
|
113
244
|
kind: "attr", name: `${tsName} @${at.name}`, owner: tsName, type: mt.type,
|
|
114
|
-
description: at.description, whenToUse: at.whenToUse, attrNames: [],
|
|
245
|
+
description: at.description, whenToUse: at.whenToUse, attrNames: [],
|
|
246
|
+
common: false, tsOnly: !mt.crossPort, sharedRoot: false, raw: at,
|
|
115
247
|
});
|
|
116
248
|
}
|
|
117
249
|
}
|
|
250
|
+
// #357 — the attrs every node accepts. These were absent from the search entirely, so
|
|
251
|
+
// `meta types title` reported nothing for `@title` — the registered attr an author is
|
|
252
|
+
// supposed to find INSTEAD of asking for a new one (that omission is exactly how #353
|
|
253
|
+
// became a request to register `@label`). They belong to no single type, so no `--type`
|
|
254
|
+
// scope excludes them.
|
|
255
|
+
for (const at of catalog.commonAttrs) {
|
|
256
|
+
entries.push({
|
|
257
|
+
kind: "attr", name: `@${at.name}`, owner: "(any node)", type: "",
|
|
258
|
+
description: at.description, whenToUse: at.whenToUse, attrNames: [],
|
|
259
|
+
common: true, tsOnly: false, sharedRoot: false, raw: at,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
118
262
|
|
|
119
263
|
const q = (f.query ?? "").toLowerCase();
|
|
120
264
|
const wantKind = (e: Entry) => f.kind.size === 0
|
|
@@ -122,7 +266,8 @@ export async function typesCommand(args: string[]): Promise<number> {
|
|
|
122
266
|
: (f.kind.has("attr") && e.kind === "attr") ||
|
|
123
267
|
((f.kind.has("subtype") || f.kind.has("type")) && e.kind === "subtype");
|
|
124
268
|
const matches = entries.filter((e) => {
|
|
125
|
-
|
|
269
|
+
// A common attr is accepted on every type, so `--type field` must not hide it.
|
|
270
|
+
if (f.type && e.type !== f.type && !e.common) return false;
|
|
126
271
|
if (!wantKind(e)) return false;
|
|
127
272
|
if (!q) return true;
|
|
128
273
|
if (e.name.toLowerCase().includes(q)) return true;
|
|
@@ -133,6 +278,13 @@ export async function typesCommand(args: string[]): Promise<number> {
|
|
|
133
278
|
// Stable sort by (type, name).
|
|
134
279
|
matches.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
|
135
280
|
|
|
281
|
+
// Structured output branches BEFORE the no-match hint below, deliberately: an empty
|
|
282
|
+
// result must still be a valid document (`"matches": []`), not a sentence on stdout.
|
|
283
|
+
if (fmt !== "text") {
|
|
284
|
+
emitStructured(buildPayload(f, matches, catalog.metamodelVersion), fmt);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
136
288
|
if (matches.length === 0) {
|
|
137
289
|
log.info(q ? `No vocabulary matches "${f.query}". Try --all to search descriptions, or drop --kind/--type filters.`
|
|
138
290
|
: "No matching vocabulary.");
|
|
@@ -142,16 +294,14 @@ export async function typesCommand(args: string[]): Promise<number> {
|
|
|
142
294
|
const total = matches.length;
|
|
143
295
|
const shown = f.limit > 0 ? matches.slice(0, f.limit) : matches;
|
|
144
296
|
|
|
145
|
-
if (f.json) {
|
|
146
|
-
log.info(JSON.stringify(shown.map((e) => e.raw), null, 2));
|
|
147
|
-
return 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
297
|
if (f.detail) {
|
|
151
298
|
for (const e of shown) {
|
|
152
|
-
|
|
153
|
-
log.info(`\n${head} (${e.kind})`);
|
|
299
|
+
log.info(`\n${e.name} (${e.kind})`);
|
|
154
300
|
if (e.description) log.info(` ${e.description}`);
|
|
301
|
+
if (e.sharedRoot)
|
|
302
|
+
log.info(` shared root: @attrs registered here apply to every ${e.type}.* subtype.`);
|
|
303
|
+
if (e.tsOnly)
|
|
304
|
+
log.info(" TypeScript-only: registered here, but not part of the cross-port metamodel contract.");
|
|
155
305
|
if (e.whenToUse) log.info(` → reach for it when: ${e.whenToUse}`);
|
|
156
306
|
if (e.kind === "subtype") {
|
|
157
307
|
const mt = e.raw as { attrs: { name: string; valueType: string | null; required: boolean; description: string }[] };
|
|
@@ -167,12 +317,13 @@ export async function typesCommand(args: string[]): Promise<number> {
|
|
|
167
317
|
for (const e of shown) {
|
|
168
318
|
if (e.kind === "subtype") {
|
|
169
319
|
const hint = e.attrNames.length ? ` (${e.attrNames.slice(0, 6).join(", ")}${e.attrNames.length > 6 ? ", …" : ""})` : "";
|
|
170
|
-
log.info(`${e.name.padEnd(28)} ${oneLine(e.description)}${hint}`);
|
|
320
|
+
log.info(`${e.name.padEnd(28)} ${oneLine(e.description)}${hint}${marks(e)}`);
|
|
171
321
|
} else {
|
|
172
|
-
log.info(`${e.name.padEnd(28)} ${oneLine(e.description)}`);
|
|
322
|
+
log.info(`${e.name.padEnd(28)} ${oneLine(e.description)}${marks(e)}`);
|
|
173
323
|
}
|
|
174
324
|
}
|
|
175
325
|
}
|
|
326
|
+
if (!f.noHeaders && !f.detail && shown.some((e) => marks(e) !== "")) log.info(`\n${LEGEND}`);
|
|
176
327
|
if (!f.noHeaders && shown.length < total)
|
|
177
328
|
log.info(`\n${shown.length} of ${total} shown — narrow with QUERY/--type/--kind or raise --limit.`);
|
|
178
329
|
else if (!f.noHeaders)
|