node-ipconfig 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 node-ipconfig contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # node-ipconfig
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Promise-based JavaScript access to Windows network configuration through the native `ipconfig.exe`. It has no runtime dependencies and never invokes a shell. Parsed configuration is returned as a JavaScript object by default (ready to use as JSON); you can also serialize it as JSON text, XML, YAML, CSV, or NDJSON.
4
+
5
+ ## Requirements and compatibility
6
+
7
+ - Node.js 18 or newer.
8
+ - Command execution is supported on Windows only. The pure `parseIpconfig(text)` parser runs on Windows, macOS, and Linux.
9
+ - Both ESM and CommonJS package entry points are provided. Public command methods return Promises.
10
+
11
+ ```sh
12
+ npm install node-ipconfig
13
+ ```
14
+
15
+ The command wrapper requires Windows. The parser and output formatters can run on any supported Node.js platform.
16
+
17
+ ## Quick start
18
+
19
+ ### ESM
20
+
21
+ ```js
22
+ import ipconfig from 'node-ipconfig';
23
+
24
+ const config = await ipconfig.getConfig({ all: true });
25
+ console.log(config.hostname);
26
+ for (const adapter of config.adapters) {
27
+ console.log(adapter.name, adapter.ipv4, adapter.ipv6);
28
+ }
29
+ ```
30
+
31
+ Select a serialized format when an integration needs a document rather than a JavaScript object:
32
+
33
+ ```js
34
+ const xml = await ipconfig.getConfig({ all: true, format: 'xml' });
35
+ const yaml = await ipconfig.getConfig({ all: true, format: 'yaml' });
36
+ const csv = await ipconfig.getConfig({ all: true, format: 'csv' });
37
+ ```
38
+
39
+ ### CommonJS
40
+
41
+ ```js
42
+ const ipconfig = require('node-ipconfig');
43
+
44
+ async function main() {
45
+ const adapters = await ipconfig.getAdapters({ all: true });
46
+ console.log(await ipconfig.getIPv4());
47
+ }
48
+ main().catch(console.error);
49
+ ```
50
+
51
+ ## API
52
+
53
+ Configuration methods accept `QueryOptions`: `all` (boolean, defaults to false), `timeout` (positive milliseconds, defaults to 15000), `signal` (`AbortSignal`), `encoding` (Node.js encoding name, defaults to UTF-8), and `rawResult` (boolean). `getConfig` additionally accepts `format`. Unknown keys and invalid values throw `TypeError`.
54
+
55
+ - `getConfig(options?)` runs `ipconfig` or `/all` and returns a `Config` object by default (`format: 'json'`). Supported formats are `json`, `xml`, `yaml`, `csv`, and `ndjson`. JSON returns the object; the other formats return a string. CSV has one row per adapter, with nested arrays and objects encoded as JSON cells. NDJSON emits one adapter record per line, including the hostname when available.
56
+ - `getRawOutput(options?)` returns stdout as a string. With `{ rawResult: true }`, it returns `{ stdout, stderr, exitCode }`.
57
+ - `getAdapters(options?)` returns `Adapter[]`.
58
+ - `getAdapter(nameOrIndex, options?)` returns one adapter; string matching is case-insensitive and the numeric index is zero-based. Missing adapters reject with `IpconfigError` code `ENOADAPTER`.
59
+ - `getIPv4()` / `getIPv6()` return flat arrays of address strings from detailed configuration.
60
+ - `getDefaultGateway()` / `getDnsServers()` return arrays of `{ adapter, address }` records.
61
+ - `getMacAddresses()` returns `{ adapter, address }` records for adapters with a physical address.
62
+ - `getHostname()` returns the host name reported by Node.js `os.hostname()`.
63
+ - `parseIpconfig(text)` synchronously parses captured text without running a process.
64
+ - `createIpconfig(options?)` creates an isolated API instance; its `platform` and `exec` options support testing with a mocked process runner.
65
+ - `execute(command, options?)` permits only the command names listed below. It returns stdout, or the execution result when `rawResult: true`.
66
+
67
+ ### Output formats
68
+
69
+ ```js
70
+ const config = await ipconfig.getConfig(); // Config object (default JSON data)
71
+ const jsonText = JSON.stringify(config, null, 2);
72
+ const xml = await ipconfig.getConfig({ format: 'xml' });
73
+ const yaml = await ipconfig.getConfig({ format: 'yaml' });
74
+ const csv = await ipconfig.getConfig({ format: 'csv' });
75
+ const lines = await ipconfig.getConfig({ format: 'ndjson' });
76
+ ```
77
+
78
+ The formatter preserves the parsed configuration fields. XML values are escaped. YAML is emitted as a YAML document. CSV uses a header row and one row per adapter; complex values are JSON-encoded in their cells. NDJSON emits one JSON adapter record per line. The original native text is available separately through `getRawOutput()`.
79
+
80
+ ### Data schema
81
+
82
+ `Config` is `{ hostname?: string, adapters: Adapter[] }`. Adapter fields include `name`, `type`, `addresses: IpAddress[]`, `ipv4: string[]`, `ipv6: string[]`, `dnsServers: string[]`, `defaultGateways: string[]`, and `connectionSpecificDnsSuffix` (string or `null`). When available, adapters also include `description`, `macAddress`, `subnetMask`, `ipv4PrefixLength`, `dhcpEnabled`, `autoconfigurationEnabled`, `dhcpServer`, `leaseObtained`, `leaseExpires`, and `mediaState`. Unknown labeled fields are preserved in `fields`. Older scalar fields such as `ipv4Address` and `ipv6Address` remain available for compatibility; when multiple addresses exist, those scalars contain the last parsed address. `IpAddress` contains `address`, `family` (4 or 6), and optional `scope` or `prefixLength`.
83
+
84
+ ## Commands and side effects
85
+
86
+ `execute` accepts `config`, `all`, `release`, `release6`, `renew`, `renew6`, `flushdns`, `displaydns`, `registerdns`, `showclassid`, `setclassid`, `showclassid6`, or `setclassid6`. Command options are limited to `timeout`, `signal`, `encoding`, `rawResult`, plus `adapter` where needed and `classId` for `setclassid` variants. Adapter names and class IDs are passed as distinct process arguments and reject control characters and slash-prefixed option injection.
87
+
88
+ Convenience methods include `flushDns(options?)`, `displayDns(options?)`, `registerDns(options?)`, `release(options?)`, and `renew(options?)`. `release` and `renew` accept optional `{ adapter, ipv6 }`; IPv6 selects `/release6` or `/renew6`. `showClassId({ adapter })`, `setClassId({ adapter, classId? })`, and their `...6` counterparts wrap class ID operations.
89
+
90
+ Configuration reads, `/displaydns`, and `/showclassid` are read-only. `/flushdns` clears the DNS resolver cache. `/registerdns`, DHCP release/renew, and class ID setting can change network state. Those commands are only run when the corresponding method or `execute` call is explicitly invoked; discovery never runs them. DHCP operations can briefly interrupt connectivity. Windows permissions may vary; run an appropriately authorized terminal when Windows denies an operation.
91
+
92
+ | JavaScript method | Native command | Effect |
93
+ | --- | --- | --- |
94
+ | `getConfig()` / `getRawOutput()` | `ipconfig` | Read current configuration |
95
+ | `{ all: true }` | `ipconfig /all` | Read detailed adapter configuration |
96
+ | `flushDns()` | `ipconfig /flushdns` | Clear DNS resolver cache |
97
+ | `displayDns()` / `getDnsCache()` | `ipconfig /displaydns` | Read cached DNS records |
98
+ | `registerDns()` | `ipconfig /registerdns` | Refresh DHCP leases and register DNS names |
99
+ | `release({ adapter?, ipv6? })` | `/release` or `/release6` | Release DHCP configuration; may interrupt connectivity |
100
+ | `renew({ adapter?, ipv6? })` | `/renew` or `/renew6` | Renew DHCP configuration; may interrupt connectivity |
101
+ | `showClassId({ adapter })` / `showClassId6(...)` | `/showclassid` or `/showclassid6` | Read DHCP class ID |
102
+ | `setClassId({ adapter, classId? })` / `setClassId6(...)` | `/setclassid` or `/setclassid6` | Change DHCP class ID |
103
+
104
+ You may also call `execute` with an allowlisted command name (`config`, `all`, `release`, `release6`, `renew`, `renew6`, `flushdns`, `displaydns`, `registerdns`, `showclassid`, `setclassid`, `showclassid6`, `setclassid6`). Commands that change system state are never run during discovery.
105
+
106
+ ## Raw output, encoding, and localization
107
+
108
+ `getRawOutput()` preserves the decoded command text, while `rawResult: true` also returns stderr and exit code. The default decoder is UTF-8; set `encoding` to a Node-supported encoding when the machine's OEM code page requires it. This avoids extra code-page dependencies, but callers must know the output encoding for their system. The parser recognizes common English, German, and Spanish labels, common adapter header forms, and preserves unknown labeled values. Windows localization and version changes mean parsing is best effort; use raw output when exact text matters.
109
+
110
+ ## Errors and security
111
+
112
+ Unsupported operating systems reject with `IpconfigError` code `ENOTSUP`. Process startup, permission, timeout, cancellation, and command errors reject with `IpconfigError`, including available command, stdout, stderr, exit code, and cause details. A command can emit useful output alongside a non-zero exit status; that text is returned, and `{ rawResult: true }` exposes the exit code. No API accepts arbitrary command strings or starts a shell. All executable arguments come from a fixed allowlist and validated values.
113
+
114
+ ## Examples
115
+
116
+ Runnable ESM example: [`examples/basic.js`](examples/basic.js).
117
+
118
+ For full API details, configuration fields, output format behavior, command effects, options, and errors, see the [API guide](docs/API.md).
119
+
120
+ ```js
121
+ const gateways = await ipconfig.getDefaultGateway();
122
+ const dns = await ipconfig.getDnsServers();
123
+ const wifi = await ipconfig.getAdapter('Wi-Fi');
124
+ const raw = await ipconfig.getRawOutput({ all: true });
125
+
126
+ // Explicit operations: these can change machine state.
127
+ await ipconfig.flushDns();
128
+ await ipconfig.renew({ adapter: wifi.name });
129
+ ```
130
+
131
+ ## Development
132
+
133
+ `npm test` runs the platform-independent mocked suite. `npm run lint` performs JavaScript syntax checks, and `npm run typecheck` checks the published declarations with TypeScript. CI runs the suite on Windows, Linux, and macOS for supported Node.js releases.
134
+
135
+ MIT licensed; see [LICENSE](LICENSE).
package/docs/API.md ADDED
@@ -0,0 +1,134 @@
1
+ # API guide
2
+
3
+ `node-ipconfig` wraps Windows `ipconfig.exe`. Its command methods return Promises and require Windows; `parseIpconfig()` and the format examples can be used on any supported Node.js platform. The package has no runtime dependencies.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install node-ipconfig
9
+ ```
10
+
11
+ Requires Node.js 18 or newer.
12
+
13
+ ## Get started
14
+
15
+ ### ECMAScript modules
16
+
17
+ ```js
18
+ import ipconfig from 'node-ipconfig';
19
+
20
+ const config = await ipconfig.getConfig({ all: true });
21
+ console.log(config.hostname);
22
+ for (const adapter of config.adapters) {
23
+ console.log(adapter.name, adapter.ipv4, adapter.ipv6);
24
+ }
25
+ ```
26
+
27
+ ### CommonJS
28
+
29
+ ```js
30
+ const ipconfig = require('node-ipconfig');
31
+
32
+ async function main() {
33
+ const adapters = await ipconfig.getAdapters({ all: true });
34
+ console.log(adapters);
35
+ }
36
+ main().catch(console.error);
37
+ ```
38
+
39
+ ## Configuration and formats
40
+
41
+ `getConfig(options?)` executes `ipconfig` and parses its output. Set `all: true` to execute `ipconfig /all`. By default it returns a JavaScript `Config` object (JSON-compatible data):
42
+
43
+ ```js
44
+ const config = await ipconfig.getConfig();
45
+ const detailed = await ipconfig.getConfig({ all: true });
46
+ ```
47
+
48
+ Set `format` to serialize the parsed configuration. JSON is the default and remains an object; the other formats return strings.
49
+
50
+ ```js
51
+ const jsonData = await ipconfig.getConfig({ format: 'json' });
52
+ const xml = await ipconfig.getConfig({ all: true, format: 'xml' });
53
+ const yaml = await ipconfig.getConfig({ all: true, format: 'yaml' });
54
+ const csv = await ipconfig.getConfig({ all: true, format: 'csv' });
55
+ const ndjson = await ipconfig.getConfig({ all: true, format: 'ndjson' });
56
+ ```
57
+
58
+ CSV has one row per adapter; nested values are JSON-encoded in cells. NDJSON has one adapter record per line. XML values are escaped. YAML is emitted as a YAML document. Supported format names are `json`, `xml`, `yaml`, `csv`, and `ndjson`; unsupported names throw `TypeError`.
59
+
60
+ `getRawOutput(options?)` returns the original decoded text. Set `rawResult: true` to receive `{ stdout, stderr, exitCode }` instead.
61
+
62
+ ## Discovery methods
63
+
64
+ | Method | Result |
65
+ | --- | --- |
66
+ | `getConfig(options?)` | Parsed configuration, or serialized output when a non-JSON format is selected |
67
+ | `getAdapters(options?)` | Adapter array |
68
+ | `getAdapter(nameOrIndex, options?)` | One adapter by case-insensitive name or zero-based index |
69
+ | `getIPv4()` / `getIPv6()` | Flat arrays of IP address strings |
70
+ | `getDefaultGateway()` | `{ adapter, address }` records |
71
+ | `getDnsServers()` | `{ adapter, address }` records |
72
+ | `getMacAddresses()` | `{ adapter, address }` records |
73
+ | `getHostname()` | Host name from Node.js `os.hostname()` |
74
+ | `getRawOutput(options?)` | Native command output as text or execution result |
75
+ | `parseIpconfig(text)` | Synchronous parser for captured output; no command execution |
76
+
77
+ Detailed fields include adapter type and description, MAC address, IPv4 and IPv6 addresses, subnet mask and prefix length, gateways, DNS servers, DHCP and autoconfiguration state, DHCP server, lease timestamps, media state, and preserved unknown labeled fields. Parsing recognizes common English, German, and Spanish labels; it is best-effort across Windows versions and locales.
78
+
79
+ ## Options
80
+
81
+ Configuration queries accept:
82
+
83
+ - `all`: boolean, defaults to `false`.
84
+ - `format`: `json`, `xml`, `yaml`, `csv`, or `ndjson`; only supported by `getConfig` and defaults to `json`.
85
+ - `timeout`: positive milliseconds, defaults to 15000.
86
+ - `signal`: `AbortSignal` to cancel execution.
87
+ - `encoding`: Node.js text encoding, defaults to `utf8`.
88
+ - `rawResult`: return `{ stdout, stderr, exitCode }` for raw command methods.
89
+
90
+ Unknown options and invalid values throw `TypeError`.
91
+
92
+ ## Explicit commands
93
+
94
+ Operations that change network state are only executed when called explicitly. `execute(command, options?)` accepts the allowlisted names below; it never accepts arbitrary command text.
95
+
96
+ | Method | `execute` name / native switch | Notes |
97
+ | --- | --- | --- |
98
+ | `execute('config')` | `ipconfig` | Read current configuration |
99
+ | `execute('all')` | `/all` | Detailed configuration |
100
+ | `flushDns()` | `/flushdns` | Clears the DNS resolver cache |
101
+ | `displayDns()` / `getDnsCache()` | `/displaydns` | Displays cached DNS records |
102
+ | `registerDns()` | `/registerdns` | Initiates manual DNS registration |
103
+ | `release(options?)` | `/release` or `/release6` | Releases DHCP configuration; may interrupt connectivity |
104
+ | `renew(options?)` | `/renew` or `/renew6` | Renews DHCP configuration; may interrupt connectivity |
105
+ | `showClassId({ adapter })` | `/showclassid` | Displays a DHCP class ID |
106
+ | `setClassId({ adapter, classId? })` | `/setclassid` | Sets or clears a DHCP class ID |
107
+ | `showClassId6({ adapter })` | `/showclassid6` | IPv6 class ID operation |
108
+ | `setClassId6({ adapter, classId? })` | `/setclassid6` | Sets or clears an IPv6 class ID |
109
+
110
+ `release()` and `renew()` accept `{ adapter, ipv6 }`; set `ipv6: true` to choose the IPv6 command. `adapter` and `classId` values are passed as separate process arguments, not through a shell. Read-only discovery never runs a state-changing command.
111
+
112
+ Run release, renew, DNS registration, and class ID changes only when their effects are appropriate. DHCP release/renew can disrupt connectivity. Windows may require an appropriately authorized terminal for some operations.
113
+
114
+ ## Errors and platform support
115
+
116
+ Command methods reject with `IpconfigError`. Its `code` identifies unsupported platforms (`ENOTSUP`), missing executables (`ENOENT`), permissions (`EACCES`), timeouts (`ETIMEDOUT`), cancellation (`ABORT_ERR`), or command failures. Available command, stdout, stderr, exit code, and cause are attached to the error. Unsupported option names, command names, or argument values throw `TypeError`.
117
+
118
+ ## Testing with a custom runner
119
+
120
+ `createIpconfig()` makes the command runner injectable for tests:
121
+
122
+ ```js
123
+ import { createIpconfig } from 'node-ipconfig';
124
+
125
+ const api = createIpconfig({
126
+ platform: 'win32',
127
+ exec(file, args, options, callback) {
128
+ callback(null, 'Windows IP Configuration', '');
129
+ }
130
+ });
131
+ const config = await api.getConfig();
132
+ ```
133
+
134
+ The parser can also be tested directly with fixture text, without mocking or requiring Windows.
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
- {
2
- "name": "node-ipconfig",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "node-ipconfig",
3
+ "version": "1.0.0",
4
+ "description": "A safe JavaScript API for Windows ipconfig output",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./src/index.cjs",
8
+ "module": "./src/index.js",
9
+ "types": "./src/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": { "types": "./src/index.d.ts", "default": "./src/index.js" },
13
+ "require": { "types": "./src/index.d.cts", "default": "./src/index.cjs" }
14
+ },
15
+ "./parser": {
16
+ "import": { "types": "./src/parser.d.ts", "default": "./src/parser.js" },
17
+ "require": {
18
+ "types": "./src/parser.d.cts",
19
+ "default": "./src/parser.cjs"
20
+ }
21
+ }
22
+ },
23
+ "files": ["src", "docs", "README.md", "LICENSE"],
24
+ "engines": { "node": ">=18" },
25
+ "scripts": {
26
+ "test": "node --test",
27
+ "test:coverage": "node --experimental-test-coverage --test",
28
+ "lint": "node --check src/index.js && node --check src/parser.js && node --check src/command.js && node --check test/ipconfig.test.js && node --check examples/basic.js",
29
+ "typecheck": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext src/index.d.ts src/index.d.cts src/parser.d.ts src/parser.d.cts"
30
+ },
31
+ "keywords": ["ipconfig", "windows", "network", "configuration", "dns"],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/aymen94/node-ipconfig.git"
35
+ },
36
+ "bugs": { "url": "https://github.com/aymen94/node-ipconfig/issues" },
37
+ "homepage": "https://github.com/aymen94/node-ipconfig#readme"
38
+ }
package/src/command.js ADDED
@@ -0,0 +1,117 @@
1
+ import { execFile } from "node:child_process";
2
+
3
+ export class IpconfigError extends Error {
4
+ constructor(
5
+ message,
6
+ {
7
+ code = "EIPCONFIG",
8
+ cause,
9
+ command,
10
+ stdout = "",
11
+ stderr = "",
12
+ exitCode,
13
+ } = {},
14
+ ) {
15
+ super(message, { cause });
16
+ this.name = "IpconfigError";
17
+ this.code = code;
18
+ if (command) this.command = command;
19
+ this.stdout = stdout;
20
+ this.stderr = stderr;
21
+ if (exitCode !== undefined) this.exitCode = exitCode;
22
+ }
23
+ }
24
+
25
+ export function createRunner({
26
+ platform = process.platform,
27
+ exec = execFile,
28
+ } = {}) {
29
+ return function run(args = [], options = {}) {
30
+ if (platform !== "win32")
31
+ return Promise.reject(
32
+ new IpconfigError("node-ipconfig is supported only on Windows.", {
33
+ code: "ENOTSUP",
34
+ }),
35
+ );
36
+ return new Promise((resolve, reject) => {
37
+ const command = ["ipconfig.exe", ...args];
38
+ const finish = (error, stdout = "", stderr = "") => {
39
+ const output =
40
+ typeof stdout === "string"
41
+ ? stdout
42
+ : stdout?.toString(options.encoding ?? "utf8");
43
+ const errorOutput =
44
+ typeof stderr === "string"
45
+ ? stderr
46
+ : stderr?.toString(options.encoding ?? "utf8");
47
+ const exitCode = Number.isInteger(error?.code)
48
+ ? error.code
49
+ : error
50
+ ? null
51
+ : 0;
52
+ // Some ipconfig commands emit useful localized diagnostics and a non-zero exit status.
53
+ if (
54
+ error &&
55
+ !output?.trim() &&
56
+ !(options.rawResult && Number.isInteger(error.code))
57
+ ) {
58
+ const code =
59
+ error.code === "EACCES" || error.code === "EPERM"
60
+ ? "EACCES"
61
+ : error.code === "ENOENT"
62
+ ? "ENOENT"
63
+ : error.code === "ETIMEDOUT" ||
64
+ (error.killed && options.timeout)
65
+ ? "ETIMEDOUT"
66
+ : error.name === "AbortError"
67
+ ? "ABORT_ERR"
68
+ : (error.code ?? "EIPCONFIG");
69
+ const message =
70
+ code === "EACCES"
71
+ ? "Permission denied while running ipconfig; retry from an appropriately authorized terminal."
72
+ : code === "ENOENT"
73
+ ? "ipconfig.exe could not be found on PATH."
74
+ : code === "ETIMEDOUT"
75
+ ? "ipconfig timed out; increase timeout or retry."
76
+ : code === "ABORT_ERR"
77
+ ? "ipconfig was aborted."
78
+ : `ipconfig failed${error.message ? `: ${error.message}` : ""}`;
79
+ reject(
80
+ new IpconfigError(message, {
81
+ code,
82
+ cause: error,
83
+ command,
84
+ stdout: output ?? "",
85
+ stderr: errorOutput ?? "",
86
+ exitCode: exitCode ?? undefined,
87
+ }),
88
+ );
89
+ return;
90
+ }
91
+ if (options.rawResult)
92
+ resolve({
93
+ stdout: output ?? "",
94
+ stderr: errorOutput ?? "",
95
+ exitCode: exitCode ?? 0,
96
+ });
97
+ else resolve((output ?? "") || (errorOutput ?? ""));
98
+ };
99
+ try {
100
+ exec(
101
+ "ipconfig.exe",
102
+ args,
103
+ {
104
+ windowsHide: true,
105
+ encoding: options.encoding ?? "utf8",
106
+ timeout: options.timeout ?? 15000,
107
+ maxBuffer: 4 * 1024 * 1024,
108
+ ...(options.signal ? { signal: options.signal } : {}),
109
+ },
110
+ finish,
111
+ );
112
+ } catch (error) {
113
+ finish(error);
114
+ }
115
+ });
116
+ };
117
+ }
package/src/format.js ADDED
@@ -0,0 +1,119 @@
1
+ const FORMATS = new Set(["json", "xml", "yaml", "csv", "ndjson"]);
2
+
3
+ function escapeXml(value) {
4
+ return String(value).replace(
5
+ /[<>&"']/g,
6
+ (character) =>
7
+ ({
8
+ "<": "&lt;",
9
+ ">": "&gt;",
10
+ "&": "&amp;",
11
+ '"': "&quot;",
12
+ "'": "&apos;",
13
+ })[character],
14
+ );
15
+ }
16
+
17
+ function xmlNode(name, value) {
18
+ if (Array.isArray(value))
19
+ return `<${name}>${value.map((item) => xmlNode("item", item)).join("")}</${name}>`;
20
+ if (value && typeof value === "object")
21
+ return `<${name}>${Object.entries(value)
22
+ .map(([key, item]) =>
23
+ xmlNode(key.replace(/[^A-Za-z0-9_.-]/g, "_") || "field", item),
24
+ )
25
+ .join("")}</${name}>`;
26
+ return `<${name}>${escapeXml(value ?? "")}</${name}>`;
27
+ }
28
+
29
+ function yamlScalar(value) {
30
+ if (value === null) return "null";
31
+ if (typeof value === "string") return JSON.stringify(value);
32
+ return String(value);
33
+ }
34
+
35
+ function yamlLines(value, indent = 0) {
36
+ const pad = " ".repeat(indent);
37
+ if (Array.isArray(value))
38
+ return value.length
39
+ ? value.flatMap((item) => {
40
+ if (item && typeof item === "object") {
41
+ const lines = yamlLines(item, indent + 2);
42
+ return [
43
+ `${pad}- ${lines[0]?.trimStart() ?? "{}"}`,
44
+ ...lines.slice(1),
45
+ ];
46
+ }
47
+ return [`${pad}- ${yamlScalar(item)}`];
48
+ })
49
+ : [`${pad}[]`];
50
+ if (value && typeof value === "object") {
51
+ const entries = Object.entries(value);
52
+ return entries.length
53
+ ? entries.flatMap(([key, item]) => {
54
+ const safeKey = /^[A-Za-z_][\w.-]*$/.test(key)
55
+ ? key
56
+ : JSON.stringify(key);
57
+ if (item && typeof item === "object")
58
+ return [`${pad}${safeKey}:`, ...yamlLines(item, indent + 2)];
59
+ return [`${pad}${safeKey}: ${yamlScalar(item)}`];
60
+ })
61
+ : [`${pad}{}`];
62
+ }
63
+ return [`${pad}${yamlScalar(value)}`];
64
+ }
65
+
66
+ function csvCell(value) {
67
+ const text =
68
+ value == null
69
+ ? ""
70
+ : typeof value === "object"
71
+ ? JSON.stringify(value)
72
+ : String(value);
73
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
74
+ }
75
+
76
+ /** Format a parsed configuration as JSON data or a serialized JSON, XML, YAML, CSV, or NDJSON document. */
77
+ export function formatConfig(config, format = "json") {
78
+ if (!FORMATS.has(format))
79
+ throw new TypeError(
80
+ `Unsupported format: ${format}. Choose json, xml, yaml, csv, or ndjson.`,
81
+ );
82
+ if (format === "json") return config;
83
+ if (format === "xml")
84
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${xmlNode("config", config)}`;
85
+ if (format === "yaml") return `${yamlLines(config).join("\n")}\n`;
86
+ if (format === "ndjson")
87
+ return `${config.adapters.map((adapter) => JSON.stringify({ ...(config.hostname ? { hostname: config.hostname } : {}), adapter })).join("\n")}${config.adapters.length ? "\n" : ""}`;
88
+ const columns = [
89
+ "name",
90
+ "type",
91
+ "description",
92
+ "macAddress",
93
+ "ipv4",
94
+ "ipv6",
95
+ "subnetMask",
96
+ "ipv4PrefixLength",
97
+ "defaultGateways",
98
+ "dnsServers",
99
+ "dhcpEnabled",
100
+ "autoconfigurationEnabled",
101
+ "dhcpServer",
102
+ "leaseObtained",
103
+ "leaseExpires",
104
+ "connectionSpecificDnsSuffix",
105
+ "mediaState",
106
+ "addresses",
107
+ "fields",
108
+ ];
109
+ return (
110
+ [
111
+ columns.join(","),
112
+ ...config.adapters.map((adapter) =>
113
+ columns.map((key) => csvCell(adapter[key])).join(","),
114
+ ),
115
+ ].join("\r\n") + "\r\n"
116
+ );
117
+ }
118
+
119
+ export const CONFIG_FORMATS = Object.freeze([...FORMATS]);
package/src/index.cjs ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ const load = () => import("./index.js");
3
+ const api = {};
4
+ for (const name of [
5
+ "execute",
6
+ "getRawOutput",
7
+ "getConfig",
8
+ "getAdapters",
9
+ "getAdapter",
10
+ "getIPv4",
11
+ "getIPv6",
12
+ "getDefaultGateway",
13
+ "getDnsServers",
14
+ "getMacAddresses",
15
+ "getHostname",
16
+ "release",
17
+ "renew",
18
+ "flushDns",
19
+ "getDnsCache",
20
+ "displayDns",
21
+ "registerDns",
22
+ "showClassId",
23
+ "setClassId",
24
+ "showClassId6",
25
+ "setClassId6",
26
+ ]) {
27
+ api[name] = (...args) =>
28
+ load().then(({ ipconfig }) => ipconfig[name](...args));
29
+ }
30
+ api.parseIpconfig = (...args) =>
31
+ load().then(({ parseIpconfig }) => parseIpconfig(...args));
32
+ api.createIpconfig = (options) => {
33
+ const instance = {};
34
+ for (const name of [
35
+ "execute",
36
+ "getRawOutput",
37
+ "getConfig",
38
+ "getAdapters",
39
+ "getAdapter",
40
+ "getIPv4",
41
+ "getIPv6",
42
+ "getDefaultGateway",
43
+ "getDnsServers",
44
+ "getMacAddresses",
45
+ "getHostname",
46
+ "release",
47
+ "renew",
48
+ "flushDns",
49
+ "getDnsCache",
50
+ "displayDns",
51
+ "registerDns",
52
+ "showClassId",
53
+ "setClassId",
54
+ "showClassId6",
55
+ "setClassId6",
56
+ ]) {
57
+ instance[name] = (...args) =>
58
+ load().then(({ createIpconfig }) =>
59
+ createIpconfig(options)[name](...args),
60
+ );
61
+ }
62
+ return instance;
63
+ };
64
+ module.exports = api;
@@ -0,0 +1,92 @@
1
+ declare namespace nodeIpconfig {
2
+ interface QueryOptions {
3
+ all?: boolean;
4
+ timeout?: number;
5
+ signal?: AbortSignal;
6
+ encoding?: BufferEncoding;
7
+ rawResult?: boolean;
8
+ }
9
+ type ConfigFormat = "json" | "xml" | "yaml" | "csv" | "ndjson";
10
+ interface ConfigOptions extends QueryOptions {
11
+ format?: ConfigFormat;
12
+ }
13
+ interface ExecutionOptions {
14
+ timeout?: number;
15
+ signal?: AbortSignal;
16
+ encoding?: BufferEncoding;
17
+ rawResult?: boolean;
18
+ }
19
+ interface Adapter {
20
+ name: string;
21
+ type: string;
22
+ addresses: Array<{
23
+ address: string;
24
+ family: 4 | 6;
25
+ scope?: string;
26
+ prefixLength?: number;
27
+ }>;
28
+ ipv4: string[];
29
+ ipv6: string[];
30
+ dnsServers: string[];
31
+ defaultGateways: string[];
32
+ [field: string]: unknown;
33
+ }
34
+ interface Config {
35
+ hostname?: string;
36
+ adapters: Adapter[];
37
+ }
38
+ interface Result {
39
+ stdout: string;
40
+ stderr: string;
41
+ exitCode: number;
42
+ }
43
+ interface Api {
44
+ execute(
45
+ command: string,
46
+ options?: ExecutionOptions & { adapter?: string; classId?: string },
47
+ ): Promise<string | Result>;
48
+ getRawOutput(options?: QueryOptions): Promise<string | Result>;
49
+ getConfig<F extends ConfigFormat = "json">(
50
+ options?: ConfigOptions & { format?: F },
51
+ ): Promise<F extends "json" ? Config : string>;
52
+ getAdapters(options?: QueryOptions): Promise<Adapter[]>;
53
+ getAdapter(
54
+ nameOrIndex: string | number,
55
+ options?: QueryOptions,
56
+ ): Promise<Adapter>;
57
+ getIPv4(): Promise<string[]>;
58
+ getIPv6(): Promise<string[]>;
59
+ getDefaultGateway(): Promise<Array<{ adapter: string; address: string }>>;
60
+ getDnsServers(): Promise<Array<{ adapter: string; address: string }>>;
61
+ getMacAddresses(): Promise<Array<{ adapter: string; address: string }>>;
62
+ getHostname(): Promise<string>;
63
+ flushDns(options?: ExecutionOptions): Promise<string | Result>;
64
+ displayDns(options?: ExecutionOptions): Promise<string | Result>;
65
+ getDnsCache(options?: ExecutionOptions): Promise<string | Result>;
66
+ registerDns(options?: ExecutionOptions): Promise<string | Result>;
67
+ release(
68
+ options?: ExecutionOptions & { adapter?: string; ipv6?: boolean },
69
+ ): Promise<string | Result>;
70
+ renew(
71
+ options?: ExecutionOptions & { adapter?: string; ipv6?: boolean },
72
+ ): Promise<string | Result>;
73
+ showClassId(
74
+ options: ExecutionOptions & { adapter: string },
75
+ ): Promise<string | Result>;
76
+ setClassId(
77
+ options: ExecutionOptions & { adapter: string; classId?: string },
78
+ ): Promise<string | Result>;
79
+ showClassId6(
80
+ options: ExecutionOptions & { adapter: string },
81
+ ): Promise<string | Result>;
82
+ setClassId6(
83
+ options: ExecutionOptions & { adapter: string; classId?: string },
84
+ ): Promise<string | Result>;
85
+ }
86
+ interface Module extends Api {
87
+ parseIpconfig(output: string): Promise<Config>;
88
+ createIpconfig(options?: { platform?: string; exec?: Function }): Api;
89
+ }
90
+ }
91
+ declare const api: nodeIpconfig.Module;
92
+ export = api;
package/src/index.d.ts ADDED
@@ -0,0 +1,132 @@
1
+ export type AddressFamily = 4 | 6;
2
+ export interface IpAddress {
3
+ address: string;
4
+ family: AddressFamily;
5
+ scope?: "link-local";
6
+ prefixLength?: number;
7
+ }
8
+ export interface Adapter {
9
+ name: string;
10
+ type: string;
11
+ description?: string;
12
+ macAddress?: string;
13
+ addresses: IpAddress[];
14
+ ipv4: string[];
15
+ ipv6: string[];
16
+ ipv4Address?: string;
17
+ ipv6Address?: string;
18
+ linkLocalIpv6Address?: string;
19
+ subnetMask?: string;
20
+ ipv4PrefixLength?: number;
21
+ defaultGateways: string[];
22
+ dnsServers: string[];
23
+ dhcpEnabled?: boolean | string;
24
+ autoconfigurationEnabled?: boolean | string;
25
+ dhcpServer?: string;
26
+ leaseObtained?: string;
27
+ leaseExpires?: string;
28
+ connectionSpecificDnsSuffix: string | null;
29
+ mediaState?: string;
30
+ fields?: Record<string, string>;
31
+ [field: string]: unknown;
32
+ }
33
+ export interface Config {
34
+ hostname?: string;
35
+ adapters: Adapter[];
36
+ }
37
+ export type ConfigFormat = "json" | "xml" | "yaml" | "csv" | "ndjson";
38
+ export type FormattedConfig<F extends ConfigFormat> = F extends "json"
39
+ ? Config
40
+ : string;
41
+ export interface ExecutionOptions {
42
+ timeout?: number;
43
+ signal?: AbortSignal;
44
+ encoding?: BufferEncoding;
45
+ rawResult?: boolean;
46
+ }
47
+ export interface QueryOptions extends ExecutionOptions {
48
+ all?: boolean;
49
+ }
50
+ export interface ConfigOptions extends QueryOptions {
51
+ format?: ConfigFormat;
52
+ }
53
+ export interface CommandOptions extends ExecutionOptions {
54
+ adapter?: string;
55
+ classId?: string;
56
+ }
57
+ export type IpconfigCommand =
58
+ | "config"
59
+ | "all"
60
+ | "release"
61
+ | "release6"
62
+ | "renew"
63
+ | "renew6"
64
+ | "flushdns"
65
+ | "displaydns"
66
+ | "registerdns"
67
+ | "showclassid"
68
+ | "setclassid"
69
+ | "showclassid6"
70
+ | "setclassid6";
71
+ export interface ExecutionResult {
72
+ stdout: string;
73
+ stderr: string;
74
+ exitCode: number;
75
+ }
76
+ export class IpconfigError extends Error {
77
+ code: string;
78
+ command?: string[];
79
+ stdout: string;
80
+ stderr: string;
81
+ exitCode?: number;
82
+ }
83
+ export function parseIpconfig(output: string): Config;
84
+ export function createIpconfig(options?: {
85
+ platform?: NodeJS.Platform | string;
86
+ exec?: Function;
87
+ }): IpconfigApi;
88
+ export interface IpconfigApi {
89
+ execute(
90
+ command: IpconfigCommand,
91
+ options?: CommandOptions,
92
+ ): Promise<string | ExecutionResult>;
93
+ getRawOutput(options?: QueryOptions): Promise<string | ExecutionResult>;
94
+ getConfig<F extends ConfigFormat = "json">(
95
+ options?: ConfigOptions & { format?: F },
96
+ ): Promise<FormattedConfig<F>>;
97
+ getAdapters(options?: QueryOptions): Promise<Adapter[]>;
98
+ getAdapter(
99
+ nameOrIndex: string | number,
100
+ options?: QueryOptions,
101
+ ): Promise<Adapter>;
102
+ getIPv4(): Promise<string[]>;
103
+ getIPv6(): Promise<string[]>;
104
+ getDefaultGateway(): Promise<Array<{ adapter: string; address: string }>>;
105
+ getDnsServers(): Promise<Array<{ adapter: string; address: string }>>;
106
+ getMacAddresses(): Promise<Array<{ adapter: string; address: string }>>;
107
+ getHostname(): Promise<string>;
108
+ flushDns(options?: ExecutionOptions): Promise<string | ExecutionResult>;
109
+ displayDns(options?: ExecutionOptions): Promise<string | ExecutionResult>;
110
+ getDnsCache(options?: ExecutionOptions): Promise<string | ExecutionResult>;
111
+ registerDns(options?: ExecutionOptions): Promise<string | ExecutionResult>;
112
+ release(
113
+ options?: ExecutionOptions & { adapter?: string; ipv6?: boolean },
114
+ ): Promise<string | ExecutionResult>;
115
+ renew(
116
+ options?: ExecutionOptions & { adapter?: string; ipv6?: boolean },
117
+ ): Promise<string | ExecutionResult>;
118
+ showClassId(
119
+ options: ExecutionOptions & { adapter: string },
120
+ ): Promise<string | ExecutionResult>;
121
+ setClassId(
122
+ options: ExecutionOptions & { adapter: string; classId?: string },
123
+ ): Promise<string | ExecutionResult>;
124
+ showClassId6(
125
+ options: ExecutionOptions & { adapter: string },
126
+ ): Promise<string | ExecutionResult>;
127
+ setClassId6(
128
+ options: ExecutionOptions & { adapter: string; classId?: string },
129
+ ): Promise<string | ExecutionResult>;
130
+ }
131
+ export const ipconfig: IpconfigApi;
132
+ export default ipconfig;
package/src/index.js ADDED
@@ -0,0 +1,244 @@
1
+ import { hostname as systemHostname } from "node:os";
2
+ import { createRunner, IpconfigError } from "./command.js";
3
+ import { parseIpconfig } from "./parser.js";
4
+ import { formatConfig } from "./format.js";
5
+
6
+ const EXECUTION_OPTIONS = new Set([
7
+ "timeout",
8
+ "signal",
9
+ "encoding",
10
+ "rawResult",
11
+ ]);
12
+ const QUERY_OPTIONS = new Set([...EXECUTION_OPTIONS, "all"]);
13
+ const CONFIG_OPTIONS = new Set([...QUERY_OPTIONS, "format"]);
14
+ const COMMANDS = Object.freeze({
15
+ config: [],
16
+ all: ["/all"],
17
+ release: ["/release"],
18
+ release6: ["/release6"],
19
+ renew: ["/renew"],
20
+ renew6: ["/renew6"],
21
+ flushdns: ["/flushdns"],
22
+ displaydns: ["/displaydns"],
23
+ registerdns: ["/registerdns"],
24
+ showclassid: ["/showclassid"],
25
+ setclassid: ["/setclassid"],
26
+ showclassid6: ["/showclassid6"],
27
+ setclassid6: ["/setclassid6"],
28
+ });
29
+
30
+ function validateOptions(options, allowed) {
31
+ if (options === undefined) options = {};
32
+ if (!options || typeof options !== "object" || Array.isArray(options))
33
+ throw new TypeError("options must be an object");
34
+ for (const key of Object.keys(options))
35
+ if (!allowed.has(key)) throw new TypeError(`Unsupported option: ${key}`);
36
+ if (
37
+ options.timeout !== undefined &&
38
+ (!Number.isInteger(options.timeout) || options.timeout <= 0)
39
+ )
40
+ throw new TypeError("timeout must be a positive integer");
41
+ if (
42
+ options.signal !== undefined &&
43
+ (typeof options.signal !== "object" ||
44
+ typeof options.signal.aborted !== "boolean")
45
+ )
46
+ throw new TypeError("signal must be an AbortSignal");
47
+ if (options.encoding !== undefined && typeof options.encoding !== "string")
48
+ throw new TypeError("encoding must be a supported Node.js encoding name");
49
+ if (options.rawResult !== undefined && typeof options.rawResult !== "boolean")
50
+ throw new TypeError("rawResult must be a boolean");
51
+ return options;
52
+ }
53
+
54
+ function queryOptions(options) {
55
+ validateOptions(options, QUERY_OPTIONS);
56
+ if (options.all !== undefined && typeof options.all !== "boolean")
57
+ throw new TypeError("all must be a boolean");
58
+ const { all, ...execution } = options;
59
+ return {
60
+ args: all ? ["/all"] : [],
61
+ execution: { ...execution, rawResult: false },
62
+ };
63
+ }
64
+
65
+ function adapterArgument(value, label = "adapter") {
66
+ if (
67
+ typeof value !== "string" ||
68
+ !value.trim() ||
69
+ value.length > 256 ||
70
+ /[\0\r\n]/.test(value) ||
71
+ value.trim().startsWith("/")
72
+ ) {
73
+ throw new TypeError(
74
+ `${label} must be a non-empty adapter name without control characters or option prefixes`,
75
+ );
76
+ }
77
+ return value.trim();
78
+ }
79
+ function classIdArgument(value) {
80
+ if (
81
+ typeof value !== "string" ||
82
+ value.length > 256 ||
83
+ /[\0\r\n]/.test(value) ||
84
+ value.startsWith("/")
85
+ ) {
86
+ throw new TypeError(
87
+ "classId must be a string without control characters or option prefixes",
88
+ );
89
+ }
90
+ return value;
91
+ }
92
+ function commandOptions(options, command) {
93
+ const adapterAllowed = [
94
+ "release",
95
+ "release6",
96
+ "renew",
97
+ "renew6",
98
+ "showclassid",
99
+ "setclassid",
100
+ "showclassid6",
101
+ "setclassid6",
102
+ ].includes(command);
103
+ const classIdAllowed = ["setclassid", "setclassid6"].includes(command);
104
+ validateOptions(
105
+ options,
106
+ new Set([
107
+ ...EXECUTION_OPTIONS,
108
+ ...(adapterAllowed ? ["adapter"] : []),
109
+ ...(classIdAllowed ? ["classId"] : []),
110
+ ]),
111
+ );
112
+ const args = [...COMMANDS[command]];
113
+ if (
114
+ ["release", "release6", "renew", "renew6"].includes(command) &&
115
+ options.adapter !== undefined
116
+ )
117
+ args.push(adapterArgument(options.adapter));
118
+ if (command === "showclassid" || command === "showclassid6")
119
+ args.push(adapterArgument(options.adapter));
120
+ if (command === "setclassid" || command === "setclassid6") {
121
+ args.push(adapterArgument(options.adapter));
122
+ if (options.classId !== undefined)
123
+ args.push(classIdArgument(options.classId));
124
+ }
125
+ const { adapter, classId, ...execution } = options;
126
+ return { args, execution };
127
+ }
128
+
129
+ function flatten(adapters, field) {
130
+ return adapters.flatMap((adapter) =>
131
+ (adapter[field] ?? []).map((address) => ({
132
+ adapter: adapter.name,
133
+ address,
134
+ })),
135
+ );
136
+ }
137
+
138
+ export function createIpconfig({ platform = process.platform, exec } = {}) {
139
+ const run = createRunner({ platform, ...(exec ? { exec } : {}) });
140
+ async function execute(command, options = {}) {
141
+ if (typeof command !== "string" || !Object.hasOwn(COMMANDS, command))
142
+ throw new TypeError(`Unsupported ipconfig command: ${String(command)}`);
143
+ const { args, execution } = commandOptions(options, command);
144
+ return run(args, execution);
145
+ }
146
+ async function getRawOutput(options = {}) {
147
+ const { args, execution } = queryOptions(options);
148
+ return run(args, { ...execution, rawResult: options.rawResult ?? false });
149
+ }
150
+ async function getConfig(options = {}) {
151
+ validateOptions(options, CONFIG_OPTIONS);
152
+ const { format = "json", ...query } = options;
153
+ if (!["json", "xml", "yaml", "csv", "ndjson"].includes(format))
154
+ throw new TypeError(
155
+ "format must be one of: json, xml, yaml, csv, ndjson",
156
+ );
157
+ const { args, execution } = queryOptions(query);
158
+ const output = await run(args, execution);
159
+ const config = parseIpconfig(output);
160
+ config.hostname ??= systemHostname();
161
+ return formatConfig(config, format);
162
+ }
163
+ async function getAdapters(options) {
164
+ validateOptions(options, QUERY_OPTIONS);
165
+ return (await getConfig(options)).adapters;
166
+ }
167
+ async function getAdapter(nameOrIndex, options) {
168
+ if (!(typeof nameOrIndex === "string" || Number.isInteger(nameOrIndex)))
169
+ throw new TypeError(
170
+ "adapter selector must be a name or zero-based integer index",
171
+ );
172
+ const adapters = await getAdapters(options);
173
+ const found =
174
+ typeof nameOrIndex === "number"
175
+ ? adapters[nameOrIndex]
176
+ : adapters.find(
177
+ ({ name }) =>
178
+ name.toLocaleLowerCase() ===
179
+ nameOrIndex.trim().toLocaleLowerCase(),
180
+ );
181
+ if (!found)
182
+ throw new IpconfigError(`Network adapter not found: ${nameOrIndex}`, {
183
+ code: "ENOADAPTER",
184
+ });
185
+ return found;
186
+ }
187
+ async function addresses(family) {
188
+ return (await getAdapters({ all: true })).flatMap(
189
+ (adapter) => adapter[family],
190
+ );
191
+ }
192
+ async function release(options = {}) {
193
+ validateOptions(
194
+ options,
195
+ new Set([...EXECUTION_OPTIONS, "adapter", "ipv6"]),
196
+ );
197
+ if (options.ipv6 !== undefined && typeof options.ipv6 !== "boolean")
198
+ throw new TypeError("ipv6 must be a boolean");
199
+ const { ipv6 = false, ...rest } = options;
200
+ return execute(ipv6 ? "release6" : "release", rest);
201
+ }
202
+ async function renew(options = {}) {
203
+ validateOptions(
204
+ options,
205
+ new Set([...EXECUTION_OPTIONS, "adapter", "ipv6"]),
206
+ );
207
+ if (options.ipv6 !== undefined && typeof options.ipv6 !== "boolean")
208
+ throw new TypeError("ipv6 must be a boolean");
209
+ const { ipv6 = false, ...rest } = options;
210
+ return execute(ipv6 ? "renew6" : "renew", rest);
211
+ }
212
+ return {
213
+ execute,
214
+ getRawOutput,
215
+ getConfig,
216
+ getAdapters,
217
+ getAdapter,
218
+ getIPv4: async () => addresses("ipv4"),
219
+ getIPv6: async () => addresses("ipv6"),
220
+ getDefaultGateway: async () =>
221
+ flatten(await getAdapters({ all: true }), "defaultGateways"),
222
+ getDnsServers: async () =>
223
+ flatten(await getAdapters({ all: true }), "dnsServers"),
224
+ getMacAddresses: async () =>
225
+ (await getAdapters({ all: true })).flatMap(({ name, macAddress }) =>
226
+ macAddress ? [{ adapter: name, address: macAddress }] : [],
227
+ ),
228
+ getHostname: async () => systemHostname(),
229
+ flushDns: (options = {}) => execute("flushdns", options),
230
+ displayDns: (options = {}) => execute("displaydns", options),
231
+ registerDns: (options = {}) => execute("registerdns", options),
232
+ release,
233
+ renew,
234
+ showClassId: (options = {}) => execute("showclassid", options),
235
+ setClassId: (options = {}) => execute("setclassid", options),
236
+ showClassId6: (options = {}) => execute("showclassid6", options),
237
+ setClassId6: (options = {}) => execute("setclassid6", options),
238
+ getDnsCache: (options = {}) => execute("displaydns", options),
239
+ };
240
+ }
241
+
242
+ export const ipconfig = createIpconfig();
243
+ export { parseIpconfig, IpconfigError, createRunner };
244
+ export default ipconfig;
package/src/parser.cjs ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ exports.parseIpconfig = (...args) =>
3
+ import("./parser.js").then(({ parseIpconfig }) => parseIpconfig(...args));
@@ -0,0 +1,6 @@
1
+ declare const parser: {
2
+ parseIpconfig(
3
+ output: string,
4
+ ): Promise<{ hostname?: string; adapters: unknown[] }>;
5
+ };
6
+ export = parser;
@@ -0,0 +1,2 @@
1
+ import type { Config } from "./index.js";
2
+ export function parseIpconfig(output: string): Config;
package/src/parser.js ADDED
@@ -0,0 +1,182 @@
1
+ const LABELS = [
2
+ ['hostname', /^(?:host name|hostname|computer name|hostname del equipo|nombre de host)$/i],
3
+ ['description', /^(?:description|beschreibung|descripción)$/i],
4
+ ['macAddress', /^(?:physical address|physikalische adresse|dirección física|mac address)$/i],
5
+ ['dhcpEnabled', /^(?:dhcp enabled|dhcp aktiviert|dhcp habilitado)$/i],
6
+ ['autoconfigurationEnabled', /^(?:autoconfiguration enabled|autokonfiguration aktiviert)$/i],
7
+ ['ipv4Address', /^(?:ipv4 address|ip address|ipv4-adresse|ip-adresse|dirección ipv4)(?: \((?:preferred|bevorzugt)\))?$/i],
8
+ ['ipv6Address', /^(?:ipv6 address|ipv6-adresse|dirección ipv6)(?: \((?:preferred|bevorzugt)\))?$/i],
9
+ ['subnetMask', /^(?:subnet mask|subnetzmaske|máscara de subred)$/i],
10
+ ['defaultGateway', /^(?:default gateway|standardgateway|puerta de enlace predeterminada)$/i],
11
+ ['dnsServers', /^(?:dns servers?|dns-server|servidores dns)$/i],
12
+ ['connectionSpecificDnsSuffix', /^(?:connection-specific dns suffix|verbindungsspezifisches dns-suffix|sufijo dns específico para la conexión)$/i],
13
+ ['linkLocalIpv6Address', /^(?:link-local ipv6 address|verbindungslokale ipv6-adresse|dirección ipv6 de vínculo local)$/i],
14
+ ['leaseObtained', /^(?:lease obtained|lease erhalten|concesión obtenida)$/i],
15
+ ['leaseExpires', /^(?:lease expires|lease läuft ab|la concesión expira)$/i],
16
+ ['dhcpServer', /^(?:dhcp server|dhcp-server|servidor dhcp)$/i],
17
+ ['winsServers', /^(?:wins servers?|wins-server)$/i],
18
+ ['mediaState', /^(?:media state|medienstatus|estado de los medios)$/i],
19
+ ['netbiosOverTcpip', /^(?:netbios over tcpip|netbios über tcpip aktiviert)$/i],
20
+ ['ipv4PrefixLength', /^(?:ipv4 prefix length|ipv4-präfixlänge)$/i]
21
+ ];
22
+
23
+ const LIST_FIELDS = new Set(["dnsServers", "defaultGateway", "winsServers"]);
24
+ const EMPTY_VALUES =
25
+ /^(?:n\/a|not applicable|none|keine|nicht zutreffend|ninguno|no disponible)$/i;
26
+ const ADDRESS =
27
+ /^(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$|^[0-9a-f]*:[0-9a-f:.%]+(?:\/\d{1,3})?$/i;
28
+
29
+ function tidy(value) {
30
+ return value
31
+ .trim()
32
+ .replace(/^\.+$/, "")
33
+ .replace(/\s*\((?:preferred|bevorzugt|preferido)\)$/i, "")
34
+ .trim();
35
+ }
36
+ function bool(value) {
37
+ if (/^(?:yes|true|ja|sí|si|enabled|aktiviert)$/i.test(value)) return true;
38
+ if (/^(?:no|false|nein|disabled|deaktiviert)$/i.test(value)) return false;
39
+ return value;
40
+ }
41
+ function fieldLabel(line) {
42
+ return line.match(/^\s*(.+?)\s*:\s*(.*)$/);
43
+ }
44
+ function normalizeLabel(value) {
45
+ return value
46
+ .trim()
47
+ .replace(/\s*\.\s*/g, " ")
48
+ .replace(/\s+/g, " ")
49
+ .trim();
50
+ }
51
+ function makeAdapter(type, name) {
52
+ return {
53
+ name: name.trim().replace(/^\*+\s*/, ""),
54
+ type: type.trim(),
55
+ addresses: [],
56
+ ipv4: [],
57
+ ipv6: [],
58
+ dnsServers: [],
59
+ defaultGateways: [],
60
+ };
61
+ }
62
+ function addAddress(adapter, value, family, scope) {
63
+ const address = value.replace(/\/\d+$/, "");
64
+ const item = { address, family, ...(scope ? { scope } : {}) };
65
+ adapter.addresses.push(item);
66
+ (family === 4 ? adapter.ipv4 : adapter.ipv6).push(address);
67
+ adapter[family === 4 ? "ipv4Address" : "ipv6Address"] = address;
68
+ const prefix = value.match(/\/(\d+)$/)?.[1];
69
+ if (prefix !== undefined) item.prefixLength = Number(prefix);
70
+ }
71
+
72
+ /** Parse localized ipconfig text into normalized adapter objects. */
73
+ export function parseIpconfig(output) {
74
+ if (typeof output !== "string")
75
+ throw new TypeError("ipconfig output must be a string");
76
+ const adapters = [];
77
+ let hostname;
78
+ let adapter;
79
+ let currentList;
80
+ for (const rawLine of output.replace(/^\uFEFF/, "").split(/\r?\n/)) {
81
+ const line = rawLine.trim();
82
+ if (!line) {
83
+ currentList = undefined;
84
+ continue;
85
+ }
86
+ const header =
87
+ line.match(/^(.+?)\s+adapter\s+(.+):$/i) ??
88
+ line.match(/^(.+?)-Adapter\s+(.+):$/i) ??
89
+ line.match(/^(.+?) adaptador\s+(.+):$/i) ??
90
+ line.match(/^(.+?)-Adapter\s+(.+):$/i);
91
+ if (header) {
92
+ adapter = makeAdapter(header[1], header[2]);
93
+ adapters.push(adapter);
94
+ currentList = undefined;
95
+ continue;
96
+ }
97
+ if (
98
+ /(?:pseudo-interface|loopback interface)/i.test(line) &&
99
+ line.endsWith(":")
100
+ ) {
101
+ adapter = makeAdapter("Loopback", line.slice(0, -1));
102
+ adapters.push(adapter);
103
+ currentList = undefined;
104
+ continue;
105
+ }
106
+ const field = fieldLabel(rawLine);
107
+ if (!adapter) {
108
+ if (field) {
109
+ const label = normalizeLabel(field[1]);
110
+ const found = LABELS.find(
111
+ ([key, regex]) => key === "hostname" && regex.test(label),
112
+ );
113
+ if (found && field[2].trim()) hostname = tidy(field[2]);
114
+ }
115
+ continue;
116
+ }
117
+ const valueLine = line.match(ADDRESS);
118
+ if (currentList && valueLine) {
119
+ const target =
120
+ currentList === "defaultGateway"
121
+ ? adapter.defaultGateways
122
+ : adapter[currentList];
123
+ target.push(tidy(valueLine[0]));
124
+ continue;
125
+ }
126
+ if (!field) continue;
127
+ const label = normalizeLabel(field[1]);
128
+ const value = tidy(field[2]);
129
+ const match = LABELS.find(([, regex]) => regex.test(label));
130
+ if (!match) {
131
+ if (label && value)
132
+ ((adapter.fields ??= {}), (adapter.fields[label] = value));
133
+ currentList = undefined;
134
+ continue;
135
+ }
136
+ const key = match[0];
137
+ if (key === "hostname") {
138
+ if (value) hostname = value;
139
+ continue;
140
+ }
141
+ if (LIST_FIELDS.has(key)) {
142
+ currentList = key;
143
+ const target =
144
+ key === "defaultGateway"
145
+ ? adapter.defaultGateways
146
+ : key === "winsServers"
147
+ ? (adapter.winsServers ??= [])
148
+ : adapter.dnsServers;
149
+ if (value && value !== "::" && !EMPTY_VALUES.test(value))
150
+ target.push(value);
151
+ continue;
152
+ }
153
+ currentList = undefined;
154
+ if (!value || EMPTY_VALUES.test(value)) continue;
155
+ if (key === "ipv4Address") addAddress(adapter, value, 4);
156
+ else if (key === "ipv6Address") addAddress(adapter, value, 6);
157
+ else if (key === "linkLocalIpv6Address") {
158
+ adapter.linkLocalIpv6Address = value;
159
+ addAddress(adapter, value, 6, "link-local");
160
+ } else if (key === "dhcpEnabled" || key === "autoconfigurationEnabled")
161
+ adapter[key] = bool(value);
162
+ else if (key === "ipv4PrefixLength") {
163
+ adapter.ipv4PrefixLength = Number(value);
164
+ } else adapter[key] = value;
165
+ }
166
+ for (const item of adapters) {
167
+ if (item.subnetMask && item.ipv4PrefixLength === undefined)
168
+ item.ipv4PrefixLength = maskPrefix(item.subnetMask);
169
+ if (item.connectionSpecificDnsSuffix === undefined)
170
+ item.connectionSpecificDnsSuffix = null;
171
+ }
172
+ return { ...(hostname ? { hostname } : {}), adapters };
173
+ }
174
+
175
+ function maskPrefix(mask) {
176
+ const octets = mask.split(".").map(Number);
177
+ if (octets.length !== 4 || octets.some((part) => part < 0 || part > 255))
178
+ return undefined;
179
+ const bits = octets.map((part) => part.toString(2).padStart(8, "0")).join("");
180
+ if (!/^1*0*$/.test(bits)) return undefined;
181
+ return bits.indexOf("0") < 0 ? 32 : bits.indexOf("0");
182
+ }