ipwhoami 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 Vineeth Krishnan
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 ADDED
@@ -0,0 +1,207 @@
1
+ # ipwhoami
2
+
3
+ IP geolocation lookup from your terminal. Query multiple providers and compare results side-by-side.
4
+
5
+ Works on **macOS**, **Linux**, and **Windows**. Zero dependencies.
6
+
7
+ ## Features
8
+
9
+ - Look up geolocation for any IP address (or auto-detect yours)
10
+ - Choose from 3 providers: [ipinfo.io](https://ipinfo.io), [ipapi.co](https://ipapi.co), [ip-api.com](http://ip-api.com)
11
+ - Compare results across all providers at once
12
+ - Raw JSON output for scripting and piping
13
+ - No API keys required
14
+ - Zero dependencies
15
+ - Cross-platform: Node.js, Bash, and PowerShell
16
+
17
+ ## Install
18
+
19
+ ### npm (recommended)
20
+
21
+ ```bash
22
+ npm install -g ipwhoami
23
+ ```
24
+
25
+ Or run directly without installing:
26
+
27
+ ```bash
28
+ npx ipwhoami 8.8.8.8
29
+ ```
30
+
31
+ ### Homebrew (macOS / Linux)
32
+
33
+ ```bash
34
+ brew tap vineethkrishnan/ipwhoami
35
+ brew install ipwhoami
36
+ ```
37
+
38
+ ### Scoop (Windows)
39
+
40
+ ```powershell
41
+ scoop bucket add ipwhoami https://github.com/vineethkrishnan/scoop-ipwhoami
42
+ scoop install ipwhoami
43
+ ```
44
+
45
+ ### Docker
46
+
47
+ ```bash
48
+ docker run --rm vineethkrishnan/ipwhoami 8.8.8.8
49
+ docker run --rm vineethkrishnan/ipwhoami -c 1.1.1.1
50
+ ```
51
+
52
+ ### Standalone Bash script (macOS / Linux)
53
+
54
+ ```bash
55
+ curl -fsSL https://raw.githubusercontent.com/vineethkrishnan/ipwhoami/main/install.sh | bash
56
+ ```
57
+
58
+ ### Standalone PowerShell script (Windows)
59
+
60
+ ```powershell
61
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/vineethkrishnan/ipwhoami/main/scripts/ipwhoami.ps1" -OutFile "$HOME\ipwhoami.ps1"
62
+ Add-Content $PROFILE 'Set-Alias ipwhoami "$HOME\ipwhoami.ps1"'
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ```
68
+ ipwhoami [options] [ip]
69
+ ```
70
+
71
+ ### Options
72
+
73
+ | Flag | Description |
74
+ |------|-------------|
75
+ | `-p, --provider NAME` | Use a specific provider: `ipinfo`, `ipapi`, `ip-api` (default: `ipinfo`) |
76
+ | `-c, --compare` | Compare results from all providers |
77
+ | `-r, --raw` | Output raw JSON |
78
+ | `-h, --help` | Show help |
79
+ | `-v, --version` | Show version |
80
+
81
+ ### Examples
82
+
83
+ ```bash
84
+ # Look up your own public IP
85
+ ipwhoami
86
+
87
+ # Look up a specific IP
88
+ ipwhoami 8.8.8.8
89
+
90
+ # Compare across all providers
91
+ ipwhoami -c 1.1.1.1
92
+
93
+ # Use a specific provider
94
+ ipwhoami -p ipapi 8.8.8.8
95
+
96
+ # Get raw JSON (great for piping)
97
+ ipwhoami -r 8.8.8.8 | jq .city
98
+ ```
99
+
100
+ ### Sample Output
101
+
102
+ ```
103
+ $ ipwhoami -c 8.8.8.8
104
+ Comparing geolocation for: 8.8.8.8
105
+ ────────────────────────────────────────
106
+
107
+ [ipinfo]
108
+ IP: 8.8.8.8
109
+ City: Mountain View
110
+ Region: California
111
+ Country: US
112
+ Org: AS15169 Google LLC
113
+ Location: 37.4056,-122.0775
114
+ Timezone: America/Los_Angeles
115
+
116
+ [ipapi]
117
+ IP: 8.8.8.8
118
+ City: Mountain View
119
+ Region: California
120
+ Country: United States
121
+ Org: Google LLC
122
+ Location: 37.4223, -122.085
123
+ Timezone: America/Los_Angeles
124
+
125
+ [ip-api]
126
+ IP: 8.8.8.8
127
+ City: Mountain View
128
+ Region: California
129
+ Country: United States
130
+ ISP: Google LLC
131
+ Location: 37.4056, -122.0775
132
+ Timezone: America/Los_Angeles
133
+ ```
134
+
135
+ ## Providers
136
+
137
+ | Provider | HTTPS | Rate Limit | Notes |
138
+ |----------|-------|------------|-------|
139
+ | [ipinfo.io](https://ipinfo.io) | Yes | 50k/month free | Default provider |
140
+ | [ipapi.co](https://ipapi.co) | Yes | 1k/day free | Good detail |
141
+ | [ip-api.com](http://ip-api.com) | No (HTTP) | 45/min free | Includes ISP info |
142
+
143
+ ## Adding a Provider
144
+
145
+ Providers live in `src/providers/`. To add a new one:
146
+
147
+ 1. Create a new file in `src/providers/` (see existing ones for the pattern)
148
+ 2. Implement the `lookup(ip)` method returning a normalized result object
149
+ 3. Register it in `src/providers/index.js`
150
+
151
+ ## Project Structure
152
+
153
+ ```
154
+ ipwhoami/
155
+ ├── bin/
156
+ │ └── ipwhoami.js # CLI entry point
157
+ ├── src/
158
+ │ ├── cli.js # Argument parsing & command routing
159
+ │ ├── colors.js # Terminal color helpers
160
+ │ ├── config.js # Constants & defaults
161
+ │ ├── formatter.js # Output formatting
162
+ │ ├── ip.js # IP validation & public IP resolution
163
+ │ └── providers/
164
+ │ ├── base.js # Shared HTTP fetch logic
165
+ │ ├── index.js # Provider registry
166
+ │ ├── ipinfo.js # ipinfo.io
167
+ │ ├── ipapi.js # ipapi.co
168
+ │ └── ip-api.js # ip-api.com
169
+ ├── scripts/
170
+ │ ├── ipwhoami.sh # Standalone Bash version
171
+ │ └── ipwhoami.ps1 # Standalone PowerShell version
172
+ ├── test/ # Unit & integration tests
173
+ ├── docs/ # Starlight documentation site
174
+ ├── Dockerfile
175
+ ├── .github/workflows/
176
+ │ ├── ci.yml # Lint & test on PRs
177
+ │ ├── release.yml # Release Please + npm + Docker + Homebrew + Scoop
178
+ │ └── deploy-docs.yml # Cloudflare Pages docs deploy
179
+ ├── package.json
180
+ ├── CHANGELOG.md
181
+ ├── LICENSE
182
+ └── README.md
183
+ ```
184
+
185
+ ## Release
186
+
187
+ Releases are fully automated via [Release Please](https://github.com/googleapis/release-please):
188
+
189
+ 1. Push commits to `main` using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, etc.)
190
+ 2. Release Please auto-creates a **Release PR** that bumps `package.json`, `src/config.js`, and `CHANGELOG.md`
191
+ 3. Merge the PR to trigger the full release pipeline:
192
+ - Creates a GitHub Release with auto-generated notes
193
+ - Publishes to npm
194
+ - Builds and pushes Docker images (Docker Hub + GHCR)
195
+ - Updates the Homebrew formula
196
+ - Updates the Scoop manifest
197
+
198
+ ## Requirements
199
+
200
+ - **npm / Homebrew / Scoop**: Node.js >= 18
201
+ - **Docker**: No requirements (Node.js included in image)
202
+ - **Bash script**: `curl` + `jq`
203
+ - **PowerShell script**: PowerShell 5.1+ or 7+
204
+
205
+ ## License
206
+
207
+ [MIT](LICENSE)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ipwhoami",
3
+ "version": "1.0.0",
4
+ "description": "IP geolocation lookup from your terminal. Query multiple providers, compare results side-by-side.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ipwhoami": "./bin/ipwhoami.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/ipwhoami.js",
17
+ "lint": "node --check src/**/*.js bin/**/*.js",
18
+ "test": "node --test test/config.test.js test/ip.test.js test/colors.test.js test/formatter.test.js test/providers.test.js test/cli.test.js",
19
+ "test:unit": "node --test test/config.test.js test/ip.test.js test/colors.test.js test/formatter.test.js test/providers.test.js",
20
+ "test:integration": "node --test test/cli.test.js"
21
+ },
22
+ "keywords": [
23
+ "ip",
24
+ "geolocation",
25
+ "ipinfo",
26
+ "ip-lookup",
27
+ "cli",
28
+ "terminal",
29
+ "network",
30
+ "geo",
31
+ "ipapi",
32
+ "ip-api",
33
+ "whois",
34
+ "location"
35
+ ],
36
+ "author": "Vineeth Krishnan",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/vineethkrishnan/ipwhoami.git"
41
+ },
42
+ "homepage": "https://github.com/vineethkrishnan/ipwhoami#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/vineethkrishnan/ipwhoami/issues"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }
package/src/cli.js ADDED
@@ -0,0 +1,146 @@
1
+ import { VERSION, DEFAULT_PROVIDER, PROVIDER_NAMES } from './config.js';
2
+ import { bold, dim, green, red } from './colors.js';
3
+ import { validateIP, getPublicIP } from './ip.js';
4
+ import { getProvider, getAllProviders } from './providers/index.js';
5
+ import { formatResult, formatRaw, formatCompareHeader } from './formatter.js';
6
+
7
+ function printUsage() {
8
+ console.log(`${bold('ipwhoami')} - IP geolocation lookup from your terminal
9
+
10
+ ${bold('USAGE')}
11
+ ipwhoami [options] [ip]
12
+
13
+ ${bold('ARGUMENTS')}
14
+ ip IP address to look up (defaults to your public IP)
15
+
16
+ ${bold('OPTIONS')}
17
+ -p, --provider NAME Use a specific provider: ${PROVIDER_NAMES.join(', ')} (default: ${DEFAULT_PROVIDER})
18
+ -c, --compare Compare results from all providers
19
+ -r, --raw Output raw JSON (no formatting)
20
+ -h, --help Show this help message
21
+ -v, --version Show version
22
+
23
+ ${bold('EXAMPLES')}
24
+ ipwhoami Look up your own public IP
25
+ ipwhoami 8.8.8.8 Look up a specific IP
26
+ ipwhoami -c Compare your IP across all providers
27
+ ipwhoami -c 1.1.1.1 Compare a specific IP across all providers
28
+ ipwhoami -p ipapi 8.8.8.8 Use a specific provider
29
+ ipwhoami -r 8.8.8.8 Raw JSON output (pipe-friendly)`);
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ const opts = {
34
+ ip: null,
35
+ provider: DEFAULT_PROVIDER,
36
+ compare: false,
37
+ raw: false,
38
+ help: false,
39
+ version: false,
40
+ };
41
+
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const arg = argv[i];
44
+ switch (arg) {
45
+ case '-h':
46
+ case '--help':
47
+ opts.help = true;
48
+ break;
49
+ case '-v':
50
+ case '--version':
51
+ opts.version = true;
52
+ break;
53
+ case '-c':
54
+ case '--compare':
55
+ opts.compare = true;
56
+ break;
57
+ case '-r':
58
+ case '--raw':
59
+ opts.raw = true;
60
+ break;
61
+ case '-p':
62
+ case '--provider':
63
+ i++;
64
+ opts.provider = argv[i];
65
+ if (!opts.provider) {
66
+ die('--provider requires a value.');
67
+ }
68
+ break;
69
+ default:
70
+ if (arg.startsWith('-')) {
71
+ die(`unknown option: ${arg}. Use --help for usage.`);
72
+ }
73
+ opts.ip = arg;
74
+ }
75
+ }
76
+
77
+ return opts;
78
+ }
79
+
80
+ function die(message) {
81
+ console.error(`${red('error:')} ${message}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ async function lookup(ip, providerName, raw) {
86
+ const provider = getProvider(providerName);
87
+ const result = await provider.lookup(ip);
88
+
89
+ if (raw) {
90
+ console.log(formatRaw(result));
91
+ } else {
92
+ console.log(formatResult(providerName, result));
93
+ }
94
+ }
95
+
96
+ async function compare(ip, raw) {
97
+ console.log(formatCompareHeader(ip));
98
+ console.log();
99
+
100
+ for (const { name, provider } of getAllProviders()) {
101
+ const result = await provider.lookup(ip);
102
+ if (raw) {
103
+ console.log(`// ${name}`);
104
+ console.log(formatRaw(result));
105
+ } else {
106
+ console.log(formatResult(name, result));
107
+ }
108
+ console.log();
109
+ }
110
+
111
+ console.log(dim('Done.'));
112
+ }
113
+
114
+ export async function run(argv) {
115
+ const opts = parseArgs(argv);
116
+
117
+ if (opts.help) {
118
+ printUsage();
119
+ return;
120
+ }
121
+
122
+ if (opts.version) {
123
+ console.log(`ipwhoami ${VERSION}`);
124
+ return;
125
+ }
126
+
127
+ let ip = opts.ip;
128
+
129
+ try {
130
+ if (!ip) {
131
+ console.log(dim('Fetching your public IP...'));
132
+ ip = await getPublicIP();
133
+ console.log(`${bold('Your IP:')} ${green(ip)}\n`);
134
+ } else {
135
+ validateIP(ip);
136
+ }
137
+
138
+ if (opts.compare) {
139
+ await compare(ip, opts.raw);
140
+ } else {
141
+ await lookup(ip, opts.provider, opts.raw);
142
+ }
143
+ } catch (err) {
144
+ die(err.message);
145
+ }
146
+ }
package/src/colors.js ADDED
@@ -0,0 +1,11 @@
1
+ const isTTY = process.stdout.isTTY;
2
+
3
+ function wrap(code) {
4
+ return isTTY ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => s;
5
+ }
6
+
7
+ export const bold = wrap('1');
8
+ export const dim = wrap('2');
9
+ export const cyan = wrap('36');
10
+ export const green = wrap('32');
11
+ export const red = wrap('31');
package/src/config.js ADDED
@@ -0,0 +1,7 @@
1
+ export const VERSION = '1.0.0';
2
+
3
+ export const SELF_IP_URL = 'https://api.ipify.org';
4
+
5
+ export const DEFAULT_PROVIDER = 'ipinfo';
6
+
7
+ export const PROVIDER_NAMES = ['ipinfo', 'ipapi', 'ip-api'];
@@ -0,0 +1,33 @@
1
+ import { bold, cyan, dim } from './colors.js';
2
+
3
+ const FIELDS = [
4
+ ['IP', 'ip'],
5
+ ['City', 'city'],
6
+ ['Region', 'region'],
7
+ ['Country', 'country'],
8
+ ['Org', 'org'],
9
+ ['Location', 'location'],
10
+ ['Timezone', 'timezone'],
11
+ ];
12
+
13
+ export function formatResult(providerName, result) {
14
+ const lines = [cyan(bold(`[${providerName}]`))];
15
+
16
+ for (const [label, key] of FIELDS) {
17
+ const value = result[key] || 'n/a';
18
+ lines.push(` ${label.padEnd(10)} ${value}`);
19
+ }
20
+
21
+ return lines.join('\n');
22
+ }
23
+
24
+ export function formatRaw(result) {
25
+ return JSON.stringify(result, null, 2);
26
+ }
27
+
28
+ export function formatCompareHeader(ip) {
29
+ return [
30
+ `${bold('Comparing geolocation for:')} ${ip}`,
31
+ dim('\u2500'.repeat(40)),
32
+ ].join('\n');
33
+ }
package/src/ip.js ADDED
@@ -0,0 +1,31 @@
1
+ import { SELF_IP_URL } from './config.js';
2
+
3
+ const IPV4_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
4
+
5
+ export function validateIP(ip) {
6
+ if (IPV4_REGEX.test(ip) || ip.includes(':')) {
7
+ return true;
8
+ }
9
+ throw new Error(`invalid IP address: ${ip}`);
10
+ }
11
+
12
+ export async function getPublicIP() {
13
+ const controller = new AbortController();
14
+ const timeout = setTimeout(() => controller.abort(), 5000);
15
+
16
+ try {
17
+ const res = await fetch(SELF_IP_URL, { signal: controller.signal });
18
+ const text = (await res.text()).trim();
19
+ if (!text) {
20
+ throw new Error('got empty response when fetching public IP.');
21
+ }
22
+ return text;
23
+ } catch (err) {
24
+ if (err.name === 'AbortError') {
25
+ throw new Error('timed out fetching your public IP. Check your internet connection.');
26
+ }
27
+ throw new Error(`failed to fetch your public IP: ${err.message}`);
28
+ } finally {
29
+ clearTimeout(timeout);
30
+ }
31
+ }
@@ -0,0 +1,19 @@
1
+ export async function fetchJSON(url) {
2
+ const controller = new AbortController();
3
+ const timeout = setTimeout(() => controller.abort(), 10000);
4
+
5
+ try {
6
+ const res = await fetch(url, { signal: controller.signal });
7
+ if (!res.ok) {
8
+ throw new Error(`HTTP ${res.status}`);
9
+ }
10
+ return await res.json();
11
+ } catch (err) {
12
+ if (err.name === 'AbortError') {
13
+ throw new Error(`request to ${url} timed out.`);
14
+ }
15
+ throw new Error(`request to ${url} failed: ${err.message}`);
16
+ } finally {
17
+ clearTimeout(timeout);
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import { ipinfo } from './ipinfo.js';
2
+ import { ipapi } from './ipapi.js';
3
+ import { ipApiCom } from './ip-api.js';
4
+
5
+ const providers = new Map([
6
+ ['ipinfo', ipinfo],
7
+ ['ipapi', ipapi],
8
+ ['ip-api', ipApiCom],
9
+ ]);
10
+
11
+ export function getProvider(name) {
12
+ const provider = providers.get(name);
13
+ if (!provider) {
14
+ throw new Error(`unknown provider: ${name}. Available: ${[...providers.keys()].join(', ')}`);
15
+ }
16
+ return provider;
17
+ }
18
+
19
+ export function getAllProviders() {
20
+ return [...providers.entries()].map(([name, provider]) => ({ name, provider }));
21
+ }
@@ -0,0 +1,22 @@
1
+ import { fetchJSON } from './base.js';
2
+
3
+ export const ipApiCom = {
4
+ name: 'ip-api',
5
+ displayName: 'ip-api.com',
6
+ url: (ip) => `http://ip-api.com/json/${ip}`,
7
+
8
+ async lookup(ip) {
9
+ const data = await fetchJSON(this.url(ip));
10
+ return {
11
+ ip: data.query,
12
+ city: data.city,
13
+ region: data.regionName,
14
+ country: data.country,
15
+ org: data.isp,
16
+ location: data.lat && data.lon
17
+ ? `${data.lat}, ${data.lon}`
18
+ : undefined,
19
+ timezone: data.timezone,
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,22 @@
1
+ import { fetchJSON } from './base.js';
2
+
3
+ export const ipapi = {
4
+ name: 'ipapi',
5
+ displayName: 'ipapi.co',
6
+ url: (ip) => `https://ipapi.co/${ip}/json`,
7
+
8
+ async lookup(ip) {
9
+ const data = await fetchJSON(this.url(ip));
10
+ return {
11
+ ip: data.ip,
12
+ city: data.city,
13
+ region: data.region,
14
+ country: data.country_name,
15
+ org: data.org,
16
+ location: data.latitude && data.longitude
17
+ ? `${data.latitude}, ${data.longitude}`
18
+ : undefined,
19
+ timezone: data.timezone,
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,20 @@
1
+ import { fetchJSON } from './base.js';
2
+
3
+ export const ipinfo = {
4
+ name: 'ipinfo',
5
+ displayName: 'ipinfo.io',
6
+ url: (ip) => `https://ipinfo.io/${ip}/json`,
7
+
8
+ async lookup(ip) {
9
+ const data = await fetchJSON(this.url(ip));
10
+ return {
11
+ ip: data.ip,
12
+ city: data.city,
13
+ region: data.region,
14
+ country: data.country,
15
+ org: data.org,
16
+ location: data.loc,
17
+ timezone: data.timezone,
18
+ };
19
+ },
20
+ };