@maschinenlesbar.org/ladesaeulenregister-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 (61) hide show
  1. package/CONTRIBUTING.md +25 -0
  2. package/LICENSE +661 -0
  3. package/LICENSING.md +47 -0
  4. package/README.md +69 -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 +91 -0
  8. package/dist/src/cli/commands/stations.js.map +1 -0
  9. package/dist/src/cli/index.d.ts +3 -0
  10. package/dist/src/cli/index.d.ts.map +1 -0
  11. package/dist/src/cli/index.js +11 -0
  12. package/dist/src/cli/index.js.map +1 -0
  13. package/dist/src/cli/io.d.ts +12 -0
  14. package/dist/src/cli/io.d.ts.map +1 -0
  15. package/dist/src/cli/io.js +7 -0
  16. package/dist/src/cli/io.js.map +1 -0
  17. package/dist/src/cli/program.d.ts +7 -0
  18. package/dist/src/cli/program.d.ts.map +1 -0
  19. package/dist/src/cli/program.js +53 -0
  20. package/dist/src/cli/program.js.map +1 -0
  21. package/dist/src/cli/run.d.ts +3 -0
  22. package/dist/src/cli/run.d.ts.map +1 -0
  23. package/dist/src/cli/run.js +85 -0
  24. package/dist/src/cli/run.js.map +1 -0
  25. package/dist/src/cli/shared.d.ts +56 -0
  26. package/dist/src/cli/shared.d.ts.map +1 -0
  27. package/dist/src/cli/shared.js +112 -0
  28. package/dist/src/cli/shared.js.map +1 -0
  29. package/dist/src/client/client.d.ts +32 -0
  30. package/dist/src/client/client.d.ts.map +1 -0
  31. package/dist/src/client/client.js +128 -0
  32. package/dist/src/client/client.js.map +1 -0
  33. package/dist/src/client/engine.d.ts +55 -0
  34. package/dist/src/client/engine.d.ts.map +1 -0
  35. package/dist/src/client/engine.js +111 -0
  36. package/dist/src/client/engine.js.map +1 -0
  37. package/dist/src/client/errors.d.ts +45 -0
  38. package/dist/src/client/errors.d.ts.map +1 -0
  39. package/dist/src/client/errors.js +57 -0
  40. package/dist/src/client/errors.js.map +1 -0
  41. package/dist/src/client/http.d.ts +26 -0
  42. package/dist/src/client/http.d.ts.map +1 -0
  43. package/dist/src/client/http.js +80 -0
  44. package/dist/src/client/http.js.map +1 -0
  45. package/dist/src/client/index.d.ts +11 -0
  46. package/dist/src/client/index.d.ts.map +1 -0
  47. package/dist/src/client/index.js +8 -0
  48. package/dist/src/client/index.js.map +1 -0
  49. package/dist/src/client/query.d.ts +9 -0
  50. package/dist/src/client/query.d.ts.map +1 -0
  51. package/dist/src/client/query.js +33 -0
  52. package/dist/src/client/query.js.map +1 -0
  53. package/dist/src/client/types.d.ts +108 -0
  54. package/dist/src/client/types.d.ts.map +1 -0
  55. package/dist/src/client/types.js +4 -0
  56. package/dist/src/client/types.js.map +1 -0
  57. package/dist/src/index.d.ts +2 -0
  58. package/dist/src/index.d.ts.map +1 -0
  59. package/dist/src/index.js +3 -0
  60. package/dist/src/index.js.map +1 -0
  61. package/package.json +69 -0
package/LICENSING.md ADDED
@@ -0,0 +1,47 @@
1
+ # Licensing
2
+
3
+ `ladesaeulenregister-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 `ladesaeulenregister-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,69 @@
1
+ # ladesaeulenregister-cli
2
+
3
+ A dependency-light **TypeScript client + CLI** for the **Ladesäulenregister** — the
4
+ Bundesnetzagentur's register of public EV charging stations in Germany (~111k
5
+ Ladeeinrichtungen). Backed by a public **ArcGIS FeatureServer**. A
6
+ [bund.dev](https://bund.dev) API.
7
+
8
+ - **No API key.** The public charging-station data 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 `LadesaeulenClient`, or the `ladesaeulen` command.
12
+
13
+ > **We provide the tool, not the data.** The data is © the Bundesnetzagentur under
14
+ > **CC BY 4.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/ladesaeulenregister-cli # the `ladesaeulen` command
20
+ # or as a library:
21
+ npm install @maschinenlesbar.org/ladesaeulenregister-cli
22
+ ```
23
+
24
+ ## CLI
25
+
26
+ ```bash
27
+ ladesaeulen stations --count # total public stations → 111524
28
+ ladesaeulen stations --where "Ort='Berlin' AND Typ='Schnellladeeinrichtung'" --count # → 660
29
+ ladesaeulen stations --near 52.52,13.405 --radius 1 --count # within 1 km of a point → 124
30
+ ladesaeulen stations --where "state='Bayern'" --limit 20 # a page of stations
31
+ ladesaeulen stations --geojson --limit 200 > stations.geojson # GeoJSON for a map
32
+ ladesaeulen count-by state # stations per Bundesland
33
+ ladesaeulen fields # the queryable columns
34
+ ```
35
+
36
+ - **`stations`** searches with an SQL `--where`, paging (`--limit`/`--offset`),
37
+ sorting (`--order-by`), field selection (`--fields`), a spatial `--near`/`--radius`,
38
+ and `--count` (just the number) or `--geojson` output.
39
+ - **`count-by <field>`** aggregates (e.g. per `state`, `Typ`, `Betreiber`).
40
+ - **`fields`** lists the queryable columns (build `--where`/`--fields`/`count-by`).
41
+
42
+ Filter values are **SQL, case-sensitive, single-quoted** (`Ort='Berlin'`). Global
43
+ flags: `--base-url`, `--timeout`, `--user-agent`, `--max-retries`,
44
+ `--max-response-bytes`, `--compact`. See [Usage.md](Usage.md).
45
+
46
+ ## Library
47
+
48
+ ```ts
49
+ import { LadesaeulenClient } from "@maschinenlesbar.org/ladesaeulenregister-cli";
50
+
51
+ const c = new LadesaeulenClient();
52
+ await c.count({ where: "Typ='Schnellladeeinrichtung'" }); // number
53
+ await c.countBy("state"); // per Bundesland
54
+ const near = await c.stations({ near: { lat: 52.52, lon: 13.405, radiusKm: 1 } });
55
+ ```
56
+
57
+ ## Documentation
58
+
59
+ - [Usage.md](Usage.md) — commands, the `--where`/spatial options, exit codes
60
+ - [DEVELOPING.md](DEVELOPING.md) — architecture, testing, the ArcGIS specifics
61
+ - [GLOSSARY.md](GLOSSARY.md) — the register's fields and terms
62
+ - [DATA_LICENSE.md](DATA_LICENSE.md) — the CC BY 4.0 data terms
63
+ - [SKILLS.md](SKILLS.md) — the Claude Code skills this repo ships
64
+
65
+ ## Licence
66
+
67
+ Code is dual-licensed **AGPL-3.0-or-later OR commercial** — see
68
+ [LICENSING.md](LICENSING.md). No external code contributions are accepted (see
69
+ [CONTRIBUTING.md](CONTRIBUTING.md)); bug reports and AGPL forks are welcome.
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { CliDeps } from "../io.js";
3
+ export declare function registerCommands(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":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AA0CxC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAwEtE"}
@@ -0,0 +1,91 @@
1
+ // Command group for the Ladesäulenregister CLI: the primary `stations` query
2
+ // (filter / page / spatial / count / geojson), a `count-by` grouped aggregation,
3
+ // and `fields` to list the queryable columns.
4
+ import { LadesaeulenValidationError } from "../../client/errors.js";
5
+ import { action, parseBoundedInt, parseIntArg, parseLatLon, parseNonEmpty, parsePositiveFloat, renderJson, } from "../shared.js";
6
+ /**
7
+ * Roughly Germany's bounding box (generous margins). Used only to catch a likely
8
+ * latitude/longitude swap in `--near` (both numbers are valid lat AND lon, so the
9
+ * parser cannot tell them apart).
10
+ */
11
+ function isOutsideGermany(lat, lon) {
12
+ return lat < 47 || lat > 56 || lon < 5 || lon > 16;
13
+ }
14
+ /** Build a StationQuery from this command's parsed options. */
15
+ function buildStationQuery(opts) {
16
+ const q = {};
17
+ if (typeof opts["where"] === "string")
18
+ q.where = opts["where"];
19
+ if (typeof opts["limit"] === "number")
20
+ q.limit = opts["limit"];
21
+ if (typeof opts["offset"] === "number")
22
+ q.offset = opts["offset"];
23
+ if (typeof opts["orderBy"] === "string")
24
+ q.orderBy = opts["orderBy"];
25
+ if (typeof opts["fields"] === "string")
26
+ q.outFields = opts["fields"];
27
+ const near = opts["near"];
28
+ const radius = opts["radius"];
29
+ if (near !== undefined || radius !== undefined) {
30
+ if (near === undefined || radius === undefined) {
31
+ throw new LadesaeulenValidationError("--near and --radius must be given together.");
32
+ }
33
+ q.near = { lat: near.lat, lon: near.lon, radiusKm: radius };
34
+ }
35
+ return q;
36
+ }
37
+ export function registerCommands(program, deps) {
38
+ program
39
+ .command("stations")
40
+ .description("Search charging stations (Ladeeinrichtungen)")
41
+ .option("--where <sql>", "SQL filter, e.g. \"Ort='Berlin' AND Typ='Schnellladeeinrichtung'\"", parseNonEmpty)
42
+ .option("--limit <n>", "max rows per request (1..10000; the server returns at most ~2000 — page with --offset)", parseBoundedInt(1, 10000), 50)
43
+ .option("--offset <n>", "rows to skip (for paging)", parseIntArg)
44
+ .option("--order-by <spec>", "sort, e.g. \"Ort ASC\" or \"max_electric_power_station DESC\"", parseNonEmpty)
45
+ .option("--fields <list>", "comma-separated field list, or '*' for all (see `fields`)", parseNonEmpty)
46
+ .option("--near <lat,lon>", "only stations near this WGS84 point (needs --radius)", parseLatLon)
47
+ .option("--radius <km>", "search radius in km for --near", parsePositiveFloat)
48
+ .option("--count", "print only the number of matching stations")
49
+ .option("--geojson", "output a GeoJSON FeatureCollection instead of ArcGIS JSON")
50
+ .action(action(deps, async ({ client, global, opts }) => {
51
+ if (opts["count"] === true && opts["geojson"] === true) {
52
+ throw new LadesaeulenValidationError("--count and --geojson cannot be combined.");
53
+ }
54
+ const q = buildStationQuery(opts);
55
+ if (q.near && isOutsideGermany(q.near.lat, q.near.lon)) {
56
+ deps.io.err(`Note: --near point (lat ${q.near.lat}, lon ${q.near.lon}) is outside Germany — ` +
57
+ "did you swap latitude and longitude? --near expects lat,lon.");
58
+ }
59
+ if (opts["count"] === true) {
60
+ renderJson(deps, global, await client.count(q));
61
+ }
62
+ else if (opts["geojson"] === true) {
63
+ renderJson(deps, global, await client.geojson(q));
64
+ }
65
+ else {
66
+ const page = await client.stations(q);
67
+ if (page.exceededTransferLimit) {
68
+ deps.io.err("Note: more stations match than were returned (the server caps a page at ~2000 rows). " +
69
+ "Page with --offset, or narrow --where.");
70
+ }
71
+ renderJson(deps, global, page);
72
+ }
73
+ }));
74
+ program
75
+ .command("count-by")
76
+ .description("Count stations grouped by a field, e.g. `count-by state` (per Bundesland)")
77
+ .argument("<field>", "field to group by (e.g. state, Typ, Betreiber, Ort)", parseNonEmpty)
78
+ .option("--where <sql>", "restrict to matching stations first", parseNonEmpty)
79
+ .action(action(deps, async ({ client, global, opts }, [field]) => {
80
+ const where = typeof opts["where"] === "string" ? opts["where"] : "1=1";
81
+ renderJson(deps, global, await client.countBy(field, where));
82
+ }));
83
+ program
84
+ .command("fields")
85
+ .description("List the queryable field names (for --where / --fields / count-by)")
86
+ .action(action(deps, async ({ client, global }) => {
87
+ const fields = await client.fields();
88
+ renderJson(deps, global, fields.map((f) => ({ name: f.name, type: f.type, alias: f.alias })));
89
+ }));
90
+ }
91
+ //# sourceMappingURL=stations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stations.js","sourceRoot":"","sources":["../../../../src/cli/commands/stations.ts"],"names":[],"mappings":"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"}
@@ -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,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 { LadesaeulenClient, LadesaeulenClientOptions } 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: LadesaeulenClientOptions): LadesaeulenClient;
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,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEvF,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,wBAAwB,GAAG,iBAAiB,CAAC;CACpE;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,CAiCjE"}
@@ -0,0 +1,53 @@
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 { LadesaeulenClient } from "../client/client.js";
9
+ import { parseIntArg, parseBoundedInt, parseHeaderValue, parseNonEmpty } from "./shared.js";
10
+ import { registerCommands } from "./commands/stations.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 LadesaeulenClient(options),
32
+ };
33
+ export function buildProgram(deps = defaultDeps) {
34
+ const program = new Command();
35
+ program
36
+ .name("ladesaeulen")
37
+ .description("CLI for the Ladesäulenregister — the Bundesnetzagentur's register of public EV " +
38
+ "charging stations in Germany (~111k Ladeeinrichtungen). No API key needed. " +
39
+ "`stations` searches with an SQL --where, paging, spatial --near/--radius, --count " +
40
+ "or --geojson; `count-by` aggregates (e.g. per Bundesland); `fields` lists the " +
41
+ "queryable columns.")
42
+ .version(VERSION)
43
+ .option("--base-url <url>", "API base URL (the ArcGIS FeatureServer)", parseNonEmpty, "https://services-eu1.arcgis.com/TJm8oSvOdJUQvQT5/arcgis/rest/services/Ladesaeulen/FeatureServer")
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
+ .showHelpAfterError();
50
+ registerCommands(program, deps);
51
+ return program;
52
+ }
53
+ //# 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,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;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,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,aAAa,CAAC;SACnB,WAAW,CACV,iFAAiF;QAC/E,6EAA6E;QAC7E,oFAAoF;QACpF,gFAAgF;QAChF,oBAAoB,CACvB;SACA,OAAO,CAAC,OAAO,CAAC;SAChB,MAAM,CACL,kBAAkB,EAClB,yCAAyC,EACzC,aAAa,EACb,iGAAiG,CAClG;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,kBAAkB,EAAE,CAAC;IAExB,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAEhC,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;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 { LadesaeulenApiError, LadesaeulenError, LadesaeulenNetworkError, LadesaeulenValidationError, } 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 LadesaeulenValidationError) {
56
+ deps.io.err(`Error: ${err.message}`);
57
+ return EXIT.USAGE;
58
+ }
59
+ if (err instanceof LadesaeulenApiError) {
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 LadesaeulenNetworkError) {
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 --limit or raise " +
73
+ "--max-response-bytes <n> (0 = unlimited).");
74
+ }
75
+ return EXIT.NETWORK;
76
+ }
77
+ if (err instanceof LadesaeulenError) {
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,56 @@
1
+ import type { CliDeps } from "./io.js";
2
+ import type { LadesaeulenClientOptions } 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, 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
+ /** commander value-parser: a strictly-positive decimal number (e.g. a radius in km). */
16
+ export declare function parsePositiveFloat(value: string): number;
17
+ /** commander value-parser for `--near`: a `lat,lon` pair in WGS84 degrees. */
18
+ export declare function parseLatLon(value: string): {
19
+ lat: number;
20
+ lon: number;
21
+ };
22
+ /**
23
+ * commander value-parser for a value that ends up in an HTTP header (User-Agent).
24
+ * Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
25
+ * Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
26
+ * checked by char code so the source stays free of control bytes.
27
+ */
28
+ export declare function parseHeaderValue(value: string): string;
29
+ export interface GlobalOptions {
30
+ baseUrl?: string;
31
+ timeout?: number;
32
+ userAgent?: string;
33
+ maxRetries?: number;
34
+ maxResponseBytes?: number;
35
+ compact?: boolean;
36
+ }
37
+ /** Translate resolved global CLI options into client options. */
38
+ export declare function toEngineOptions(global: GlobalOptions): LadesaeulenClientOptions;
39
+ /** Render a JSON value to stdout, pretty by default, compact with --compact. */
40
+ export declare function renderJson(deps: CliDeps, global: GlobalOptions, value: unknown): void;
41
+ export interface ActionContext {
42
+ client: ReturnType<CliDeps["createClient"]>;
43
+ global: GlobalOptions;
44
+ /** This command's own parsed options. */
45
+ opts: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * Wrap an async command action with consistent global-option resolution and
49
+ * client construction. The callback receives a context (client + resolved global
50
+ * options + this command's options) and the command's positional arguments.
51
+ *
52
+ * Commander invokes actions as (arg1, ..., argN, options, command); we slice off
53
+ * the trailing options object and command instance to recover the positionals.
54
+ */
55
+ export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
56
+ //# 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,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE;;;;;;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,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQvE;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;CACnB;AAED,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,wBAAwB,CAQ/E;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"}
@@ -0,0 +1,112 @@
1
+ // Shared helpers used across CLI command groups: option parsers, the global
2
+ // option resolver, and JSON rendering.
3
+ import { InvalidArgumentError } from "commander";
4
+ /**
5
+ * commander value-parser: a plain base-10 non-negative integer.
6
+ *
7
+ * Uses a strict regex rather than `Number()` coercion, which would otherwise
8
+ * accept empty/whitespace strings (`Number("") === 0`), hex/binary/scientific
9
+ * literals, signs, padding and decimals.
10
+ */
11
+ export function parseIntArg(value) {
12
+ if (!/^[0-9]+$/.test(value)) {
13
+ throw new InvalidArgumentError("Expected a non-negative integer.");
14
+ }
15
+ const n = Number(value);
16
+ if (!Number.isSafeInteger(n)) {
17
+ throw new InvalidArgumentError("Expected a non-negative integer.");
18
+ }
19
+ return n;
20
+ }
21
+ /** commander value-parser: a non-empty (after trimming) string. */
22
+ export function parseNonEmpty(value) {
23
+ if (value.trim() === "") {
24
+ throw new InvalidArgumentError("Expected a non-empty value.");
25
+ }
26
+ return value;
27
+ }
28
+ /** Build a commander value-parser for an integer constrained to [min, max]. */
29
+ export function parseBoundedInt(min, max) {
30
+ return (value) => {
31
+ const n = parseIntArg(value);
32
+ if (n < min)
33
+ throw new InvalidArgumentError(`Must be >= ${min}.`);
34
+ if (n > max)
35
+ throw new InvalidArgumentError(`Must be <= ${max}.`);
36
+ return n;
37
+ };
38
+ }
39
+ /** commander value-parser: a strictly-positive decimal number (e.g. a radius in km). */
40
+ export function parsePositiveFloat(value) {
41
+ const n = Number(value);
42
+ if (!Number.isFinite(n) || n <= 0) {
43
+ throw new InvalidArgumentError("Expected a positive number.");
44
+ }
45
+ return n;
46
+ }
47
+ /** commander value-parser for `--near`: a `lat,lon` pair in WGS84 degrees. */
48
+ export function parseLatLon(value) {
49
+ const m = /^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/.exec(value);
50
+ if (!m)
51
+ throw new InvalidArgumentError("Expected 'lat,lon' (e.g. 52.52,13.405).");
52
+ const lat = Number(m[1]);
53
+ const lon = Number(m[2]);
54
+ if (lat < -90 || lat > 90)
55
+ throw new InvalidArgumentError("Latitude must be between -90 and 90.");
56
+ if (lon < -180 || lon > 180)
57
+ throw new InvalidArgumentError("Longitude must be between -180 and 180.");
58
+ return { lat, lon };
59
+ }
60
+ /**
61
+ * commander value-parser for a value that ends up in an HTTP header (User-Agent).
62
+ * Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
63
+ * Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
64
+ * checked by char code so the source stays free of control bytes.
65
+ */
66
+ export function parseHeaderValue(value) {
67
+ for (let i = 0; i < value.length; i++) {
68
+ const c = value.charCodeAt(i);
69
+ if ((c < 0x20 && c !== 0x09) || c === 0x7f) {
70
+ throw new InvalidArgumentError("Value contains control characters.");
71
+ }
72
+ }
73
+ return value;
74
+ }
75
+ /** Translate resolved global CLI options into client options. */
76
+ export function toEngineOptions(global) {
77
+ const options = {};
78
+ if (global.baseUrl !== undefined)
79
+ options.baseUrl = global.baseUrl;
80
+ if (global.timeout !== undefined)
81
+ options.timeoutMs = global.timeout;
82
+ if (global.userAgent !== undefined)
83
+ options.userAgent = global.userAgent;
84
+ if (global.maxRetries !== undefined)
85
+ options.maxRetries = global.maxRetries;
86
+ if (global.maxResponseBytes !== undefined)
87
+ options.maxResponseBytes = global.maxResponseBytes;
88
+ return options;
89
+ }
90
+ /** Render a JSON value to stdout, pretty by default, compact with --compact. */
91
+ export function renderJson(deps, global, value) {
92
+ const text = global.compact ? JSON.stringify(value) : JSON.stringify(value, null, 2);
93
+ deps.io.out(text);
94
+ }
95
+ /**
96
+ * Wrap an async command action with consistent global-option resolution and
97
+ * client construction. The callback receives a context (client + resolved global
98
+ * options + this command's options) and the command's positional arguments.
99
+ *
100
+ * Commander invokes actions as (arg1, ..., argN, options, command); we slice off
101
+ * the trailing options object and command instance to recover the positionals.
102
+ */
103
+ export function action(deps, fn) {
104
+ return async (...args) => {
105
+ const command = args[args.length - 1];
106
+ const positionals = args.slice(0, Math.max(0, args.length - 2));
107
+ const global = command.optsWithGlobals();
108
+ const client = deps.createClient(toEngineOptions(global));
109
+ await fn({ client, global, opts: command.opts() }, positionals);
110
+ };
111
+ }
112
+ //# sourceMappingURL=shared.js.map