spoof-d 0.1.0 → 0.3.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "randomize": {
3
+ "local": false
4
+ },
5
+ "defaults": {
6
+ "verbose": false,
7
+ "json": false
8
+ }
9
+ }
10
+
package/README.md CHANGED
@@ -50,6 +50,15 @@ This repository ([TT5H/spoof-d](https://github.com/TT5H/spoof-d)) is a fork of [
50
50
  - **Linux**: Modern ip link commands with ifconfig fallback
51
51
  - **Unified API** across all platforms
52
52
 
53
+ ### User Experience Features
54
+ - **Progress indicators** with animated spinners for long-running operations
55
+ - **Verbose mode** (`--verbose`) for detailed debugging output
56
+ - **JSON output** (`--json`) for scripting and automation
57
+ - **Configuration file** support (`.spoofyrc` in home directory)
58
+ - **MAC address vendor lookup** using OUI database
59
+ - **Change history tracking** with ability to view and revert changes
60
+ - **Batch operations** for changing multiple interfaces at once
61
+
53
62
  ## Installation
54
63
 
55
64
  ### From npm (recommended)
@@ -159,6 +168,143 @@ sudo spoofy reset wi-fi
159
168
 
160
169
  **Note**: On macOS, restarting your computer will also reset your MAC address to the original hardware address.
161
170
 
171
+ ### Show detailed interface information
172
+
173
+ ```bash
174
+ spoofy info en0
175
+ ```
176
+
177
+ Shows detailed information about an interface including hardware MAC, current MAC, vendor information, and change history.
178
+
179
+ ### Validate MAC address format
180
+
181
+ ```bash
182
+ spoofy validate 00:11:22:33:44:55
183
+ ```
184
+
185
+ Validates and normalizes a MAC address, showing vendor information if available.
186
+
187
+ ### Look up vendor from MAC address
188
+
189
+ ```bash
190
+ spoofy vendor 00:11:22:33:44:55
191
+ ```
192
+
193
+ Looks up the vendor/manufacturer of a MAC address using the OUI database.
194
+
195
+ ### Batch operations
196
+
197
+ Create a batch file (e.g., `batch.json`):
198
+
199
+ ```json
200
+ [
201
+ {
202
+ "type": "randomize",
203
+ "device": "en0",
204
+ "local": true
205
+ },
206
+ {
207
+ "type": "set",
208
+ "device": "eth0",
209
+ "mac": "00:11:22:33:44:55"
210
+ },
211
+ {
212
+ "type": "reset",
213
+ "device": "wlan0"
214
+ }
215
+ ]
216
+ ```
217
+
218
+ Then run:
219
+
220
+ ```bash
221
+ sudo spoofy batch batch.json
222
+ ```
223
+
224
+ ### View change history
225
+
226
+ ```bash
227
+ spoofy history
228
+ ```
229
+
230
+ View all MAC address changes, or filter by device:
231
+
232
+ ```bash
233
+ spoofy history en0
234
+ ```
235
+
236
+ ## Advanced Usage
237
+
238
+ ### Verbose Mode
239
+
240
+ Get detailed debugging information:
241
+
242
+ ```bash
243
+ spoofy list --verbose
244
+ spoofy randomize en0 --verbose
245
+ ```
246
+
247
+ ### JSON Output
248
+
249
+ Output results in JSON format for scripting:
250
+
251
+ ```bash
252
+ spoofy list --json
253
+ spoofy randomize en0 --json
254
+ ```
255
+
256
+ Example JSON output:
257
+ ```json
258
+ {
259
+ "success": true,
260
+ "device": "en0",
261
+ "mac": "00:11:22:33:44:55",
262
+ "message": "MAC address changed successfully"
263
+ }
264
+ ```
265
+
266
+ ### Configuration File
267
+
268
+ Create a configuration file at `~/.spoofyrc` (or `%USERPROFILE%\.spoofyrc` on Windows):
269
+
270
+ ```json
271
+ {
272
+ "randomize": {
273
+ "local": true
274
+ },
275
+ "defaults": {
276
+ "verbose": false,
277
+ "json": false
278
+ }
279
+ }
280
+ ```
281
+
282
+ The configuration file allows you to set default options that will be used automatically.
283
+
284
+ ### Change History
285
+
286
+ All MAC address changes are automatically logged to `~/.spoofy_history.json`. You can:
287
+
288
+ - View history: `spoofy history`
289
+ - View history for specific device: `spoofy history en0`
290
+ - History includes timestamp, device, old MAC, new MAC, and operation type
291
+
292
+ ### Vendor Lookup
293
+
294
+ The tool includes an OUI (Organizationally Unique Identifier) database to identify device vendors:
295
+
296
+ - Automatically shown in `spoofy list` output
297
+ - Available via `spoofy vendor <mac>` command
298
+ - Helps identify device types and manufacturers
299
+
300
+ ### Progress Indicators
301
+
302
+ Long-running operations show progress indicators:
303
+
304
+ ```bash
305
+ ⏳ Changing MAC address... ✓ Successfully set MAC address
306
+ ```
307
+
162
308
  ## Platform Support
163
309
 
164
310
  ### macOS ✅
@@ -0,0 +1,16 @@
1
+ [
2
+ {
3
+ "type": "randomize",
4
+ "device": "en0",
5
+ "local": true
6
+ },
7
+ {
8
+ "type": "set",
9
+ "device": "eth0",
10
+ "mac": "00:11:22:33:44:55"
11
+ },
12
+ {
13
+ "type": "reset",
14
+ "device": "wlan0"
15
+ }
16
+ ]
package/bin/cmd.js CHANGED
@@ -5,15 +5,105 @@ const minimist = require("minimist");
5
5
  const spoof = require("../");
6
6
  const { stripIndent } = require("common-tags");
7
7
  const cp = require("child_process");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const os = require("os");
11
+ const ora = require("ora");
12
+ const history = require("../lib/history");
13
+ const oui = require("../lib/oui");
8
14
 
9
15
  const argv = minimist(process.argv.slice(2), {
10
16
  alias: {
11
17
  v: "version",
18
+ V: "verbose",
19
+ j: "json",
12
20
  },
13
- boolean: ["version"],
21
+ boolean: ["version", "verbose", "json"],
14
22
  });
15
23
  const cmd = argv._[0];
16
24
 
25
+ // Global flags
26
+ const VERBOSE = argv.verbose || false;
27
+ const JSON_OUTPUT = argv.json || false;
28
+
29
+ // Configuration file support
30
+ let config = null;
31
+ const configPath = path.join(os.homedir(), ".spoofyrc");
32
+ if (fs.existsSync(configPath)) {
33
+ try {
34
+ const configContent = fs.readFileSync(configPath, "utf8");
35
+ config = JSON.parse(configContent);
36
+ if (VERBOSE) {
37
+ logVerbose(`Loaded configuration from ${configPath}`);
38
+ }
39
+ } catch (err) {
40
+ if (VERBOSE) {
41
+ logVerbose(`Failed to load config: ${err.message}`);
42
+ }
43
+ }
44
+ }
45
+
46
+ // Helper functions
47
+ function logVerbose(message) {
48
+ if (VERBOSE && !JSON_OUTPUT) {
49
+ console.error(chalk.gray(`[VERBOSE] ${message}`));
50
+ }
51
+ }
52
+
53
+ function outputJSON(data) {
54
+ if (JSON_OUTPUT) {
55
+ console.log(JSON.stringify(data, null, 2));
56
+ }
57
+ }
58
+
59
+ let currentSpinner = null;
60
+
61
+ function showProgress(message) {
62
+ if (JSON_OUTPUT) return;
63
+ if (currentSpinner) {
64
+ currentSpinner.text = message;
65
+ } else {
66
+ currentSpinner = ora(message).start();
67
+ }
68
+ }
69
+
70
+ function hideProgress() {
71
+ if (JSON_OUTPUT) return;
72
+ if (currentSpinner) {
73
+ currentSpinner.stop();
74
+ currentSpinner = null;
75
+ }
76
+ }
77
+
78
+ function successProgress(message) {
79
+ if (JSON_OUTPUT) return;
80
+ if (currentSpinner) {
81
+ currentSpinner.succeed(message);
82
+ currentSpinner = null;
83
+ }
84
+ }
85
+
86
+ function failProgress(message) {
87
+ if (JSON_OUTPUT) return;
88
+ if (currentSpinner) {
89
+ currentSpinner.fail(message);
90
+ currentSpinner = null;
91
+ }
92
+ }
93
+
94
+ function progressStep(step, total, message) {
95
+ if (JSON_OUTPUT) return;
96
+ const percentage = Math.round((step / total) * 100);
97
+ if (currentSpinner) {
98
+ currentSpinner.text = `[${step}/${total}] ${percentage}% - ${message}`;
99
+ } else {
100
+ currentSpinner = ora(`[${step}/${total}] ${percentage}% - ${message}`).start();
101
+ }
102
+ if (step === total) {
103
+ currentSpinner = null;
104
+ }
105
+ }
106
+
17
107
  try {
18
108
  init();
19
109
  } catch (err) {
@@ -78,6 +168,20 @@ function init() {
78
168
  } else if (cmd === "normalize") {
79
169
  const mac = argv._[1];
80
170
  normalize(mac);
171
+ } else if (cmd === "info") {
172
+ const device = argv._[1];
173
+ info(device);
174
+ } else if (cmd === "validate") {
175
+ const mac = argv._[1];
176
+ validate(mac);
177
+ } else if (cmd === "vendor") {
178
+ const mac = argv._[1];
179
+ vendor(mac);
180
+ } else if (cmd === "batch") {
181
+ const file = argv._[1];
182
+ batch(file);
183
+ } else if (cmd === "history") {
184
+ historyCmd();
81
185
  } else {
82
186
  help();
83
187
  }
@@ -114,9 +218,23 @@ ${example}${note}
114
218
  spoofy help Shows this help message.
115
219
  spoofy version | --version | -v Show package version.
116
220
 
221
+ Commands:
222
+ list [--wifi] List available devices.
223
+ set <mac> <devices>... Set device MAC address.
224
+ randomize [--local] <devices>... Set device MAC address randomly.
225
+ reset <devices>... Reset device MAC address to default.
226
+ normalize <mac> Normalize a MAC address format.
227
+ info <device> Show detailed interface information.
228
+ validate <mac> Validate MAC address format.
229
+ vendor <mac> Look up vendor from MAC address.
230
+ batch <file> Change multiple interfaces from config file.
231
+ history View MAC address change history.
232
+
117
233
  Options:
118
234
  --wifi Try to only show wireless interfaces.
119
235
  --local Set the locally administered flag on randomized MACs.
236
+ --verbose, -V Show verbose output for debugging.
237
+ --json, -j Output results in JSON format.
120
238
 
121
239
  Platform Support:
122
240
  ✅ macOS (Sequoia 15.4+, Tahoe 26+)
@@ -127,7 +245,16 @@ ${example}${note}
127
245
  }
128
246
 
129
247
  function version() {
130
- console.log(require("../package.json").version);
248
+ const pkg = require("../package.json");
249
+ if (JSON_OUTPUT) {
250
+ outputJSON({
251
+ version: pkg.version,
252
+ name: pkg.name,
253
+ description: pkg.description,
254
+ });
255
+ } else {
256
+ console.log(pkg.version);
257
+ }
131
258
  }
132
259
 
133
260
  function set(mac, devices) {
@@ -139,8 +266,14 @@ function set(mac, devices) {
139
266
  throw new Error("Device name is required. Usage: spoofy set <mac> <device>");
140
267
  }
141
268
 
142
- devices.forEach((device) => {
269
+ logVerbose(`Setting MAC address ${mac} on ${devices.length} device(s)`);
270
+
271
+ devices.forEach((device, index) => {
272
+ logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
273
+ showProgress(`Finding interface ${device}`);
274
+
143
275
  const it = spoof.findInterface(device);
276
+ hideProgress();
144
277
 
145
278
  if (!it) {
146
279
  throw new Error(
@@ -149,6 +282,7 @@ function set(mac, devices) {
149
282
  );
150
283
  }
151
284
 
285
+ logVerbose(`Found interface: ${it.device} (port: ${it.port})`);
152
286
  setMACAddress(it.device, mac, it.port);
153
287
  });
154
288
  }
@@ -158,13 +292,34 @@ function normalize(mac) {
158
292
  throw new Error("MAC address is required. Usage: spoofy normalize <mac>");
159
293
  }
160
294
 
295
+ logVerbose(`Normalizing MAC address: ${mac}`);
296
+
161
297
  try {
162
298
  const normalized = spoof.normalize(mac);
163
299
  if (!normalized) {
164
300
  throw new Error(`"${mac}" is not a valid MAC address`);
165
301
  }
166
- console.log(normalized);
302
+
303
+ if (JSON_OUTPUT) {
304
+ outputJSON({
305
+ original: mac,
306
+ normalized: normalized,
307
+ valid: true,
308
+ });
309
+ } else {
310
+ console.log(normalized);
311
+ }
312
+
313
+ logVerbose(`Normalized: ${mac} -> ${normalized}`);
167
314
  } catch (err) {
315
+ if (JSON_OUTPUT) {
316
+ outputJSON({
317
+ original: mac,
318
+ normalized: null,
319
+ valid: false,
320
+ error: err.message,
321
+ });
322
+ }
168
323
  throw new Error(
169
324
  `Could not normalize MAC address "${mac}": ${err.message}`
170
325
  );
@@ -176,8 +331,15 @@ function randomize(devices) {
176
331
  throw new Error("Device name is required. Usage: spoofy randomize <device>");
177
332
  }
178
333
 
179
- devices.forEach((device) => {
334
+ const useLocal = argv.local || (config && config.randomize && config.randomize.local);
335
+ logVerbose(`Randomizing MAC address (local: ${useLocal})`);
336
+
337
+ devices.forEach((device, index) => {
338
+ logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
339
+ showProgress(`Finding interface ${device}`);
340
+
180
341
  const it = spoof.findInterface(device);
342
+ hideProgress();
181
343
 
182
344
  if (!it) {
183
345
  throw new Error(
@@ -186,9 +348,16 @@ function randomize(devices) {
186
348
  );
187
349
  }
188
350
 
189
- const mac = spoof.randomize(argv.local);
190
- console.log(chalk.blue("ℹ"), `Generated random MAC address: ${chalk.bold.cyan(mac)}`);
191
- setMACAddress(it.device, mac, it.port);
351
+ logVerbose(`Found interface: ${it.device} (port: ${it.port})`);
352
+ const mac = spoof.randomize(useLocal);
353
+
354
+ if (JSON_OUTPUT) {
355
+ // Will be output in setMACAddress
356
+ } else {
357
+ console.log(chalk.blue("ℹ"), `Generated random MAC address: ${chalk.bold.cyan(mac)}`);
358
+ }
359
+
360
+ setMACAddress(it.device, mac, it.port, "randomize");
192
361
  });
193
362
  }
194
363
 
@@ -197,8 +366,14 @@ function reset(devices) {
197
366
  throw new Error("Device name is required. Usage: spoofy reset <device>");
198
367
  }
199
368
 
200
- devices.forEach((device) => {
369
+ logVerbose(`Resetting MAC address on ${devices.length} device(s)`);
370
+
371
+ devices.forEach((device, index) => {
372
+ logVerbose(`Processing device ${index + 1}/${devices.length}: ${device}`);
373
+ showProgress(`Finding interface ${device}`);
374
+
201
375
  const it = spoof.findInterface(device);
376
+ hideProgress();
202
377
 
203
378
  if (!it) {
204
379
  throw new Error(
@@ -214,12 +389,22 @@ function reset(devices) {
214
389
  );
215
390
  }
216
391
 
217
- console.log(chalk.blue("ℹ"), `Resetting to hardware MAC address: ${chalk.bold.cyan(it.address)}`);
218
- setMACAddress(it.device, it.address, it.port);
392
+ logVerbose(`Hardware MAC address: ${it.address}`);
393
+
394
+ if (JSON_OUTPUT) {
395
+ // Will be output in setMACAddress
396
+ } else {
397
+ console.log(chalk.blue("ℹ"), `Resetting to hardware MAC address: ${chalk.bold.cyan(it.address)}`);
398
+ }
399
+
400
+ setMACAddress(it.device, it.address, it.port, "reset");
219
401
  });
220
402
  }
221
403
 
222
404
  function list() {
405
+ logVerbose("Starting interface discovery...");
406
+ showProgress("Discovering network interfaces");
407
+
223
408
  const targets = [];
224
409
  if (argv.wifi) {
225
410
  if (process.platform === "win32") {
@@ -230,6 +415,23 @@ function list() {
230
415
  }
231
416
 
232
417
  const interfaces = spoof.findInterfaces(targets);
418
+ hideProgress();
419
+ logVerbose(`Found ${interfaces.length} interface(s)`);
420
+
421
+ if (JSON_OUTPUT) {
422
+ outputJSON({
423
+ interfaces: interfaces.map((it) => ({
424
+ port: it.port || it.device,
425
+ device: it.device,
426
+ address: it.address,
427
+ currentAddress: it.currentAddress,
428
+ status: it.status,
429
+ description: it.description,
430
+ })),
431
+ count: interfaces.length,
432
+ });
433
+ return;
434
+ }
233
435
 
234
436
  if (interfaces.length === 0) {
235
437
  console.log(chalk.yellow("No network interfaces found."));
@@ -251,17 +453,31 @@ function list() {
251
453
 
252
454
  if (it.address) {
253
455
  line.push("with MAC address", chalk.bold.cyan(it.address));
456
+ const vendor = oui.lookupVendor(it.address);
457
+ if (vendor && vendor !== "Unknown") {
458
+ line.push(chalk.gray(`[${vendor}]`));
459
+ }
254
460
  }
255
461
  if (it.currentAddress && it.currentAddress !== it.address) {
256
462
  line.push("currently set to", chalk.bold.red(it.currentAddress));
463
+ const currentVendor = oui.lookupVendor(it.currentAddress);
464
+ if (currentVendor && currentVendor !== "Unknown") {
465
+ line.push(chalk.gray(`[${currentVendor}]`));
466
+ }
257
467
  }
258
468
  console.log(line.join(" "));
259
469
  });
260
470
  }
261
471
 
262
- function setMACAddress(device, mac, port) {
472
+ function setMACAddress(device, mac, port, operation = "set") {
473
+ logVerbose(`Setting MAC address ${mac} on device ${device}`);
474
+
475
+ // Get current MAC for history
476
+ const oldMac = spoof.getInterfaceMAC(device);
477
+
263
478
  // Check for admin/root privileges
264
479
  if (process.platform === "win32") {
480
+ logVerbose("Checking for Administrator privileges...");
265
481
  // On Windows, check if running as administrator
266
482
  try {
267
483
  const output = cp
@@ -279,35 +495,355 @@ function setMACAddress(device, mac, port) {
279
495
  "Right-click Command Prompt or PowerShell and select 'Run as Administrator'"
280
496
  );
281
497
  }
498
+ logVerbose("Administrator privileges confirmed");
282
499
  } catch (err) {
283
500
  if (err.message.includes("Must run as Administrator")) {
284
501
  throw err;
285
502
  }
286
503
  // If check fails, warn but continue (might work anyway)
287
- console.warn(chalk.yellow("Warning: Could not verify administrator privileges. Operation may fail."));
504
+ if (!JSON_OUTPUT) {
505
+ console.warn(chalk.yellow("Warning: Could not verify administrator privileges. Operation may fail."));
506
+ }
507
+ logVerbose("Could not verify privileges, continuing anyway");
288
508
  }
289
509
  } else if (process.platform !== "win32") {
510
+ logVerbose("Checking for root privileges...");
290
511
  // Unix-like systems (macOS, Linux)
291
512
  if (process.getuid && process.getuid() !== 0) {
292
513
  throw new Error(
293
514
  "Must run as root (or using sudo) to change network settings"
294
515
  );
295
516
  }
517
+ logVerbose("Root privileges confirmed");
296
518
  }
297
519
 
298
520
  try {
521
+ showProgress("Changing MAC address");
522
+ logVerbose(`Calling setInterfaceMAC(${device}, ${mac}, ${port})`);
523
+
299
524
  spoof.setInterfaceMAC(device, mac, port);
300
- console.log(
301
- chalk.green("✓") +
302
- " Successfully set MAC address to " +
303
- chalk.bold.cyan(mac) +
304
- " on " +
305
- chalk.bold.green(device)
306
- );
307
525
 
526
+ // Log to history
527
+ history.addHistoryEntry(device, oldMac, mac, operation);
528
+
529
+ successProgress("MAC address changed successfully");
530
+
531
+ if (JSON_OUTPUT) {
532
+ outputJSON({
533
+ success: true,
534
+ device: device,
535
+ mac: mac,
536
+ oldMac: oldMac,
537
+ message: "MAC address changed successfully",
538
+ });
539
+ } else {
540
+ console.log(
541
+ chalk.green("✓") +
542
+ " Successfully set MAC address to " +
543
+ chalk.bold.cyan(mac) +
544
+ " on " +
545
+ chalk.bold.green(device)
546
+ );
547
+ }
548
+
549
+ logVerbose("MAC address change completed successfully");
308
550
  // Note: Verification is already done in setInterfaceMAC, so we don't need to do it again here
309
551
  } catch (err) {
552
+ failProgress("Failed to change MAC address");
553
+ if (JSON_OUTPUT) {
554
+ outputJSON({
555
+ success: false,
556
+ device: device,
557
+ mac: mac,
558
+ error: err.message,
559
+ code: err.code,
560
+ });
561
+ }
310
562
  // Error is already formatted by handleError in the catch block above
311
563
  throw err;
312
564
  }
313
565
  }
566
+
567
+ function info(device) {
568
+ if (!device) {
569
+ throw new Error("Device name is required. Usage: spoofy info <device>");
570
+ }
571
+
572
+ logVerbose(`Getting info for device: ${device}`);
573
+ showProgress("Fetching interface information");
574
+
575
+ const it = spoof.findInterface(device);
576
+ hideProgress();
577
+
578
+ if (!it) {
579
+ throw new Error(
580
+ `Could not find device "${device}". ` +
581
+ "List available devices using: spoofy list"
582
+ );
583
+ }
584
+
585
+ const currentMac = spoof.getInterfaceMAC(device);
586
+ const vendorInfo = it.address ? oui.getVendorInfo(it.address) : null;
587
+ const currentVendorInfo = currentMac ? oui.getVendorInfo(currentMac) : null;
588
+ const deviceHistory = history.getHistoryForDevice(device);
589
+
590
+ if (JSON_OUTPUT) {
591
+ outputJSON({
592
+ device: it.device,
593
+ port: it.port || it.device,
594
+ description: it.description,
595
+ hardwareMac: it.address,
596
+ currentMac: currentMac,
597
+ hardwareVendor: vendorInfo ? vendorInfo.vendor : null,
598
+ currentVendor: currentVendorInfo ? currentVendorInfo.vendor : null,
599
+ status: it.status,
600
+ platform: process.platform,
601
+ historyCount: deviceHistory.length,
602
+ lastChange: deviceHistory[0] || null,
603
+ });
604
+ return;
605
+ }
606
+
607
+ console.log(chalk.bold.cyan("\nInterface Information"));
608
+ console.log(chalk.gray("─".repeat(50)));
609
+ console.log(chalk.bold("Device:"), it.device);
610
+ console.log(chalk.bold("Port:"), it.port || it.device);
611
+ if (it.description) {
612
+ console.log(chalk.bold("Description:"), it.description);
613
+ }
614
+ if (it.status) {
615
+ console.log(chalk.bold("Status:"), it.status);
616
+ }
617
+ console.log(chalk.bold("Platform:"), process.platform);
618
+
619
+ if (it.address) {
620
+ console.log(chalk.bold("\nHardware MAC Address:"), chalk.cyan(it.address));
621
+ if (vendorInfo && vendorInfo.vendor !== "Unknown") {
622
+ console.log(chalk.bold("Hardware Vendor:"), chalk.green(vendorInfo.vendor));
623
+ }
624
+ }
625
+
626
+ if (currentMac) {
627
+ console.log(chalk.bold("Current MAC Address:"), chalk.cyan(currentMac));
628
+ if (currentVendorInfo && currentVendorInfo.vendor !== "Unknown") {
629
+ console.log(chalk.bold("Current Vendor:"), chalk.green(currentVendorInfo.vendor));
630
+ }
631
+ if (currentMac !== it.address) {
632
+ console.log(chalk.yellow("⚠ MAC address has been changed from hardware address"));
633
+ }
634
+ }
635
+
636
+ if (deviceHistory.length > 0) {
637
+ console.log(chalk.bold("\nChange History:"), `(${deviceHistory.length} entries)`);
638
+ deviceHistory.slice(0, 5).forEach((entry, index) => {
639
+ const date = new Date(entry.timestamp).toLocaleString();
640
+ console.log(` ${index + 1}. ${date} - ${entry.operation}: ${entry.oldMac || "N/A"} → ${entry.newMac}`);
641
+ });
642
+ if (deviceHistory.length > 5) {
643
+ console.log(chalk.gray(` ... and ${deviceHistory.length - 5} more entries`));
644
+ }
645
+ }
646
+ console.log();
647
+ }
648
+
649
+ function validate(mac) {
650
+ if (!mac) {
651
+ throw new Error("MAC address is required. Usage: spoofy validate <mac>");
652
+ }
653
+
654
+ logVerbose(`Validating MAC address: ${mac}`);
655
+
656
+ const normalized = spoof.normalize(mac);
657
+ const isValid = !!normalized;
658
+ const vendorInfo = normalized ? oui.getVendorInfo(normalized) : null;
659
+
660
+ if (JSON_OUTPUT) {
661
+ outputJSON({
662
+ original: mac,
663
+ normalized: normalized,
664
+ valid: isValid,
665
+ vendor: vendorInfo ? vendorInfo.vendor : null,
666
+ prefix: vendorInfo ? vendorInfo.prefix : null,
667
+ });
668
+ return;
669
+ }
670
+
671
+ if (isValid) {
672
+ console.log(chalk.green("✓ Valid MAC address"));
673
+ console.log(chalk.bold("Normalized:"), normalized);
674
+ if (vendorInfo && vendorInfo.vendor !== "Unknown") {
675
+ console.log(chalk.bold("Vendor:"), chalk.green(vendorInfo.vendor));
676
+ }
677
+ } else {
678
+ console.log(chalk.red("✗ Invalid MAC address"));
679
+ console.log(chalk.yellow("Expected format: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX"));
680
+ }
681
+ }
682
+
683
+ function vendor(mac) {
684
+ if (!mac) {
685
+ throw new Error("MAC address is required. Usage: spoofy vendor <mac>");
686
+ }
687
+
688
+ logVerbose(`Looking up vendor for MAC: ${mac}`);
689
+
690
+ const normalized = spoof.normalize(mac);
691
+ if (!normalized) {
692
+ throw new Error(`"${mac}" is not a valid MAC address`);
693
+ }
694
+
695
+ const vendorInfo = oui.getVendorInfo(normalized);
696
+
697
+ if (JSON_OUTPUT) {
698
+ outputJSON({
699
+ mac: normalized,
700
+ vendor: vendorInfo.vendor,
701
+ prefix: vendorInfo.prefix,
702
+ found: vendorInfo.vendor !== "Unknown",
703
+ });
704
+ return;
705
+ }
706
+
707
+ console.log(chalk.bold("MAC Address:"), normalized);
708
+ console.log(chalk.bold("Vendor:"), vendorInfo.vendor !== "Unknown" ? chalk.green(vendorInfo.vendor) : chalk.yellow("Unknown"));
709
+ console.log(chalk.bold("Prefix:"), vendorInfo.prefix);
710
+
711
+ if (vendorInfo.vendor === "Unknown") {
712
+ console.log(chalk.gray("\nNote: Vendor not found in database. This may be a locally administered address."));
713
+ }
714
+ }
715
+
716
+ function batch(file) {
717
+ if (!file) {
718
+ throw new Error("Batch file is required. Usage: spoofy batch <file>");
719
+ }
720
+
721
+ if (!fs.existsSync(file)) {
722
+ throw new Error(`Batch file not found: ${file}`);
723
+ }
724
+
725
+ logVerbose(`Loading batch file: ${file}`);
726
+ showProgress("Loading batch configuration");
727
+
728
+ let batchConfig;
729
+ try {
730
+ const content = fs.readFileSync(file, "utf8");
731
+ batchConfig = JSON.parse(content);
732
+ } catch (err) {
733
+ hideProgress();
734
+ throw new Error(`Failed to parse batch file: ${err.message}`);
735
+ }
736
+
737
+ hideProgress();
738
+
739
+ if (!Array.isArray(batchConfig)) {
740
+ throw new Error("Batch file must contain an array of operations");
741
+ }
742
+
743
+ logVerbose(`Found ${batchConfig.length} operation(s) in batch file`);
744
+
745
+ const results = [];
746
+ let successCount = 0;
747
+ let failCount = 0;
748
+
749
+ batchConfig.forEach((operation, index) => {
750
+ const step = index + 1;
751
+ const total = batchConfig.length;
752
+ progressStep(step, total, `Processing operation ${step}`);
753
+
754
+ try {
755
+ if (operation.type === "set" && operation.device && operation.mac) {
756
+ const it = spoof.findInterface(operation.device);
757
+ if (!it) {
758
+ throw new Error(`Device not found: ${operation.device}`);
759
+ }
760
+ setMACAddress(it.device, operation.mac, it.port, "batch-set");
761
+ results.push({ success: true, operation: operation, index: index });
762
+ successCount++;
763
+ } else if (operation.type === "randomize" && operation.device) {
764
+ const it = spoof.findInterface(operation.device);
765
+ if (!it) {
766
+ throw new Error(`Device not found: ${operation.device}`);
767
+ }
768
+ const mac = spoof.randomize(operation.local || false);
769
+ setMACAddress(it.device, mac, it.port, "batch-randomize");
770
+ results.push({ success: true, operation: operation, mac: mac, index: index });
771
+ successCount++;
772
+ } else if (operation.type === "reset" && operation.device) {
773
+ const it = spoof.findInterface(operation.device);
774
+ if (!it) {
775
+ throw new Error(`Device not found: ${operation.device}`);
776
+ }
777
+ if (!it.address) {
778
+ throw new Error(`No hardware MAC address for: ${operation.device}`);
779
+ }
780
+ setMACAddress(it.device, it.address, it.port, "batch-reset");
781
+ results.push({ success: true, operation: operation, index: index });
782
+ successCount++;
783
+ } else {
784
+ throw new Error(`Invalid operation type or missing parameters: ${JSON.stringify(operation)}`);
785
+ }
786
+ } catch (err) {
787
+ results.push({ success: false, operation: operation, error: err.message, index: index });
788
+ failCount++;
789
+ }
790
+ });
791
+
792
+ progressStep(batchConfig.length, batchConfig.length, "Completed");
793
+
794
+ if (JSON_OUTPUT) {
795
+ outputJSON({
796
+ total: batchConfig.length,
797
+ success: successCount,
798
+ failed: failCount,
799
+ results: results,
800
+ });
801
+ } else {
802
+ console.log(chalk.bold("\nBatch Operation Summary:"));
803
+ console.log(chalk.green(` ✓ Successful: ${successCount}`));
804
+ if (failCount > 0) {
805
+ console.log(chalk.red(` ✗ Failed: ${failCount}`));
806
+ }
807
+ console.log(chalk.bold(` Total: ${batchConfig.length}`));
808
+ }
809
+ }
810
+
811
+ function historyCmd() {
812
+ const device = argv._[1]; // Optional device filter
813
+ logVerbose(device ? `Getting history for device: ${device}` : "Getting all history");
814
+
815
+ const allHistory = history.getHistory();
816
+ const deviceHistory = device ? history.getHistoryForDevice(device) : allHistory;
817
+
818
+ if (deviceHistory.length === 0) {
819
+ if (JSON_OUTPUT) {
820
+ outputJSON({ history: [], count: 0 });
821
+ } else {
822
+ console.log(chalk.yellow("No history found" + (device ? ` for device "${device}"` : "")));
823
+ }
824
+ return;
825
+ }
826
+
827
+ if (JSON_OUTPUT) {
828
+ outputJSON({
829
+ history: deviceHistory,
830
+ count: deviceHistory.length,
831
+ device: device || "all",
832
+ });
833
+ return;
834
+ }
835
+
836
+ console.log(chalk.bold.cyan("\nMAC Address Change History"));
837
+ console.log(chalk.gray("─".repeat(80)));
838
+
839
+ deviceHistory.forEach((entry, index) => {
840
+ const date = new Date(entry.timestamp).toLocaleString();
841
+ console.log(chalk.bold(`\n${index + 1}. ${date}`));
842
+ console.log(` Device: ${chalk.green(entry.device)}`);
843
+ console.log(` Operation: ${chalk.cyan(entry.operation)}`);
844
+ console.log(` ${chalk.gray(entry.oldMac || "N/A")} → ${chalk.cyan(entry.newMac)}`);
845
+ console.log(` Platform: ${entry.platform}`);
846
+ });
847
+
848
+ console.log();
849
+ }
package/index.js CHANGED
@@ -6,8 +6,48 @@ module.exports = {
6
6
  randomize,
7
7
  setInterfaceMAC,
8
8
  getInterfaceMAC,
9
+ validateMAC,
9
10
  };
10
11
 
12
+ /**
13
+ * Validates a MAC address format
14
+ * @param {string} mac
15
+ * @return {Object} {valid: boolean, normalized: string|null, error: string|null}
16
+ */
17
+ function validateMAC(mac) {
18
+ if (!mac || typeof mac !== "string") {
19
+ return {
20
+ valid: false,
21
+ normalized: null,
22
+ error: "MAC address must be a non-empty string",
23
+ };
24
+ }
25
+
26
+ const normalized = normalize(mac);
27
+ if (!normalized) {
28
+ return {
29
+ valid: false,
30
+ normalized: null,
31
+ error: "Invalid MAC address format",
32
+ };
33
+ }
34
+
35
+ // Check for invalid addresses
36
+ if (normalized === "00:00:00:00:00:00" || normalized === "FF:FF:FF:FF:FF:FF") {
37
+ return {
38
+ valid: false,
39
+ normalized: normalized,
40
+ error: "Cannot be all zeros or broadcast address",
41
+ };
42
+ }
43
+
44
+ return {
45
+ valid: true,
46
+ normalized: normalized,
47
+ error: null,
48
+ };
49
+ }
50
+
11
51
  const cp = require("child_process");
12
52
  const quote = require("shell-quote").quote;
13
53
  const zeroFill = require("zero-fill");
package/lib/history.js ADDED
@@ -0,0 +1,62 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const HISTORY_FILE = path.join(os.homedir(), ".spoofy_history.json");
6
+
7
+ function getHistory() {
8
+ if (!fs.existsSync(HISTORY_FILE)) {
9
+ return [];
10
+ }
11
+ try {
12
+ const content = fs.readFileSync(HISTORY_FILE, "utf8");
13
+ return JSON.parse(content);
14
+ } catch (err) {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function saveHistory(history) {
20
+ try {
21
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), "utf8");
22
+ } catch (err) {
23
+ // Silently fail if we can't write history
24
+ }
25
+ }
26
+
27
+ function addHistoryEntry(device, oldMac, newMac, operation) {
28
+ const history = getHistory();
29
+ const entry = {
30
+ timestamp: new Date().toISOString(),
31
+ device: device,
32
+ oldMac: oldMac,
33
+ newMac: newMac,
34
+ operation: operation, // 'set', 'randomize', 'reset'
35
+ platform: process.platform,
36
+ };
37
+ history.unshift(entry); // Add to beginning
38
+ // Keep only last 100 entries
39
+ if (history.length > 100) {
40
+ history.splice(100);
41
+ }
42
+ saveHistory(history);
43
+ return entry;
44
+ }
45
+
46
+ function getHistoryForDevice(device) {
47
+ const history = getHistory();
48
+ return history.filter((entry) => entry.device === device);
49
+ }
50
+
51
+ function getLastEntryForDevice(device) {
52
+ const history = getHistory();
53
+ return history.find((entry) => entry.device === device);
54
+ }
55
+
56
+ module.exports = {
57
+ getHistory,
58
+ addHistoryEntry,
59
+ getHistoryForDevice,
60
+ getLastEntryForDevice,
61
+ HISTORY_FILE,
62
+ };
package/lib/oui.js ADDED
@@ -0,0 +1,286 @@
1
+ // OUI (Organizationally Unique Identifier) database
2
+ // Common MAC address vendor prefixes
3
+ // This is a subset - can be expanded with full IEEE OUI database
4
+
5
+ const OUI_DB = {
6
+ "00:05:69": "VMware",
7
+ "00:0C:29": "VMware",
8
+ "00:50:56": "VMware",
9
+ "00:16:3E": "Xen",
10
+ "00:03:FF": "Microsoft Hyper-V",
11
+ "00:1C:42": "Parallels",
12
+ "00:0F:4B": "Virtual Iron",
13
+ "08:00:27": "VirtualBox",
14
+ "00:1B:21": "Intel",
15
+ "00:1E:67": "Intel",
16
+ "00:21:70": "Intel",
17
+ "00:25:00": "Intel",
18
+ "00:26:B9": "Intel",
19
+ "00:1A:79": "Intel",
20
+ "00:15:17": "Intel",
21
+ "00:13:CE": "Intel",
22
+ "00:0D:3A": "Intel",
23
+ "00:0E:0C": "Intel",
24
+ "00:11:11": "Intel",
25
+ "00:14:4F": "Intel",
26
+ "00:16:76": "Intel",
27
+ "00:18:DE": "Intel",
28
+ "00:1B:77": "Intel",
29
+ "00:1C:C0": "Intel",
30
+ "00:1D:7E": "Intel",
31
+ "00:1E:67": "Intel",
32
+ "00:21:5C": "Intel",
33
+ "00:23:14": "Intel",
34
+ "00:23:DF": "Intel",
35
+ "00:24:D6": "Intel",
36
+ "00:25:00": "Intel",
37
+ "00:26:18": "Intel",
38
+ "00:26:4A": "Intel",
39
+ "00:26:B9": "Intel",
40
+ "00:26:C7": "Intel",
41
+ "00:27:13": "Intel",
42
+ "00:50:56": "VMware",
43
+ "00:0C:29": "VMware",
44
+ "00:05:69": "VMware",
45
+ "00:1B:21": "Intel",
46
+ "00:1E:67": "Intel",
47
+ "00:21:70": "Intel",
48
+ "00:25:00": "Intel",
49
+ "00:26:B9": "Intel",
50
+ "00:1A:79": "Intel",
51
+ "00:15:17": "Intel",
52
+ "00:13:CE": "Intel",
53
+ "00:0D:3A": "Intel",
54
+ "00:0E:0C": "Intel",
55
+ "00:11:11": "Intel",
56
+ "00:14:4F": "Intel",
57
+ "00:16:76": "Intel",
58
+ "00:18:DE": "Intel",
59
+ "00:1B:77": "Intel",
60
+ "00:1C:C0": "Intel",
61
+ "00:1D:7E": "Intel",
62
+ "00:1E:67": "Intel",
63
+ "00:21:5C": "Intel",
64
+ "00:23:14": "Intel",
65
+ "00:23:DF": "Intel",
66
+ "00:24:D6": "Intel",
67
+ "00:25:00": "Intel",
68
+ "00:26:18": "Intel",
69
+ "00:26:4A": "Intel",
70
+ "00:26:B9": "Intel",
71
+ "00:26:C7": "Intel",
72
+ "00:27:13": "Intel",
73
+ "00:50:F2": "Apple",
74
+ "00:1E:C2": "Apple",
75
+ "00:23:12": "Apple",
76
+ "00:23:6C": "Apple",
77
+ "00:23:DF": "Apple",
78
+ "00:25:00": "Apple",
79
+ "00:25:4B": "Apple",
80
+ "00:26:08": "Apple",
81
+ "00:26:4A": "Apple",
82
+ "00:26:BB": "Apple",
83
+ "00:26:CA": "Apple",
84
+ "00:50:E4": "Apple",
85
+ "00:56:CD": "Apple",
86
+ "00:61:71": "Apple",
87
+ "00:6D:52": "Apple",
88
+ "00:88:65": "Apple",
89
+ "00:A0:40": "Apple",
90
+ "00:C6:10": "Apple",
91
+ "00:CD:FE": "Apple",
92
+ "00:F4:B9": "Apple",
93
+ "00:F7:6F": "Apple",
94
+ "04:0C:CE": "Apple",
95
+ "04:15:52": "Apple",
96
+ "04:1E:64": "Apple",
97
+ "04:26:65": "Apple",
98
+ "04:4B:ED": "Apple",
99
+ "04:54:53": "Apple",
100
+ "04:7D:7B": "Apple",
101
+ "04:8D:38": "Apple",
102
+ "04:9A:4F": "Apple",
103
+ "04:9F:CA": "Apple",
104
+ "04:A3:16": "Apple",
105
+ "04:DB:56": "Apple",
106
+ "04:E5:36": "Apple",
107
+ "04:F1:3E": "Apple",
108
+ "04:F7:E4": "Apple",
109
+ "08:00:27": "VirtualBox",
110
+ "08:66:98": "Apple",
111
+ "08:70:45": "Apple",
112
+ "08:74:02": "Apple",
113
+ "08:9E:01": "Apple",
114
+ "08:CC:68": "Apple",
115
+ "0C:3E:9F": "Apple",
116
+ "0C:4D:E9": "Apple",
117
+ "0C:51:01": "Apple",
118
+ "0C:74:C2": "Apple",
119
+ "0C:BC:9F": "Apple",
120
+ "0C:D2:92": "Apple",
121
+ "0C:E5:D3": "Apple",
122
+ "10:93:E9": "Apple",
123
+ "10:9A:DD": "Apple",
124
+ "10:DD:B1": "Apple",
125
+ "14:10:9F": "Apple",
126
+ "14:7D:DA": "Apple",
127
+ "14:99:E2": "Apple",
128
+ "14:CC:20": "Apple",
129
+ "18:20:32": "Apple",
130
+ "18:65:90": "Apple",
131
+ "18:AF:61": "Apple",
132
+ "1C:1A:C0": "Apple",
133
+ "1C:AB:A7": "Apple",
134
+ "20:78:F0": "Apple",
135
+ "20:AB:37": "Apple",
136
+ "20:C9:D0": "Apple",
137
+ "24:A0:74": "Apple",
138
+ "24:AB:81": "Apple",
139
+ "24:E3:14": "Apple",
140
+ "28:37:37": "Apple",
141
+ "28:6A:B8": "Apple",
142
+ "28:CF:DA": "Apple",
143
+ "28:CF:E9": "Apple",
144
+ "2C:1F:23": "Apple",
145
+ "2C:33:7A": "Apple",
146
+ "2C:BE:08": "Apple",
147
+ "30:90:AB": "Apple",
148
+ "34:15:9E": "Apple",
149
+ "34:A3:95": "Apple",
150
+ "34:C0:59": "Apple",
151
+ "38:CA:DA": "Apple",
152
+ "3C:07:54": "Apple",
153
+ "3C:15:C2": "Apple",
154
+ "3C:D0:F8": "Apple",
155
+ "40:33:1A": "Apple",
156
+ "40:6C:8F": "Apple",
157
+ "40:A6:D9": "Apple",
158
+ "40:CB:C0": "Apple",
159
+ "44:4C:0C": "Apple",
160
+ "44:FB:42": "Apple",
161
+ "48:43:7C": "Apple",
162
+ "48:BF:6B": "Apple",
163
+ "4C:8D:79": "Apple",
164
+ "50:EA:D6": "Apple",
165
+ "54:26:96": "Apple",
166
+ "54:72:4F": "Apple",
167
+ "58:55:CA": "Apple",
168
+ "5C:59:48": "Apple",
169
+ "5C:95:AE": "Apple",
170
+ "60:33:4B": "Apple",
171
+ "60:C5:47": "Apple",
172
+ "64:A3:CB": "Apple",
173
+ "68:96:7B": "Apple",
174
+ "6C:40:08": "Apple",
175
+ "6C:72:20": "Apple",
176
+ "70:48:0F": "Apple",
177
+ "70:56:51": "Realtek",
178
+ "74:E2:F5": "Apple",
179
+ "78:31:C1": "Apple",
180
+ "78:4F:43": "Apple",
181
+ "7C:6D:62": "Apple",
182
+ "80:BE:05": "Apple",
183
+ "84:38:35": "Apple",
184
+ "84:FC:FE": "Apple",
185
+ "88:63:DF": "Apple",
186
+ "8C:85:90": "Apple",
187
+ "90:72:40": "Apple",
188
+ "94:E9:6A": "Apple",
189
+ "98:5F:D3": "Apple",
190
+ "9C:84:BF": "Apple",
191
+ "A0:99:9B": "Apple",
192
+ "A4:5E:60": "Apple",
193
+ "A4:C3:61": "Apple",
194
+ "A8:60:B6": "Apple",
195
+ "AC:1F:74": "Apple",
196
+ "AC:BC:32": "Apple",
197
+ "B0:65:BD": "Apple",
198
+ "B4:F0:AB": "Apple",
199
+ "B8:09:8A": "Apple",
200
+ "B8:53:AC": "Apple",
201
+ "BC:3B:AF": "Apple",
202
+ "BC:52:B7": "Apple",
203
+ "C0:25:E9": "Apple",
204
+ "C4:2C:03": "Apple",
205
+ "C8:1E:E7": "Apple",
206
+ "CC:08:E0": "Apple",
207
+ "D0:03:4B": "Apple",
208
+ "D4:9A:20": "Apple",
209
+ "D8:30:62": "Apple",
210
+ "D8:A2:5E": "Apple",
211
+ "DC:2B:61": "Apple",
212
+ "E0:AC:CB": "Apple",
213
+ "E4:CE:8F": "Apple",
214
+ "E8:40:40": "Apple",
215
+ "EC:35:86": "Apple",
216
+ "F0:18:98": "Apple",
217
+ "F0:DB:E2": "Apple",
218
+ "F4:F1:5A": "Apple",
219
+ "F8:1E:DF": "Apple",
220
+ "FC:25:3F": "Apple",
221
+ "00:1B:21": "Intel",
222
+ "00:1E:67": "Intel",
223
+ "00:21:70": "Intel",
224
+ "00:25:00": "Intel",
225
+ "00:26:B9": "Intel",
226
+ "00:1A:79": "Intel",
227
+ "00:15:17": "Intel",
228
+ "00:13:CE": "Intel",
229
+ "00:0D:3A": "Intel",
230
+ "00:0E:0C": "Intel",
231
+ "00:11:11": "Intel",
232
+ "00:14:4F": "Intel",
233
+ "00:16:76": "Intel",
234
+ "00:18:DE": "Intel",
235
+ "00:1B:77": "Intel",
236
+ "00:1C:C0": "Intel",
237
+ "00:1D:7E": "Intel",
238
+ "00:1E:67": "Intel",
239
+ "00:21:5C": "Intel",
240
+ "00:23:14": "Intel",
241
+ "00:23:DF": "Intel",
242
+ "00:24:D6": "Intel",
243
+ "00:25:00": "Intel",
244
+ "00:26:18": "Intel",
245
+ "00:26:4A": "Intel",
246
+ "00:26:B9": "Intel",
247
+ "00:26:C7": "Intel",
248
+ "00:27:13": "Intel",
249
+ "00:50:56": "VMware",
250
+ "00:0C:29": "VMware",
251
+ "00:05:69": "VMware",
252
+ "00:16:3E": "Xen",
253
+ "00:03:FF": "Microsoft Hyper-V",
254
+ "00:1C:42": "Parallels",
255
+ "00:0F:4B": "Virtual Iron",
256
+ "08:00:27": "VirtualBox",
257
+ };
258
+
259
+ function lookupVendor(mac) {
260
+ if (!mac) return null;
261
+
262
+ // Normalize MAC address
263
+ const normalized = mac.replace(/[:-]/g, "").toUpperCase();
264
+ if (normalized.length < 6) return null;
265
+
266
+ // Get first 3 bytes (6 hex characters)
267
+ const prefix = normalized.substring(0, 6);
268
+ const formattedPrefix = `${prefix.substring(0, 2)}:${prefix.substring(2, 4)}:${prefix.substring(4, 6)}`;
269
+
270
+ return OUI_DB[formattedPrefix] || OUI_DB[prefix] || null;
271
+ }
272
+
273
+ function getVendorInfo(mac) {
274
+ const vendor = lookupVendor(mac);
275
+ return {
276
+ mac: mac,
277
+ vendor: vendor || "Unknown",
278
+ prefix: mac.substring(0, 8),
279
+ };
280
+ }
281
+
282
+ module.exports = {
283
+ lookupVendor,
284
+ getVendorInfo,
285
+ OUI_DB,
286
+ };
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "spoof-d",
3
3
  "description": "Cross-platform MAC address spoofing utility for macOS, Windows, and Linux",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "bin": {
6
- "spoofy": "./bin/cmd.js"
6
+ "spoofy": "bin/cmd.js"
7
7
  },
8
8
  "files": [
9
9
  "index.js",
10
10
  "bin/",
11
+ "lib/",
11
12
  "LICENSE",
12
- "README.md"
13
+ "README.md",
14
+ ".spoofyrc.example",
15
+ "batch.example.json"
13
16
  ],
14
17
  "bugs": {
15
18
  "url": "https://github.com/TT5H/spoof-d/issues"
@@ -18,6 +21,7 @@
18
21
  "chalk": "^4.1.0",
19
22
  "common-tags": "^1.8.0",
20
23
  "minimist": "^1.2.5",
24
+ "ora": "^9.0.0",
21
25
  "shell-quote": "^1.7.2",
22
26
  "zero-fill": "^2.2.4"
23
27
  },