evo-cli 0.11.1__tar.gz → 0.11.2__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 (51) hide show
  1. {evo_cli-0.11.1 → evo_cli-0.11.2}/PKG-INFO +1 -1
  2. evo_cli-0.11.2/evo_cli/VERSION +1 -0
  3. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/cli.py +4 -0
  4. evo_cli-0.11.2/evo_cli/commands/hwid.py +237 -0
  5. evo_cli-0.11.2/evo_cli/commands/hwid_reset.py +197 -0
  6. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/PKG-INFO +1 -1
  7. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/SOURCES.txt +2 -0
  8. evo_cli-0.11.1/evo_cli/VERSION +0 -1
  9. {evo_cli-0.11.1 → evo_cli-0.11.2}/Containerfile +0 -0
  10. {evo_cli-0.11.1 → evo_cli-0.11.2}/HISTORY.md +0 -0
  11. {evo_cli-0.11.1 → evo_cli-0.11.2}/LICENSE +0 -0
  12. {evo_cli-0.11.1 → evo_cli-0.11.2}/MANIFEST.in +0 -0
  13. {evo_cli-0.11.1 → evo_cli-0.11.2}/README.md +0 -0
  14. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/__init__.py +0 -0
  15. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/__main__.py +0 -0
  16. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/base.py +0 -0
  17. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/__init__.py +0 -0
  18. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/claude_code.py +0 -0
  19. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/cloudflare.py +0 -0
  20. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/fix_claude.py +0 -0
  21. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/gdrive.py +0 -0
  22. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/gh.py +0 -0
  23. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/localproxy.py +0 -0
  24. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/mcp.py +0 -0
  25. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/miniconda.py +0 -0
  26. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/netcheck.py +0 -0
  27. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/opencode.py +0 -0
  28. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/plantuml.py +0 -0
  29. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/site2s.py +0 -0
  30. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/ssh.py +0 -0
  31. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/sysmon.py +0 -0
  32. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/commands/wifi.py +0 -0
  33. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/console.py +0 -0
  34. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli/mcp_registry.py +0 -0
  35. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/dependency_links.txt +0 -0
  36. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/entry_points.txt +0 -0
  37. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/requires.txt +0 -0
  38. {evo_cli-0.11.1 → evo_cli-0.11.2}/evo_cli.egg-info/top_level.txt +0 -0
  39. {evo_cli-0.11.1 → evo_cli-0.11.2}/pyproject.toml +0 -0
  40. {evo_cli-0.11.1 → evo_cli-0.11.2}/setup.cfg +0 -0
  41. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/__init__.py +0 -0
  42. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_claude_code.py +0 -0
  43. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_cli.py +0 -0
  44. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_console.py +0 -0
  45. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_fix_claude.py +0 -0
  46. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_gh.py +0 -0
  47. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_mcp.py +0 -0
  48. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_opencode.py +0 -0
  49. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_plantuml.py +0 -0
  50. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_sysmon.py +0 -0
  51. {evo_cli-0.11.1 → evo_cli-0.11.2}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.11.2
@@ -6,6 +6,8 @@ from evo_cli.commands.cloudflare import cfssh
6
6
  from evo_cli.commands.fix_claude import f_claude
7
7
  from evo_cli.commands.gdrive import gdrive
8
8
  from evo_cli.commands.gh import setup_gh
9
+ from evo_cli.commands.hwid import hwid
10
+ from evo_cli.commands.hwid_reset import hwid_reset
9
11
  from evo_cli.commands.localproxy import localproxy
10
12
  from evo_cli.commands.mcp import mcp_group
11
13
  from evo_cli.commands.miniconda import miniconda
@@ -54,6 +56,8 @@ cli.add_command(setup_group)
54
56
  cli.add_command(cfssh)
55
57
  cli.add_command(f_claude)
56
58
  cli.add_command(gdrive)
59
+ cli.add_command(hwid)
60
+ cli.add_command(hwid_reset)
57
61
  cli.add_command(site2s)
58
62
  cli.add_command(localproxy)
59
63
  cli.add_command(mcp_group)
@@ -0,0 +1,237 @@
1
+ import hashlib
2
+ import json as jsonlib
3
+ import platform
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ import uuid
8
+
9
+ import rich_click as click
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from evo_cli.console import console, error, info, step
15
+
16
+ # Each identifier: (key, human label, contributes to the composite HWID hash).
17
+ # Only stable, hard-to-change identifiers feed the hash; disk serial and MAC
18
+ # are shown for reference but excluded (disks get swapped, MACs are plural).
19
+ ID_SPECS = [
20
+ ("machine_guid", "Machine GUID", True),
21
+ ("system_uuid", "System UUID", True),
22
+ ("board_serial", "Baseboard serial", True),
23
+ ("bios_serial", "BIOS serial", True),
24
+ ("cpu_id", "CPU ID", True),
25
+ ("disk_serial", "Disk serial", False),
26
+ ("mac", "MAC address", False),
27
+ ]
28
+
29
+ # Placeholder strings firmware vendors leave in serial fields - useless as IDs.
30
+ JUNK_VALUES = {
31
+ "",
32
+ "0",
33
+ "none",
34
+ "n/a",
35
+ "na",
36
+ "null",
37
+ "to be filled by o.e.m.",
38
+ "to be filled by o.e.m",
39
+ "default string",
40
+ "system serial number",
41
+ "system product name",
42
+ "system uuid",
43
+ "00000000-0000-0000-0000-000000000000",
44
+ "ffffffff-ffff-ffff-ffff-ffffffffffff",
45
+ "0123456789",
46
+ }
47
+
48
+ EPILOG = Text.from_markup(
49
+ "[bold]Examples[/bold]\n\n"
50
+ " [cyan]evo hwid[/cyan] show hardware identifiers and the composite HWID\n"
51
+ " [cyan]evo hwid --json[/cyan] machine-readable output\n"
52
+ " [cyan]evo hwid --raw[/cyan] also print the pre-hash composite string\n"
53
+ " [cyan]evo hwid --salt myapp[/cyan] namespace the HWID for a specific app/license\n\n"
54
+ "[dim]The HWID is a SHA-256 over the stable identifiers that are present\n"
55
+ "(machine GUID, system UUID, baseboard / BIOS serial, CPU ID). It stays\n"
56
+ "constant across reboots and reinstalls on the same physical machine.[/dim]"
57
+ )
58
+
59
+
60
+ def _run(cmd, timeout=10):
61
+ """Run a command, return stdout on success, else None (never raises)."""
62
+ try:
63
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
64
+ except (OSError, subprocess.SubprocessError):
65
+ return None
66
+ return out.stdout if out.returncode == 0 else None
67
+
68
+
69
+ def _clean(value):
70
+ """Normalise a raw identifier; drop firmware placeholder junk to None."""
71
+ if value is None:
72
+ return None
73
+ value = str(value).strip()
74
+ if value.lower() in JUNK_VALUES:
75
+ return None
76
+ return value or None
77
+
78
+
79
+ def _read_file(path):
80
+ try:
81
+ with open(path) as handle:
82
+ return handle.read().strip()
83
+ except OSError:
84
+ return None
85
+
86
+
87
+ def _mac_address():
88
+ """Primary NIC MAC as AA:BB:CC:DD:EE:FF, or None if unavailable/random."""
89
+ node = uuid.getnode()
90
+ # getnode() sets the multicast bit when it has to invent a random address.
91
+ if (node >> 40) & 0x1:
92
+ return None
93
+ return ":".join(f"{(node >> shift) & 0xFF:02X}" for shift in range(40, -1, -8))
94
+
95
+
96
+ # --- per-platform collection -------------------------------------------------
97
+ _PS_SCRIPT = (
98
+ "$ErrorActionPreference='SilentlyContinue';"
99
+ "[pscustomobject]@{"
100
+ "machine_guid=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography' -Name MachineGuid).MachineGuid;"
101
+ "system_uuid=(Get-CimInstance Win32_ComputerSystemProduct).UUID;"
102
+ "board_serial=(Get-CimInstance Win32_BaseBoard).SerialNumber;"
103
+ "bios_serial=(Get-CimInstance Win32_BIOS).SerialNumber;"
104
+ "cpu_id=(Get-CimInstance Win32_Processor | Select-Object -First 1).ProcessorId;"
105
+ "disk_serial=(Get-CimInstance Win32_DiskDrive | Select-Object -First 1).SerialNumber"
106
+ "} | ConvertTo-Json -Compress"
107
+ )
108
+
109
+
110
+ def _collect_windows(ids):
111
+ out = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", _PS_SCRIPT])
112
+ if out:
113
+ try:
114
+ parsed = jsonlib.loads(out)
115
+ except (ValueError, TypeError):
116
+ parsed = {}
117
+ for key in ("machine_guid", "system_uuid", "board_serial", "bios_serial", "cpu_id", "disk_serial"):
118
+ ids[key] = _clean(parsed.get(key))
119
+
120
+
121
+ def _collect_darwin(ids):
122
+ out = _run(["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"])
123
+ if out:
124
+ uuid_match = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', out)
125
+ serial_match = re.search(r'"IOPlatformSerialNumber"\s*=\s*"([^"]+)"', out)
126
+ ids["system_uuid"] = _clean(uuid_match.group(1) if uuid_match else None)
127
+ ids["board_serial"] = _clean(serial_match.group(1) if serial_match else None)
128
+
129
+
130
+ def _collect_linux(ids):
131
+ ids["machine_guid"] = _clean(_read_file("/etc/machine-id") or _read_file("/var/lib/dbus/machine-id"))
132
+ # DMI fields under /sys; product_uuid and board_serial usually need root.
133
+ ids["system_uuid"] = _clean(_read_file("/sys/class/dmi/id/product_uuid"))
134
+ ids["board_serial"] = _clean(_read_file("/sys/class/dmi/id/board_serial"))
135
+ ids["bios_serial"] = _clean(_read_file("/sys/class/dmi/id/product_serial"))
136
+
137
+
138
+ def collect():
139
+ system = platform.system()
140
+ ids = {key: None for key, _, _ in ID_SPECS}
141
+ if system == "Windows":
142
+ _collect_windows(ids)
143
+ elif system == "Darwin":
144
+ _collect_darwin(ids)
145
+ elif system == "Linux":
146
+ _collect_linux(ids)
147
+ ids["mac"] = _mac_address()
148
+ return system, ids
149
+
150
+
151
+ # --- composite HWID ----------------------------------------------------------
152
+ def compute_hwid(ids, salt=None):
153
+ used = [key for key, _, in_hash in ID_SPECS if in_hash and ids.get(key)]
154
+ if not used:
155
+ return None, None, None, used
156
+ composite = "|".join(ids[key] for key in used)
157
+ raw = f"{salt}|{composite}" if salt else composite
158
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest().upper()
159
+ return digest, digest[:16], composite, used
160
+
161
+
162
+ # --- rendering ---------------------------------------------------------------
163
+ def render(system, ids, hwid, short, composite, used, show_raw):
164
+ table = Table(title="Hardware identifiers", title_style="accent", show_header=True, header_style="accent")
165
+ table.add_column("Identifier", style="info", no_wrap=True)
166
+ table.add_column("Value", style="default", overflow="fold")
167
+ table.add_column("In HWID", justify="center", no_wrap=True)
168
+
169
+ for key, label, in_hash in ID_SPECS:
170
+ value = ids.get(key)
171
+ value_cell = value if value else "[dim]unavailable[/dim]"
172
+ if in_hash:
173
+ mark = "[success]yes[/success]" if value else "[dim]-[/dim]"
174
+ else:
175
+ mark = "[dim]no[/dim]"
176
+ table.add_row(label, value_cell, mark)
177
+ console.print(table)
178
+ console.print(f"[dim]platform: {system or 'unknown'}[/dim]")
179
+
180
+ step("HWID")
181
+ if not hwid:
182
+ error("No stable hardware identifier could be read on this machine.")
183
+ info("On Linux, system UUID and board serial usually need root (try with sudo).")
184
+ return
185
+
186
+ if show_raw:
187
+ console.print(f"[dim]composite:[/dim] {composite}")
188
+ console.print(
189
+ Panel(
190
+ f"[bold accent]{hwid}[/bold accent]\n[dim]short:[/dim] [accent]{short}[/accent]",
191
+ title="HWID (SHA-256)",
192
+ border_style="accent",
193
+ expand=False,
194
+ )
195
+ )
196
+ console.print(f"[dim]derived from: {', '.join(used)}[/dim]")
197
+
198
+
199
+ @click.command("hwid", epilog=EPILOG)
200
+ @click.option("--salt", "salt", default=None, help="Namespace the HWID with an app/license string before hashing.")
201
+ @click.option("--raw", "show_raw", is_flag=True, help="Also print the pre-hash composite string.")
202
+ @click.option("--json", "as_json", is_flag=True, help="Print result as JSON.")
203
+ def hwid(salt, show_raw, as_json):
204
+ """Show this machine's hardware ID (HWID).
205
+
206
+ Reads the stable hardware identifiers available on the current platform -
207
+ **machine GUID**, **system UUID**, **baseboard / BIOS serial** and **CPU
208
+ ID** - then folds them into a single **SHA-256 composite HWID** that stays
209
+ constant across reboots and OS reinstalls on the same physical machine.
210
+
211
+ Disk serial and MAC address are shown for reference but kept out of the hash
212
+ (disks get replaced, machines have several MACs). Use `--salt` to derive a
213
+ per-application HWID, and `--json` for machine-readable output.
214
+ """
215
+ try:
216
+ system, ids = collect()
217
+ hwid_value, short, composite, used = compute_hwid(ids, salt)
218
+
219
+ if as_json:
220
+ payload = {
221
+ "platform": system,
222
+ "identifiers": ids,
223
+ "salt": salt,
224
+ "components": used,
225
+ "hwid": hwid_value,
226
+ "hwid_short": short,
227
+ }
228
+ console.print_json(jsonlib.dumps(payload, ensure_ascii=False))
229
+ return
230
+
231
+ step("evo hwid")
232
+ render(system, ids, hwid_value, short, composite, used, show_raw)
233
+ except click.ClickException:
234
+ raise
235
+ except Exception as exc:
236
+ error(str(exc))
237
+ sys.exit(1)
@@ -0,0 +1,197 @@
1
+ import os
2
+ import platform
3
+ import sys
4
+ import uuid
5
+
6
+ import rich_click as click
7
+ from rich.text import Text
8
+
9
+ from evo_cli.console import console, error, info, step, success, warning
10
+
11
+ # Only the OS-generated machine identifier is rotated here. Firmware / hardware
12
+ # identifiers (system UUID, baseboard / BIOS serial, CPU ID) live in SMBIOS and
13
+ # are read-only from software, so they are left untouched - see the docstring.
14
+ LINUX_MACHINE_ID = "/etc/machine-id"
15
+ LINUX_DBUS_MACHINE_ID = "/var/lib/dbus/machine-id"
16
+ WIN_CRYPTO_KEY = r"SOFTWARE\Microsoft\Cryptography"
17
+
18
+ EPILOG = Text.from_markup(
19
+ "[bold]Examples[/bold]\n\n"
20
+ " [cyan]evo hwid-reset[/cyan] generate a new machine identifier (asks first)\n"
21
+ " [cyan]evo hwid-reset --dry-run[/cyan] preview the change without writing anything\n"
22
+ " [cyan]evo hwid-reset -y[/cyan] apply without the confirmation prompt\n"
23
+ " [cyan]evo hwid-reset --set <guid>[/cyan] restore a specific value you saved earlier\n\n"
24
+ "[dim]Only the OS-level identifier is rotated: the Windows Machine GUID\n"
25
+ "(HKLM\\SOFTWARE\\Microsoft\\Cryptography) or the Linux machine-id. Firmware\n"
26
+ "identifiers (system UUID, board / BIOS serial, CPU ID) are read-only and stay\n"
27
+ "the same, so 'evo hwid' will change only in its machine_guid component.\n"
28
+ "Needs Administrator (Windows) or root (Linux). Save the old value to restore it.[/dim]"
29
+ )
30
+
31
+
32
+ def _normalize_guid(value):
33
+ """Validate/normalise a Windows Machine GUID to lowercase, no braces."""
34
+ try:
35
+ return str(uuid.UUID(value.strip().strip("{}")))
36
+ except (ValueError, AttributeError):
37
+ raise click.BadParameter(f"not a valid GUID: {value}")
38
+
39
+
40
+ def _normalize_machine_id(value):
41
+ """Validate/normalise a Linux machine-id to 32 lowercase hex chars."""
42
+ cleaned = value.strip().lower().replace("-", "")
43
+ try:
44
+ return uuid.UUID(cleaned).hex
45
+ except ValueError:
46
+ raise click.BadParameter(f"not a valid machine-id: {value}")
47
+
48
+
49
+ # --- Windows -----------------------------------------------------------------
50
+ def _win_is_admin():
51
+ import ctypes
52
+
53
+ try:
54
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
55
+ except (AttributeError, OSError):
56
+ return False
57
+
58
+
59
+ def _win_read_guid():
60
+ import winreg
61
+
62
+ try:
63
+ key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, WIN_CRYPTO_KEY, 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY)
64
+ try:
65
+ value, _ = winreg.QueryValueEx(key, "MachineGuid")
66
+ return value
67
+ finally:
68
+ winreg.CloseKey(key)
69
+ except OSError:
70
+ return None
71
+
72
+
73
+ def _win_write_guid(value):
74
+ import winreg
75
+
76
+ key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, WIN_CRYPTO_KEY, 0, winreg.KEY_SET_VALUE | winreg.KEY_WOW64_64KEY)
77
+ try:
78
+ winreg.SetValueEx(key, "MachineGuid", 0, winreg.REG_SZ, value)
79
+ finally:
80
+ winreg.CloseKey(key)
81
+
82
+
83
+ def reset_windows(set_value, dry_run, assume_yes):
84
+ old = _win_read_guid()
85
+ new = _normalize_guid(set_value) if set_value else str(uuid.uuid4())
86
+
87
+ info(r"Machine GUID - HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid")
88
+ console.print(f" [dim]old:[/dim] {old or '[dim]unavailable[/dim]'}")
89
+ console.print(f" [accent]new:[/accent] {new}")
90
+
91
+ if dry_run:
92
+ info("dry-run: nothing was changed.")
93
+ return
94
+ if not _win_is_admin():
95
+ error("Administrator privileges are required. Re-run evo from an elevated terminal.")
96
+ sys.exit(1)
97
+ if not assume_yes and not click.confirm("Write the new Machine GUID now?", default=False):
98
+ info("aborted - nothing changed.")
99
+ return
100
+
101
+ try:
102
+ _win_write_guid(new)
103
+ except PermissionError:
104
+ error("Access denied writing the registry. Run from an elevated (Administrator) terminal.")
105
+ sys.exit(1)
106
+ success("Machine GUID updated.")
107
+ if old:
108
+ warning(f"To restore the previous value: evo hwid-reset --set {old}")
109
+ info("Run 'evo hwid' to see the new composite HWID.")
110
+
111
+
112
+ # --- Linux -------------------------------------------------------------------
113
+ def _read(path):
114
+ try:
115
+ with open(path) as handle:
116
+ return handle.read().strip()
117
+ except OSError:
118
+ return None
119
+
120
+
121
+ def reset_linux(set_value, dry_run, assume_yes):
122
+ old = _read(LINUX_MACHINE_ID)
123
+ new = _normalize_machine_id(set_value) if set_value else uuid.uuid4().hex
124
+
125
+ info(f"machine-id - {LINUX_MACHINE_ID} (and {LINUX_DBUS_MACHINE_ID})")
126
+ console.print(f" [dim]old:[/dim] {old or '[dim]unavailable[/dim]'}")
127
+ console.print(f" [accent]new:[/accent] {new}")
128
+
129
+ if dry_run:
130
+ info("dry-run: nothing was changed.")
131
+ return
132
+ if os.geteuid() != 0:
133
+ error("Root privileges are required. Re-run with sudo.")
134
+ sys.exit(1)
135
+ if not assume_yes and not click.confirm("Write the new machine-id now?", default=False):
136
+ info("aborted - nothing changed.")
137
+ return
138
+
139
+ try:
140
+ for path in (LINUX_MACHINE_ID, LINUX_DBUS_MACHINE_ID):
141
+ if path == LINUX_DBUS_MACHINE_ID and not os.path.exists(os.path.dirname(path)):
142
+ continue
143
+ with open(path, "w") as handle:
144
+ handle.write(new + "\n")
145
+ except OSError as exc:
146
+ error(f"Failed to write machine-id: {exc}")
147
+ sys.exit(1)
148
+ success("machine-id updated.")
149
+ if old:
150
+ warning(f"To restore the previous value: evo hwid-reset --set {old}")
151
+ info("A reboot is recommended so all services pick up the new machine-id.")
152
+ info("Run 'evo hwid' to see the new composite HWID.")
153
+
154
+
155
+ # --- macOS -------------------------------------------------------------------
156
+ def reset_darwin():
157
+ warning("macOS has no software-writable machine identifier to reset.")
158
+ info("The system UUID and hardware serial are stored in firmware and are read-only.")
159
+
160
+
161
+ @click.command("hwid-reset", epilog=EPILOG)
162
+ @click.option("--set", "set_value", default=None, help="Set an explicit value instead of generating one.")
163
+ @click.option("--dry-run", "dry_run", is_flag=True, help="Show what would change without modifying anything.")
164
+ @click.option("-y", "--yes", "assume_yes", is_flag=True, help="Skip the confirmation prompt.")
165
+ def hwid_reset(set_value, dry_run, assume_yes):
166
+ """Reset this machine's OS-level hardware identifier.
167
+
168
+ Rotates the only identifier that software can legitimately regenerate: the
169
+ **Windows Machine GUID** (`HKLM\\SOFTWARE\\Microsoft\\Cryptography`) or the
170
+ **Linux machine-id** (`/etc/machine-id`). This is the same operation Windows
171
+ `sysprep` and `systemd-machine-id-setup` perform when a machine is cloned
172
+ from an image.
173
+
174
+ Firmware and hardware identifiers used by `evo hwid` - **system UUID**,
175
+ **baseboard / BIOS serial** and **CPU ID** - live in SMBIOS and are read-only
176
+ from software, so they are left unchanged; only the `machine_guid` component
177
+ of the composite HWID changes. The old value is printed so you can restore it
178
+ with `--set`. Requires Administrator (Windows) or root (Linux); use
179
+ `--dry-run` to preview.
180
+ """
181
+ step("evo hwid-reset")
182
+ system = platform.system()
183
+ try:
184
+ if system == "Windows":
185
+ reset_windows(set_value, dry_run, assume_yes)
186
+ elif system == "Linux":
187
+ reset_linux(set_value, dry_run, assume_yes)
188
+ elif system == "Darwin":
189
+ reset_darwin()
190
+ else:
191
+ error(f"Unsupported platform: {system or 'unknown'}")
192
+ sys.exit(1)
193
+ except click.ClickException:
194
+ raise
195
+ except Exception as exc:
196
+ error(str(exc))
197
+ sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -23,6 +23,8 @@ evo_cli/commands/cloudflare.py
23
23
  evo_cli/commands/fix_claude.py
24
24
  evo_cli/commands/gdrive.py
25
25
  evo_cli/commands/gh.py
26
+ evo_cli/commands/hwid.py
27
+ evo_cli/commands/hwid_reset.py
26
28
  evo_cli/commands/localproxy.py
27
29
  evo_cli/commands/mcp.py
28
30
  evo_cli/commands/miniconda.py
@@ -1 +0,0 @@
1
- 0.11.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes