@sdxc/ip 2026.10.13 → 2026.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,22 @@ if (isSuccess(range) && isSuccess(ip)) range.data.contains(ip.data); // true
60
60
  IP.Range.parse("10.0.0.1/8"); // failure: IP.Error { code: "host-bits-set" }
61
61
  ```
62
62
 
63
+ ## Command line
64
+
65
+ ```sh
66
+ bunx @sdxc/ip classify 192.168.1.10
67
+ ```
68
+
69
+ With [`@sdxc/kit`](https://www.npmjs.com/package/@sdxc/kit) installed, `sdxc pkg ip …` runs the same command line, using the version your project installs.
70
+
71
+ `classify` prints the address's version, canonical form, classification, whether it is public, and the IPv4 an IPv4-mapped, NAT64 or 6to4 address carries. With no address, or `-`, it reads stdin; `--json` prints the same as an object.
72
+
73
+ ```sh
74
+ bunx @sdxc/ip contains 10.0.0.0/8 10.1.2.3
75
+ ```
76
+
77
+ `contains` exits 0 when the range holds the address and 1 when it does not. Both commands exit 2 for text that is not an address or a range. `bunx @sdxc/ip --help` lists every flag.
78
+
63
79
  ## API
64
80
 
65
81
  ### `IP.parse(text: string): Result<IP, IP.Error>`
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The `sdxc-ip classify` handler: parses the address and prints its version, canonical text,
3
+ * IANA special-purpose classification and any IPv4 it carries, as aligned lines or as JSON.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * Classifies the address; `--json` prints `{ address, version, classification, public,
10
+ * embeddedIPv4 }`, with `embeddedIPv4` null outside the IPv4-mapped, NAT64 and 6to4 prefixes.
11
+ * Text that is not an address exits 2.
12
+ */
13
+ declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
14
+ readonly address: import("@sdxc/cli").Argument<string>;
15
+ }, {
16
+ readonly json: import("@sdxc/cli").Flag<boolean>;
17
+ }>, []>;
18
+ export default _default;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The `sdxc-ip classify` handler: parses the address and prints its version, canonical text,
3
+ * IANA special-purpose classification and any IPv4 it carries, as aligned lines or as JSON.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import { createCommand, success } from "@sdxc/cli";
9
+ import { isFailure } from "@sdxc/result";
10
+ import { IP_COMMANDS } from "./cli-contract.js";
11
+ import { readAddress } from "./cli-input.js";
12
+ /**
13
+ * Classifies the address; `--json` prints `{ address, version, classification, public,
14
+ * embeddedIPv4 }`, with `embeddedIPv4` null outside the IPv4-mapped, NAT64 and 6to4 prefixes.
15
+ * Text that is not an address exits 2.
16
+ */
17
+ export default createCommand(IP_COMMANDS.classify, async (ctx) => {
18
+ let parsed = await readAddress(ctx.arguments.address, ctx.stdin);
19
+ if (isFailure(parsed))
20
+ return parsed;
21
+ let ip = parsed.data;
22
+ let report = {
23
+ address: ip.toString(),
24
+ version: ip.version,
25
+ classification: ip.classification,
26
+ public: ip.isPublic,
27
+ embeddedIPv4: ip.embeddedIPv4?.toString() ?? null,
28
+ };
29
+ if (ctx.flags.json) {
30
+ ctx.stdout(`${JSON.stringify(report, null, 2)}\n`);
31
+ return success();
32
+ }
33
+ let lines = [
34
+ `address ${report.address}`,
35
+ `version ${report.version}`,
36
+ `classification ${report.classification}`,
37
+ `public ${report.public ? "yes" : "no"}`,
38
+ ];
39
+ if (report.embeddedIPv4 !== null)
40
+ lines.push(`embedded IPv4 ${report.embeddedIPv4}`);
41
+ ctx.stdout(`${lines.join("\n")}\n`);
42
+ return success();
43
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The `sdxc-ip contains` handler: parses a range and an address and answers membership in the
3
+ * exit code, so a shell script can branch on it; stdout says the same for a person or as JSON.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ /**
9
+ * Exits 0 when the range holds the address and 1 when it does not; an address of the other
10
+ * version is always outside. A malformed range, one with host bits set, or text that is not an
11
+ * address exits 2. `--json` prints `{ range, address, contains }` with both in canonical text.
12
+ */
13
+ declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
14
+ readonly range: import("@sdxc/cli").Argument<string>;
15
+ readonly address: import("@sdxc/cli").Argument<string>;
16
+ }, {
17
+ readonly json: import("@sdxc/cli").Flag<boolean>;
18
+ }>, []>;
19
+ export default _default;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The `sdxc-ip contains` handler: parses a range and an address and answers membership in the
3
+ * exit code, so a shell script can branch on it; stdout says the same for a person or as JSON.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import { createCommand, exit, success } from "@sdxc/cli";
9
+ import { isFailure } from "@sdxc/result";
10
+ import { IP_COMMANDS } from "./cli-contract.js";
11
+ import { readAddress } from "./cli-input.js";
12
+ import { IP } from "./ip.js";
13
+ /**
14
+ * Exits 0 when the range holds the address and 1 when it does not; an address of the other
15
+ * version is always outside. A malformed range, one with host bits set, or text that is not an
16
+ * address exits 2. `--json` prints `{ range, address, contains }` with both in canonical text.
17
+ */
18
+ export default createCommand(IP_COMMANDS.contains, async (ctx) => {
19
+ let range = IP.Range.parse(ctx.arguments.range);
20
+ if (isFailure(range)) {
21
+ let reason = range.error.code === "host-bits-set"
22
+ ? `${JSON.stringify(ctx.arguments.range)} has bits set past its prefix; write the network's first address`
23
+ : `Not an IP range: ${JSON.stringify(ctx.arguments.range)}`;
24
+ return exit(2, reason);
25
+ }
26
+ let address = await readAddress(ctx.arguments.address, ctx.stdin);
27
+ if (isFailure(address))
28
+ return address;
29
+ let contains = range.data.contains(address.data);
30
+ if (ctx.flags.json) {
31
+ let report = { range: range.data.toString(), address: address.data.toString(), contains };
32
+ ctx.stdout(`${JSON.stringify(report, null, 2)}\n`);
33
+ }
34
+ else {
35
+ ctx.stdout(`${address.data.toString()} is ${contains ? "inside" : "outside"} ${range.data.toString()}\n`);
36
+ }
37
+ return contains ? success() : exit(1);
38
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The `sdxc-ip` command line as a contract: `classify` names what an address is for, and
3
+ * `contains` tests whether a range holds an address. Help, usage errors and each handler's
4
+ * typed `ctx.arguments` and `ctx.flags` all come from this one declaration.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** Every command `sdxc-ip` takes. */
10
+ export declare const IP_COMMANDS: import("@sdxc/cli").Group<Omit<{
11
+ readonly description: "sdxc-ip — IPv4 and IPv6 addresses against the IANA special-purpose registries";
12
+ readonly classify: import("@sdxc/cli").Command<{
13
+ readonly address: import("@sdxc/cli").Argument<string>;
14
+ }, {
15
+ readonly json: import("@sdxc/cli").Flag<boolean>;
16
+ }>;
17
+ readonly contains: import("@sdxc/cli").Command<{
18
+ readonly range: import("@sdxc/cli").Argument<string>;
19
+ readonly address: import("@sdxc/cli").Argument<string>;
20
+ }, {
21
+ readonly json: import("@sdxc/cli").Flag<boolean>;
22
+ }>;
23
+ }, "description" | "hidden" | "arguments" | "flags" | "tasks">, Record<never, never>, Record<never, never>>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The `sdxc-ip` command line as a contract: `classify` names what an address is for, and
3
+ * `contains` tests whether a range holds an address. Help, usage errors and each handler's
4
+ * typed `ctx.arguments` and `ctx.flags` all come from this one declaration.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { argument, command, commands, flag } from "@sdxc/cli";
10
+ /**
11
+ * Accepts any text, so the package needs no schema library; each handler parses the text
12
+ * itself and answers its own exit 2 naming the rejected value.
13
+ */
14
+ const TEXT = {
15
+ "~standard": {
16
+ version: 1,
17
+ vendor: "@sdxc/ip",
18
+ types: { input: undefined, output: "" },
19
+ validate(value) {
20
+ if (typeof value === "string")
21
+ return { value };
22
+ return { issues: [{ message: `expects text; got ${JSON.stringify(value)}` }] };
23
+ },
24
+ },
25
+ };
26
+ /** The `--json` flag every report-printing command takes. */
27
+ function json() {
28
+ return flag.boolean("json", { description: "Print the result as JSON" });
29
+ }
30
+ /** Every command `sdxc-ip` takes. */
31
+ export const IP_COMMANDS = commands({
32
+ description: "sdxc-ip — IPv4 and IPv6 addresses against the IANA special-purpose registries",
33
+ classify: command({
34
+ description: "Print an address's version, canonical form and IANA special-purpose classification",
35
+ arguments: {
36
+ address: argument(TEXT, {
37
+ default: "-",
38
+ description: "The address, or - to read it from stdin",
39
+ }),
40
+ },
41
+ flags: { json: json() },
42
+ examples: ["classify 192.168.1.10", "classify ::ffff:10.0.0.1 --json", "classify - < ip.txt"],
43
+ }),
44
+ contains: command({
45
+ description: "Exit 0 when the range holds the address and 1 when it does not",
46
+ arguments: {
47
+ range: argument(TEXT, { description: "The range as network/prefix" }),
48
+ address: argument(TEXT, {
49
+ default: "-",
50
+ description: "The address, or - to read it from stdin",
51
+ }),
52
+ },
53
+ flags: { json: json() },
54
+ examples: ["contains 10.0.0.0/8 10.1.2.3", "contains 2001:db8::/32 2001:db8::1 --json"],
55
+ }),
56
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What every `sdxc-ip` command shares: reading the address from its argument or stdin and
3
+ * parsing it, so both commands refuse the same input with the same exit 2 and message.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import type { ExitError, HandlerContext } from "@sdxc/cli";
9
+ import type { Result } from "@sdxc/result";
10
+ import type { IP_COMMANDS } from "./cli-contract.js";
11
+ import { IP } from "./ip.js";
12
+ /** The stdin every command reads a `-` address from. */
13
+ type Stdin = HandlerContext<typeof IP_COMMANDS.classify>["stdin"];
14
+ /**
15
+ * The address an argument names: the text as typed, or stdin's text trimmed for `-`. A
16
+ * terminal on stdin exits 2, so a command never waits on a person typing.
17
+ *
18
+ * @param value - The argument as typed
19
+ * @param stdin - The run's stdin, read when `value` is `-`
20
+ * @returns The parsed address, or an exit 2 naming the rejected text
21
+ */
22
+ export declare function readAddress(value: string, stdin: Stdin): Promise<Result<IP, ExitError>>;
23
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What every `sdxc-ip` command shares: reading the address from its argument or stdin and
3
+ * parsing it, so both commands refuse the same input with the same exit 2 and message.
4
+ *
5
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
6
+ * @copyright Sergio Xalambrí 2026
7
+ */
8
+ import { exit } from "@sdxc/cli";
9
+ import { isFailure, success } from "@sdxc/result";
10
+ import { IP } from "./ip.js";
11
+ /**
12
+ * The address an argument names: the text as typed, or stdin's text trimmed for `-`. A
13
+ * terminal on stdin exits 2, so a command never waits on a person typing.
14
+ *
15
+ * @param value - The argument as typed
16
+ * @param stdin - The run's stdin, read when `value` is `-`
17
+ * @returns The parsed address, or an exit 2 naming the rejected text
18
+ */
19
+ export async function readAddress(value, stdin) {
20
+ let text = value;
21
+ if (text === "-") {
22
+ if (stdin.isTTY)
23
+ return exit(2, "No address: pass one, or pipe it on stdin");
24
+ text = (await stdin.text()).trim();
25
+ }
26
+ let parsed = IP.parse(text);
27
+ if (isFailure(parsed))
28
+ return exit(2, `Not an IP address: ${JSON.stringify(text)}`);
29
+ return success(parsed.data);
30
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sdxc-ip` command-line program: the contract in `cli-contract.ts` with each command mapped
4
+ * onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 the address
5
+ * is outside the range, 2 usage or text that is not an address or a range.
6
+ *
7
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
8
+ * @copyright Sergio Xalambrí 2026
9
+ */
10
+ /** The `sdxc-ip` program, run by the bin entry below and by tests through `invoke()`. */
11
+ export declare const cli: import("@sdxc/cli").CLI<import("@sdxc/cli").Group<Omit<{
12
+ readonly description: "sdxc-ip — IPv4 and IPv6 addresses against the IANA special-purpose registries";
13
+ readonly classify: import("@sdxc/cli").Command<{
14
+ readonly address: import("@sdxc/cli").Argument<string>;
15
+ }, {
16
+ readonly json: import("@sdxc/cli").Flag<boolean>;
17
+ }>;
18
+ readonly contains: import("@sdxc/cli").Command<{
19
+ readonly range: import("@sdxc/cli").Argument<string>;
20
+ readonly address: import("@sdxc/cli").Argument<string>;
21
+ }, {
22
+ readonly json: import("@sdxc/cli").Flag<boolean>;
23
+ }>;
24
+ }, "description" | "hidden" | "arguments" | "flags" | "tasks">, Record<never, never>, Record<never, never>>, []>;
package/dist/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sdxc-ip` command-line program: the contract in `cli-contract.ts` with each command mapped
4
+ * onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 the address
5
+ * is outside the range, 2 usage or text that is not an address or a range.
6
+ *
7
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
8
+ * @copyright Sergio Xalambrí 2026
9
+ */
10
+ import { createCLI, lazy } from "@sdxc/cli";
11
+ import { IP_COMMANDS } from "./cli-contract.js";
12
+ /** The `sdxc-ip` program, run by the bin entry below and by tests through `invoke()`. */
13
+ export const cli = createCLI({ name: "sdxc-ip", commands: IP_COMMANDS });
14
+ cli.map(IP_COMMANDS.classify, lazy(() => import("./cli-classify.js")));
15
+ cli.map(IP_COMMANDS.contains, lazy(() => import("./cli-contains.js")));
16
+ if (import.meta.main) {
17
+ process.exit(await cli.run(process.argv.slice(2)));
18
+ }
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@sdxc/ip",
3
- "version": "2026.10.13",
3
+ "version": "2026.11.10",
4
4
  "description": "IPv4 and IPv6 addresses and ranges as value objects that classify against the IANA special-purpose registries",
5
5
  "license": "MIT",
6
+ "bin": {
7
+ "sdxc-ip": "dist/cli.js"
8
+ },
6
9
  "type": "module",
7
10
  "exports": {
8
11
  ".": "./dist/index.js"
9
12
  },
10
13
  "dependencies": {
14
+ "@sdxc/cli": "2026.11.10",
11
15
  "@sdxc/result": "2026.10.13"
12
16
  },
13
- "gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
17
+ "gitHead": "6191b60a13885b1d826884d574c4a393f9b8eeee",
14
18
  "publishConfig": {
15
19
  "access": "public"
16
20
  },
@@ -19,7 +23,7 @@
19
23
  "url": "git+https://github.com/sergiodxa/monorepo.git",
20
24
  "directory": "packages/ip"
21
25
  },
22
- "homepage": "https://sdxc.sergiodxa.com/api/ip",
26
+ "homepage": "https://sdxc.dev/api/ip",
23
27
  "author": {
24
28
  "name": "Sergio Xalambrí",
25
29
  "url": "https://sergiodxa.com"