@maschinenlesbar.org/marktstammdatenregister-cli 0.0.1
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/CONTRIBUTING.md +25 -0
- package/LICENSE +661 -0
- package/LICENSING.md +47 -0
- package/README.md +72 -0
- package/dist/src/cli/commands/units.d.ts +4 -0
- package/dist/src/cli/commands/units.d.ts.map +1 -0
- package/dist/src/cli/commands/units.js +64 -0
- package/dist/src/cli/commands/units.js.map +1 -0
- package/dist/src/cli/index.d.ts +3 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +11 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/io.d.ts +12 -0
- package/dist/src/cli/io.d.ts.map +1 -0
- package/dist/src/cli/io.js +7 -0
- package/dist/src/cli/io.js.map +1 -0
- package/dist/src/cli/program.d.ts +7 -0
- package/dist/src/cli/program.d.ts.map +1 -0
- package/dist/src/cli/program.js +54 -0
- package/dist/src/cli/program.js.map +1 -0
- package/dist/src/cli/run.d.ts +3 -0
- package/dist/src/cli/run.d.ts.map +1 -0
- package/dist/src/cli/run.js +85 -0
- package/dist/src/cli/run.js.map +1 -0
- package/dist/src/cli/shared.d.ts +53 -0
- package/dist/src/cli/shared.d.ts.map +1 -0
- package/dist/src/cli/shared.js +96 -0
- package/dist/src/cli/shared.js.map +1 -0
- package/dist/src/client/client.d.ts +35 -0
- package/dist/src/client/client.d.ts.map +1 -0
- package/dist/src/client/client.js +109 -0
- package/dist/src/client/client.js.map +1 -0
- package/dist/src/client/engine.d.ts +55 -0
- package/dist/src/client/engine.d.ts.map +1 -0
- package/dist/src/client/engine.js +114 -0
- package/dist/src/client/engine.js.map +1 -0
- package/dist/src/client/errors.d.ts +42 -0
- package/dist/src/client/errors.d.ts.map +1 -0
- package/dist/src/client/errors.js +52 -0
- package/dist/src/client/errors.js.map +1 -0
- package/dist/src/client/http.d.ts +26 -0
- package/dist/src/client/http.d.ts.map +1 -0
- package/dist/src/client/http.js +80 -0
- package/dist/src/client/http.js.map +1 -0
- package/dist/src/client/index.d.ts +11 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +8 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/query.d.ts +9 -0
- package/dist/src/client/query.d.ts.map +1 -0
- package/dist/src/client/query.js +33 -0
- package/dist/src/client/query.js.map +1 -0
- package/dist/src/client/types.d.ts +103 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +4 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSING.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Licensing
|
|
2
|
+
|
|
3
|
+
`marktstammdatenregister-cli` is **dual-licensed**. You may use it under **either** of the
|
|
4
|
+
following, at your choice:
|
|
5
|
+
|
|
6
|
+
## 1. Open source — AGPL-3.0-or-later (default)
|
|
7
|
+
|
|
8
|
+
Free to use, modify, and distribute under the
|
|
9
|
+
[GNU Affero General Public License v3.0 or later](LICENSE).
|
|
10
|
+
|
|
11
|
+
The key obligation is the AGPL's **§13 network clause**: if you run a modified
|
|
12
|
+
version of this software to provide a service to others over a network, you must
|
|
13
|
+
make the complete corresponding source code of your modified version available
|
|
14
|
+
to the users of that service. The usual copyleft terms also apply when you
|
|
15
|
+
distribute the software or works based on it.
|
|
16
|
+
|
|
17
|
+
This is the right option for open-source projects, internal and personal use,
|
|
18
|
+
evaluation, and anyone able to comply with the AGPL.
|
|
19
|
+
|
|
20
|
+
## 2. Commercial license
|
|
21
|
+
|
|
22
|
+
If the AGPL's obligations do not fit your situation — for example, you want to:
|
|
23
|
+
|
|
24
|
+
- embed `marktstammdatenregister-cli` in a **closed-source / proprietary product**, or
|
|
25
|
+
- run a **modified version as a proprietary network service (SaaS)** without
|
|
26
|
+
disclosing your source, or
|
|
27
|
+
- keep your modifications private,
|
|
28
|
+
|
|
29
|
+
then a separate **commercial license** is available. It grants use of the same
|
|
30
|
+
code without the AGPL's copyleft and network-disclosure obligations, under
|
|
31
|
+
negotiated terms.
|
|
32
|
+
|
|
33
|
+
**To obtain a commercial license, contact:** sebs@2xs.org
|
|
34
|
+
|
|
35
|
+
## Why this is possible
|
|
36
|
+
|
|
37
|
+
The copyright holder owns 100% of the source code in this repository and can
|
|
38
|
+
therefore offer it under more than one license simultaneously. This project does
|
|
39
|
+
**not** accept external code contributions (see [CONTRIBUTING.md](CONTRIBUTING.md)),
|
|
40
|
+
which keeps copyright ownership single and unambiguous — a prerequisite for
|
|
41
|
+
offering the commercial option. The only third-party dependency that ships with
|
|
42
|
+
the package, [`commander`](https://github.com/tj/commander.js), is MIT-licensed
|
|
43
|
+
and compatible with both licensing options.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
Copyright © 2026 Sebastian Schürmann. All rights reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# marktstammdatenregister-cli
|
|
2
|
+
|
|
3
|
+
A dependency-light **TypeScript client + CLI** for the **Marktstammdatenregister
|
|
4
|
+
(MaStR)** — the Bundesnetzagentur's register of the German electricity & gas market:
|
|
5
|
+
~9 million generation and consumption units (solar, wind, storage, CHP, …). Wraps
|
|
6
|
+
the public "erweiterte öffentliche Einheitensuche". A [bund.dev](https://bund.dev) API.
|
|
7
|
+
|
|
8
|
+
- **No API key.** The public search is open.
|
|
9
|
+
- **Zero runtime HTTP dependencies.** Built on `node:http`/`https`; the CLI's only
|
|
10
|
+
runtime dependency is `commander`.
|
|
11
|
+
- **Library + CLI.** Use the typed `MastrClient`, or the `mastr` command.
|
|
12
|
+
|
|
13
|
+
> **We provide the tool, not the data.** MaStR data is © the Bundesnetzagentur under
|
|
14
|
+
> **DL-DE-BY-2.0** — free to use with attribution. See [DATA_LICENSE.md](DATA_LICENSE.md).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @maschinenlesbar.org/marktstammdatenregister-cli # the `mastr` command
|
|
20
|
+
# or as a library:
|
|
21
|
+
npm install @maschinenlesbar.org/marktstammdatenregister-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CLI
|
|
25
|
+
|
|
26
|
+
Four datasets, each paged and filterable. Prints `{ total, data }` (or just the
|
|
27
|
+
count with `--total`).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
mastr stromerzeugung --total # how many electricity-generation units? → 9063887
|
|
31
|
+
mastr stromerzeugung --filter "Energieträger~eq~'2495'" --total # …how many are solar? → 6267133
|
|
32
|
+
mastr stromerzeugung --sort "Bruttoleistung-desc" --page-size 20 --iso-dates
|
|
33
|
+
mastr gaserzeugung --page 1 --page-size 50
|
|
34
|
+
mastr filters stromerzeugung # the filterable columns + dropdown codes
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Categories: `stromerzeugung`, `stromverbrauch`, `gaserzeugung`, `gasverbrauch`.
|
|
38
|
+
|
|
39
|
+
**Filtering** uses the register's own syntax — `FilterName~op~'value'~[and|or]~…`
|
|
40
|
+
(ops `eq|neq|sw|ct|nct|ew|null|nn`); discover the German field names and dropdown
|
|
41
|
+
codes with `mastr filters <category>`. A **wrong `FilterName` is silently ignored**
|
|
42
|
+
(you get the unfiltered set) and a **wrong `--sort` field returns 0 rows** — verify
|
|
43
|
+
with `mastr filters` and sanity-check with `--total`. **Dates** come as Microsoft
|
|
44
|
+
`/Date(ms)/` strings — add `--iso-dates` to convert them to ISO-8601.
|
|
45
|
+
|
|
46
|
+
Global flags: `--base-url`, `--timeout`, `--user-agent`, `--max-retries`,
|
|
47
|
+
`--max-response-bytes`, `--compact`, `--iso-dates`. See [Usage.md](Usage.md).
|
|
48
|
+
|
|
49
|
+
## Library
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { MastrClient, parseMsDate } from "@maschinenlesbar.org/marktstammdatenregister-cli";
|
|
53
|
+
|
|
54
|
+
const mastr = new MastrClient();
|
|
55
|
+
const solar = await mastr.stromerzeugung({ filter: "Energieträger~eq~'2495'", pageSize: 10 });
|
|
56
|
+
solar.total; // total matching units
|
|
57
|
+
parseMsDate(String(solar.data[0]?.EinheitRegistrierungsdatum)); // Date | null
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
- [Usage.md](Usage.md) — commands, filter syntax, exit codes
|
|
63
|
+
- [DEVELOPING.md](DEVELOPING.md) — architecture, testing, the live-API quirks
|
|
64
|
+
- [GLOSSARY.md](GLOSSARY.md) — MaStR domain terms
|
|
65
|
+
- [DATA_LICENSE.md](DATA_LICENSE.md) — the DL-DE-BY-2.0 data terms
|
|
66
|
+
- [SKILLS.md](SKILLS.md) — the Claude Code skills this repo ships
|
|
67
|
+
|
|
68
|
+
## Licence
|
|
69
|
+
|
|
70
|
+
Code is dual-licensed **AGPL-3.0-or-later OR commercial** — see
|
|
71
|
+
[LICENSING.md](LICENSING.md). No external code contributions are accepted (see
|
|
72
|
+
[CONTRIBUTING.md](CONTRIBUTING.md)); bug reports and AGPL forks are welcome.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"units.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/units.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAgCxC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAyCtE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Command group for the MaStR CLI: one command per unit category (electricity /
|
|
2
|
+
// gas × generation / consumption), each a paged, optionally-filtered search, plus a
|
|
3
|
+
// `filters` command to discover the filterable columns and their dropdown codes.
|
|
4
|
+
import { Argument } from "commander";
|
|
5
|
+
import { action, parseBoundedInt, parseNonEmpty, renderJson } from "../shared.js";
|
|
6
|
+
const CATEGORIES = [
|
|
7
|
+
{ name: "stromerzeugung", desc: "Electricity-generation units (Stromerzeugung)" },
|
|
8
|
+
{ name: "stromverbrauch", desc: "Electricity-consumption units (Stromverbrauch)" },
|
|
9
|
+
{ name: "gaserzeugung", desc: "Gas-generation units (Gaserzeugung)" },
|
|
10
|
+
{ name: "gasverbrauch", desc: "Gas-consumption units (Gasverbrauch)" },
|
|
11
|
+
];
|
|
12
|
+
const RUN = {
|
|
13
|
+
stromerzeugung: (c, q) => c.stromerzeugung(q),
|
|
14
|
+
stromverbrauch: (c, q) => c.stromverbrauch(q),
|
|
15
|
+
gaserzeugung: (c, q) => c.gaserzeugung(q),
|
|
16
|
+
gasverbrauch: (c, q) => c.gasverbrauch(q),
|
|
17
|
+
};
|
|
18
|
+
/** Build a UnitQuery from this command's parsed options. */
|
|
19
|
+
function buildQuery(opts) {
|
|
20
|
+
const q = {};
|
|
21
|
+
if (typeof opts["page"] === "number")
|
|
22
|
+
q.page = opts["page"];
|
|
23
|
+
if (typeof opts["pageSize"] === "number")
|
|
24
|
+
q.pageSize = opts["pageSize"];
|
|
25
|
+
if (typeof opts["sort"] === "string")
|
|
26
|
+
q.sort = opts["sort"];
|
|
27
|
+
if (typeof opts["filter"] === "string")
|
|
28
|
+
q.filter = opts["filter"];
|
|
29
|
+
// `--total` needs only the match count (returned regardless of page size), so
|
|
30
|
+
// request a single row instead of fetching and discarding a full page.
|
|
31
|
+
if (opts["total"] === true)
|
|
32
|
+
q.pageSize = 1;
|
|
33
|
+
return q;
|
|
34
|
+
}
|
|
35
|
+
export function registerCommands(program, deps) {
|
|
36
|
+
for (const cat of CATEGORIES) {
|
|
37
|
+
program
|
|
38
|
+
.command(cat.name)
|
|
39
|
+
.description(cat.desc)
|
|
40
|
+
.option("--page <n>", "1-based page number", parseBoundedInt(1, 1_000_000), 1)
|
|
41
|
+
.option("--page-size <n>", "rows per page (1..5000)", parseBoundedInt(1, 5000), 25)
|
|
42
|
+
.option("--sort <spec>", "sort: FieldKey-asc | FieldKey-desc (e.g. Bruttoleistung-desc)", parseNonEmpty)
|
|
43
|
+
.option("--filter <spec>", "filter: FilterName~op~'value'~[and|or]~… (see `filters`; ops eq|neq|sw|ct|nct|ew|null|nn)", parseNonEmpty)
|
|
44
|
+
.option("--total", "print only the total match count, not the rows")
|
|
45
|
+
.action(action(deps, async ({ client, global, opts }) => {
|
|
46
|
+
const page = await RUN[cat.name](client, buildQuery(opts));
|
|
47
|
+
// An unknown --sort field makes the server return 0 rows (not an error),
|
|
48
|
+
// which reads like "no matches". Nudge the user toward the likely cause.
|
|
49
|
+
if (page.total === 0 && typeof opts["sort"] === "string") {
|
|
50
|
+
deps.io.err("Note: 0 results with --sort set. If you expected matches, an unknown sort " +
|
|
51
|
+
"field returns 0 rows — verify the sort column key (see `mastr filters`).");
|
|
52
|
+
}
|
|
53
|
+
renderJson(deps, global, opts["total"] === true ? page.total : page);
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
program
|
|
57
|
+
.command("filters")
|
|
58
|
+
.description("List the filterable columns (names, types, dropdown codes) for a category")
|
|
59
|
+
.addArgument(new Argument("<category>", "unit category").choices(CATEGORIES.map((c) => c.name)))
|
|
60
|
+
.action(action(deps, async ({ client, global }, [category]) => {
|
|
61
|
+
renderJson(deps, global, await client.filterColumns(category));
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=units.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"units.js","sourceRoot":"","sources":["../../../../src/cli/commands/units.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bin shim: parse argv, run the CLI, and set the process exit code. All real
|
|
3
|
+
// logic lives in run.ts (testable without spawning a subprocess).
|
|
4
|
+
import { run } from "./run.js";
|
|
5
|
+
run(process.argv.slice(2)).then((code) => {
|
|
6
|
+
process.exitCode = code;
|
|
7
|
+
}, (err) => {
|
|
8
|
+
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA,6EAA6E;AAC7E,kEAAkE;AAElE,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC7B,CAAC,IAAI,EAAE,EAAE;IACP,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,EACD,CAAC,GAAY,EAAE,EAAE;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChG,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CACF,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MastrClient, MastrClientOptions } from "../client/client.js";
|
|
2
|
+
export interface CliIO {
|
|
3
|
+
out(text: string): void;
|
|
4
|
+
err(text: string): void;
|
|
5
|
+
}
|
|
6
|
+
export interface CliDeps {
|
|
7
|
+
io: CliIO;
|
|
8
|
+
/** Build a client from the resolved global options (injectable for tests). */
|
|
9
|
+
createClient(options: MastrClientOptions): MastrClient;
|
|
10
|
+
}
|
|
11
|
+
export declare const defaultIO: CliIO;
|
|
12
|
+
//# sourceMappingURL=io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../../src/cli/io.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE3E,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,KAAK,CAAC;IACV,8EAA8E;IAC9E,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAAC;CACxD;AAED,eAAO,MAAM,SAAS,EAAE,KAGvB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// I/O seam for the CLI. Everything the CLI writes goes through a CliIO object so
|
|
2
|
+
// tests can capture output instead of hitting the real stdout/stderr.
|
|
3
|
+
export const defaultIO = {
|
|
4
|
+
out: (text) => process.stdout.write(text + "\n"),
|
|
5
|
+
err: (text) => process.stderr.write(text + "\n"),
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io.js","sourceRoot":"","sources":["../../../src/cli/io.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,sEAAsE;AAetE,MAAM,CAAC,MAAM,SAAS,GAAU;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;IAChD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;CACjD,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { CliDeps } from "./io.js";
|
|
3
|
+
export declare const VERSION: string;
|
|
4
|
+
/** Default dependencies: real client + real stdout/stderr. */
|
|
5
|
+
export declare const defaultDeps: CliDeps;
|
|
6
|
+
export declare function buildProgram(deps?: CliDeps): Command;
|
|
7
|
+
//# sourceMappingURL=program.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../../src/cli/program.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAsBvC,eAAO,MAAM,OAAO,QAAgB,CAAC;AAErC,8DAA8D;AAC9D,eAAO,MAAM,WAAW,EAAE,OAGzB,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,GAAE,OAAqB,GAAG,OAAO,CAkCjE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Assemble the full commander program. The program is built around an injectable
|
|
2
|
+
// CliDeps so the entire CLI can be driven in tests with a mocked client and
|
|
3
|
+
// captured output.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { defaultIO } from "./io.js";
|
|
8
|
+
import { MastrClient } from "../client/client.js";
|
|
9
|
+
import { parseIntArg, parseBoundedInt, parseHeaderValue, parseNonEmpty } from "./shared.js";
|
|
10
|
+
import { registerCommands } from "./commands/units.js";
|
|
11
|
+
/**
|
|
12
|
+
* Single source of truth for the version: read from package.json at runtime
|
|
13
|
+
* rather than duplicating a literal that can silently drift after a release bump.
|
|
14
|
+
* From the compiled location (dist/src/cli/program.js) package.json is three
|
|
15
|
+
* directories up; the same offset holds for the source under src/cli.
|
|
16
|
+
*/
|
|
17
|
+
function readVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const pkgUrl = new URL("../../../package.json", import.meta.url);
|
|
20
|
+
const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8"));
|
|
21
|
+
return pkg.version ?? "0.0.0";
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "0.0.0";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const VERSION = readVersion();
|
|
28
|
+
/** Default dependencies: real client + real stdout/stderr. */
|
|
29
|
+
export const defaultDeps = {
|
|
30
|
+
io: defaultIO,
|
|
31
|
+
createClient: (options) => new MastrClient(options),
|
|
32
|
+
};
|
|
33
|
+
export function buildProgram(deps = defaultDeps) {
|
|
34
|
+
const program = new Command();
|
|
35
|
+
program
|
|
36
|
+
.name("mastr")
|
|
37
|
+
.description("CLI for the Marktstammdatenregister (MaStR) — the Bundesnetzagentur's register " +
|
|
38
|
+
"of the German electricity & gas market (~9M generation/consumption units). No " +
|
|
39
|
+
"API key needed. Each command searches one dataset, paged with --page/--page-size " +
|
|
40
|
+
"and narrowed with --filter (see `filters` for the field names and codes). Use " +
|
|
41
|
+
"--total for just the match count, and --iso-dates to convert /Date(…)/ timestamps.")
|
|
42
|
+
.version(VERSION)
|
|
43
|
+
.option("--base-url <url>", "API base URL", parseNonEmpty, "https://www.marktstammdatenregister.de/MaStR")
|
|
44
|
+
.option("--timeout <ms>", "per-request timeout in ms (0 = no timeout)", parseIntArg)
|
|
45
|
+
.option("--user-agent <ua>", "User-Agent header value", parseHeaderValue)
|
|
46
|
+
.option("--max-retries <n>", "retries for transient 429/503 responses (0..10)", parseBoundedInt(0, 10))
|
|
47
|
+
.option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", parseIntArg)
|
|
48
|
+
.option("--compact", "print JSON on a single line instead of pretty-printed")
|
|
49
|
+
.option("--iso-dates", "rewrite MaStR /Date(ms)/ timestamps to ISO-8601")
|
|
50
|
+
.showHelpAfterError();
|
|
51
|
+
registerCommands(program, deps);
|
|
52
|
+
return program;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=program.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program.js","sourceRoot":"","sources":["../../../src/cli/program.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,4EAA4E;AAC5E,mBAAmB;AAEnB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAyB,CAAC;QAC5F,OAAO,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;AAErC,8DAA8D;AAC9D,MAAM,CAAC,MAAM,WAAW,GAAY;IAClC,EAAE,EAAE,SAAS;IACb,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC;CACpD,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,OAAgB,WAAW;IACtD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,OAAO,CAAC;SACb,WAAW,CACV,iFAAiF;QAC/E,gFAAgF;QAChF,mFAAmF;QACnF,gFAAgF;QAChF,oFAAoF,CACvF;SACA,OAAO,CAAC,OAAO,CAAC;SAChB,MAAM,CACL,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,8CAA8C,CAC/C;SACA,MAAM,CAAC,gBAAgB,EAAE,4CAA4C,EAAE,WAAW,CAAC;SACnF,MAAM,CAAC,mBAAmB,EAAE,yBAAyB,EAAE,gBAAgB,CAAC;SACxE,MAAM,CAAC,mBAAmB,EAAE,iDAAiD,EAAE,eAAe,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;SACtG,MAAM,CACL,0BAA0B,EAC1B,kEAAkE,EAClE,WAAW,CACZ;SACA,MAAM,CAAC,WAAW,EAAE,uDAAuD,CAAC;SAC5E,MAAM,CAAC,aAAa,EAAE,iDAAiD,CAAC;SACxE,kBAAkB,EAAE,CAAC;IAExB,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAqCvC,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDtF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Run the CLI and resolve to a process exit code. Kept separate from the bin
|
|
2
|
+
// shim so tests can call run() directly with injected deps and assert on the
|
|
3
|
+
// captured output and exit code without spawning a subprocess.
|
|
4
|
+
import { CommanderError } from "commander";
|
|
5
|
+
import { buildProgram, defaultDeps } from "./program.js";
|
|
6
|
+
import { MastrApiError, MastrError, MastrNetworkError, MastrValidationError, } from "../client/errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Process exit codes. Distinct codes let scripts tell apart a usage error, a
|
|
9
|
+
* missing resource, a transport failure, and a catch-all.
|
|
10
|
+
*/
|
|
11
|
+
const EXIT = {
|
|
12
|
+
/** Usage / parse / client-side validation error. */
|
|
13
|
+
USAGE: 2,
|
|
14
|
+
/** HTTP 404 — resource not found. */
|
|
15
|
+
NOT_FOUND: 4,
|
|
16
|
+
/** Network / transport failure (DNS, connection, timeout, size-cap). */
|
|
17
|
+
NETWORK: 6,
|
|
18
|
+
/** Any other error. */
|
|
19
|
+
OTHER: 1,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Apply exitOverride + output redirection to every command in the tree.
|
|
23
|
+
* commander does not propagate these to subcommands, so a parse error on a
|
|
24
|
+
* subcommand would otherwise call process.exit() and bypass our error handling.
|
|
25
|
+
*/
|
|
26
|
+
function configureTree(command, deps) {
|
|
27
|
+
command.exitOverride();
|
|
28
|
+
command.configureOutput({
|
|
29
|
+
writeOut: (str) => deps.io.out(str.replace(/\n$/, "")),
|
|
30
|
+
writeErr: (str) => deps.io.err(str.replace(/\n$/, "")),
|
|
31
|
+
});
|
|
32
|
+
for (const child of command.commands)
|
|
33
|
+
configureTree(child, deps);
|
|
34
|
+
}
|
|
35
|
+
export async function run(argv, deps = defaultDeps) {
|
|
36
|
+
const program = buildProgram(deps);
|
|
37
|
+
configureTree(program, deps);
|
|
38
|
+
// A bare invocation (no command) is a help request, not an error: print help
|
|
39
|
+
// to stdout and exit 0, matching `--help`.
|
|
40
|
+
if (argv.length === 0) {
|
|
41
|
+
deps.io.out(program.helpInformation().replace(/\n$/, ""));
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await program.parseAsync(argv, { from: "user" });
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof CommanderError) {
|
|
50
|
+
// Help/version requests exit 0; every genuine usage/parse error maps to a
|
|
51
|
+
// single USAGE code (commander's own exitCode is 1, indistinguishable from
|
|
52
|
+
// the catch-all).
|
|
53
|
+
return err.exitCode === 0 ? 0 : EXIT.USAGE;
|
|
54
|
+
}
|
|
55
|
+
if (err instanceof MastrValidationError) {
|
|
56
|
+
deps.io.err(`Error: ${err.message}`);
|
|
57
|
+
return EXIT.USAGE;
|
|
58
|
+
}
|
|
59
|
+
if (err instanceof MastrApiError) {
|
|
60
|
+
deps.io.err(`Error: ${err.message}`);
|
|
61
|
+
if (err.status === 404)
|
|
62
|
+
return EXIT.NOT_FOUND;
|
|
63
|
+
// A 3xx means the base URL redirected (the canonical host answers directly),
|
|
64
|
+
// so it is a base-URL misconfiguration — a usage error.
|
|
65
|
+
if (err.status !== undefined && err.status >= 300 && err.status < 400)
|
|
66
|
+
return EXIT.USAGE;
|
|
67
|
+
return EXIT.OTHER;
|
|
68
|
+
}
|
|
69
|
+
if (err instanceof MastrNetworkError) {
|
|
70
|
+
deps.io.err(`Error: ${err.message}`);
|
|
71
|
+
if (/maxResponseBytes/.test(err.message)) {
|
|
72
|
+
deps.io.err("Hint: the response exceeded the size cap. Lower --page-size or raise " +
|
|
73
|
+
"--max-response-bytes <n> (0 = unlimited).");
|
|
74
|
+
}
|
|
75
|
+
return EXIT.NETWORK;
|
|
76
|
+
}
|
|
77
|
+
if (err instanceof MastrError) {
|
|
78
|
+
deps.io.err(`Error: ${err.message}`);
|
|
79
|
+
return EXIT.OTHER;
|
|
80
|
+
}
|
|
81
|
+
deps.io.err(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
return EXIT.OTHER;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CliDeps } from "./io.js";
|
|
2
|
+
import type { MastrClientOptions } from "../client/client.js";
|
|
3
|
+
/**
|
|
4
|
+
* commander value-parser: a plain base-10 non-negative integer.
|
|
5
|
+
*
|
|
6
|
+
* Uses a strict regex rather than `Number()` coercion, which would otherwise
|
|
7
|
+
* accept empty/whitespace strings (`Number("") === 0`), hex/binary/scientific
|
|
8
|
+
* literals (`0x10`, `0b10`, `1e3`), signs, padding and decimals.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseIntArg(value: string): number;
|
|
11
|
+
/** commander value-parser: a non-empty (after trimming) string. */
|
|
12
|
+
export declare function parseNonEmpty(value: string): string;
|
|
13
|
+
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
14
|
+
export declare function parseBoundedInt(min: number, max: number): (value: string) => number;
|
|
15
|
+
/**
|
|
16
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent).
|
|
17
|
+
* Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
|
|
18
|
+
* Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
|
|
19
|
+
* checked by char code so the source stays free of control bytes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseHeaderValue(value: string): string;
|
|
22
|
+
export interface GlobalOptions {
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
userAgent?: string;
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
maxResponseBytes?: number;
|
|
28
|
+
compact?: boolean;
|
|
29
|
+
isoDates?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Translate resolved global CLI options into client options. */
|
|
32
|
+
export declare function toEngineOptions(global: GlobalOptions): MastrClientOptions;
|
|
33
|
+
/**
|
|
34
|
+
* Render a JSON value to stdout, pretty by default and compact with --compact.
|
|
35
|
+
* With --iso-dates, every MaStR `"/Date(ms)/"` string is rewritten to ISO-8601 first.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderJson(deps: CliDeps, global: GlobalOptions, value: unknown): void;
|
|
38
|
+
export interface ActionContext {
|
|
39
|
+
client: ReturnType<CliDeps["createClient"]>;
|
|
40
|
+
global: GlobalOptions;
|
|
41
|
+
/** This command's own parsed options. */
|
|
42
|
+
opts: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Wrap an async command action with consistent global-option resolution and
|
|
46
|
+
* client construction. The callback receives a context (client + resolved global
|
|
47
|
+
* options + this command's options) and the command's positional arguments.
|
|
48
|
+
*
|
|
49
|
+
* Commander invokes actions as (arg1, ..., argN, options, command); we slice off
|
|
50
|
+
* the trailing options object and command instance to recover the positionals.
|
|
51
|
+
*/
|
|
52
|
+
export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
|
|
53
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/cli/shared.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASjD;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAOnF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAQzE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAIrF;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;IAC5C,MAAM,EAAE,aAAa,CAAC;IACtB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CACpB,IAAI,EAAE,OAAO,EACb,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAQvC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Shared helpers used across CLI command groups: option parsers, the global
|
|
2
|
+
// option resolver, and JSON rendering (with the --iso-dates transform).
|
|
3
|
+
import { InvalidArgumentError } from "commander";
|
|
4
|
+
import { isoifyDates } from "../client/client.js";
|
|
5
|
+
/**
|
|
6
|
+
* commander value-parser: a plain base-10 non-negative integer.
|
|
7
|
+
*
|
|
8
|
+
* Uses a strict regex rather than `Number()` coercion, which would otherwise
|
|
9
|
+
* accept empty/whitespace strings (`Number("") === 0`), hex/binary/scientific
|
|
10
|
+
* literals (`0x10`, `0b10`, `1e3`), signs, padding and decimals.
|
|
11
|
+
*/
|
|
12
|
+
export function parseIntArg(value) {
|
|
13
|
+
if (!/^[0-9]+$/.test(value)) {
|
|
14
|
+
throw new InvalidArgumentError("Expected a non-negative integer.");
|
|
15
|
+
}
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
if (!Number.isSafeInteger(n)) {
|
|
18
|
+
throw new InvalidArgumentError("Expected a non-negative integer.");
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
/** commander value-parser: a non-empty (after trimming) string. */
|
|
23
|
+
export function parseNonEmpty(value) {
|
|
24
|
+
if (value.trim() === "") {
|
|
25
|
+
throw new InvalidArgumentError("Expected a non-empty value.");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
30
|
+
export function parseBoundedInt(min, max) {
|
|
31
|
+
return (value) => {
|
|
32
|
+
const n = parseIntArg(value);
|
|
33
|
+
if (n < min)
|
|
34
|
+
throw new InvalidArgumentError(`Must be >= ${min}.`);
|
|
35
|
+
if (n > max)
|
|
36
|
+
throw new InvalidArgumentError(`Must be <= ${max}.`);
|
|
37
|
+
return n;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent).
|
|
42
|
+
* Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
|
|
43
|
+
* Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
|
|
44
|
+
* checked by char code so the source stays free of control bytes.
|
|
45
|
+
*/
|
|
46
|
+
export function parseHeaderValue(value) {
|
|
47
|
+
for (let i = 0; i < value.length; i++) {
|
|
48
|
+
const c = value.charCodeAt(i);
|
|
49
|
+
if ((c < 0x20 && c !== 0x09) || c === 0x7f) {
|
|
50
|
+
throw new InvalidArgumentError("Value contains control characters.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
/** Translate resolved global CLI options into client options. */
|
|
56
|
+
export function toEngineOptions(global) {
|
|
57
|
+
const options = {};
|
|
58
|
+
if (global.baseUrl !== undefined)
|
|
59
|
+
options.baseUrl = global.baseUrl;
|
|
60
|
+
if (global.timeout !== undefined)
|
|
61
|
+
options.timeoutMs = global.timeout;
|
|
62
|
+
if (global.userAgent !== undefined)
|
|
63
|
+
options.userAgent = global.userAgent;
|
|
64
|
+
if (global.maxRetries !== undefined)
|
|
65
|
+
options.maxRetries = global.maxRetries;
|
|
66
|
+
if (global.maxResponseBytes !== undefined)
|
|
67
|
+
options.maxResponseBytes = global.maxResponseBytes;
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Render a JSON value to stdout, pretty by default and compact with --compact.
|
|
72
|
+
* With --iso-dates, every MaStR `"/Date(ms)/"` string is rewritten to ISO-8601 first.
|
|
73
|
+
*/
|
|
74
|
+
export function renderJson(deps, global, value) {
|
|
75
|
+
const shaped = global.isoDates ? isoifyDates(value) : value;
|
|
76
|
+
const text = global.compact ? JSON.stringify(shaped) : JSON.stringify(shaped, null, 2);
|
|
77
|
+
deps.io.out(text);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Wrap an async command action with consistent global-option resolution and
|
|
81
|
+
* client construction. The callback receives a context (client + resolved global
|
|
82
|
+
* options + this command's options) and the command's positional arguments.
|
|
83
|
+
*
|
|
84
|
+
* Commander invokes actions as (arg1, ..., argN, options, command); we slice off
|
|
85
|
+
* the trailing options object and command instance to recover the positionals.
|
|
86
|
+
*/
|
|
87
|
+
export function action(deps, fn) {
|
|
88
|
+
return async (...args) => {
|
|
89
|
+
const command = args[args.length - 1];
|
|
90
|
+
const positionals = args.slice(0, Math.max(0, args.length - 2));
|
|
91
|
+
const global = command.optsWithGlobals();
|
|
92
|
+
const client = deps.createClient(toEngineOptions(global));
|
|
93
|
+
await fn({ client, global, opts: command.opts() }, positionals);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/cli/shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type EngineOptions } from "./engine.js";
|
|
2
|
+
import type { FilterColumn, UnitCategory, UnitPage, UnitQuery } from "./types.js";
|
|
3
|
+
/** Options for the MaStR client (engine options only — the API needs no auth). */
|
|
4
|
+
export type MastrClientOptions = EngineOptions;
|
|
5
|
+
/**
|
|
6
|
+
* Parse a MaStR Microsoft-AJAX date string (`"/Date(1548979200000)/"`) into a Date.
|
|
7
|
+
* Returns null if the string is not in that format.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseMsDate(value: string): Date | null;
|
|
10
|
+
/**
|
|
11
|
+
* Recursively rewrite every `"/Date(ms)/"` string in a value to an ISO-8601 string,
|
|
12
|
+
* returning a new value. Used by the CLI's `--iso-dates`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isoifyDates<T>(value: T): T;
|
|
15
|
+
export declare class MastrClient {
|
|
16
|
+
private readonly engine;
|
|
17
|
+
constructor(options?: MastrClientOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Fetch one page of units for a category. Sends the full Kendo param set —
|
|
20
|
+
* `sort`, `page`, `pageSize`, `group`, `filter` — always, because the server
|
|
21
|
+
* rejects a request with `group`/`filter` missing ("Die Anfrage ist Null.").
|
|
22
|
+
*/
|
|
23
|
+
units(category: UnitCategory, query?: UnitQuery): Promise<UnitPage>;
|
|
24
|
+
/** Electricity-generation units (`Stromerzeugung`). */
|
|
25
|
+
stromerzeugung(query?: UnitQuery): Promise<UnitPage>;
|
|
26
|
+
/** Electricity-consumption units (`Stromverbrauch`). */
|
|
27
|
+
stromverbrauch(query?: UnitQuery): Promise<UnitPage>;
|
|
28
|
+
/** Gas-generation units (`Gaserzeugung`). */
|
|
29
|
+
gaserzeugung(query?: UnitQuery): Promise<UnitPage>;
|
|
30
|
+
/** Gas-consumption units (`Gasverbrauch`). */
|
|
31
|
+
gasverbrauch(query?: UnitQuery): Promise<UnitPage>;
|
|
32
|
+
/** The filterable columns (names, types, dropdown codes) for a category. */
|
|
33
|
+
filterColumns(category: UnitCategory): Promise<FilterColumn[]>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/client/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,KAAK,EAAE,YAAY,EAAa,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAgB,MAAM,YAAY,CAAC;AAe3G,kFAAkF;AAClF,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAgB1C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;gBAE3B,OAAO,GAAE,kBAAuB;IAI5C;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,GAAE,SAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqB7E,uDAAuD;IACvD,cAAc,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAGpD,wDAAwD;IACxD,cAAc,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAGpD,6CAA6C;IAC7C,YAAY,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAGlD,8CAA8C;IAC9C,YAAY,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIlD,4EAA4E;IACtE,aAAa,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAKrE"}
|