devbits 1.3.3__tar.gz → 1.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {devbits-1.3.3 → devbits-1.4.1}/PKG-INFO +72 -7
  2. {devbits-1.3.3 → devbits-1.4.1}/README.md +71 -6
  3. {devbits-1.3.3 → devbits-1.4.1}/devbits/__init__.py +1 -1
  4. {devbits-1.3.3 → devbits-1.4.1}/devbits/cli.py +153 -11
  5. {devbits-1.3.3 → devbits-1.4.1}/devbits/gui.py +313 -13
  6. devbits-1.4.1/devbits/network.py +967 -0
  7. {devbits-1.3.3 → devbits-1.4.1}/devbits/scripts.py +9 -0
  8. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/PKG-INFO +72 -7
  9. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/SOURCES.txt +1 -0
  10. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/entry_points.txt +2 -0
  11. {devbits-1.3.3 → devbits-1.4.1}/pyproject.toml +4 -2
  12. {devbits-1.3.3 → devbits-1.4.1}/tests/test_gui_cli.py +45 -9
  13. devbits-1.4.1/tests/test_netsurvey.py +346 -0
  14. devbits-1.3.3/devbits/network.py +0 -311
  15. {devbits-1.3.3 → devbits-1.4.1}/LICENSE +0 -0
  16. {devbits-1.3.3 → devbits-1.4.1}/devbits/cache.py +0 -0
  17. {devbits-1.3.3 → devbits-1.4.1}/devbits/image.py +0 -0
  18. {devbits-1.3.3 → devbits-1.4.1}/devbits/media.py +0 -0
  19. {devbits-1.3.3 → devbits-1.4.1}/devbits/project.py +0 -0
  20. {devbits-1.3.3 → devbits-1.4.1}/devbits/tui.py +0 -0
  21. {devbits-1.3.3 → devbits-1.4.1}/devbits/utils.py +0 -0
  22. {devbits-1.3.3 → devbits-1.4.1}/devbits/wifi.py +0 -0
  23. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/dependency_links.txt +0 -0
  24. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/requires.txt +0 -0
  25. {devbits-1.3.3 → devbits-1.4.1}/devbits.egg-info/top_level.txt +0 -0
  26. {devbits-1.3.3 → devbits-1.4.1}/setup.cfg +0 -0
  27. {devbits-1.3.3 → devbits-1.4.1}/tests/test_cli.py +0 -0
  28. {devbits-1.3.3 → devbits-1.4.1}/tests/test_tui.py +0 -0
  29. {devbits-1.3.3 → devbits-1.4.1}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devbits
3
- Version: 1.3.3
3
+ Version: 1.4.1
4
4
  Summary: A lightweight CLI toolkit for daily development utilities.
5
5
  Author: Bruce Chuang
6
6
  License-Expression: MIT
@@ -42,8 +42,8 @@ devbits <command> [options]
42
42
  Use `--help` on any command for detailed usage and parameter descriptions:
43
43
 
44
44
  ```bash
45
- devbits clipvideo --help
46
- clipvideo --help
45
+ devbits editvideo --help
46
+ editvideo --help
47
47
  ```
48
48
 
49
49
  ## Commands
@@ -52,7 +52,7 @@ clipvideo --help
52
52
 
53
53
  | Command | Description |
54
54
  |---------|-------------|
55
- | `clipvideo` | Trim a video by time (seconds) or frame range. Includes `--gui` for browser-based editing. |
55
+ | `editvideo` | Trim a video by time (seconds) or frame range. Includes `--gui` for browser-based editing. (Formerly `clipvideo`, which still works but prints a rename notice.) |
56
56
  | `video2images` | Extract frames from a video. |
57
57
  | `video2gif` | Convert a video (or a portion) to animated GIF. |
58
58
  | `images2video` | Assemble an image sequence into an MP4 video. |
@@ -85,16 +85,17 @@ clipvideo --help
85
85
  | Command | Description |
86
86
  |---------|-------------|
87
87
  | `netscan` | List devices connected to your local network (Wi-Fi / router) with their IP, MAC, and hostname. `--lookup` adds the manufacturer. |
88
+ | `netsurvey` | Map **every network segment** visible on the current link — your own interfaces, foreign subnets on the same switch, the addresses in use, and the conflicts between them. Built for shared networks (trade shows, offices). |
88
89
  | `wifi` | Manage Wi-Fi: `list`, `connect` (arrow-key picker + hidden password prompt), `on`, `off`, `forget`. Linux and Windows fully; macOS without `list`. |
89
90
 
90
91
  ## Examples
91
92
 
92
93
  ```bash
93
94
  # Trim video from 5s to 20s
94
- clipvideo movie.mp4 --start 5.0 --end 20.0
95
+ editvideo movie.mp4 --start 5.0 --end 20.0
95
96
 
96
97
  # Open interactive clip editor in the browser
97
- clipvideo movie.mp4 --gui
98
+ editvideo movie.mp4 --gui
98
99
 
99
100
  # Convert video to GIF (3.5s–10s at 15 fps)
100
101
  video2gif movie.mp4 --start 3.5 --end 10.0 --fps 15
@@ -124,6 +125,18 @@ netscan --lookup
124
125
  # Scan a specific subnet, faster, without hostname lookups
125
126
  netscan --network 192.168.1.0/24 --timeout 0.5 --no-resolve
126
127
 
128
+ # Map every network segment and address around you (passive, ~2s)
129
+ netsurvey
130
+
131
+ # Segments and conflicts only, no address lists
132
+ netsurvey --summary
133
+
134
+ # Also ping-sweep your ranges and the discovered ones
135
+ netsurvey --sweep
136
+
137
+ # Check specific ranges before assigning them at a venue
138
+ netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24
139
+
127
140
  # Show the Wi-Fi networks in range (Linux / Windows)
128
141
  wifi list
129
142
 
@@ -146,6 +159,58 @@ wifi forget OldCafe
146
159
  > laptops that use a randomized/private MAC show up as `(private)` and can't be
147
160
  > attributed to a vendor.
148
161
 
162
+ ### Surveying a shared network
163
+
164
+ `netscan` answers "who is on **my** subnet?". `netsurvey` answers "what is on
165
+ this **wire**?" — the question that matters in an exhibition hall, an office, or
166
+ a co-working space where other people's routers hand out their own ranges:
167
+
168
+ ```
169
+ INTERFACES
170
+ en0 192.168.0.122/24 gateway 192.168.0.1
171
+ utun6 10.2.244.44/32
172
+
173
+ SEGMENTS (3 total, 2 yours, 9 address(es) in use)
174
+ 192.168.0.0/24 local (en0) swept 6 host(s)
175
+ 192.168.0.1 06:f2:67:75:4d:e2 gateway,arp,ping gateway / router
176
+ 192.168.0.122 4e:e5:41:93:7c:1f self,arp,mdns this device
177
+ ...
178
+ 10.77.0.0/24 foreign passive 2 host(s)
179
+ 10.77.0.9 de:ad:be:ef:00:01 ssdp,arp
180
+
181
+ CONFLICTS (2)
182
+ ! [duplicate-ip] 192.168.0.50 answers from 2 MAC addresses (…) — two devices claim the same address
183
+ - [shared-l2] 1 foreign segment(s) share this link: 10.77.0.0/24
184
+ ```
185
+
186
+ Three independent sources are combined, so segments you have no address in still
187
+ show up:
188
+
189
+ - **every local interface** with its real netmask (`ip` / `ifconfig` / `ipconfig`);
190
+ - the **ARP / neighbour cache**, which lists link neighbours *regardless of
191
+ subnet* — this is what exposes somebody else's range on the same switch;
192
+ - **SSDP and mDNS** answers, sent out of each interface (skip with `--no-passive`).
193
+
194
+ The default pass is passive and takes about two seconds. `--sweep` additionally
195
+ ping-sweeps your ranges and the discovered ones so each address list is complete;
196
+ `--include CIDR` sweeps a specific range you are about to assign. Ranges larger
197
+ than `--max-sweep` (4096 hosts) are narrowed to the /24 around your own address
198
+ or skipped, with a note on stderr.
199
+
200
+ Reported conflicts:
201
+
202
+ | Kind | Meaning |
203
+ |------|---------|
204
+ | `duplicate-ip` | One address answers from two MACs — two devices claim it. |
205
+ | `overlap` | A foreign range, or a second local interface (VPN!), overlaps one of your subnets. |
206
+ | `no-dhcp` | An interface self-assigned a `169.254.x.x` address; no DHCP server answered. |
207
+ | `router-bridge` | One MAC answers for addresses in several segments — a router joining them. |
208
+ | `shared-l2` | Foreign segments are present on your link (informational). |
209
+
210
+ > Survey only networks you are entitled to. `netsurvey` sends ordinary pings and
211
+ > standard discovery multicast — nothing privileged — but probing networks you
212
+ > don't administer may still violate policy or law.
213
+
149
214
  ### Wi-Fi
150
215
 
151
216
  `wifi connect` lists everything in range — move with ↑/↓, press Enter to join,
@@ -225,7 +290,7 @@ dependency for one platform, devbits doesn't scan on macOS at all.
225
290
  When `-o` / `--output` is omitted, the output filename is derived from the input:
226
291
 
227
292
  ```
228
- clipvideo movie.mp4 → movie_clip.mp4
293
+ editvideo movie.mp4 → movie_clip.mp4
229
294
  video2gif movie.mp4 → movie.gif
230
295
  resizeimage photo.jpg → photo_resized.jpg
231
296
  recolor logo.png → logo_revised.png
@@ -25,8 +25,8 @@ devbits <command> [options]
25
25
  Use `--help` on any command for detailed usage and parameter descriptions:
26
26
 
27
27
  ```bash
28
- devbits clipvideo --help
29
- clipvideo --help
28
+ devbits editvideo --help
29
+ editvideo --help
30
30
  ```
31
31
 
32
32
  ## Commands
@@ -35,7 +35,7 @@ clipvideo --help
35
35
 
36
36
  | Command | Description |
37
37
  |---------|-------------|
38
- | `clipvideo` | Trim a video by time (seconds) or frame range. Includes `--gui` for browser-based editing. |
38
+ | `editvideo` | Trim a video by time (seconds) or frame range. Includes `--gui` for browser-based editing. (Formerly `clipvideo`, which still works but prints a rename notice.) |
39
39
  | `video2images` | Extract frames from a video. |
40
40
  | `video2gif` | Convert a video (or a portion) to animated GIF. |
41
41
  | `images2video` | Assemble an image sequence into an MP4 video. |
@@ -68,16 +68,17 @@ clipvideo --help
68
68
  | Command | Description |
69
69
  |---------|-------------|
70
70
  | `netscan` | List devices connected to your local network (Wi-Fi / router) with their IP, MAC, and hostname. `--lookup` adds the manufacturer. |
71
+ | `netsurvey` | Map **every network segment** visible on the current link — your own interfaces, foreign subnets on the same switch, the addresses in use, and the conflicts between them. Built for shared networks (trade shows, offices). |
71
72
  | `wifi` | Manage Wi-Fi: `list`, `connect` (arrow-key picker + hidden password prompt), `on`, `off`, `forget`. Linux and Windows fully; macOS without `list`. |
72
73
 
73
74
  ## Examples
74
75
 
75
76
  ```bash
76
77
  # Trim video from 5s to 20s
77
- clipvideo movie.mp4 --start 5.0 --end 20.0
78
+ editvideo movie.mp4 --start 5.0 --end 20.0
78
79
 
79
80
  # Open interactive clip editor in the browser
80
- clipvideo movie.mp4 --gui
81
+ editvideo movie.mp4 --gui
81
82
 
82
83
  # Convert video to GIF (3.5s–10s at 15 fps)
83
84
  video2gif movie.mp4 --start 3.5 --end 10.0 --fps 15
@@ -107,6 +108,18 @@ netscan --lookup
107
108
  # Scan a specific subnet, faster, without hostname lookups
108
109
  netscan --network 192.168.1.0/24 --timeout 0.5 --no-resolve
109
110
 
111
+ # Map every network segment and address around you (passive, ~2s)
112
+ netsurvey
113
+
114
+ # Segments and conflicts only, no address lists
115
+ netsurvey --summary
116
+
117
+ # Also ping-sweep your ranges and the discovered ones
118
+ netsurvey --sweep
119
+
120
+ # Check specific ranges before assigning them at a venue
121
+ netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24
122
+
110
123
  # Show the Wi-Fi networks in range (Linux / Windows)
111
124
  wifi list
112
125
 
@@ -129,6 +142,58 @@ wifi forget OldCafe
129
142
  > laptops that use a randomized/private MAC show up as `(private)` and can't be
130
143
  > attributed to a vendor.
131
144
 
145
+ ### Surveying a shared network
146
+
147
+ `netscan` answers "who is on **my** subnet?". `netsurvey` answers "what is on
148
+ this **wire**?" — the question that matters in an exhibition hall, an office, or
149
+ a co-working space where other people's routers hand out their own ranges:
150
+
151
+ ```
152
+ INTERFACES
153
+ en0 192.168.0.122/24 gateway 192.168.0.1
154
+ utun6 10.2.244.44/32
155
+
156
+ SEGMENTS (3 total, 2 yours, 9 address(es) in use)
157
+ 192.168.0.0/24 local (en0) swept 6 host(s)
158
+ 192.168.0.1 06:f2:67:75:4d:e2 gateway,arp,ping gateway / router
159
+ 192.168.0.122 4e:e5:41:93:7c:1f self,arp,mdns this device
160
+ ...
161
+ 10.77.0.0/24 foreign passive 2 host(s)
162
+ 10.77.0.9 de:ad:be:ef:00:01 ssdp,arp
163
+
164
+ CONFLICTS (2)
165
+ ! [duplicate-ip] 192.168.0.50 answers from 2 MAC addresses (…) — two devices claim the same address
166
+ - [shared-l2] 1 foreign segment(s) share this link: 10.77.0.0/24
167
+ ```
168
+
169
+ Three independent sources are combined, so segments you have no address in still
170
+ show up:
171
+
172
+ - **every local interface** with its real netmask (`ip` / `ifconfig` / `ipconfig`);
173
+ - the **ARP / neighbour cache**, which lists link neighbours *regardless of
174
+ subnet* — this is what exposes somebody else's range on the same switch;
175
+ - **SSDP and mDNS** answers, sent out of each interface (skip with `--no-passive`).
176
+
177
+ The default pass is passive and takes about two seconds. `--sweep` additionally
178
+ ping-sweeps your ranges and the discovered ones so each address list is complete;
179
+ `--include CIDR` sweeps a specific range you are about to assign. Ranges larger
180
+ than `--max-sweep` (4096 hosts) are narrowed to the /24 around your own address
181
+ or skipped, with a note on stderr.
182
+
183
+ Reported conflicts:
184
+
185
+ | Kind | Meaning |
186
+ |------|---------|
187
+ | `duplicate-ip` | One address answers from two MACs — two devices claim it. |
188
+ | `overlap` | A foreign range, or a second local interface (VPN!), overlaps one of your subnets. |
189
+ | `no-dhcp` | An interface self-assigned a `169.254.x.x` address; no DHCP server answered. |
190
+ | `router-bridge` | One MAC answers for addresses in several segments — a router joining them. |
191
+ | `shared-l2` | Foreign segments are present on your link (informational). |
192
+
193
+ > Survey only networks you are entitled to. `netsurvey` sends ordinary pings and
194
+ > standard discovery multicast — nothing privileged — but probing networks you
195
+ > don't administer may still violate policy or law.
196
+
132
197
  ### Wi-Fi
133
198
 
134
199
  `wifi connect` lists everything in range — move with ↑/↓, press Enter to join,
@@ -208,7 +273,7 @@ dependency for one platform, devbits doesn't scan on macOS at all.
208
273
  When `-o` / `--output` is omitted, the output filename is derived from the input:
209
274
 
210
275
  ```
211
- clipvideo movie.mp4 → movie_clip.mp4
276
+ editvideo movie.mp4 → movie_clip.mp4
212
277
  video2gif movie.mp4 → movie.gif
213
278
  resizeimage photo.jpg → photo_resized.jpg
214
279
  recolor logo.png → logo_revised.png
@@ -1,3 +1,3 @@
1
1
  """devbits: A lightweight CLI toolkit for daily development utilities."""
2
2
 
3
- __version__ = "1.3.3"
3
+ __version__ = "1.4.1"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import ipaddress
4
5
  import os
5
6
  import shutil
6
7
  import sys
@@ -33,6 +34,9 @@ _ANSI = {
33
34
  "signal": "\033[32m",
34
35
  "lock": "\033[33m",
35
36
  "dim": "\033[2m",
37
+ "segment": "\033[1;36m",
38
+ "warn": "\033[1;31m",
39
+ "note": "\033[2m",
36
40
  "reset": "\033[0m",
37
41
  }
38
42
 
@@ -191,20 +195,20 @@ def build_parser() -> argparse.ArgumentParser:
191
195
  help="Output size as 'width,height' in pixels, e.g. 640,360.")
192
196
  p.set_defaults(func=cmd_video2gif)
193
197
 
194
- # ── clipvideo ──────────────────────────────────────────────
198
+ # ── editvideo ──────────────────────────────────────────────
195
199
  p = sub.add_parser(
196
- "clipvideo",
197
- help="Clip (trim) a video by time range or frame range.",
200
+ "editvideo",
201
+ help="Edit (trim) a video by time range or frame range.",
198
202
  formatter_class=argparse.RawDescriptionHelpFormatter,
199
203
  description=(
200
204
  "Trim a portion of a video. You can specify the range in seconds\n"
201
205
  "(--start / --end) or in frame indices (--start-frame / --end-frame).\n"
202
206
  "If no range is given, the interactive browser-based editor opens.\n\n"
203
207
  "Examples:\n"
204
- " devbits clipvideo movie.mp4 # opens the GUI editor\n"
205
- " devbits clipvideo movie.mp4 --start 5.0 --end 20.0\n"
206
- " devbits clipvideo movie.mp4 --start-frame 150 --end-frame 600\n"
207
- " devbits clipvideo --gui # GUI with no initial video"
208
+ " devbits editvideo movie.mp4 # opens the GUI editor\n"
209
+ " devbits editvideo movie.mp4 --start 5.0 --end 20.0\n"
210
+ " devbits editvideo movie.mp4 --start-frame 150 --end-frame 600\n"
211
+ " devbits editvideo --gui # GUI with no initial video"
208
212
  ),
209
213
  )
210
214
  p.add_argument("video", type=Path, nargs="?", default=None,
@@ -221,7 +225,7 @@ def build_parser() -> argparse.ArgumentParser:
221
225
  help="End frame index (0-based, inclusive).")
222
226
  p.add_argument("--gui", action="store_true",
223
227
  help="Open interactive browser-based clip editor.")
224
- p.set_defaults(func=cmd_clipvideo)
228
+ p.set_defaults(func=cmd_editvideo)
225
229
 
226
230
  # ── resizevideo ────────────────────────────────────────────
227
231
  p = sub.add_parser(
@@ -491,6 +495,58 @@ def build_parser() -> argparse.ArgumentParser:
491
495
  help="Disable colored output (also honors NO_COLOR).")
492
496
  p.set_defaults(func=cmd_netscan)
493
497
 
498
+ # ── netsurvey ──────────────────────────────────────────────
499
+ p = sub.add_parser(
500
+ "netsurvey",
501
+ help="Map every network segment and address in use around you.",
502
+ formatter_class=argparse.RawDescriptionHelpFormatter,
503
+ description=(
504
+ "Survey the whole environment instead of a single subnet: list your own\n"
505
+ "interfaces and their ranges, every segment visible on the same wire, the\n"
506
+ "addresses in use in each, and the clashes between them.\n\n"
507
+ "Built for shared networks — a trade-show hall, an office, a co-working\n"
508
+ "space — where someone else's router hands out an overlapping range and\n"
509
+ "traffic quietly goes to the wrong place. Reports duplicate IPs, ranges\n"
510
+ "that overlap your own, links with no DHCP, and routers bridging two\n"
511
+ "segments.\n\n"
512
+ "The default pass is passive and takes a couple of seconds: it reads the\n"
513
+ "ARP / neighbour cache (which lists neighbours on *foreign* subnets too)\n"
514
+ "and asks for SSDP and mDNS answers. --sweep additionally ping-sweeps\n"
515
+ "your ranges and the discovered ones so each address list is complete.\n\n"
516
+ "Survey only networks you are entitled to; probing networks you don't\n"
517
+ "administer may violate policy or law.\n\n"
518
+ "Examples:\n"
519
+ " devbits netsurvey # quick passive map\n"
520
+ " devbits netsurvey --summary # segments only\n"
521
+ " devbits netsurvey --sweep # also enumerate every host\n"
522
+ " devbits netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24\n"
523
+ " devbits netsurvey --sweep --group-prefix 16 --timeout 0.4"
524
+ ),
525
+ )
526
+ p.add_argument("--sweep", action="store_true",
527
+ help="Also ping-sweep your own and the discovered segments (slower, complete).")
528
+ p.add_argument("--include", metavar="CIDR", action="append", default=[],
529
+ help="Extra range to sweep, e.g. 10.0.0.0/24. Repeatable.")
530
+ p.add_argument("--group-prefix", type=int, default=24, metavar="N",
531
+ help="Prefix used to group addresses outside your own subnets. Default: 24")
532
+ p.add_argument("--timeout", type=float, default=0.6,
533
+ help="Per-host ping timeout in seconds. Default: 0.6")
534
+ p.add_argument("--workers", type=int, default=128,
535
+ help="Number of concurrent ping workers. Default: 128")
536
+ p.add_argument("--discover-timeout", type=float, default=2.0, metavar="SECONDS",
537
+ help="How long to listen for SSDP / mDNS answers. Default: 2.0")
538
+ p.add_argument("--no-passive", action="store_true",
539
+ help="Skip the multicast discovery step; read the ARP cache only.")
540
+ p.add_argument("--resolve", action="store_true",
541
+ help="Reverse-DNS each address for its hostname (slower).")
542
+ p.add_argument("--max-sweep", type=int, default=4096, metavar="HOSTS",
543
+ help="Refuse to sweep a range larger than this. Default: 4096")
544
+ p.add_argument("--summary", action="store_true",
545
+ help="List segments and conflicts only, without the addresses.")
546
+ p.add_argument("--no-color", action="store_true",
547
+ help="Disable colored output (also honors NO_COLOR).")
548
+ p.set_defaults(func=cmd_netsurvey)
549
+
494
550
  # ── wifi ───────────────────────────────────────────────────
495
551
  p = sub.add_parser(
496
552
  "wifi",
@@ -644,7 +700,7 @@ def cmd_video2gif(args: argparse.Namespace) -> None:
644
700
  print(video_to_gif(video, output, args.fps, args.start, args.end, args.size))
645
701
 
646
702
 
647
- def cmd_clipvideo(args: argparse.Namespace) -> None:
703
+ def cmd_editvideo(args: argparse.Namespace) -> None:
648
704
  has_range = any(v is not None for v in (args.start, args.end, args.start_frame, args.end_frame))
649
705
  # Open the interactive editor when --gui is set, or by default when a video
650
706
  # is provided without an explicit trim range.
@@ -741,8 +797,6 @@ def cmd_samplefiles(args: argparse.Namespace) -> None:
741
797
 
742
798
 
743
799
  def cmd_netscan(args: argparse.Namespace) -> None:
744
- import ipaddress
745
-
746
800
  color = _use_color(False if args.no_color else None)
747
801
  network = ipaddress.ip_network(args.network, strict=False) if args.network else None
748
802
 
@@ -771,6 +825,85 @@ def cmd_netscan(args: argparse.Namespace) -> None:
771
825
  print(f"Found {len(devices)} device(s).")
772
826
 
773
827
 
828
+ def _survey_source_label(sources: set[str], width: int = 22) -> str:
829
+ """Condense a device's discovery sources into a stable, readable column."""
830
+ order = ["self", "gateway", "arp", "ping", "ssdp", "mdns"]
831
+ label = ",".join(source for source in order if source in sources) or "-"
832
+ if len(label) > width - 1:
833
+ label = label[: width - 2] + "…"
834
+ return f"{label:<{width}}"
835
+
836
+
837
+ def cmd_netsurvey(args: argparse.Namespace) -> None:
838
+ from .network import survey_network
839
+
840
+ color = _use_color(False if args.no_color else None)
841
+ if not 8 <= args.group_prefix <= 32:
842
+ raise ValueError("--group-prefix must be between 8 and 32")
843
+
844
+ survey, notes = survey_network(
845
+ include=args.include,
846
+ group_prefix=args.group_prefix,
847
+ sweep=args.sweep,
848
+ passive=not args.no_passive,
849
+ timeout=args.timeout,
850
+ workers=args.workers,
851
+ discover_timeout=args.discover_timeout,
852
+ resolve=args.resolve,
853
+ max_sweep_hosts=args.max_sweep,
854
+ progress=lambda message: print(message, file=sys.stderr),
855
+ )
856
+
857
+ print(_colorize("INTERFACES", "header", color))
858
+ for nic in survey.interfaces:
859
+ note = "gateway " + survey.gateway if survey.gateway and \
860
+ ipaddress.ip_address(survey.gateway) in nic.network else ""
861
+ if nic.is_link_local:
862
+ note = "self-assigned (no DHCP)"
863
+ name_col = _colorize(f"{nic.name:<12}", "self" if nic.ip == survey.self_ip else "dim", color)
864
+ print(f" {name_col}{f'{nic.ip}/{nic.prefix}':<22}{_colorize(note, 'note', color)}".rstrip())
865
+
866
+ print()
867
+ local_count = sum(1 for segment in survey.segments if segment.is_local)
868
+ print(_colorize(
869
+ f"SEGMENTS ({len(survey.segments)} total, {local_count} yours, "
870
+ f"{survey.device_count} address(es) in use)", "header", color,
871
+ ))
872
+ for segment in survey.segments:
873
+ scope = f"local ({', '.join(segment.interfaces)})" if segment.is_local else "foreign"
874
+ how = "swept" if segment.swept else "passive"
875
+ head = f" {str(segment.network):<20}{scope:<24}{how:<10}{len(segment.devices)} host(s)"
876
+ print(_colorize(head.rstrip(), "segment" if segment.is_local else "warn", color))
877
+ if args.summary:
878
+ continue
879
+ for device in segment.devices:
880
+ note = "this device" if device.is_self else ("gateway / router" if device.is_gateway else "")
881
+ kind = "self" if device.is_self else ("gateway" if device.is_gateway else None)
882
+ ip_col = _colorize(f"{device.ip:<16}", kind, color) if kind else f"{device.ip:<16}"
883
+ mac_col = _colorize(f"{device.mac or '-':<20}", "mac", color)
884
+ host_col = _colorize(f"{device.hostname or '-':<28}", "host", color) if args.resolve else ""
885
+ seen_col = _colorize(_survey_source_label(device.sources), "note", color)
886
+ print(f" {ip_col}{mac_col}{host_col}{seen_col}{note}".rstrip())
887
+
888
+ print()
889
+ if survey.conflicts:
890
+ print(_colorize(f"CONFLICTS ({len(survey.conflicts)})", "header", color))
891
+ for conflict in survey.conflicts:
892
+ marker = "!" if conflict.kind != "shared-l2" else "-"
893
+ kind = "warn" if conflict.kind != "shared-l2" else "note"
894
+ print(_colorize(f" {marker} [{conflict.kind}] {conflict.message}", kind, color))
895
+ else:
896
+ print("No address or range conflicts detected.")
897
+
898
+ for note in notes:
899
+ print(_colorize(f" note: {note}", "note", color), file=sys.stderr)
900
+ if not args.sweep and not args.include:
901
+ print(_colorize(
902
+ "Passive pass only — run with --sweep to enumerate every address in each segment.",
903
+ "note", color,
904
+ ), file=sys.stderr)
905
+
906
+
774
907
  # ---------------------------------------------------------------------------
775
908
  # wifi
776
909
  # ---------------------------------------------------------------------------
@@ -1008,6 +1141,15 @@ def cmd_wifi_forget(args: argparse.Namespace) -> None:
1008
1141
 
1009
1142
 
1010
1143
  def main(argv: list[str] | None = None) -> int:
1144
+ argv = list(sys.argv[1:] if argv is None else argv)
1145
+ if argv and argv[0] == "clipvideo":
1146
+ # Deprecated alias kept so existing invocations keep working.
1147
+ print(
1148
+ "Note: 'clipvideo' has been renamed to 'editvideo' and will be "
1149
+ "removed in a future release. Please use 'editvideo' instead.",
1150
+ file=sys.stderr,
1151
+ )
1152
+ argv[0] = "editvideo"
1011
1153
  parser = build_parser()
1012
1154
  args = parser.parse_args(argv)
1013
1155
  try: