@maschinenlesbar.org/marktstammdatenregister-cli 0.0.8 → 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/README.md +19 -8
- package/dist/src/cli/commands/units.d.ts +0 -1
- package/dist/src/cli/commands/units.js +30 -24
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/index.js +3 -1
- package/dist/src/cli/io.d.ts +18 -1
- package/dist/src/cli/io.js +26 -1
- package/dist/src/cli/program.d.ts +0 -1
- package/dist/src/cli/program.js +7 -7
- package/dist/src/cli/run.d.ts +10 -1
- package/dist/src/cli/run.js +27 -2
- package/dist/src/cli/shared.d.ts +33 -18
- package/dist/src/cli/shared.js +49 -58
- package/dist/src/client/client.d.ts +46 -7
- package/dist/src/client/client.js +143 -14
- package/dist/src/client/engine.d.ts +95 -14
- package/dist/src/client/engine.js +382 -56
- package/dist/src/client/errors.d.ts +18 -2
- package/dist/src/client/errors.js +54 -4
- package/dist/src/client/filter.d.ts +47 -3
- package/dist/src/client/filter.js +184 -5
- package/dist/src/client/http.d.ts +13 -1
- package/dist/src/client/http.js +58 -33
- package/dist/src/client/index.d.ts +6 -5
- package/dist/src/client/index.js +5 -5
- package/dist/src/client/query.d.ts +0 -1
- package/dist/src/client/query.js +0 -1
- package/dist/src/client/types.d.ts +4 -2
- package/dist/src/client/types.js +0 -1
- package/dist/src/client/validate.d.ts +65 -0
- package/dist/src/client/validate.js +143 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/package.json +4 -3
- package/dist/src/cli/commands/units.d.ts.map +0 -1
- package/dist/src/cli/commands/units.js.map +0 -1
- package/dist/src/cli/index.d.ts.map +0 -1
- package/dist/src/cli/index.js.map +0 -1
- package/dist/src/cli/io.d.ts.map +0 -1
- package/dist/src/cli/io.js.map +0 -1
- package/dist/src/cli/program.d.ts.map +0 -1
- package/dist/src/cli/program.js.map +0 -1
- package/dist/src/cli/run.d.ts.map +0 -1
- package/dist/src/cli/run.js.map +0 -1
- package/dist/src/cli/shared.d.ts.map +0 -1
- package/dist/src/cli/shared.js.map +0 -1
- package/dist/src/client/client.d.ts.map +0 -1
- package/dist/src/client/client.js.map +0 -1
- package/dist/src/client/engine.d.ts.map +0 -1
- package/dist/src/client/engine.js.map +0 -1
- package/dist/src/client/errors.d.ts.map +0 -1
- package/dist/src/client/errors.js.map +0 -1
- package/dist/src/client/filter.d.ts.map +0 -1
- package/dist/src/client/filter.js.map +0 -1
- package/dist/src/client/http.d.ts.map +0 -1
- package/dist/src/client/http.js.map +0 -1
- package/dist/src/client/index.d.ts.map +0 -1
- package/dist/src/client/index.js.map +0 -1
- package/dist/src/client/query.d.ts.map +0 -1
- package/dist/src/client/query.js.map +0 -1
- package/dist/src/client/types.d.ts.map +0 -1
- package/dist/src/client/types.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -27,14 +27,16 @@ npm install -g @maschinenlesbar.org/marktstammdatenregister-cli # the `mastr`
|
|
|
27
27
|
npm install @maschinenlesbar.org/marktstammdatenregister-cli
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
Requires **Node.js 22.12+**.
|
|
31
|
+
|
|
30
32
|
## CLI
|
|
31
33
|
|
|
32
34
|
Four datasets, each paged and filterable. Prints `{ total, data }` (or just the
|
|
33
35
|
count with `--total`).
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
mastr stromerzeugung --total # how many electricity-generation units? →
|
|
37
|
-
mastr stromerzeugung --filter "Energieträger~eq~'2495'" --total # …how many are solar? →
|
|
38
|
+
mastr stromerzeugung --total # how many electricity-generation units? → 9562366 (2026-10-05; it grows)
|
|
39
|
+
mastr stromerzeugung --filter "Energieträger~eq~'2495'" --total # …how many are solar? → 6545851
|
|
38
40
|
mastr stromerzeugung --sort "Bruttoleistung-desc" --page-size 20 --iso-dates
|
|
39
41
|
mastr gaserzeugung --page 1 --page-size 50
|
|
40
42
|
mastr filters stromerzeugung # the filterable columns + dropdown codes
|
|
@@ -46,12 +48,15 @@ Categories: `stromerzeugung`, `stromverbrauch`, `gaserzeugung`, `gasverbrauch`.
|
|
|
46
48
|
(ops `eq|neq|sw|ct|nct|ew|null|nn`, plus the strict `gt`/`lt` for numbers and dates);
|
|
47
49
|
there is **no working `~or~`** (the register drops everything after it, so the CLI
|
|
48
50
|
rejects it) — for several codes of one dropdown, list them in one value:
|
|
49
|
-
`Energieträger~eq~'2497,2498'`. Discover the German field names and dropdown codes with `mastr filters <category>`.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
`Energieträger~eq~'2497,2498'`. Discover the German field names and dropdown codes with `mastr filters <category>`.
|
|
52
|
+
The register silently ignores a FilterName it doesn't know (you'd get the unfiltered set)
|
|
53
|
+
and answers an unknown dropdown code with 0 rows, so the CLI checks every FilterName and
|
|
54
|
+
every `eq`/`neq` dropdown code against the category's columns (one extra request) and
|
|
55
|
+
rejects an unknown one (exit 2, with "did you mean"); a name typed with a decomposed umlaut,
|
|
56
|
+
padding or in another case is sent the register's way. A malformed spec or an unknown
|
|
57
|
+
operator (e.g. `gte`) is rejected too. Several `--filter` flags are joined with `~and~`;
|
|
58
|
+
any other option given twice is a usage error. A **wrong `--sort` key returns 0 rows**:
|
|
59
|
+
sanity-check with `--total`; sort keys are record field names
|
|
55
60
|
(`Bruttoleistung`), not FilterNames — list them with
|
|
56
61
|
`mastr stromerzeugung --page-size 1 --compact | jq '.data[0] | keys'`. **Dates** come as Microsoft
|
|
57
62
|
`/Date(ms)/` strings — add `--iso-dates` to convert them to ISO-8601.
|
|
@@ -67,9 +72,15 @@ import { MastrClient, parseMsDate } from "@maschinenlesbar.org/marktstammdatenre
|
|
|
67
72
|
const mastr = new MastrClient();
|
|
68
73
|
const solar = await mastr.stromerzeugung({ filter: "Energieträger~eq~'2495'", pageSize: 10 });
|
|
69
74
|
solar.total; // total matching units
|
|
75
|
+
await mastr.count("stromerzeugung", { filter: "Energieträger~eq~'2495'" }); // just the count, one-row request
|
|
70
76
|
parseMsDate(String(solar.data[0]?.EinheitRegistrierungsdatum)); // Date | null
|
|
71
77
|
```
|
|
72
78
|
|
|
79
|
+
The client checks a filter's FilterNames and dropdown codes against the category's columns
|
|
80
|
+
(`filterColumns()`, fetched once per client and category) and throws `MastrValidationError`
|
|
81
|
+
for one the register would ignore; `new MastrClient({ allowUnknownFilters: true })` skips that
|
|
82
|
+
check and its request. A query key it doesn't know (`filtr`) is refused the same way.
|
|
83
|
+
|
|
73
84
|
Build filters from user input with `buildFilter`, not string interpolation: a filter
|
|
74
85
|
value cannot contain `~` (the register splits on it and has no escape), so
|
|
75
86
|
`` `Ort~eq~'${input}'` `` lets an input like `Münster'~and~Energieträger~eq~'2497` add a
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `filters` command to discover the filterable columns and their dropdown codes.
|
|
4
4
|
import { Argument } from "commander";
|
|
5
5
|
import { MAX_PAGE, MAX_PAGE_SIZE } from "../../client/client.js";
|
|
6
|
-
import { action, parseBoundedInt, parseFilter,
|
|
6
|
+
import { action, once, parseBoundedInt, parseFilter, parseSort, renderJson } from "../shared.js";
|
|
7
7
|
// `sortKey` is a record field that exists in that category's rows (live 2026-09-26):
|
|
8
8
|
// only stromerzeugung rows carry Bruttoleistung/Nettonennleistung, so the stderr hint
|
|
9
9
|
// must not suggest it for the other three.
|
|
@@ -19,21 +19,22 @@ const RUN = {
|
|
|
19
19
|
gaserzeugung: (c, q) => c.gaserzeugung(q),
|
|
20
20
|
gasverbrauch: (c, q) => c.gasverbrauch(q),
|
|
21
21
|
};
|
|
22
|
+
/** Build a CountQuery (filter and sort) from this command's parsed options. */
|
|
23
|
+
function buildCountQuery(opts) {
|
|
24
|
+
const q = {};
|
|
25
|
+
if (typeof opts["sort"] === "string")
|
|
26
|
+
q.sort = opts["sort"];
|
|
27
|
+
if (typeof opts["filter"] === "string")
|
|
28
|
+
q.filter = opts["filter"];
|
|
29
|
+
return q;
|
|
30
|
+
}
|
|
22
31
|
/** Build a UnitQuery from this command's parsed options. */
|
|
23
32
|
function buildQuery(opts) {
|
|
24
|
-
const q =
|
|
33
|
+
const q = buildCountQuery(opts);
|
|
25
34
|
if (typeof opts["page"] === "number")
|
|
26
35
|
q.page = opts["page"];
|
|
27
36
|
if (typeof opts["pageSize"] === "number")
|
|
28
37
|
q.pageSize = opts["pageSize"];
|
|
29
|
-
if (typeof opts["sort"] === "string")
|
|
30
|
-
q.sort = opts["sort"];
|
|
31
|
-
if (typeof opts["filter"] === "string")
|
|
32
|
-
q.filter = opts["filter"];
|
|
33
|
-
// `--total` needs only the match count (returned regardless of page size), so
|
|
34
|
-
// request a single row instead of fetching and discarding a full page.
|
|
35
|
-
if (opts["total"] === true)
|
|
36
|
-
q.pageSize = 1;
|
|
37
38
|
return q;
|
|
38
39
|
}
|
|
39
40
|
export function registerCommands(program, deps) {
|
|
@@ -41,16 +42,22 @@ export function registerCommands(program, deps) {
|
|
|
41
42
|
program
|
|
42
43
|
.command(cat.name)
|
|
43
44
|
.description(cat.desc)
|
|
44
|
-
.option("--page <n>", "1-based page number", parseBoundedInt(1, MAX_PAGE), 1)
|
|
45
|
-
.option("--page-size <n>", `rows per page (1..${MAX_PAGE_SIZE})`, parseBoundedInt(1, MAX_PAGE_SIZE), 25)
|
|
45
|
+
.option("--page <n>", "1-based page number", once("--page", parseBoundedInt(1, MAX_PAGE)), 1)
|
|
46
|
+
.option("--page-size <n>", `rows per page (1..${MAX_PAGE_SIZE})`, once("--page-size", parseBoundedInt(1, MAX_PAGE_SIZE)), 25)
|
|
46
47
|
.option("--sort <spec>", "sort: FieldKey-asc | FieldKey-desc, where FieldKey is a record field name, not a " +
|
|
47
|
-
`FilterName (e.g. ${cat.sortKey}-desc)`,
|
|
48
|
+
`FilterName (e.g. ${cat.sortKey}-desc)`, once("--sort", parseSort))
|
|
48
49
|
.option("--filter <spec>", "filter: FilterName~op~'value'~and~… (see `filters`; ops eq|neq|sw|ct|nct|ew|null|nn|gt|lt, " +
|
|
49
|
-
"gt/lt strict; null/nn take ''). A malformed spec
|
|
50
|
-
"
|
|
51
|
-
|
|
50
|
+
"gt/lt strict; null/nn take ''). A malformed spec, an unknown op, a FilterName the " +
|
|
51
|
+
"category doesn't have or a dropdown code it doesn't list is rejected. Repeatable: " +
|
|
52
|
+
"several --filter are joined with ~and~. No ~or~: for several dropdown codes use one " +
|
|
53
|
+
"comma list, e.g. Energieträger~eq~'2497,2498'", parseFilter)
|
|
54
|
+
.option("--total", "print only the total match count, not the rows (a one-row request; --page and --page-size are ignored)")
|
|
52
55
|
.action(action(deps, async ({ client, global, opts }) => {
|
|
53
|
-
|
|
56
|
+
// `--total` is the library's count(): a one-row request for the match count.
|
|
57
|
+
const total = opts["total"] === true
|
|
58
|
+
? await client.count(cat.name, buildCountQuery(opts))
|
|
59
|
+
: undefined;
|
|
60
|
+
const page = total === undefined ? await RUN[cat.name](client, buildQuery(opts)) : { total };
|
|
54
61
|
// An unknown --sort key or --filter operator makes the server return 0 rows
|
|
55
62
|
// (not an error), which reads like "no matches". Nudge the user toward the
|
|
56
63
|
// likely cause. Sort keys are the record's field names (`Bruttoleistung`);
|
|
@@ -61,14 +68,14 @@ export function registerCommands(program, deps) {
|
|
|
61
68
|
"not the FilterNames from `mastr filters`; list them with " +
|
|
62
69
|
`\`mastr ${cat.name} --page-size 1 --compact | jq '.data[0] | keys'\`.`);
|
|
63
70
|
}
|
|
64
|
-
// An unknown operator is already a usage error
|
|
65
|
-
// that silently gives 0 rows is a value the register can't match.
|
|
71
|
+
// An unknown operator, FilterName or dropdown code is already a usage error; what
|
|
72
|
+
// is left that silently gives 0 rows is a value the register can't match.
|
|
66
73
|
if (page.total === 0 && typeof opts["filter"] === "string") {
|
|
67
|
-
deps.io.err("Note: 0 results with --filter set. If you expected matches, check the values:
|
|
68
|
-
"
|
|
69
|
-
"
|
|
74
|
+
deps.io.err("Note: 0 results with --filter set. If you expected matches, check the values: eq " +
|
|
75
|
+
"on a text column matches the whole text (ct matches a part), decimals take a " +
|
|
76
|
+
"point ('4999.999'), and gt/lt are strict.");
|
|
70
77
|
}
|
|
71
|
-
renderJson(deps, global,
|
|
78
|
+
renderJson(deps, global, total ?? page);
|
|
72
79
|
}));
|
|
73
80
|
}
|
|
74
81
|
program
|
|
@@ -79,4 +86,3 @@ export function registerCommands(program, deps) {
|
|
|
79
86
|
renderJson(deps, global, await client.filterColumns(category));
|
|
80
87
|
}));
|
|
81
88
|
}
|
|
82
|
-
//# sourceMappingURL=units.js.map
|
package/dist/src/cli/index.d.ts
CHANGED
package/dist/src/cli/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Bin shim: parse argv, run the CLI, and set the process exit code. All real
|
|
3
3
|
// logic lives in run.ts (testable without spawning a subprocess).
|
|
4
|
+
import { handleOutputErrors } from "./io.js";
|
|
4
5
|
import { run } from "./run.js";
|
|
6
|
+
// Before run(): a reader that closes the pipe early (`| head`) must not end in a stack trace.
|
|
7
|
+
handleOutputErrors();
|
|
5
8
|
run(process.argv.slice(2)).then((code) => {
|
|
6
9
|
process.exitCode = code;
|
|
7
10
|
}, (err) => {
|
|
8
11
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
9
12
|
process.exitCode = 1;
|
|
10
13
|
});
|
|
11
|
-
//# sourceMappingURL=index.js.map
|
package/dist/src/cli/io.d.ts
CHANGED
|
@@ -8,5 +8,22 @@ export interface CliDeps {
|
|
|
8
8
|
/** Build a client from the resolved global options (injectable for tests). */
|
|
9
9
|
createClient(options: MastrClientOptions): MastrClient;
|
|
10
10
|
}
|
|
11
|
+
/** The two process streams, as far as `handleOutputErrors` needs them. */
|
|
12
|
+
export interface OutputStreams {
|
|
13
|
+
stdout: Pick<NodeJS.WriteStream, "on">;
|
|
14
|
+
stderr: Pick<NodeJS.WriteStream, "on">;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Handle write errors on stdout/stderr, which Node otherwise reports as an
|
|
18
|
+
* unhandled 'error' event: a raw stack trace and exit 1.
|
|
19
|
+
*
|
|
20
|
+
* A reader that stops early — `| head`, `| jq` exiting on the first match, a closed
|
|
21
|
+
* pager — closes the pipe while the CLI is still writing, and the next write fails
|
|
22
|
+
* with EPIPE. That is ordinary use, so the process exits 0 at once, quietly. Any
|
|
23
|
+
* other stdout error prints one `Output error: <message>` line to stderr and exits
|
|
24
|
+
* 1. On stderr an EPIPE is ignored, so a failed run keeps its exit code; any other stderr
|
|
25
|
+
* error exits 1 silently (there is nowhere left to report it).
|
|
26
|
+
* The bin shim installs this once, before `run()`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function handleOutputErrors(streams?: OutputStreams, exit?: (code: number) => void): void;
|
|
11
29
|
export declare const defaultIO: CliIO;
|
|
12
|
-
//# sourceMappingURL=io.d.ts.map
|
package/dist/src/cli/io.js
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
// I/O seam for the CLI. Everything the CLI writes goes through a CliIO object so
|
|
2
2
|
// tests can capture output instead of hitting the real stdout/stderr.
|
|
3
|
+
/**
|
|
4
|
+
* Handle write errors on stdout/stderr, which Node otherwise reports as an
|
|
5
|
+
* unhandled 'error' event: a raw stack trace and exit 1.
|
|
6
|
+
*
|
|
7
|
+
* A reader that stops early — `| head`, `| jq` exiting on the first match, a closed
|
|
8
|
+
* pager — closes the pipe while the CLI is still writing, and the next write fails
|
|
9
|
+
* with EPIPE. That is ordinary use, so the process exits 0 at once, quietly. Any
|
|
10
|
+
* other stdout error prints one `Output error: <message>` line to stderr and exits
|
|
11
|
+
* 1. On stderr an EPIPE is ignored, so a failed run keeps its exit code; any other stderr
|
|
12
|
+
* error exits 1 silently (there is nowhere left to report it).
|
|
13
|
+
* The bin shim installs this once, before `run()`.
|
|
14
|
+
*/
|
|
15
|
+
export function handleOutputErrors(streams = process, exit = (code) => process.exit(code)) {
|
|
16
|
+
streams.stdout.on("error", (err) => {
|
|
17
|
+
if (err.code === "EPIPE")
|
|
18
|
+
return exit(0);
|
|
19
|
+
process.stderr.write(`Output error: ${err.message}\n`);
|
|
20
|
+
exit(1);
|
|
21
|
+
});
|
|
22
|
+
// stderr's reader going away doesn't make a failed run a success: ignore EPIPE there and
|
|
23
|
+
// let the run's own exit code stand (`2>&1 | true` must not turn a usage error into 0).
|
|
24
|
+
streams.stderr.on("error", (err) => {
|
|
25
|
+
if (err.code !== "EPIPE")
|
|
26
|
+
exit(1);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
3
29
|
export const defaultIO = {
|
|
4
30
|
out: (text) => process.stdout.write(text + "\n"),
|
|
5
31
|
err: (text) => process.stderr.write(text + "\n"),
|
|
6
32
|
};
|
|
7
|
-
//# sourceMappingURL=io.js.map
|
package/dist/src/cli/program.js
CHANGED
|
@@ -7,7 +7,8 @@ import { Command } from "commander";
|
|
|
7
7
|
import { defaultIO } from "./io.js";
|
|
8
8
|
import { MastrClient } from "../client/client.js";
|
|
9
9
|
import { MAX_TIMEOUT_MS } from "../client/http.js";
|
|
10
|
-
import {
|
|
10
|
+
import { MAX_RETRIES } from "../client/engine.js";
|
|
11
|
+
import { once, parseIntArg, parseBoundedInt, parseHeaderValue, parseBaseUrl } from "./shared.js";
|
|
11
12
|
import { registerCommands } from "./commands/units.js";
|
|
12
13
|
/**
|
|
13
14
|
* Single source of truth for the version: read from package.json at runtime
|
|
@@ -41,15 +42,14 @@ export function buildProgram(deps = defaultDeps) {
|
|
|
41
42
|
"and narrowed with --filter (see `filters` for the field names and codes). Use " +
|
|
42
43
|
"--total for just the match count, and --iso-dates to convert /Date(…)/ timestamps.")
|
|
43
44
|
.version(VERSION)
|
|
44
|
-
.option("--base-url <url>", "API base URL (http/https only)", parseBaseUrl, "https://www.marktstammdatenregister.de/MaStR")
|
|
45
|
-
.option("--timeout <ms>", "time limit per request in ms, whole response included (0 = no timeout)", parseBoundedInt(0, MAX_TIMEOUT_MS))
|
|
46
|
-
.option("--user-agent <ua>", "User-Agent header value", parseHeaderValue)
|
|
47
|
-
.option("--max-retries <n>",
|
|
48
|
-
.option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", parseIntArg)
|
|
45
|
+
.option("--base-url <url>", "API base URL (http/https only)", once("--base-url", parseBaseUrl), "https://www.marktstammdatenregister.de/MaStR")
|
|
46
|
+
.option("--timeout <ms>", "time limit per request in ms, whole response included (0 = no timeout)", once("--timeout", parseBoundedInt(0, MAX_TIMEOUT_MS)))
|
|
47
|
+
.option("--user-agent <ua>", "User-Agent header value", once("--user-agent", parseHeaderValue))
|
|
48
|
+
.option("--max-retries <n>", `retries for transient 429/503 responses and reset connections (0..${MAX_RETRIES}; each waits the server's Retry-After, up to 30 s)`, once("--max-retries", parseBoundedInt(0, MAX_RETRIES)))
|
|
49
|
+
.option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", once("--max-response-bytes", parseIntArg))
|
|
49
50
|
.option("--compact", "print JSON on a single line instead of pretty-printed")
|
|
50
51
|
.option("--iso-dates", "rewrite MaStR /Date(ms)/ timestamps to ISO-8601")
|
|
51
52
|
.showHelpAfterError();
|
|
52
53
|
registerCommands(program, deps);
|
|
53
54
|
return program;
|
|
54
55
|
}
|
|
55
|
-
//# sourceMappingURL=program.js.map
|
package/dist/src/cli/run.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import type { CliDeps } from "./io.js";
|
|
2
|
+
/**
|
|
3
|
+
* `deps` with an `io` that redacts the credentials of every argument from everything it
|
|
4
|
+
* prints. Commander echoes rejected values in its errors (`option '--base-url <url>'
|
|
5
|
+
* argument '…' is invalid`, an unknown command, a surplus argument), so whatever path a
|
|
6
|
+
* credential takes to stdout or stderr, the exact userinfo (as `credentialsIn` finds it,
|
|
7
|
+
* plus its JSON-quoted form) is replaced by `***`. A pattern alone can't delimit a password
|
|
8
|
+
* with spaces, quotes, `#`, `?` or `/`; the exact strings can. Without credentials the
|
|
9
|
+
* output passes through unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withRedactedOutput(deps: CliDeps, argv: readonly string[]): CliDeps;
|
|
2
12
|
export declare function run(argv: string[], deps?: CliDeps): Promise<number>;
|
|
3
|
-
//# sourceMappingURL=run.d.ts.map
|
package/dist/src/cli/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// captured output and exit code without spawning a subprocess.
|
|
4
4
|
import { CommanderError } from "commander";
|
|
5
5
|
import { buildProgram, defaultDeps } from "./program.js";
|
|
6
|
-
import { MastrApiError, MastrError, MastrNetworkError, MastrValidationError, } from "../client/errors.js";
|
|
6
|
+
import { MastrApiError, MastrError, MastrNetworkError, MastrValidationError, credentialsIn, redactCredentials, } from "../client/errors.js";
|
|
7
7
|
/**
|
|
8
8
|
* Process exit codes. Distinct codes let scripts tell apart a usage error, a
|
|
9
9
|
* missing resource, a transport failure, and a catch-all.
|
|
@@ -32,7 +32,33 @@ function configureTree(command, deps) {
|
|
|
32
32
|
for (const child of command.commands)
|
|
33
33
|
configureTree(child, deps);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* `deps` with an `io` that redacts the credentials of every argument from everything it
|
|
37
|
+
* prints. Commander echoes rejected values in its errors (`option '--base-url <url>'
|
|
38
|
+
* argument '…' is invalid`, an unknown command, a surplus argument), so whatever path a
|
|
39
|
+
* credential takes to stdout or stderr, the exact userinfo (as `credentialsIn` finds it,
|
|
40
|
+
* plus its JSON-quoted form) is replaced by `***`. A pattern alone can't delimit a password
|
|
41
|
+
* with spaces, quotes, `#`, `?` or `/`; the exact strings can. Without credentials the
|
|
42
|
+
* output passes through unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export function withRedactedOutput(deps, argv) {
|
|
45
|
+
// An `--option=value` token is echoed as its value alone.
|
|
46
|
+
const values = argv.map((token) => token.startsWith("-") && token.includes("=") ? token.slice(token.indexOf("=") + 1) : token);
|
|
47
|
+
const secrets = new Set();
|
|
48
|
+
for (const source of [...argv, ...values]) {
|
|
49
|
+
for (const secret of credentialsIn(source)) {
|
|
50
|
+
secrets.add(secret);
|
|
51
|
+
secrets.add(JSON.stringify(secret).slice(1, -1));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (secrets.size === 0)
|
|
55
|
+
return deps;
|
|
56
|
+
const list = [...secrets];
|
|
57
|
+
const redact = (text) => redactCredentials(text, list);
|
|
58
|
+
return { ...deps, io: { out: (text) => deps.io.out(redact(text)), err: (text) => deps.io.err(redact(text)) } };
|
|
59
|
+
}
|
|
35
60
|
export async function run(argv, deps = defaultDeps) {
|
|
61
|
+
deps = withRedactedOutput(deps, argv);
|
|
36
62
|
const program = buildProgram(deps);
|
|
37
63
|
configureTree(program, deps);
|
|
38
64
|
// A bare invocation (no command) is a help request, not an error: print help
|
|
@@ -82,4 +108,3 @@ export async function run(argv, deps = defaultDeps) {
|
|
|
82
108
|
return EXIT.OTHER;
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
|
-
//# sourceMappingURL=run.js.map
|
package/dist/src/cli/shared.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CliDeps } from "./io.js";
|
|
2
2
|
import type { MastrClientOptions } from "../client/client.js";
|
|
3
|
+
import { type Problem } from "../client/validate.js";
|
|
3
4
|
/**
|
|
4
5
|
* commander value-parser: a plain base-10 non-negative integer.
|
|
5
6
|
*
|
|
@@ -8,32 +9,47 @@ import type { MastrClientOptions } from "../client/client.js";
|
|
|
8
9
|
* literals (`0x10`, `0b10`, `1e3`), signs, padding and decimals.
|
|
9
10
|
*/
|
|
10
11
|
export declare function parseIntArg(value: string): number;
|
|
11
|
-
/**
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Build a commander value-parser from a library rule: the rule's reason becomes the
|
|
14
|
+
* usage error, a valid value passes unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseWith(problem: Problem<string>): (value: string) => string;
|
|
17
|
+
/** commander value-parser: a non-empty (after trimming) string (the library's `nonBlankProblem`). */
|
|
18
|
+
export declare const parseNonEmpty: (value: string) => string;
|
|
19
|
+
/** commander value-parser for `--sort`: the library's `sortProblem` (not blank). */
|
|
20
|
+
export declare const parseSort: (value: string) => string;
|
|
13
21
|
/**
|
|
14
22
|
* commander value-parser for `--filter`: non-blank, and not a spec the register would
|
|
15
|
-
* misread (see `filterProblem
|
|
23
|
+
* misread (see `filterProblem`, run on the `normalizeFilter`ed spec as the client sends
|
|
24
|
+
* it), so it becomes a usage error before any request. The flag can be repeated: a second
|
|
25
|
+
* `--filter` is joined to the first with `~and~` (it used to replace it silently, and
|
|
26
|
+
* "solar in Bavaria" became "every unit in Bavaria"). The FilterNames and dropdown codes
|
|
27
|
+
* are checked by the client against the category's columns.
|
|
16
28
|
*/
|
|
17
|
-
export declare function parseFilter(value: string): string;
|
|
29
|
+
export declare function parseFilter(value: string, previous?: string): string;
|
|
18
30
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
* Wrap a value-parser so its option may be given only once: commander keeps the last of a
|
|
32
|
+
* repeated option and drops the others without a word (`--sort A --sort B` sorted by B,
|
|
33
|
+
* `--page 2 --page 3` fetched page 3). A repeat is a usage error naming the flag. A fresh
|
|
34
|
+
* program is built per `run()`, so the state lives as long as one parse.
|
|
35
|
+
*/
|
|
36
|
+
export declare function once<T>(flag: string, parse: (value: string) => T): (value: string) => T;
|
|
37
|
+
/**
|
|
38
|
+
* commander value-parser for `--base-url`: the library's `baseUrlProblem` (non-blank,
|
|
39
|
+
* no whitespace, an absolute http(s) URL without a query or fragment), whose reason
|
|
40
|
+
* becomes a usage error (exit 2) naming the value the user typed. The CLI keeps no
|
|
41
|
+
* rules of its own; the client constructor enforces the same rule for library callers.
|
|
26
42
|
*/
|
|
27
|
-
export declare
|
|
43
|
+
export declare const parseBaseUrl: (value: string) => string;
|
|
28
44
|
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
29
45
|
export declare function parseBoundedInt(min: number, max: number): (value: string) => number;
|
|
30
46
|
/**
|
|
31
|
-
* commander value-parser for a value that ends up in an HTTP header (User-Agent)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
47
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent):
|
|
48
|
+
* the library's `headerValueProblem` (not blank, Latin-1 without control
|
|
49
|
+
* characters; tab is fine), whose reason becomes the usage error. The engine runs
|
|
50
|
+
* the same rule on `userAgent`.
|
|
35
51
|
*/
|
|
36
|
-
export declare
|
|
52
|
+
export declare const parseHeaderValue: (value: string) => string;
|
|
37
53
|
export interface GlobalOptions {
|
|
38
54
|
baseUrl?: string;
|
|
39
55
|
timeout?: number;
|
|
@@ -74,4 +90,3 @@ export interface ActionContext {
|
|
|
74
90
|
* the trailing options object and command instance to recover the positionals.
|
|
75
91
|
*/
|
|
76
92
|
export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
|
|
77
|
-
//# sourceMappingURL=shared.d.ts.map
|
package/dist/src/cli/shared.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { InvalidArgumentError } from "commander";
|
|
4
4
|
import { isoifyDates } from "../client/client.js";
|
|
5
5
|
import { MastrParseError } from "../client/errors.js";
|
|
6
|
-
import { filterProblem } from "../client/filter.js";
|
|
6
|
+
import { filterProblem, normalizeFilter } from "../client/filter.js";
|
|
7
7
|
import { isBidiControl } from "../client/engine.js";
|
|
8
|
+
import { baseUrlProblem, headerValueProblem, nonBlankProblem, sortProblem, } from "../client/validate.js";
|
|
8
9
|
/**
|
|
9
10
|
* commander value-parser: a plain base-10 non-negative integer.
|
|
10
11
|
*
|
|
@@ -22,60 +23,59 @@ export function parseIntArg(value) {
|
|
|
22
23
|
}
|
|
23
24
|
return n;
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return value
|
|
26
|
+
/**
|
|
27
|
+
* Build a commander value-parser from a library rule: the rule's reason becomes the
|
|
28
|
+
* usage error, a valid value passes unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export function parseWith(problem) {
|
|
31
|
+
return (value) => {
|
|
32
|
+
const reason = problem(value);
|
|
33
|
+
if (reason !== undefined)
|
|
34
|
+
throw new InvalidArgumentError(reason);
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
31
37
|
}
|
|
38
|
+
/** commander value-parser: a non-empty (after trimming) string (the library's `nonBlankProblem`). */
|
|
39
|
+
export const parseNonEmpty = parseWith(nonBlankProblem);
|
|
40
|
+
/** commander value-parser for `--sort`: the library's `sortProblem` (not blank). */
|
|
41
|
+
export const parseSort = parseWith(sortProblem);
|
|
32
42
|
/**
|
|
33
43
|
* commander value-parser for `--filter`: non-blank, and not a spec the register would
|
|
34
|
-
* misread (see `filterProblem
|
|
44
|
+
* misread (see `filterProblem`, run on the `normalizeFilter`ed spec as the client sends
|
|
45
|
+
* it), so it becomes a usage error before any request. The flag can be repeated: a second
|
|
46
|
+
* `--filter` is joined to the first with `~and~` (it used to replace it silently, and
|
|
47
|
+
* "solar in Bavaria" became "every unit in Bavaria"). The FilterNames and dropdown codes
|
|
48
|
+
* are checked by the client against the category's columns.
|
|
35
49
|
*/
|
|
36
|
-
export function parseFilter(value) {
|
|
50
|
+
export function parseFilter(value, previous) {
|
|
37
51
|
parseNonEmpty(value);
|
|
38
|
-
const problem = filterProblem(value);
|
|
52
|
+
const problem = filterProblem(normalizeFilter(value));
|
|
39
53
|
if (problem !== undefined)
|
|
40
54
|
throw new InvalidArgumentError(problem);
|
|
41
|
-
return value
|
|
55
|
+
return previous === undefined ? value : `${previous}~and~${value}`;
|
|
42
56
|
}
|
|
43
57
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `data:` / `ftp:`) into a friendly usage error (exit 2) with the value the user
|
|
49
|
-
* actually typed, rather than a network error (exit 6) echoing the fully built
|
|
50
|
-
* request URL. The transport check stays the enforcement point for library callers.
|
|
58
|
+
* Wrap a value-parser so its option may be given only once: commander keeps the last of a
|
|
59
|
+
* repeated option and drops the others without a word (`--sort A --sort B` sorted by B,
|
|
60
|
+
* `--page 2 --page 3` fetched page 3). A repeat is a usage error naming the flag. A fresh
|
|
61
|
+
* program is built per `run()`, so the state lives as long as one parse.
|
|
51
62
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
throw new InvalidArgumentError("Expected an absolute http(s) URL.");
|
|
63
|
-
}
|
|
64
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
65
|
-
throw new InvalidArgumentError("Only http and https URLs are supported.");
|
|
66
|
-
}
|
|
67
|
-
// Paths are appended to the base URL as a string, so a query or fragment would
|
|
68
|
-
// swallow every request path ("http://h/#f" requests "/" for every command).
|
|
69
|
-
if (/[?#]/.test(value)) {
|
|
70
|
-
throw new InvalidArgumentError("A base URL cannot have a query (?) or fragment (#).");
|
|
71
|
-
}
|
|
72
|
-
// new URL() trims surrounding whitespace silently; the raw value is what the
|
|
73
|
-
// engine uses, so reject it rather than guess.
|
|
74
|
-
if (value !== value.trim()) {
|
|
75
|
-
throw new InvalidArgumentError("A base URL cannot have surrounding whitespace.");
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
63
|
+
export function once(flag, parse) {
|
|
64
|
+
let seen = false;
|
|
65
|
+
return (value) => {
|
|
66
|
+
if (seen)
|
|
67
|
+
throw new InvalidArgumentError(`${flag} was given more than once; give it once.`);
|
|
68
|
+
seen = true;
|
|
69
|
+
return parse(value);
|
|
70
|
+
};
|
|
78
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* commander value-parser for `--base-url`: the library's `baseUrlProblem` (non-blank,
|
|
74
|
+
* no whitespace, an absolute http(s) URL without a query or fragment), whose reason
|
|
75
|
+
* becomes a usage error (exit 2) naming the value the user typed. The CLI keeps no
|
|
76
|
+
* rules of its own; the client constructor enforces the same rule for library callers.
|
|
77
|
+
*/
|
|
78
|
+
export const parseBaseUrl = parseWith(baseUrlProblem);
|
|
79
79
|
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
80
80
|
export function parseBoundedInt(min, max) {
|
|
81
81
|
return (value) => {
|
|
@@ -88,20 +88,12 @@ export function parseBoundedInt(min, max) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* commander value-parser for a value that ends up in an HTTP header (User-Agent)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent):
|
|
92
|
+
* the library's `headerValueProblem` (not blank, Latin-1 without control
|
|
93
|
+
* characters; tab is fine), whose reason becomes the usage error. The engine runs
|
|
94
|
+
* the same rule on `userAgent`.
|
|
95
95
|
*/
|
|
96
|
-
export
|
|
97
|
-
for (let i = 0; i < value.length; i++) {
|
|
98
|
-
const c = value.charCodeAt(i);
|
|
99
|
-
if ((c < 0x20 && c !== 0x09) || c === 0x7f) {
|
|
100
|
-
throw new InvalidArgumentError("Value contains control characters.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
96
|
+
export const parseHeaderValue = parseWith(headerValueProblem);
|
|
105
97
|
/** Translate resolved global CLI options into client options. */
|
|
106
98
|
export function toEngineOptions(global) {
|
|
107
99
|
const options = {};
|
|
@@ -176,4 +168,3 @@ export function action(deps, fn) {
|
|
|
176
168
|
await fn({ client, global, opts: command.opts() }, positionals);
|
|
177
169
|
};
|
|
178
170
|
}
|
|
179
|
-
//# sourceMappingURL=shared.js.map
|