@hawkeyexl/manni 0.0.0-bootstrap.0 → 0.2.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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/chunk-2OALUJV2.js +10820 -0
- package/dist/chunk-2OALUJV2.js.map +1 -0
- package/dist/chunk-OXH2ZM7E.js +1050 -0
- package/dist/chunk-OXH2ZM7E.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +914 -0
- package/dist/cli.js.map +1 -0
- package/dist/docmeta.d.ts +2 -0
- package/dist/docmeta.js +10 -0
- package/dist/docmeta.js.map +1 -0
- package/dist/index.d.ts +1623 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/package.json +104 -3
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
COMMON_FORMATS,
|
|
4
|
+
COMMON_FORMAT_LIST,
|
|
5
|
+
DEFAULT_VENDOR_DIR,
|
|
6
|
+
DocmetaError,
|
|
7
|
+
FILL_FORMATS,
|
|
8
|
+
FILL_FORMAT_LIST,
|
|
9
|
+
OMITTED_WHEN_CLEAN,
|
|
10
|
+
QUERY_FORMATS,
|
|
11
|
+
QUERY_FORMAT_LIST,
|
|
12
|
+
REPORT_FORMATS,
|
|
13
|
+
REPORT_FORMAT_LIST,
|
|
14
|
+
ToolError,
|
|
15
|
+
getSchemasInfo,
|
|
16
|
+
isCommonFormat,
|
|
17
|
+
isFillFormat,
|
|
18
|
+
isMachineFormat,
|
|
19
|
+
isParamName,
|
|
20
|
+
isQueryFindingsFormat,
|
|
21
|
+
isQueryFormat,
|
|
22
|
+
isReportFormat,
|
|
23
|
+
package_default,
|
|
24
|
+
palette,
|
|
25
|
+
programName,
|
|
26
|
+
render,
|
|
27
|
+
renderFill,
|
|
28
|
+
renderGet,
|
|
29
|
+
renderQuery,
|
|
30
|
+
renderQueryCsv,
|
|
31
|
+
rowsToFindings,
|
|
32
|
+
runFill,
|
|
33
|
+
runGet,
|
|
34
|
+
runInferSchema,
|
|
35
|
+
runQuery,
|
|
36
|
+
runValidate,
|
|
37
|
+
runVendorSchema,
|
|
38
|
+
setProgramName,
|
|
39
|
+
shouldColor,
|
|
40
|
+
supportedExtensions,
|
|
41
|
+
toJsonText
|
|
42
|
+
} from "./chunk-2OALUJV2.js";
|
|
43
|
+
|
|
44
|
+
// src/shared/run.ts
|
|
45
|
+
import { realpathSync } from "node:fs";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
|
+
import { CommanderError } from "commander";
|
|
48
|
+
function fail(err) {
|
|
49
|
+
const msg = err instanceof ToolError ? err.message : `Unexpected error: ${err.message}`;
|
|
50
|
+
process.stderr.write(`${programName()}: ${msg}
|
|
51
|
+
`);
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
async function runProgram(program, argv = process.argv) {
|
|
55
|
+
setProgramName(program.name());
|
|
56
|
+
try {
|
|
57
|
+
await program.parseAsync(argv);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof CommanderError) {
|
|
60
|
+
process.exitCode = err.exitCode === 0 ? 0 : 2;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isMainModule(moduleUrl) {
|
|
67
|
+
const entry = process.argv[1];
|
|
68
|
+
if (!entry) return false;
|
|
69
|
+
try {
|
|
70
|
+
return realpathSync(entry) === fileURLToPath(moduleUrl);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function runIfMain(moduleUrl, build) {
|
|
76
|
+
if (!isMainModule(moduleUrl)) return;
|
|
77
|
+
runProgram(build()).catch(fail);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/meta/cli.ts
|
|
81
|
+
import { existsSync } from "node:fs";
|
|
82
|
+
import { basename, extname, relative, resolve as resolvePath } from "node:path";
|
|
83
|
+
import { Command, Option } from "commander";
|
|
84
|
+
import picomatch from "picomatch";
|
|
85
|
+
|
|
86
|
+
// src/meta/reporters/infer.ts
|
|
87
|
+
var OUTLIERS_SHOWN = 5;
|
|
88
|
+
var ENUM_SHOWN = 3;
|
|
89
|
+
var SAMPLE_WIDTH = 46;
|
|
90
|
+
function count(n) {
|
|
91
|
+
return n.toLocaleString("en-US");
|
|
92
|
+
}
|
|
93
|
+
function percent(n) {
|
|
94
|
+
return `${n.toFixed(1)}%`;
|
|
95
|
+
}
|
|
96
|
+
function truncate(text, width) {
|
|
97
|
+
return text.length <= width ? text : `${text.slice(0, width - 1)}\u2026`;
|
|
98
|
+
}
|
|
99
|
+
function sampleCell(key) {
|
|
100
|
+
if (key.enumValues) {
|
|
101
|
+
const shown = key.enumValues.slice(0, ENUM_SHOWN).map((v) => typeof v === "string" ? v : JSON.stringify(v));
|
|
102
|
+
if (key.enumValues.length > ENUM_SHOWN) shown.push("\u2026");
|
|
103
|
+
return truncate(shown.join(" | "), SAMPLE_WIDTH);
|
|
104
|
+
}
|
|
105
|
+
if (key.sample === void 0) return "";
|
|
106
|
+
return truncate(toJsonText(key.sample) ?? "", SAMPLE_WIDTH);
|
|
107
|
+
}
|
|
108
|
+
function typesCell(key) {
|
|
109
|
+
if (key.types.length > 1) {
|
|
110
|
+
return key.types.map((t) => `${t.type} \xD7${count(t.count)}`).join(", ");
|
|
111
|
+
}
|
|
112
|
+
if (key.enumValues) return `${key.dominantType} (${key.enumValues.length} enum)`;
|
|
113
|
+
if (key.format) return `${key.dominantType} (${key.format})`;
|
|
114
|
+
return key.dominantType;
|
|
115
|
+
}
|
|
116
|
+
function renderInfer(result, opts = {}) {
|
|
117
|
+
const c = palette(opts.color ?? false);
|
|
118
|
+
const lines = [];
|
|
119
|
+
const headline = [
|
|
120
|
+
`${count(result.filesScanned)} file${result.filesScanned === 1 ? "" : "s"} scanned`
|
|
121
|
+
];
|
|
122
|
+
headline.push(
|
|
123
|
+
`${count(result.filesWithoutMetadata)} with no metadata block`
|
|
124
|
+
);
|
|
125
|
+
if (result.unreadable.length > 0) {
|
|
126
|
+
headline.push(`${count(result.unreadable.length)} unreadable`);
|
|
127
|
+
}
|
|
128
|
+
if (result.gitignoreSkipped > 0) {
|
|
129
|
+
headline.push(`${count(result.gitignoreSkipped)} skipped by .gitignore`);
|
|
130
|
+
}
|
|
131
|
+
lines.push(headline.join(c.dim(" \xB7 ")));
|
|
132
|
+
if (result.keys.length === 0) {
|
|
133
|
+
lines.push("", c.dim("No metadata keys found."));
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
const rows = result.keys.map((k) => ({
|
|
137
|
+
key: k.key,
|
|
138
|
+
coverage: percent(k.coverage),
|
|
139
|
+
types: typesCell(k),
|
|
140
|
+
sample: sampleCell(k)
|
|
141
|
+
}));
|
|
142
|
+
const width = (pick, head) => Math.max(head.length, ...rows.map((r) => pick(r).length));
|
|
143
|
+
const wKey = width((r) => r.key, "key");
|
|
144
|
+
const wCov = width((r) => r.coverage, "coverage");
|
|
145
|
+
const wTypes = width((r) => r.types, "types");
|
|
146
|
+
lines.push(
|
|
147
|
+
"",
|
|
148
|
+
c.dim(
|
|
149
|
+
`${"key".padEnd(wKey)} ${"coverage".padStart(wCov)} ${"types".padEnd(wTypes)} sample`
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
for (const r of rows) {
|
|
153
|
+
lines.push(
|
|
154
|
+
`${c.cyan(r.key.padEnd(wKey))} ${r.coverage.padStart(wCov)} ${r.types.padEnd(wTypes)} ${c.dim(r.sample)}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (result.hiddenByMinCoverage > 0) {
|
|
158
|
+
lines.push(
|
|
159
|
+
"",
|
|
160
|
+
c.dim(
|
|
161
|
+
`${count(result.hiddenByMinCoverage)} key(s) below --min-coverage are not shown.`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const withOutliers = result.keys.filter((k) => k.outlierCount > 0);
|
|
166
|
+
if (withOutliers.length > 0) {
|
|
167
|
+
lines.push(
|
|
168
|
+
"",
|
|
169
|
+
c.bold("Type outliers \u2014 these are data errors, not a union:")
|
|
170
|
+
);
|
|
171
|
+
for (const k of withOutliers) {
|
|
172
|
+
for (const o of k.outliers.slice(0, OUTLIERS_SHOWN)) {
|
|
173
|
+
const at = o.line === void 0 ? o.file : `${o.file}:${o.line}`;
|
|
174
|
+
lines.push(` ${k.key} ${at} ${o.type} (expected ${k.dominantType})`);
|
|
175
|
+
}
|
|
176
|
+
const rest = k.outlierCount - Math.min(k.outliers.length, OUTLIERS_SHOWN);
|
|
177
|
+
if (rest > 0) lines.push(c.dim(` ${k.key} \u2026and ${count(rest)} more`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const u of result.unreadable) {
|
|
181
|
+
lines.push("", `${c.yellow("!")} ${u.file}: ${u.message}`);
|
|
182
|
+
}
|
|
183
|
+
if (result.out !== void 0) {
|
|
184
|
+
const properties = Object.keys(result.draft.properties ?? {}).length;
|
|
185
|
+
lines.push(
|
|
186
|
+
"",
|
|
187
|
+
`Wrote a draft schema to ${c.cyan(result.out)} \u2014 ${count(properties)} propert${properties === 1 ? "y" : "ies"}, nothing required.`,
|
|
188
|
+
c.dim(
|
|
189
|
+
"Nothing is required on purpose: what your docset contains is not the same as what your standard should demand. Promote a key with `validate --write-baseline` behind it."
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/meta/cli.ts
|
|
197
|
+
function collect(value, prev) {
|
|
198
|
+
return prev.concat([value]);
|
|
199
|
+
}
|
|
200
|
+
async function readStdin() {
|
|
201
|
+
const chunks = [];
|
|
202
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
203
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
204
|
+
}
|
|
205
|
+
function resolveColor(program) {
|
|
206
|
+
const noColor = program.opts().color === false;
|
|
207
|
+
return shouldColor({ noColor, isTTY: process.stdout.isTTY });
|
|
208
|
+
}
|
|
209
|
+
function formatFor(command, own) {
|
|
210
|
+
if (command.getOptionValueSource("format") === "cli") return own;
|
|
211
|
+
const parent = command.parent;
|
|
212
|
+
if (parent && parent.getOptionValueSource("format") === "cli") {
|
|
213
|
+
return parent.opts().format;
|
|
214
|
+
}
|
|
215
|
+
return own;
|
|
216
|
+
}
|
|
217
|
+
function assertCommonFormat(value) {
|
|
218
|
+
const format = String(value);
|
|
219
|
+
if (!isCommonFormat(format)) {
|
|
220
|
+
throw new DocmetaError(
|
|
221
|
+
`Unknown --format "${format}". Use ${COMMON_FORMAT_LIST}.`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return format;
|
|
225
|
+
}
|
|
226
|
+
var STDIN = "-";
|
|
227
|
+
function looksLikePath(token, cwd, alone) {
|
|
228
|
+
if (token === STDIN) return false;
|
|
229
|
+
const scan = picomatch.scan(token);
|
|
230
|
+
const shaped = /[\\/]/.test(token) || extname(token) !== "" || scan.isGlob;
|
|
231
|
+
if ((shaped || alone) && existsSync(resolvePath(cwd, token))) return true;
|
|
232
|
+
if (token.includes(",") || token.startsWith("/")) return false;
|
|
233
|
+
if (scan.isGlob) return true;
|
|
234
|
+
const ext = extname(token).toLowerCase();
|
|
235
|
+
if (ext !== "" && supportedExtensions().includes(ext)) return true;
|
|
236
|
+
return /[\\/]/.test(token);
|
|
237
|
+
}
|
|
238
|
+
function resolveGetInputs(fieldsArg, pathsArg, fieldsOption, cwd) {
|
|
239
|
+
const flag = typeof fieldsOption === "string" ? fieldsOption : void 0;
|
|
240
|
+
if (flag === void 0 && fieldsArg !== void 0 && looksLikePath(fieldsArg, cwd, pathsArg.length === 0)) {
|
|
241
|
+
throw new DocmetaError(
|
|
242
|
+
`"${fieldsArg}" looks like a path, not a field list. Pass fields first (manni meta get title ${fieldsArg}) or use --fields.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const source = flag ?? (fieldsArg === STDIN ? void 0 : fieldsArg);
|
|
246
|
+
const fields = source === void 0 ? [] : splitList(source);
|
|
247
|
+
if (fields.length === 0) {
|
|
248
|
+
throw new DocmetaError(
|
|
249
|
+
"Specify at least one field to get. Pass fields first (manni meta get title docs/a.md) or use --fields."
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const paths = fieldsArg !== void 0 && (flag !== void 0 || fieldsArg === STDIN) ? [fieldsArg, ...pathsArg] : pathsArg;
|
|
253
|
+
return { fields, paths };
|
|
254
|
+
}
|
|
255
|
+
function resolveQueryInputs(sqlArg, pathsArg, queryOption, cwd, sqlOptional = false) {
|
|
256
|
+
const flag = typeof queryOption === "string" ? queryOption : void 0;
|
|
257
|
+
if (flag === void 0 && sqlArg !== void 0 && // A token with whitespace is never a path: every real statement has
|
|
258
|
+
// spaces, and without this test `SELECT count(*) FROM docs` reads as a
|
|
259
|
+
// *glob* — picomatch parses `(*)` as an extglob — the one SQL shape the
|
|
260
|
+
// comma test inside looksLikePath does not catch.
|
|
261
|
+
!/\s/.test(sqlArg) && // With SQL optional there is no "forgot the statement" reading to
|
|
262
|
+
// protect, so a shapeless token that really exists on disk is a path
|
|
263
|
+
// even when other paths follow — hence `alone` is forced on.
|
|
264
|
+
looksLikePath(sqlArg, cwd, sqlOptional || pathsArg.length === 0)) {
|
|
265
|
+
if (sqlOptional) return { sql: "", paths: [sqlArg, ...pathsArg] };
|
|
266
|
+
throw new DocmetaError(
|
|
267
|
+
`"${sqlArg}" looks like a path, not SQL. Pass the SQL first (manni meta query "SELECT _path FROM docs" ${sqlArg}) or use --query.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const source = flag ?? (sqlArg === STDIN ? void 0 : sqlArg);
|
|
271
|
+
const sql = source?.trim() ?? "";
|
|
272
|
+
if (sql === "" && !sqlOptional) {
|
|
273
|
+
throw new DocmetaError(
|
|
274
|
+
'Specify SQL to run. Pass it first (manni meta query "SELECT _path FROM docs" docs/) or use --query.'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const paths = sqlArg !== void 0 && (flag !== void 0 || sqlArg === STDIN) ? [sqlArg, ...pathsArg] : pathsArg;
|
|
278
|
+
return { sql, paths };
|
|
279
|
+
}
|
|
280
|
+
function parseQueryParams(raw) {
|
|
281
|
+
const params = /* @__PURE__ */ Object.create(null);
|
|
282
|
+
for (const item of raw) {
|
|
283
|
+
const typed = item.indexOf(":=");
|
|
284
|
+
const plain = item.indexOf("=");
|
|
285
|
+
const useTyped = typed !== -1 && (plain === -1 || typed < plain);
|
|
286
|
+
const at = useTyped ? typed : plain;
|
|
287
|
+
if (at <= 0) {
|
|
288
|
+
throw new DocmetaError(
|
|
289
|
+
`--param "${item}" is not name=value. Use name=value to bind a string, or name:=json for a typed value.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const name = item.slice(0, at);
|
|
293
|
+
const bare = /^[$:@]/.test(name) ? name.slice(1) : name;
|
|
294
|
+
if (!isParamName(bare)) {
|
|
295
|
+
throw new DocmetaError(
|
|
296
|
+
`--param name "${name}" cannot be bound: a SQL named parameter is letters, digits, and underscores, starting with a letter or underscore.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
const value = item.slice(at + (useTyped ? 2 : 1));
|
|
300
|
+
if (!useTyped) {
|
|
301
|
+
params[name] = value;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
params[name] = JSON.parse(value);
|
|
306
|
+
} catch {
|
|
307
|
+
throw new DocmetaError(
|
|
308
|
+
`--param ${name}:=${value} is not valid JSON. Quote a string \u2014 --param '${name}:="${value}"' \u2014 or drop the colon (--param ${name}=${value}) to bind it as a string.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return params;
|
|
313
|
+
}
|
|
314
|
+
function renderQueryFindings(run, format) {
|
|
315
|
+
if (run.changes) {
|
|
316
|
+
throw new DocmetaError(
|
|
317
|
+
`--format ${format} renders result rows with a \`path\` column; this statement produced pending changes, not rows. Use pretty or json for the change preview.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const results = [...rowsToFindings(
|
|
321
|
+
"query",
|
|
322
|
+
run.columns,
|
|
323
|
+
run.rows
|
|
324
|
+
)].map(([file, errors]) => ({
|
|
325
|
+
file,
|
|
326
|
+
format: "query",
|
|
327
|
+
ok: false,
|
|
328
|
+
schemas: [],
|
|
329
|
+
errors
|
|
330
|
+
}));
|
|
331
|
+
const summary = {
|
|
332
|
+
files: results.length,
|
|
333
|
+
passed: 0,
|
|
334
|
+
failed: results.length,
|
|
335
|
+
errors: results.reduce((n, r) => n + r.errors.length, 0)
|
|
336
|
+
};
|
|
337
|
+
const text = render(format, results, summary, {
|
|
338
|
+
frame: run.frame,
|
|
339
|
+
classname: "manni.query",
|
|
340
|
+
onNotice: notice
|
|
341
|
+
});
|
|
342
|
+
if (text.length > 0 || !OMITTED_WHEN_CLEAN.has(format)) {
|
|
343
|
+
process.stdout.write(`${text}
|
|
344
|
+
`);
|
|
345
|
+
}
|
|
346
|
+
process.exitCode = run.rows.length > 0 ? 1 : 0;
|
|
347
|
+
}
|
|
348
|
+
var COMMAND_NAMES = ["validate", "get", "query", "fill", "schemas"];
|
|
349
|
+
function editDistance(a, b) {
|
|
350
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
351
|
+
for (let i = 1; i <= a.length; i++) {
|
|
352
|
+
const row = [i];
|
|
353
|
+
for (let j = 1; j <= b.length; j++) {
|
|
354
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
355
|
+
row.push(
|
|
356
|
+
Math.min(
|
|
357
|
+
(row[j - 1] ?? 0) + 1,
|
|
358
|
+
(prev[j] ?? 0) + 1,
|
|
359
|
+
(prev[j - 1] ?? 0) + cost
|
|
360
|
+
)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
prev = row;
|
|
364
|
+
}
|
|
365
|
+
return prev[b.length] ?? 0;
|
|
366
|
+
}
|
|
367
|
+
function suggestCommand(token, cwd) {
|
|
368
|
+
if (token === void 0 || token === "-") return;
|
|
369
|
+
if (/[\\/.]/.test(token)) return;
|
|
370
|
+
if (picomatch.scan(token).isGlob) return;
|
|
371
|
+
if (existsSync(resolvePath(cwd, token))) return;
|
|
372
|
+
const near = COMMAND_NAMES.map(
|
|
373
|
+
(name) => [name, editDistance(token.toLowerCase(), name)]
|
|
374
|
+
).filter(([, d]) => d > 0 && d <= 2).sort((x, y) => x[1] - y[1]);
|
|
375
|
+
const best = near[0];
|
|
376
|
+
if (!best) return;
|
|
377
|
+
throw new DocmetaError(
|
|
378
|
+
`Unknown command "${token}". Did you mean "${best[0]}"?`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
function configOption(value) {
|
|
382
|
+
if (value === false) return { noConfig: true };
|
|
383
|
+
return typeof value === "string" ? { configPath: value } : {};
|
|
384
|
+
}
|
|
385
|
+
function reportConfig(toStdout, cwd) {
|
|
386
|
+
return (info) => {
|
|
387
|
+
const where = (relative(cwd, info.dir) || ".").replace(/\\/g, "/");
|
|
388
|
+
const line = `Using ${basename(info.path)} (${where})
|
|
389
|
+
`;
|
|
390
|
+
(toStdout ? process.stdout : process.stderr).write(line);
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function notice(message) {
|
|
394
|
+
process.stderr.write(`${programName()}: ${message}
|
|
395
|
+
`);
|
|
396
|
+
}
|
|
397
|
+
function gitignoreFlag(value) {
|
|
398
|
+
return value === false ? false : void 0;
|
|
399
|
+
}
|
|
400
|
+
function splitList(value) {
|
|
401
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
402
|
+
}
|
|
403
|
+
function numeric(name, value, min, max) {
|
|
404
|
+
if (value === void 0) return;
|
|
405
|
+
if (!Number.isFinite(value) || value < min || value > max) {
|
|
406
|
+
throw new DocmetaError(
|
|
407
|
+
`${name} must be a number between ${min} and ${max}, got ${value}.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function buildProgram() {
|
|
412
|
+
const program = new Command();
|
|
413
|
+
program.name("meta").description(
|
|
414
|
+
"Validate the presence and format of document metadata against JSON Schema."
|
|
415
|
+
).version(package_default.version, "-V, --version").option("--no-color", "disable colored output").showHelpAfterError("(add --help for usage)").exitOverride();
|
|
416
|
+
program.command("validate", { isDefault: true }).description("Validate metadata in the given files/dirs/globs").argument(
|
|
417
|
+
"[paths...]",
|
|
418
|
+
"files, directories, or globs to validate (use - for stdin)"
|
|
419
|
+
).option(
|
|
420
|
+
"-s, --schema <ref>",
|
|
421
|
+
"schema to validate against; repeatable; overrides $schema/config",
|
|
422
|
+
collect,
|
|
423
|
+
[]
|
|
424
|
+
).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option(
|
|
425
|
+
"-f, --format <format>",
|
|
426
|
+
`output: ${REPORT_FORMATS.join(" | ")}`,
|
|
427
|
+
"pretty"
|
|
428
|
+
).option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file").option("-q, --quiet", "in pretty output, hide passing files").option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "check files .gitignore covers").option(
|
|
429
|
+
"--offline",
|
|
430
|
+
"never fetch a remote schema; resolve URL refs from the schema cache"
|
|
431
|
+
).addOption(
|
|
432
|
+
new Option(
|
|
433
|
+
"--baseline [path]",
|
|
434
|
+
"compare against a baseline; fail only on new findings"
|
|
435
|
+
)
|
|
436
|
+
).addOption(
|
|
437
|
+
new Option(
|
|
438
|
+
"--write-baseline [path]",
|
|
439
|
+
"record current findings as the baseline, then exit 0 (stdin findings are not recorded and still fail)"
|
|
440
|
+
)
|
|
441
|
+
).option("--no-baseline", "ignore a baseline configured by `baseline:`").option(
|
|
442
|
+
"--no-checks",
|
|
443
|
+
"skip the corpus checks configured by `checks:` for this run"
|
|
444
|
+
).addHelpText(
|
|
445
|
+
"after",
|
|
446
|
+
[
|
|
447
|
+
"",
|
|
448
|
+
"Examples:",
|
|
449
|
+
" manni meta validate docs/ # walk a directory",
|
|
450
|
+
' manni meta validate "**/*.md" -f github # CI annotations',
|
|
451
|
+
' manni meta validate "**/*.md" -f sarif > o.sarif # code scanning',
|
|
452
|
+
" manni meta validate page.md -s google:okf:0.1 -s ./my.schema.json",
|
|
453
|
+
" cat page.md | manni meta validate - --as markdown",
|
|
454
|
+
" manni meta validate --write-baseline # record today's backlog",
|
|
455
|
+
" manni meta validate --baseline # fail only on new findings"
|
|
456
|
+
].join("\n")
|
|
457
|
+
).action(
|
|
458
|
+
async (paths, options, command) => {
|
|
459
|
+
try {
|
|
460
|
+
suggestCommand(paths[0], process.cwd());
|
|
461
|
+
const format = options.format;
|
|
462
|
+
if (!isReportFormat(format)) {
|
|
463
|
+
throw new DocmetaError(
|
|
464
|
+
`Unknown --format "${format}". Use ${REPORT_FORMAT_LIST}.`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const exts = options.ext ? splitList(options.ext) : void 0;
|
|
468
|
+
const stdinContent = paths.includes("-") ? await readStdin() : void 0;
|
|
469
|
+
const { results, summary, frame } = await runValidate({
|
|
470
|
+
inputs: paths,
|
|
471
|
+
cliSchemas: options.schema,
|
|
472
|
+
exts,
|
|
473
|
+
exclude: options.exclude,
|
|
474
|
+
as: options.as,
|
|
475
|
+
...configOption(options.config),
|
|
476
|
+
onConfigLoaded: reportConfig(
|
|
477
|
+
!isMachineFormat(format),
|
|
478
|
+
process.cwd()
|
|
479
|
+
),
|
|
480
|
+
stdinContent,
|
|
481
|
+
// `undefined` rather than `false` when the flag is absent, so config
|
|
482
|
+
// `allowEmpty:` still wins (the cores do `opts ?? config`).
|
|
483
|
+
allowEmpty: options.allowEmpty ? true : void 0,
|
|
484
|
+
respectGitignore: gitignoreFlag(options.gitignore),
|
|
485
|
+
// `undefined` rather than `false` when absent, so config `offline:`
|
|
486
|
+
// still decides.
|
|
487
|
+
offline: options.offline ? true : void 0,
|
|
488
|
+
onNotice: notice,
|
|
489
|
+
// `--baseline` and `--no-baseline` share one commander attribute, the
|
|
490
|
+
// same three-state `undefined | string | false` split as `-c` /
|
|
491
|
+
// `--no-config`: absent leaves the config in charge, a string names a
|
|
492
|
+
// file, `false` suppresses a configured one for this run.
|
|
493
|
+
baseline: options.baseline,
|
|
494
|
+
writeBaseline: options.writeBaseline,
|
|
495
|
+
// Like `gitignore`: only the explicit `--no-checks` travels, so
|
|
496
|
+
// absence stays "run them when the corpus rule allows".
|
|
497
|
+
checks: options.checks ? void 0 : false
|
|
498
|
+
});
|
|
499
|
+
const color = resolveColor(command.parent ?? command);
|
|
500
|
+
const text = render(format, results, summary, {
|
|
501
|
+
color,
|
|
502
|
+
quiet: Boolean(options.quiet),
|
|
503
|
+
// Non-presentational: SARIF needs to know where the run stood before
|
|
504
|
+
// it can name a file the way the repository does.
|
|
505
|
+
frame,
|
|
506
|
+
onNotice: notice
|
|
507
|
+
});
|
|
508
|
+
if (text.length > 0 || !OMITTED_WHEN_CLEAN.has(format)) {
|
|
509
|
+
process.stdout.write(`${text}
|
|
510
|
+
`);
|
|
511
|
+
}
|
|
512
|
+
process.exitCode = summary.failed > 0 ? 1 : 0;
|
|
513
|
+
} catch (err) {
|
|
514
|
+
fail(err);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
program.command("get").description("Print metadata field values from the given files/dirs/globs").argument(
|
|
519
|
+
"[fields]",
|
|
520
|
+
"comma-separated metadata fields to print, unless --fields is given"
|
|
521
|
+
).argument(
|
|
522
|
+
"[paths...]",
|
|
523
|
+
"files, directories, or globs to read (use - for stdin)"
|
|
524
|
+
).option(
|
|
525
|
+
"--fields <list>",
|
|
526
|
+
"comma-separated metadata fields to print; every positional is then a path"
|
|
527
|
+
).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option(
|
|
528
|
+
"-f, --format <format>",
|
|
529
|
+
`output: ${COMMON_FORMATS.join(" | ")}`,
|
|
530
|
+
"pretty"
|
|
531
|
+
).option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file").option(
|
|
532
|
+
"-q, --quiet",
|
|
533
|
+
"in pretty output, hide files where every requested field is unset"
|
|
534
|
+
).option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "read files .gitignore covers").option(
|
|
535
|
+
"--offline",
|
|
536
|
+
"accepted and ignored: get loads no schema, so there is nothing to fetch"
|
|
537
|
+
).addHelpText(
|
|
538
|
+
"after",
|
|
539
|
+
[
|
|
540
|
+
"",
|
|
541
|
+
"Examples:",
|
|
542
|
+
" manni meta get title,type docs/intro.md",
|
|
543
|
+
" manni meta get --fields title,type docs/intro.md",
|
|
544
|
+
" manni meta get author.name,/author/email docs/intro.md",
|
|
545
|
+
' manni meta get type "**/*.md" -f json',
|
|
546
|
+
" cat page.md | manni meta get title - --as markdown"
|
|
547
|
+
].join("\n")
|
|
548
|
+
).action(
|
|
549
|
+
async (fieldsArg, pathsArg, options, command) => {
|
|
550
|
+
try {
|
|
551
|
+
const format = assertCommonFormat(options.format);
|
|
552
|
+
const { fields, paths } = resolveGetInputs(
|
|
553
|
+
fieldsArg,
|
|
554
|
+
pathsArg,
|
|
555
|
+
options.fields,
|
|
556
|
+
process.cwd()
|
|
557
|
+
);
|
|
558
|
+
const exts = options.ext ? splitList(options.ext) : void 0;
|
|
559
|
+
const stdinContent = paths.includes("-") ? await readStdin() : void 0;
|
|
560
|
+
const results = await runGet({
|
|
561
|
+
fields,
|
|
562
|
+
inputs: paths,
|
|
563
|
+
as: options.as,
|
|
564
|
+
exclude: options.exclude,
|
|
565
|
+
exts,
|
|
566
|
+
...configOption(options.config),
|
|
567
|
+
onConfigLoaded: reportConfig(format === "pretty", process.cwd()),
|
|
568
|
+
stdinContent,
|
|
569
|
+
allowEmpty: options.allowEmpty ? true : void 0,
|
|
570
|
+
respectGitignore: gitignoreFlag(options.gitignore),
|
|
571
|
+
offline: options.offline ? true : void 0,
|
|
572
|
+
onNotice: notice
|
|
573
|
+
});
|
|
574
|
+
switch (format) {
|
|
575
|
+
case "json":
|
|
576
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
577
|
+
`);
|
|
578
|
+
break;
|
|
579
|
+
case "pretty": {
|
|
580
|
+
const text = renderGet(results, fields, {
|
|
581
|
+
color: resolveColor(command.parent ?? command),
|
|
582
|
+
quiet: Boolean(options.quiet)
|
|
583
|
+
});
|
|
584
|
+
if (text.length > 0) process.stdout.write(`${text}
|
|
585
|
+
`);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
default: {
|
|
589
|
+
const unreachable = format;
|
|
590
|
+
throw new DocmetaError(
|
|
591
|
+
`Unknown --format ${JSON.stringify(unreachable)}. Use ${COMMON_FORMAT_LIST}.`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
process.exitCode = results.some((r) => r.error !== void 0) ? 1 : 0;
|
|
596
|
+
} catch (err) {
|
|
597
|
+
fail(err);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
program.command("query").description("Run SQL over the metadata of the given files/dirs/globs").argument("[sql]", "SQL to run against the `docs` table, unless --query is given").argument(
|
|
602
|
+
"[paths...]",
|
|
603
|
+
"files, directories, or globs to load (use - for stdin)"
|
|
604
|
+
).option(
|
|
605
|
+
"--query <sql>",
|
|
606
|
+
"SQL to run against the `docs` table; every positional is then a path"
|
|
607
|
+
).option(
|
|
608
|
+
"-s, --schema <ref>",
|
|
609
|
+
"schema set a DDL statement evolves; repeatable; replaces the per-file resolution for the DDL planner only",
|
|
610
|
+
collect,
|
|
611
|
+
[]
|
|
612
|
+
).option(
|
|
613
|
+
"--check",
|
|
614
|
+
"treat returned rows as findings: exit 1 if the query returns any"
|
|
615
|
+
).option(
|
|
616
|
+
"--param <name=value>",
|
|
617
|
+
"bind a named SQL parameter ($name/:name/@name) as a string; name:=value parses the value as JSON; repeatable",
|
|
618
|
+
collect,
|
|
619
|
+
[]
|
|
620
|
+
).option(
|
|
621
|
+
"--db <path>",
|
|
622
|
+
"also write the built database to this file; SQL is then optional"
|
|
623
|
+
).option(
|
|
624
|
+
"--dry-run",
|
|
625
|
+
"preview a mutating statement's diff without applying it (--check implies this)"
|
|
626
|
+
).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option(
|
|
627
|
+
"-f, --format <format>",
|
|
628
|
+
`output: ${QUERY_FORMATS.join(" | ")} (github, sarif, junit need --check)`,
|
|
629
|
+
"pretty"
|
|
630
|
+
).option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file").option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "load files .gitignore covers").option(
|
|
631
|
+
"--offline",
|
|
632
|
+
"accepted and ignored: query resolves schemas from disk and built-ins, never the network"
|
|
633
|
+
).addHelpText(
|
|
634
|
+
"after",
|
|
635
|
+
[
|
|
636
|
+
"",
|
|
637
|
+
"One row per file in a table named `docs`: your top-level metadata keys",
|
|
638
|
+
"as columns, plus _path, _format, _present, and _data (all metadata as",
|
|
639
|
+
"JSON, for json_each/->> reach into nested values).",
|
|
640
|
+
"",
|
|
641
|
+
"Examples:",
|
|
642
|
+
' manni meta query "SELECT _path, title FROM docs WHERE draft = 1" docs/',
|
|
643
|
+
' manni meta query "SELECT t.value tag, count(*) n FROM docs, json_each(docs.tags) t GROUP BY tag" docs/',
|
|
644
|
+
' manni meta query --check "SELECT slug, count(*) n FROM docs GROUP BY slug HAVING n > 1" docs/',
|
|
645
|
+
' manni meta query -f csv "SELECT _path, title, last_reviewed FROM docs" docs/ > stale.csv',
|
|
646
|
+
` manni meta query --param author="O'Brien" "SELECT _path FROM docs WHERE author = \\$author" docs/`,
|
|
647
|
+
' cat page.md | manni meta query "SELECT title FROM docs" - --as markdown',
|
|
648
|
+
" manni meta query --db docs.db docs/ # export only; open with any SQLite UI"
|
|
649
|
+
].join("\n")
|
|
650
|
+
).action(
|
|
651
|
+
async (sqlArg, pathsArg, options, command) => {
|
|
652
|
+
try {
|
|
653
|
+
const format = options.format;
|
|
654
|
+
if (!isQueryFormat(format)) {
|
|
655
|
+
throw new DocmetaError(
|
|
656
|
+
`Unknown --format "${format}". Use ${QUERY_FORMAT_LIST}.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (isQueryFindingsFormat(format) && !options.check) {
|
|
660
|
+
throw new DocmetaError(
|
|
661
|
+
`--format ${format} renders findings, which only --check produces. Add --check, or use pretty, json, or csv.`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const { sql, paths } = resolveQueryInputs(
|
|
665
|
+
sqlArg,
|
|
666
|
+
pathsArg,
|
|
667
|
+
options.query,
|
|
668
|
+
process.cwd(),
|
|
669
|
+
typeof options.db === "string"
|
|
670
|
+
);
|
|
671
|
+
if (format === "csv" && sql === "") {
|
|
672
|
+
throw new DocmetaError(
|
|
673
|
+
"--format csv renders result rows, and a --db export without SQL produces none. Use pretty or json for the export summary."
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (isQueryFindingsFormat(format) && sql === "") {
|
|
677
|
+
throw new DocmetaError(
|
|
678
|
+
`--format ${format} renders findings, which need a statement to produce rows \u2014 a --db export without SQL has none. Pass the SQL, or drop --check -f ${format}.`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
if (options.param.length > 0 && sql === "") {
|
|
682
|
+
throw new DocmetaError(
|
|
683
|
+
"--param needs a statement to bind into; a --db export without SQL references no parameters. Pass the SQL, or drop --param."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (options.schema.length > 0 && sql === "") {
|
|
687
|
+
throw new DocmetaError(
|
|
688
|
+
"-s names the schema set DDL evolves; without SQL there is no statement to evolve it. Pass the SQL, or drop -s."
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const params = parseQueryParams(options.param);
|
|
692
|
+
const exts = options.ext ? splitList(options.ext) : void 0;
|
|
693
|
+
const stdinContent = paths.includes(STDIN) ? await readStdin() : void 0;
|
|
694
|
+
const dryRun = Boolean(options.dryRun) || Boolean(options.check) || format === "csv";
|
|
695
|
+
const run = await runQuery({
|
|
696
|
+
sql,
|
|
697
|
+
db: options.db,
|
|
698
|
+
dryRun,
|
|
699
|
+
...Object.keys(params).length > 0 ? { params } : {},
|
|
700
|
+
...options.schema.length > 0 ? { schemas: options.schema } : {},
|
|
701
|
+
inputs: paths,
|
|
702
|
+
as: options.as,
|
|
703
|
+
exclude: options.exclude,
|
|
704
|
+
exts,
|
|
705
|
+
...configOption(options.config),
|
|
706
|
+
onConfigLoaded: reportConfig(format === "pretty", process.cwd()),
|
|
707
|
+
stdinContent,
|
|
708
|
+
allowEmpty: options.allowEmpty ? true : void 0,
|
|
709
|
+
respectGitignore: gitignoreFlag(options.gitignore),
|
|
710
|
+
offline: options.offline ? true : void 0,
|
|
711
|
+
onNotice: notice
|
|
712
|
+
});
|
|
713
|
+
if (run.db && sql === "") {
|
|
714
|
+
process.stdout.write(
|
|
715
|
+
format === "json" ? `${JSON.stringify(run.db, null, 2)}
|
|
716
|
+
` : `Wrote ${run.db.path} (${run.db.files} files, ${run.db.columns} columns)
|
|
717
|
+
`
|
|
718
|
+
);
|
|
719
|
+
process.exitCode = 0;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (run.db) {
|
|
723
|
+
notice(`wrote ${run.db.path} (${run.db.files} files)`);
|
|
724
|
+
}
|
|
725
|
+
if (isQueryFindingsFormat(format)) {
|
|
726
|
+
renderQueryFindings(run, format);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
switch (format) {
|
|
730
|
+
case "csv":
|
|
731
|
+
if (run.changes) {
|
|
732
|
+
throw new DocmetaError(
|
|
733
|
+
"--format csv renders result rows; this statement produced changes, which have no tabular shape \u2014 nothing was applied. Use pretty or json to preview them, and run without -f csv to apply."
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
process.stdout.write(`${renderQueryCsv(run)}
|
|
737
|
+
`);
|
|
738
|
+
break;
|
|
739
|
+
case "json":
|
|
740
|
+
process.stdout.write(
|
|
741
|
+
`${JSON.stringify(run.changes ?? run.rows, null, 2)}
|
|
742
|
+
`
|
|
743
|
+
);
|
|
744
|
+
break;
|
|
745
|
+
case "pretty": {
|
|
746
|
+
const text = renderQuery(run, {
|
|
747
|
+
color: resolveColor(command.parent ?? command),
|
|
748
|
+
check: Boolean(options.check),
|
|
749
|
+
dryRun
|
|
750
|
+
});
|
|
751
|
+
if (text.length > 0) process.stdout.write(`${text}
|
|
752
|
+
`);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
default: {
|
|
756
|
+
const unreachable = format;
|
|
757
|
+
throw new DocmetaError(
|
|
758
|
+
`Unknown --format ${JSON.stringify(unreachable)}. Use ${QUERY_FORMAT_LIST}.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const findings = run.changes ? run.changes.length : run.rows.length;
|
|
763
|
+
process.exitCode = options.check && findings > 0 ? 1 : 0;
|
|
764
|
+
} catch (err) {
|
|
765
|
+
fail(err);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
program.command("fill").description(
|
|
770
|
+
"Infer missing or invalid metadata and write values above the confidence threshold"
|
|
771
|
+
).argument(
|
|
772
|
+
"[paths...]",
|
|
773
|
+
"files, directories, or globs to fill (use - for stdin)"
|
|
774
|
+
).option(
|
|
775
|
+
"-s, --schema <ref>",
|
|
776
|
+
"schema to fill against; repeatable; overrides $schema/config",
|
|
777
|
+
collect,
|
|
778
|
+
[]
|
|
779
|
+
).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option("--fields <list>", "comma-separated fields to fill").option("--confidence <n>", "minimum confidence to write (0-1)", parseFloat).option("--dry-run", "report proposals without writing them").option(
|
|
780
|
+
"--provider <name>",
|
|
781
|
+
"provider: auto (default), anthropic, openai, claude-cli, llama-cpp, mock"
|
|
782
|
+
).option(
|
|
783
|
+
"--model <model>",
|
|
784
|
+
"model override; needs a named provider, from here or config"
|
|
785
|
+
).option("--no-cache", "bypass the proposal cache").option(
|
|
786
|
+
"--local",
|
|
787
|
+
"run inference on this machine; refuses any hosted provider, claude-cli included"
|
|
788
|
+
).option("--max-turns <n>", "stop after this many inference calls", parseFloat).option(
|
|
789
|
+
"--chunk-chars <n>",
|
|
790
|
+
"characters of document per call (default 12000)",
|
|
791
|
+
parseFloat
|
|
792
|
+
).option("--concurrency <n>", "files inferred in parallel", parseFloat).option(
|
|
793
|
+
"-f, --format <format>",
|
|
794
|
+
`output: ${FILL_FORMATS.join(" | ")}`,
|
|
795
|
+
"pretty"
|
|
796
|
+
).option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file").option(
|
|
797
|
+
"-q, --quiet",
|
|
798
|
+
"in pretty output, hide files with nothing written and nothing required left undone"
|
|
799
|
+
).option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "fill files .gitignore covers").option(
|
|
800
|
+
"--offline",
|
|
801
|
+
"never fetch a remote schema; resolve URL refs from the schema cache"
|
|
802
|
+
).addHelpText(
|
|
803
|
+
"after",
|
|
804
|
+
[
|
|
805
|
+
"",
|
|
806
|
+
"Examples:",
|
|
807
|
+
" manni meta fill docs/ --dry-run # preview proposals",
|
|
808
|
+
" manni meta fill docs/ --confidence 0.9 # only near-certain values",
|
|
809
|
+
" manni meta fill page.md --fields description",
|
|
810
|
+
" manni meta fill docs/ -f github # CI annotations",
|
|
811
|
+
" cat page.md | manni meta fill - --as markdown # filled doc to stdout"
|
|
812
|
+
].join("\n")
|
|
813
|
+
).action(async (paths, options, command) => {
|
|
814
|
+
try {
|
|
815
|
+
const format = options.format;
|
|
816
|
+
if (!isFillFormat(format)) {
|
|
817
|
+
throw new DocmetaError(
|
|
818
|
+
`Unknown --format "${format}". Use ${FILL_FORMAT_LIST}.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
numeric("--confidence", options.confidence, 0, 1);
|
|
822
|
+
numeric("--max-turns", options.maxTurns, 1, Number.MAX_SAFE_INTEGER);
|
|
823
|
+
numeric(
|
|
824
|
+
"--chunk-chars",
|
|
825
|
+
options.chunkChars,
|
|
826
|
+
1,
|
|
827
|
+
Number.MAX_SAFE_INTEGER
|
|
828
|
+
);
|
|
829
|
+
numeric("--concurrency", options.concurrency, 1, 64);
|
|
830
|
+
const exts = options.ext ? splitList(options.ext) : void 0;
|
|
831
|
+
const usingStdin = paths.includes(STDIN);
|
|
832
|
+
if (usingStdin && format === "github") {
|
|
833
|
+
throw new DocmetaError(
|
|
834
|
+
'Cannot use --format github with stdin ("-"): the filled document owns stdout, so the annotations would go to stderr, where GitHub ignores them. Pass file paths instead.'
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const stdinContent = usingStdin ? await readStdin() : void 0;
|
|
838
|
+
const run = await runFill({
|
|
839
|
+
inputs: paths,
|
|
840
|
+
cliSchemas: options.schema,
|
|
841
|
+
exts,
|
|
842
|
+
exclude: options.exclude,
|
|
843
|
+
as: options.as,
|
|
844
|
+
...configOption(options.config),
|
|
845
|
+
// With `-` the filled document owns stdout, so the notice is a
|
|
846
|
+
// diagnostic there just as the report is.
|
|
847
|
+
onConfigLoaded: reportConfig(
|
|
848
|
+
format === "pretty" && !usingStdin,
|
|
849
|
+
process.cwd()
|
|
850
|
+
),
|
|
851
|
+
stdinContent,
|
|
852
|
+
allowEmpty: options.allowEmpty ? true : void 0,
|
|
853
|
+
respectGitignore: gitignoreFlag(options.gitignore),
|
|
854
|
+
offline: options.offline ? true : void 0,
|
|
855
|
+
onNotice: notice,
|
|
856
|
+
fields: options.fields ? splitList(options.fields) : void 0,
|
|
857
|
+
confidence: options.confidence,
|
|
858
|
+
dryRun: Boolean(options.dryRun),
|
|
859
|
+
provider: options.provider,
|
|
860
|
+
model: options.model,
|
|
861
|
+
cache: options.cache,
|
|
862
|
+
local: Boolean(options.local),
|
|
863
|
+
maxTurns: options.maxTurns,
|
|
864
|
+
chunkChars: options.chunkChars,
|
|
865
|
+
concurrency: options.concurrency,
|
|
866
|
+
includeContent: usingStdin
|
|
867
|
+
});
|
|
868
|
+
const color = resolveColor(command.parent ?? command);
|
|
869
|
+
const report = renderFill(format, run, {
|
|
870
|
+
color,
|
|
871
|
+
quiet: Boolean(options.quiet)
|
|
872
|
+
});
|
|
873
|
+
if (usingStdin) {
|
|
874
|
+
const filled = run.results[0]?.content;
|
|
875
|
+
if (filled != null) process.stdout.write(filled);
|
|
876
|
+
if (report.length > 0) process.stderr.write(`${report}
|
|
877
|
+
`);
|
|
878
|
+
} else if (report.length > 0) {
|
|
879
|
+
process.stdout.write(`${report}
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
process.exitCode = run.summary.requiredSkipped > 0 || run.summary.errors > 0 ? 1 : 0;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
fail(err);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
const schemas = program.command("schemas").description("List built-in schemas and supported input formats").option(
|
|
888
|
+
"-f, --format <format>",
|
|
889
|
+
`output: ${COMMON_FORMATS.join(" | ")}`,
|
|
890
|
+
"pretty"
|
|
891
|
+
).action((options, command) => {
|
|
892
|
+
try {
|
|
893
|
+
const format = assertCommonFormat(options.format);
|
|
894
|
+
const info = getSchemasInfo();
|
|
895
|
+
if (format === "json") {
|
|
896
|
+
process.stdout.write(`${JSON.stringify(info, null, 2)}
|
|
897
|
+
`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const c = palette(resolveColor(command.parent ?? command));
|
|
901
|
+
const lines = [c.bold("Built-in schemas:")];
|
|
902
|
+
for (const b of info.builtins) {
|
|
903
|
+
lines.push(` ${c.cyan(b.id)} ${c.dim("\u2014")} ${b.title}`);
|
|
904
|
+
}
|
|
905
|
+
lines.push("", c.bold("Input formats:"));
|
|
906
|
+
for (const f of info.formats) {
|
|
907
|
+
const tags = [
|
|
908
|
+
f.implemented ? c.green("implemented") : c.dim("planned")
|
|
909
|
+
];
|
|
910
|
+
if (f.implemented) {
|
|
911
|
+
tags.push(f.writable ? c.green("writable") : c.dim("read-only"));
|
|
912
|
+
}
|
|
913
|
+
lines.push(
|
|
914
|
+
` ${f.name} (${f.extensions.join(", ")}) [${tags.join(", ")}]`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
process.stdout.write(`${lines.join("\n")}
|
|
918
|
+
`);
|
|
919
|
+
} catch (err) {
|
|
920
|
+
fail(err);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
schemas.command("infer").description(
|
|
924
|
+
"Report metadata coverage across a docset, and draft a schema from what is there"
|
|
925
|
+
).argument(
|
|
926
|
+
"[paths...]",
|
|
927
|
+
"files, directories, or globs to scan (use - for stdin)"
|
|
928
|
+
).option("--out <path>", "write the draft schema here").option(
|
|
929
|
+
"--min-coverage <pct>",
|
|
930
|
+
"hide keys below this coverage percentage",
|
|
931
|
+
parseFloat,
|
|
932
|
+
0
|
|
933
|
+
).option("--ext <list>", "comma-separated extensions for directory walks").option("--exclude <glob>", "glob to exclude; repeatable", collect, []).option("--as <format>", "force an input format (e.g. markdown, mdx)").option(
|
|
934
|
+
"-f, --format <format>",
|
|
935
|
+
`output: ${COMMON_FORMATS.join(" | ")}`,
|
|
936
|
+
"pretty"
|
|
937
|
+
).option("-c, --config <path>", "path to a manni config file").option("--no-config", "ignore any discovered config file").option("--allow-empty", "treat zero matched files as success").option("--no-gitignore", "scan files .gitignore covers").option(
|
|
938
|
+
"--offline",
|
|
939
|
+
"accepted and ignored: infer loads no schema and no provider"
|
|
940
|
+
).addHelpText(
|
|
941
|
+
"after",
|
|
942
|
+
[
|
|
943
|
+
"",
|
|
944
|
+
"Examples:",
|
|
945
|
+
" manni meta schemas infer docs/ # what does this repo have?",
|
|
946
|
+
" manni meta schemas infer docs/ --min-coverage 5 # drop the long tail",
|
|
947
|
+
" manni meta schemas infer docs/ --out ./schemas/permissive.json",
|
|
948
|
+
" manni meta schemas infer docs/ -f json",
|
|
949
|
+
"",
|
|
950
|
+
"Purely statistical and offline: no provider, no network, no model. The",
|
|
951
|
+
"draft never marks anything `required` \u2014 coverage is your decision to",
|
|
952
|
+
"make, not the tool's."
|
|
953
|
+
].join("\n")
|
|
954
|
+
).action(async (paths, options, command) => {
|
|
955
|
+
try {
|
|
956
|
+
const format = assertCommonFormat(formatFor(command, options.format));
|
|
957
|
+
numeric("--min-coverage", options.minCoverage, 0, 100);
|
|
958
|
+
const exts = options.ext ? splitList(options.ext) : void 0;
|
|
959
|
+
const stdinContent = paths.includes(STDIN) ? await readStdin() : void 0;
|
|
960
|
+
const result = await runInferSchema({
|
|
961
|
+
inputs: paths,
|
|
962
|
+
exts,
|
|
963
|
+
exclude: options.exclude,
|
|
964
|
+
as: options.as,
|
|
965
|
+
...configOption(options.config),
|
|
966
|
+
onConfigLoaded: reportConfig(format === "pretty", process.cwd()),
|
|
967
|
+
stdinContent,
|
|
968
|
+
out: options.out,
|
|
969
|
+
minCoverage: options.minCoverage,
|
|
970
|
+
// Same shape as the other four input-taking commands: only an
|
|
971
|
+
// explicit `--allow-empty` / `--no-gitignore` travels, so config
|
|
972
|
+
// `allowEmpty:` and `respectGitignore:` still decide otherwise.
|
|
973
|
+
allowEmpty: options.allowEmpty ? true : void 0,
|
|
974
|
+
respectGitignore: gitignoreFlag(options.gitignore),
|
|
975
|
+
// Accepted and ignored — `infer` never fetches. `undefined` rather
|
|
976
|
+
// than `false` all the same, so this stays the same shape as the
|
|
977
|
+
// other four if it ever does gain a meaning.
|
|
978
|
+
offline: options.offline ? true : void 0,
|
|
979
|
+
onNotice: notice
|
|
980
|
+
});
|
|
981
|
+
if (format === "json") {
|
|
982
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
983
|
+
`);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const text = renderInfer(result, {
|
|
987
|
+
color: resolveColor(command.parent?.parent ?? command)
|
|
988
|
+
});
|
|
989
|
+
process.stdout.write(`${text}
|
|
990
|
+
`);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
fail(err);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
schemas.command("vendor").description(
|
|
996
|
+
"Download a remote schema into this repository and pin it in config"
|
|
997
|
+
).argument("<url>", "http(s) URL of the schema to vendor").option(
|
|
998
|
+
"--dir <path>",
|
|
999
|
+
"directory for the committed copy",
|
|
1000
|
+
DEFAULT_VENDOR_DIR
|
|
1001
|
+
).option("-c, --config <path>", "path to a manni config file").addHelpText(
|
|
1002
|
+
"after",
|
|
1003
|
+
[
|
|
1004
|
+
"",
|
|
1005
|
+
"Examples:",
|
|
1006
|
+
" manni meta schemas vendor https://schemas.example.com/house/2.1.json",
|
|
1007
|
+
" manni meta schemas vendor https://schemas.example.com/house/2.1.json --dir ./contracts",
|
|
1008
|
+
"",
|
|
1009
|
+
"Commit both the downloaded file and the config change: the point of",
|
|
1010
|
+
"vendoring is that CI validates against a copy in your own history."
|
|
1011
|
+
].join("\n")
|
|
1012
|
+
).action(async (url, options) => {
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await runVendorSchema({
|
|
1015
|
+
url,
|
|
1016
|
+
dir: options.dir,
|
|
1017
|
+
// `typeof === "string"`, not `!== undefined`. `vendor` is the one
|
|
1018
|
+
// config-taking command with no `--no-config`, and the parity rule in
|
|
1019
|
+
// CLAUDE.md points straight at adding one — at which point commander
|
|
1020
|
+
// starts passing `false` here. The annotation on `VendorCliOptions`
|
|
1021
|
+
// says that cannot happen, so it would not be caught at the point the
|
|
1022
|
+
// flag is declared; this test is what keeps a `false` out of a field
|
|
1023
|
+
// that must be a path.
|
|
1024
|
+
...typeof options.config === "string" ? { configPath: options.config } : {},
|
|
1025
|
+
onNotice: notice
|
|
1026
|
+
});
|
|
1027
|
+
const configNote = result.configCreated ? "created" : result.replaced ? "reference updated" : "reference added";
|
|
1028
|
+
process.stdout.write(
|
|
1029
|
+
[
|
|
1030
|
+
`Vendored ${result.url}`,
|
|
1031
|
+
` file ${result.file} (${result.bytes} bytes${result.unchanged ? ", unchanged" : ""})`,
|
|
1032
|
+
` integrity ${result.integrity}`,
|
|
1033
|
+
` config ${result.config} (${configNote})`,
|
|
1034
|
+
"",
|
|
1035
|
+
`Commit ${result.file} and ${result.config} so CI validates against this copy.`
|
|
1036
|
+
].join("\n") + "\n"
|
|
1037
|
+
);
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
fail(err);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
return program;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export {
|
|
1046
|
+
fail,
|
|
1047
|
+
runIfMain,
|
|
1048
|
+
buildProgram
|
|
1049
|
+
};
|
|
1050
|
+
//# sourceMappingURL=chunk-OXH2ZM7E.js.map
|