@maschinenlesbar.org/pegel-online-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.
Files changed (65) hide show
  1. package/CONTRIBUTING.md +25 -0
  2. package/LICENSE +661 -0
  3. package/LICENSING.md +47 -0
  4. package/README.md +203 -0
  5. package/dist/src/cli/commands/stations.d.ts +4 -0
  6. package/dist/src/cli/commands/stations.d.ts.map +1 -0
  7. package/dist/src/cli/commands/stations.js +45 -0
  8. package/dist/src/cli/commands/stations.js.map +1 -0
  9. package/dist/src/cli/commands/timeseries.d.ts +4 -0
  10. package/dist/src/cli/commands/timeseries.d.ts.map +1 -0
  11. package/dist/src/cli/commands/timeseries.js +38 -0
  12. package/dist/src/cli/commands/timeseries.js.map +1 -0
  13. package/dist/src/cli/index.d.ts +3 -0
  14. package/dist/src/cli/index.d.ts.map +1 -0
  15. package/dist/src/cli/index.js +6 -0
  16. package/dist/src/cli/index.js.map +1 -0
  17. package/dist/src/cli/io.d.ts +13 -0
  18. package/dist/src/cli/io.d.ts.map +1 -0
  19. package/dist/src/cli/io.js +7 -0
  20. package/dist/src/cli/io.js.map +1 -0
  21. package/dist/src/cli/program.d.ts +7 -0
  22. package/dist/src/cli/program.d.ts.map +1 -0
  23. package/dist/src/cli/program.js +52 -0
  24. package/dist/src/cli/program.js.map +1 -0
  25. package/dist/src/cli/run.d.ts +3 -0
  26. package/dist/src/cli/run.d.ts.map +1 -0
  27. package/dist/src/cli/run.js +62 -0
  28. package/dist/src/cli/run.js.map +1 -0
  29. package/dist/src/cli/shared.d.ts +44 -0
  30. package/dist/src/cli/shared.d.ts.map +1 -0
  31. package/dist/src/cli/shared.js +81 -0
  32. package/dist/src/cli/shared.js.map +1 -0
  33. package/dist/src/client/client.d.ts +28 -0
  34. package/dist/src/client/client.d.ts.map +1 -0
  35. package/dist/src/client/client.js +79 -0
  36. package/dist/src/client/client.js.map +1 -0
  37. package/dist/src/client/engine.d.ts +54 -0
  38. package/dist/src/client/engine.d.ts.map +1 -0
  39. package/dist/src/client/engine.js +120 -0
  40. package/dist/src/client/engine.js.map +1 -0
  41. package/dist/src/client/errors.d.ts +33 -0
  42. package/dist/src/client/errors.d.ts.map +1 -0
  43. package/dist/src/client/errors.js +40 -0
  44. package/dist/src/client/errors.js.map +1 -0
  45. package/dist/src/client/http.d.ts +26 -0
  46. package/dist/src/client/http.d.ts.map +1 -0
  47. package/dist/src/client/http.js +82 -0
  48. package/dist/src/client/http.js.map +1 -0
  49. package/dist/src/client/index.d.ts +10 -0
  50. package/dist/src/client/index.d.ts.map +1 -0
  51. package/dist/src/client/index.js +8 -0
  52. package/dist/src/client/index.js.map +1 -0
  53. package/dist/src/client/query.d.ts +9 -0
  54. package/dist/src/client/query.d.ts.map +1 -0
  55. package/dist/src/client/query.js +33 -0
  56. package/dist/src/client/query.js.map +1 -0
  57. package/dist/src/client/types.d.ts +71 -0
  58. package/dist/src/client/types.d.ts.map +1 -0
  59. package/dist/src/client/types.js +4 -0
  60. package/dist/src/client/types.js.map +1 -0
  61. package/dist/src/index.d.ts +2 -0
  62. package/dist/src/index.d.ts.map +1 -0
  63. package/dist/src/index.js +3 -0
  64. package/dist/src/index.js.map +1 -0
  65. package/package.json +66 -0
package/LICENSING.md ADDED
@@ -0,0 +1,47 @@
1
+ # Licensing
2
+
3
+ `pegel-online-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 `pegel-online-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,203 @@
1
+ # pegel-online-cli
2
+
3
+ A TypeScript **API client** and **command-line interface** for the open
4
+ [PEGELONLINE REST API v2](https://www.pegelonline.wsv.de/webservice/dokuRestapi)
5
+ (`pegelonline.wsv.de`) operated by the **WSV** (Wasserstraßen- und
6
+ Schifffahrtsverwaltung des Bundes) — live **water levels** and related timeseries
7
+ across the German federal waterway network.
8
+
9
+ - **Zero runtime HTTP dependencies** — built on Node's built-in `http`/`https` (no axios, no fetch polyfill).
10
+ - **One small dependency** for the CLI: [`commander`](https://github.com/tj/commander.js).
11
+ - **Strongly typed** — typed stations, timeseries and measurement shapes.
12
+ - **Well tested** — unit tests on Node's built-in test runner (`node --test`), every HTTP response mocked.
13
+ - **Read-only, no auth** — the PEGELONLINE API needs no key; this client only reads.
14
+
15
+ New to PEGELONLINE, or terms like *Gewässer*, *timeseries* (`W`/`Q`), MNW/MHW or
16
+ NSW/HSW? See **[GLOSSARY.md](GLOSSARY.md)** for the domain concepts and the
17
+ project's own vocabulary.
18
+
19
+ ## Requirements
20
+
21
+ - Node.js **>= 20** (uses the stable built-in test runner, ESM and top-level `await`).
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build # compiles TypeScript to dist/
28
+ ```
29
+
30
+ Run the CLI without a global install:
31
+
32
+ ```bash
33
+ node dist/src/cli/index.js --help
34
+ # or, after `npm link` / global install:
35
+ pegel --help
36
+ ```
37
+
38
+ ---
39
+
40
+ ## CLI usage
41
+
42
+ Every command prints pretty JSON to stdout (`--compact` for a single line). A
43
+ `<station>` may be a **uuid**, **number**, **shortname** or **longname**. A
44
+ `[timeseries]` defaults to **`W`** (water level); others include `Q` (flow),
45
+ `WT` (water temperature), `LT` (air temperature), depending on the station.
46
+
47
+ ### Global options
48
+
49
+ | Option | Description |
50
+ | --- | --- |
51
+ | `--base-url <url>` | API base URL (default `https://www.pegelonline.wsv.de`) |
52
+ | `--timeout <ms>` | Per-request timeout (default `30000`) |
53
+ | `--user-agent <ua>` | `User-Agent` header value |
54
+ | `--max-retries <n>` | Retries for transient `429`/`503` responses (default `2`) |
55
+ | `--max-response-bytes <n>` | Cap response body size in bytes (`0` = unlimited; default 100 MiB) |
56
+ | `--compact` | Print JSON on a single line |
57
+
58
+ Global options are accepted **before or after** the command, e.g. both
59
+ `pegel --compact waters` and `pegel waters --compact` work; placing them before
60
+ the command is recommended for clarity.
61
+
62
+ ### Commands
63
+
64
+ ```text
65
+ stations list [--ids <id> ...] [--waters <shortname>] [--fuzzy-id <id>]
66
+ [--include-timeseries] [--include-current] [--include-characteristic]
67
+ stations get <station> [--include-...]
68
+ timeseries <station> [timeseries] metadata for a timeseries
69
+ current <station> [timeseries] the current measurement
70
+ measurements <station> [timeseries] [--start <iso|P7D>] [--end <iso>]
71
+ waters list all bodies of water (Gewässer)
72
+ ```
73
+
74
+ ### Examples
75
+
76
+ ```bash
77
+ # All stations on the Rhine, with their current water level
78
+ pegel stations list --waters RHEIN --include-current
79
+
80
+ # One station
81
+ pegel stations get BONN --include-timeseries --include-current
82
+
83
+ # Current water level at Bonn
84
+ pegel current BONN
85
+
86
+ # Last 3 days of measurements
87
+ pegel measurements BONN W --start P3D
88
+
89
+ # Characteristic (gauge-mark) values, embedded in the timeseries
90
+ pegel stations get BONN --include-timeseries --include-characteristic
91
+
92
+ # Bodies of water
93
+ pegel waters
94
+ ```
95
+
96
+ Exit codes: `0` success, `2` for usage/parse errors (unknown command/option, missing argument, invalid flag value), `4` on a `404` from the API, `1` for any other (runtime/network) error.
97
+
98
+ ---
99
+
100
+ ## Library usage
101
+
102
+ ```ts
103
+ import { PegelOnlineClient, PegelApiError } from "@maschinenlesbar.org/pegel-online-cli";
104
+
105
+ const client = new PegelOnlineClient(); // defaults to https://www.pegelonline.wsv.de
106
+
107
+ const rhine = await client.stations.list({ waters: "RHEIN", includeCurrentMeasurement: true });
108
+ const bonn = await client.stations.get("BONN", { includeTimeseries: true });
109
+ const now = await client.timeseries.currentMeasurement("BONN", "W");
110
+ const series = await client.timeseries.measurements("BONN", "W", { start: "P3D" });
111
+
112
+ try {
113
+ await client.stations.get("DOES-NOT-EXIST");
114
+ } catch (err) {
115
+ if (err instanceof PegelApiError) console.error(err.status, err.detail);
116
+ }
117
+ ```
118
+
119
+ ### Client options
120
+
121
+ ```ts
122
+ new PegelOnlineClient({
123
+ baseUrl: "https://www.pegelonline.wsv.de",
124
+ timeoutMs: 15_000,
125
+ maxRetries: 3, // 429 / 503 are retried with linear backoff
126
+ maxResponseBytes: 50 << 20, // abort responses larger than 50 MiB (0 = unlimited)
127
+ userAgent: "my-app/1.0",
128
+ transport: customTransport, // inject your own HTTP transport
129
+ });
130
+ ```
131
+
132
+ ### Resource groups
133
+
134
+ `client.stations` (`.list` / `.get`), `client.timeseries` (`.get` / `.currentMeasurement` /
135
+ `.measurements`), and `client.waters()`. Characteristic (gauge-mark) values are
136
+ available via the `includeCharacteristicValues` embed on `.get` / `.list`.
137
+
138
+ ---
139
+
140
+ ## Architecture
141
+
142
+ ```
143
+ src/
144
+ client/
145
+ types.ts # Station / TimeseriesInfo / CurrentMeasurement / Measurement + param objects
146
+ query.ts # dependency-free query-string builder
147
+ http.ts # the Transport interface + default node:http/https transport
148
+ engine.ts # URL building, retry/backoff, redirects, JSON decoding, error mapping
149
+ errors.ts # PegelError / PegelApiError / PegelNetworkError / PegelParseError
150
+ client.ts # PegelOnlineClient — stations + timeseries resources over the engine
151
+ cli/
152
+ io.ts # injectable I/O seam (stdout/stderr)
153
+ shared.ts # option parsers, global-option resolver, JSON renderer
154
+ commands/ # stations + timeseries/measurements/waters
155
+ program.ts # assembles the commander program from injectable deps
156
+ run.ts # parses argv -> exit code (no process.exit; testable)
157
+ index.ts # #! bin shim
158
+ ```
159
+
160
+ **Design notes**
161
+
162
+ - The HTTP layer is a single `Transport` function (`(req) => Promise<HttpResponse>`). The default
163
+ uses `node:http`/`node:https`; tests inject a mock. This keeps the client free of any HTTP framework.
164
+ - The CLI is built around injectable `CliDeps` (client factory + I/O), so the whole program can be
165
+ driven in-process by tests with a mocked client and captured output — no subprocesses.
166
+ - The engine follows HTTP redirects, so trailing-slash and host normalisations are handled transparently.
167
+
168
+ ---
169
+
170
+ ## Testing
171
+
172
+ ```bash
173
+ npm test # builds, then runs `node --test` over dist/test
174
+ ```
175
+
176
+ - **`query.test.ts`** — query-string serialisation.
177
+ - **`http.test.ts`** — the default transport against a real loopback `http.createServer`.
178
+ - **`engine.test.ts`** — URL building, JSON decoding, error mapping, 429/503 retry — mocked transport.
179
+ - **`client.test.ts`** — every endpoint's method/URL/query mapping — mocked transport.
180
+ - **`cli.test.ts`** — end-to-end command parsing, validation and exit codes — mocked client.
181
+
182
+ ## Continuous integration
183
+
184
+ GitHub Actions workflows under `.github/workflows/`:
185
+
186
+ - **ci.yml** — type-check, build and test on Node 20/22/24 for every push and PR.
187
+ - **release.yml** — on a `v*` tag: verify the tag matches `package.json`, test, `npm pack`, and create a GitHub Release with the tarball.
188
+ - **publish.yml** — manual dispatch: publish to npm via OIDC **Trusted Publishing** (no stored `NPM_TOKEN`) with provenance.
189
+ - **docs.yml** — build TypeDoc API docs and deploy to GitHub Pages on each `v*` tag.
190
+
191
+ ## License
192
+
193
+ **Dual-licensed** — use it under **either**:
194
+
195
+ - **[AGPL-3.0-or-later](LICENSE)** (default, free). Note the AGPL's §13 network
196
+ clause: if you run a modified version as a network service, you must offer that
197
+ modified source to the service's users.
198
+ - **Commercial license** (paid), for closed-source / proprietary or SaaS use
199
+ without the AGPL's obligations.
200
+
201
+ See **[LICENSING.md](LICENSING.md)** for details, and **[CONTRIBUTING.md](CONTRIBUTING.md)**
202
+ for the contribution policy (this project does not accept external code
203
+ contributions). Commercial enquiries: **sebs@2xs.org**.
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { CliDeps } from "../io.js";
3
+ export declare function registerStationCommands(program: Command, deps: CliDeps): void;
4
+ //# sourceMappingURL=stations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stations.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/stations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAyBxC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAiC7E"}
@@ -0,0 +1,45 @@
1
+ import { Option } from "commander";
2
+ import { action, renderJson, requireArg } from "../shared.js";
3
+ /** commander accumulator for a repeatable string option. */
4
+ function collect(value, previous = []) {
5
+ return previous.concat([value]);
6
+ }
7
+ /** Read the three include flags off a parsed-options object. */
8
+ function includesFrom(opts) {
9
+ return {
10
+ includeTimeseries: opts["includeTimeseries"],
11
+ includeCurrentMeasurement: opts["includeCurrent"],
12
+ includeCharacteristicValues: opts["includeCharacteristic"],
13
+ };
14
+ }
15
+ function addIncludeOptions(cmd) {
16
+ return cmd
17
+ .addOption(new Option("--include-timeseries", "embed each station's timeseries list"))
18
+ .addOption(new Option("--include-current", "embed the current measurement"))
19
+ .addOption(new Option("--include-characteristic", "embed characteristic (gauge-mark) values"));
20
+ }
21
+ export function registerStationCommands(program, deps) {
22
+ const stations = program.command("stations").description("Measuring stations");
23
+ const list = stations
24
+ .command("list")
25
+ .description("List/filter stations")
26
+ .option("--ids <id>", "station id (uuid/number/shortname/longname); repeatable", collect)
27
+ .option("--waters <shortname>", "filter by water shortname (see `waters`)")
28
+ .option("--fuzzy-id <id>", "fuzzy id match");
29
+ addIncludeOptions(list).action(action(deps, async ({ client, global, opts }) => {
30
+ const params = {
31
+ ids: opts["ids"],
32
+ waters: opts["waters"],
33
+ fuzzyId: opts["fuzzyId"],
34
+ ...includesFrom(opts),
35
+ };
36
+ renderJson(deps, global, await client.stations.list(params));
37
+ }));
38
+ const get = stations
39
+ .command("get <station>")
40
+ .description("Get one station by uuid/number/shortname/longname");
41
+ addIncludeOptions(get).action(action(deps, async ({ client, global, opts }, [station]) => {
42
+ renderJson(deps, global, await client.stations.get(requireArg("station", station), includesFrom(opts)));
43
+ }));
44
+ }
45
+ //# sourceMappingURL=stations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stations.js","sourceRoot":"","sources":["../../../../src/cli/commands/stations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,4DAA4D;AAC5D,SAAS,OAAO,CAAC,KAAa,EAAE,WAAqB,EAAE;IACrD,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,gEAAgE;AAChE,SAAS,YAAY,CAAC,IAA6B;IACjD,OAAO;QACL,iBAAiB,EAAE,IAAI,CAAC,mBAAmB,CAAwB;QACnE,yBAAyB,EAAE,IAAI,CAAC,gBAAgB,CAAwB;QACxE,2BAA2B,EAAE,IAAI,CAAC,uBAAuB,CAAwB;KAClF,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAY;IACrC,OAAO,GAAG;SACP,SAAS,CAAC,IAAI,MAAM,CAAC,sBAAsB,EAAE,sCAAsC,CAAC,CAAC;SACrF,SAAS,CAAC,IAAI,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,CAAC,CAAC;SAC3E,SAAS,CAAC,IAAI,MAAM,CAAC,0BAA0B,EAAE,0CAA0C,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAgB,EAAE,IAAa;IACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC;IAE/E,MAAM,IAAI,GAAG,QAAQ;SAClB,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,sBAAsB,CAAC;SACnC,MAAM,CAAC,YAAY,EAAE,yDAAyD,EAAE,OAAO,CAAC;SACxF,MAAM,CAAC,sBAAsB,EAAE,0CAA0C,CAAC;SAC1E,MAAM,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;IAC/C,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,CAC5B,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QAC9C,MAAM,MAAM,GAAsB;YAChC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAyB;YACxC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAuB;YAC5C,OAAO,EAAE,IAAI,CAAC,SAAS,CAAuB;YAC9C,GAAG,YAAY,CAAC,IAAI,CAAC;SACtB,CAAC;QACF,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,GAAG,GAAG,QAAQ;SACjB,OAAO,CAAC,eAAe,CAAC;SACxB,WAAW,CAAC,mDAAmD,CAAC,CAAC;IACpE,iBAAiB,CAAC,GAAG,CAAC,CAAC,MAAM,CAC3B,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE;QACzD,UAAU,CACR,IAAI,EACJ,MAAM,EACN,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAC9E,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { CliDeps } from "../io.js";
3
+ export declare function registerTimeseriesCommands(program: Command, deps: CliDeps): void;
4
+ //# sourceMappingURL=timeseries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeseries.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/timeseries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AASxC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAqDhF"}
@@ -0,0 +1,38 @@
1
+ import { action, renderJson, requireArg, timeseriesOr } from "../shared.js";
2
+ /** An empty option value (e.g. `--start ""`) should be omitted, not sent blank. */
3
+ function optStr(value) {
4
+ const s = value;
5
+ return s !== undefined && s !== "" ? s : undefined;
6
+ }
7
+ export function registerTimeseriesCommands(program, deps) {
8
+ program
9
+ .command("timeseries <station> [timeseries]")
10
+ .description("Timeseries metadata (timeseries defaults to 'W' = water level)")
11
+ .action(action(deps, async ({ client, global }, [station, ts]) => {
12
+ renderJson(deps, global, await client.timeseries.get(requireArg("station", station), timeseriesOr(ts)));
13
+ }));
14
+ program
15
+ .command("current <station> [timeseries]")
16
+ .description("The current measurement (timeseries defaults to 'W')")
17
+ .action(action(deps, async ({ client, global }, [station, ts]) => {
18
+ renderJson(deps, global, await client.timeseries.currentMeasurement(requireArg("station", station), timeseriesOr(ts)));
19
+ }));
20
+ program
21
+ .command("measurements <station> [timeseries]")
22
+ .description("A window of measurements (timeseries defaults to 'W')")
23
+ .option("--start <iso>", "window start: ISO-8601 instant, or a period like P7D")
24
+ .option("--end <iso>", "window end: ISO-8601 instant")
25
+ .action(action(deps, async ({ client, global, opts }, [station, ts]) => {
26
+ renderJson(deps, global, await client.timeseries.measurements(requireArg("station", station), timeseriesOr(ts), {
27
+ start: optStr(opts["start"]),
28
+ end: optStr(opts["end"]),
29
+ }));
30
+ }));
31
+ program
32
+ .command("waters")
33
+ .description("List all bodies of water (Gewässer)")
34
+ .action(action(deps, async ({ client, global }) => {
35
+ renderJson(deps, global, await client.waters());
36
+ }));
37
+ }
38
+ //# sourceMappingURL=timeseries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeseries.js","sourceRoot":"","sources":["../../../../src/cli/commands/timeseries.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5E,mFAAmF;AACnF,SAAS,MAAM,CAAC,KAAc;IAC5B,MAAM,CAAC,GAAG,KAA2B,CAAC;IACtC,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,OAAgB,EAAE,IAAa;IACxE,OAAO;SACJ,OAAO,CAAC,mCAAmC,CAAC;SAC5C,WAAW,CAAC,gEAAgE,CAAC;SAC7E,MAAM,CACL,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;QACvD,UAAU,CACR,IAAI,EACJ,MAAM,EACN,MAAM,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,CAC9E,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEJ,OAAO;SACJ,OAAO,CAAC,gCAAgC,CAAC;SACzC,WAAW,CAAC,sDAAsD,CAAC;SACnE,MAAM,CACL,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;QACvD,UAAU,CACR,IAAI,EACJ,MAAM,EACN,MAAM,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,CAC7F,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEJ,OAAO;SACJ,OAAO,CAAC,qCAAqC,CAAC;SAC9C,WAAW,CAAC,uDAAuD,CAAC;SACpE,MAAM,CAAC,eAAe,EAAE,sDAAsD,CAAC;SAC/E,MAAM,CAAC,aAAa,EAAE,8BAA8B,CAAC;SACrD,MAAM,CACL,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE;QAC7D,UAAU,CACR,IAAI,EACJ,MAAM,EACN,MAAM,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE;YACrF,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5B,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;SACzB,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IAEJ,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,qCAAqC,CAAC;SAClD,MAAM,CACL,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;QACxC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CACH,CAAC;AACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Binary entry point. Thin shim around run(); all logic lives in run.ts/program.ts.
3
+ import { run } from "./run.js";
4
+ const exitCode = await run(process.argv.slice(2));
5
+ process.exitCode = exitCode;
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA,oFAAoF;AAEpF,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { PegelOnlineClient } from "../client/client.js";
2
+ import type { EngineOptions } from "../client/engine.js";
3
+ export interface CliIO {
4
+ out(text: string): void;
5
+ err(text: string): void;
6
+ }
7
+ export interface CliDeps {
8
+ io: CliIO;
9
+ /** Build a client from the resolved global options (injectable for tests). */
10
+ createClient(options: EngineOptions): PegelOnlineClient;
11
+ }
12
+ export declare const defaultIO: CliIO;
13
+ //# 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,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,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,aAAa,GAAG,iBAAiB,CAAC;CACzD;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;AAgBtE,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/filesystem. */
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;AAuBvC,eAAO,MAAM,OAAO,QAAgB,CAAC;AAErC,yEAAyE;AACzE,eAAO,MAAM,WAAW,EAAE,OAGzB,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,GAAE,OAAqB,GAAG,OAAO,CA0BjE"}
@@ -0,0 +1,52 @@
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 { PegelOnlineClient } from "../client/client.js";
9
+ import { parseIntArg } from "./shared.js";
10
+ import { registerStationCommands } from "./commands/stations.js";
11
+ import { registerTimeseriesCommands } from "./commands/timeseries.js";
12
+ /**
13
+ * Single source of truth for the version: read from package.json at runtime
14
+ * rather than duplicating a literal that can silently drift after a release bump.
15
+ * From the compiled location (dist/src/cli/program.js) package.json is three
16
+ * directories up; the same offset holds for the source under src/cli.
17
+ */
18
+ function readVersion() {
19
+ try {
20
+ const pkgUrl = new URL("../../../package.json", import.meta.url);
21
+ const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8"));
22
+ return pkg.version ?? "0.0.0";
23
+ }
24
+ catch {
25
+ return "0.0.0";
26
+ }
27
+ }
28
+ export const VERSION = readVersion();
29
+ /** Default dependencies: real client + real stdout/stderr/filesystem. */
30
+ export const defaultDeps = {
31
+ io: defaultIO,
32
+ createClient: (options) => new PegelOnlineClient(options),
33
+ };
34
+ export function buildProgram(deps = defaultDeps) {
35
+ const program = new Command();
36
+ program
37
+ .name("pegel")
38
+ .description("CLI for the open PEGELONLINE water-level REST API " +
39
+ "(https://www.pegelonline.wsv.de/webservices/rest-api/v2)")
40
+ .version(VERSION)
41
+ .option("--base-url <url>", "API base URL", "https://www.pegelonline.wsv.de")
42
+ .option("--timeout <ms>", "per-request timeout in milliseconds", parseIntArg)
43
+ .option("--user-agent <ua>", "User-Agent header value")
44
+ .option("--max-retries <n>", "retries for transient 429/503 responses", parseIntArg)
45
+ .option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", parseIntArg)
46
+ .option("--compact", "print JSON on a single line instead of pretty-printed")
47
+ .showHelpAfterError();
48
+ registerStationCommands(program, deps);
49
+ registerTimeseriesCommands(program, deps);
50
+ return program;
51
+ }
52
+ //# 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,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAEtE;;;;;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,yEAAyE;AACzE,MAAM,CAAC,MAAM,WAAW,GAAY;IAClC,EAAE,EAAE,SAAS;IACb,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,iBAAiB,CAAC,OAAO,CAAC;CAC1D,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,oDAAoD;QAClD,0DAA0D,CAC7D;SACA,OAAO,CAAC,OAAO,CAAC;SAChB,MAAM,CAAC,kBAAkB,EAAE,cAAc,EAAE,gCAAgC,CAAC;SAC5E,MAAM,CAAC,gBAAgB,EAAE,qCAAqC,EAAE,WAAW,CAAC;SAC5E,MAAM,CAAC,mBAAmB,EAAE,yBAAyB,CAAC;SACtD,MAAM,CAAC,mBAAmB,EAAE,yCAAyC,EAAE,WAAW,CAAC;SACnF,MAAM,CACL,0BAA0B,EAC1B,kEAAkE,EAClE,WAAW,CACZ;SACA,MAAM,CAAC,WAAW,EAAE,uDAAuD,CAAC;SAC5E,kBAAkB,EAAE,CAAC;IAExB,uBAAuB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACvC,0BAA0B,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE1C,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { CliDeps } from "./io.js";
2
+ export declare function run(argv: string[], deps?: CliDeps): Promise<number>;
3
+ //# sourceMappingURL=run.d.ts.map
@@ -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;AAqBvC,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCtF"}
@@ -0,0 +1,62 @@
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 { PegelApiError, PegelError } from "../client/errors.js";
7
+ /**
8
+ * Apply exitOverride + output redirection to every command in the tree.
9
+ * commander does not propagate these to subcommands, so a parse error on a
10
+ * subcommand would otherwise call process.exit() and bypass our error handling.
11
+ */
12
+ function configureTree(command, deps) {
13
+ command.exitOverride();
14
+ command.configureOutput({
15
+ writeOut: (str) => deps.io.out(str.replace(/\n$/, "")),
16
+ writeErr: (str) => deps.io.err(str.replace(/\n$/, "")),
17
+ });
18
+ for (const child of command.commands)
19
+ configureTree(child, deps);
20
+ }
21
+ /** Distinct exit code for usage/parse errors, so scripts can tell a user mistake
22
+ * apart from a runtime/network failure (which exit 1). */
23
+ const USAGE_EXIT = 2;
24
+ export async function run(argv, deps = defaultDeps) {
25
+ const program = buildProgram(deps);
26
+ configureTree(program, deps);
27
+ // A bare invocation with no command should show help on stdout and exit 0,
28
+ // matching `--help`, rather than erroring out with help on stderr.
29
+ if (argv.length === 0) {
30
+ deps.io.out(program.helpInformation().replace(/\n$/, ""));
31
+ return 0;
32
+ }
33
+ try {
34
+ await program.parseAsync(argv, { from: "user" });
35
+ return 0;
36
+ }
37
+ catch (err) {
38
+ if (err instanceof CommanderError) {
39
+ // Help/version requests are not errors -> exit 0.
40
+ if (err.code === "commander.help" || err.code === "commander.helpDisplayed")
41
+ return 0;
42
+ if (err.code === "commander.version")
43
+ return 0;
44
+ // Every other CommanderError is a usage/parse error -> distinct exit code.
45
+ return USAGE_EXIT;
46
+ }
47
+ if (err instanceof PegelApiError) {
48
+ deps.io.err(`Error: ${err.message}`);
49
+ // Map a few notable statuses to distinct exit codes for scripting.
50
+ if (err.status === 404)
51
+ return 4;
52
+ return 1;
53
+ }
54
+ if (err instanceof PegelError) {
55
+ deps.io.err(`Error: ${err.message}`);
56
+ return 1;
57
+ }
58
+ deps.io.err(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
59
+ return 1;
60
+ }
61
+ }
62
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,+DAA+D;AAE/D,OAAO,EAAE,cAAc,EAAgB,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEhE;;;;GAIG;AACH,SAAS,aAAa,CAAC,OAAgB,EAAE,IAAa;IACpD,OAAO,CAAC,YAAY,EAAE,CAAC;IACvB,OAAO,CAAC,eAAe,CAAC;QACtB,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACtD,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;KACvD,CAAC,CAAC;IACH,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ;QAAE,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACnE,CAAC;AAED;2DAC2D;AAC3D,MAAM,UAAU,GAAG,CAAC,CAAC;AAErB,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,IAAc,EAAE,OAAgB,WAAW;IACnE,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE7B,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1D,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;YAClC,kDAAkD;YAClD,IAAI,GAAG,CAAC,IAAI,KAAK,gBAAgB,IAAI,GAAG,CAAC,IAAI,KAAK,yBAAyB;gBAAE,OAAO,CAAC,CAAC;YACtF,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB;gBAAE,OAAO,CAAC,CAAC;YAC/C,2EAA2E;YAC3E,OAAO,UAAU,CAAC;QACpB,CAAC;QACD,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;YACjC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACrC,mEAAmE;YACnE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,CAAC,CAAC;YACjC,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACrC,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,qBAAqB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrF,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { CliDeps } from "./io.js";
2
+ import type { EngineOptions } from "../client/engine.js";
3
+ /** commander value-parser: a non-negative integer. */
4
+ export declare function parseIntArg(value: string): number;
5
+ /**
6
+ * Validate a required positional argument: reject an empty/blank value rather
7
+ * than forwarding it into the URL path (which would produce a malformed request
8
+ * like `/stations//W/...`). Returns the trimmed value.
9
+ */
10
+ export declare function requireArg(name: string, value: string | undefined): string;
11
+ /**
12
+ * Normalise an optional `[timeseries]` positional: an empty/blank value behaves
13
+ * like omitting it and defaults to "W" (water level), matching the documented
14
+ * default. (`??` alone would forward an empty string into the path.)
15
+ */
16
+ export declare function timeseriesOr(value: string | undefined, fallback?: string): string;
17
+ export interface GlobalOptions {
18
+ baseUrl?: string;
19
+ timeout?: number;
20
+ userAgent?: string;
21
+ maxRetries?: number;
22
+ maxResponseBytes?: number;
23
+ compact?: boolean;
24
+ }
25
+ /** Translate resolved global CLI options into client EngineOptions. */
26
+ export declare function toEngineOptions(global: GlobalOptions): EngineOptions;
27
+ /** Render a JSON value to stdout, pretty by default, compact with --compact. */
28
+ export declare function renderJson(deps: CliDeps, global: GlobalOptions, value: unknown): void;
29
+ export interface ActionContext {
30
+ client: ReturnType<CliDeps["createClient"]>;
31
+ global: GlobalOptions;
32
+ /** This command's own parsed options. */
33
+ opts: Record<string, unknown>;
34
+ }
35
+ /**
36
+ * Wrap an async command action with consistent global-option resolution and
37
+ * client construction. The callback receives a context (client + resolved global
38
+ * options + this command's options) and the command's positional arguments.
39
+ *
40
+ * Commander invokes actions as (arg1, ..., argN, options, command); we slice off
41
+ * the trailing options object and command instance to recover the positionals.
42
+ */
43
+ export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
44
+ //# 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,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAU1E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,SAAM,GAAG,MAAM,CAE9E;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;CACnB;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAQpE;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAGrF;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"}