-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaliencore.py
More file actions
616 lines (540 loc) · 28.2 KB
/
aliencore.py
File metadata and controls
616 lines (540 loc) · 28.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
"""
AlienCore - aliencore.py
Main entry point.
Usage:
python aliencore.py --firstrun Show first-run settings GUI then start
python aliencore.py --settings Open settings GUI only (service stays running)
python aliencore.py --dryrun Show what tweaks WOULD be applied, touch nothing
python aliencore.py --install Install as Windows service (run as Admin)
python aliencore.py --uninstall Remove Windows service (run as Admin)
python aliencore.py --restore Restore all system defaults and exit
python aliencore.py Run normally (foreground, for testing)
"""
# copykitten
import sys
import os
import argparse
import logging
import threading
import ctypes
# ── Bootstrap paths so all imports work regardless of CWD ────────────────────
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
from core import logger as log_setup
from core import config_manager as cfg
from core import hardware, sensors, tweaks, profiles, monitor
from core import elevation
from core.constants import APP_NAME, VERSION, LOG_PATH
# ─────────────────────────────────────────────────────────────────────────────
# Single-instance mutex
# ─────────────────────────────────────────────────────────────────────────────
_MUTEX_NAME = "AlienCore_SingleInstance_v1"
_mutex_handle = None # kept alive for the lifetime of the process
def _acquire_single_instance_lock() -> bool:
"""
Acquire a named Windows mutex. Returns True if this is the only running
instance; False if another process already holds the mutex.
The handle is stored in _mutex_handle so it stays open (and the mutex
stays owned) until the process exits, at which point Windows releases it
automatically.
"""
global _mutex_handle
ERROR_ALREADY_EXISTS = 183
handle = ctypes.windll.kernel32.CreateMutexW(None, True, _MUTEX_NAME)
err = ctypes.windll.kernel32.GetLastError()
if err == ERROR_ALREADY_EXISTS:
ctypes.windll.kernel32.CloseHandle(handle)
return False
_mutex_handle = handle # keep open — released on process exit
return True
# ─────────────────────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────────────────────
def main():
args = _parse_args()
# Always load config first
cfg.load()
c = cfg.get()
# Set up logging
log_setup.setup(
log_enabled=c["service"]["log_enabled"],
)
root_logger = logging.getLogger("aliencore")
root_logger.info("=" * 60)
root_logger.info("AlienCore v%s starting (pid=%d, admin=%s)",
VERSION, os.getpid(), elevation.is_admin())
root_logger.info("=" * 60)
# ── Auto-elevate for main/firstrun runs ──────────────────────────────────
# LHM (CPU temps via WinRing0 MSR), SMBus DIMM temps, and AWCC WMI all
# require administrator rights. Without them the sensor bar shows '---'.
# Only main + firstrun need elevation; --settings is a subprocess that
# inherits the parent's token, --dryrun/--restore have their own
# privilege semantics.
if _should_auto_elevate(args) and not elevation.is_admin():
root_logger.warning("Not running as admin — requesting UAC elevation "
"(pass --no-elevate to skip).")
if elevation.relaunch_as_admin():
sys.exit(0) # elevated copy takes over; this instance exits
root_logger.warning("UAC declined or failed — continuing without admin. "
"CPU temp, DIMM, NVMe sensors may show '---'.")
# ── Settings only ─────────────────────────────────────────────────────────
if args.settings:
cfg.load()
from core import auth as _auth
_auth.load_session()
# YubiKey dev-unlock is in-memory only and lives in whichever process
# detected it. The parent app process re-detects in _show_paywall();
# this Settings subprocess must do the same independently or every
# gated panel renders locked even when a dev YubiKey is plugged in.
# Run sync so the panels render with the correct license bits the
# first time — deferring to a thread would render gates locked then
# unlock them mid-session, which is glitchy and the user can't tell
# whether their key worked. The 300 ms – 5 s PowerShell cost is
# invisible during prewarm (subprocess waits hidden on a named event
# before being shown — see session 40) and only user-visible on the
# rare cold-spawn fallback when prewarm crashed.
if not _auth.is_pro():
try:
_auth.try_dev_unlock()
except Exception:
pass
# Load cached hardware profile so boost_tracker has correct thresholds
hw = hardware.build_profile(force_refresh=False)
from core import boost_tracker as _bt
_bt.configure(max_freq_mhz=hw.get("cpu", {}).get("max_freq_mhz", 0))
# Start sensor thread so live panels (GPU boost, DIMM temps) have real data
sensors.start()
from gui.settings_gui import open_settings
open_settings(is_first_run=False, prewarm=args.prewarm)
sensors.stop()
from core import lhm_manager
lhm_manager.stop()
return
# ── Login dialog only (subprocess) ────────────────────────────────────────
# Launched from Settings → Account → Sign In. Running a second tk.Tk()
# root inside the settings process breaks keyboard event routing, so we
# subprocess-isolate the login dialog the same way we do AI chat.
if args.login:
cfg.load()
from core import auth as _auth
_auth.load_session()
if _auth.is_logged_in():
return # already signed in; nothing to do
from gui.login_dialog import show as show_login
show_login()
return
# ── Paywall dialog only (subprocess) ──────────────────────────────────────
# Hard-lock shown when the trial has expired and no base license is owned.
# Subprocessed for the same Tk-isolation reason as login: the paywall's
# tk.Tk() must not share a process with the bar/tray Tk roots that may
# follow if the user purchases successfully.
if args.paywall:
cfg.load()
from core import auth as _auth
_auth.load_session()
# If the gate condition no longer holds (license arrived between the
# parent's check and now, or the user pulled their YubiKey), exit
# immediately so the parent re-evaluates and continues.
if not _auth.needs_paywall():
return
from gui.paywall_dialog import show as show_paywall
show_paywall()
return
# ── AI chat only (subprocess) ────────────────────────────────────────────
# Running the chat as its own process keeps it out of the main Tk
# interpreter. When the chat lived on a background thread with a second
# tk.Tk() root, the sensor bar shrank and misbehaved any time the chat
# window was open — two Tk roots in one process fight over geometry and
# z-order. Subprocess isolation makes the issue structurally impossible.
if args.ai_chat:
cfg.load()
from core import auth as _auth
_auth.load_session()
# AI chat is a Pro feature — fire dev-unlock whenever the user lacks
# Pro, not only when they're not logged in. Without this, a dev with
# a real signed-in (trial-expired or Base-only) account opens the
# chat and sees the Pro-locked screen even with the YubiKey plugged
# in. See settings handler above for the same fix on the gated
# Settings panels.
if not _auth.is_pro():
try:
_auth.try_dev_unlock()
except Exception:
pass
# The chat needs live sensor readings to build its system-context
# snapshot; start sensors but skip the heavier monitor / tweaks stack.
sensors.start()
try:
from gui.ai_chat import open_chat
open_chat()
finally:
sensors.stop()
from core import lhm_manager
lhm_manager.stop()
return
# ── Restore defaults ──────────────────────────────────────────────────────
if args.restore:
tweaks.restore_defaults(dry_run=False)
print("System defaults restored.")
return
# ── Dry run ───────────────────────────────────────────────────────────────
if args.dryrun:
hw = hardware.build_profile(force_refresh=True)
print("\n[DRY RUN] The following tweaks WOULD be applied:\n")
tweaks.apply_baseline(hw, dry_run=True)
tweaks.apply_profile("idle", hw, dry_run=True)
print("\n[DRY RUN] Done. Nothing was changed.")
return
# ── Single-instance guard ─────────────────────────────────────────────────
if not _acquire_single_instance_lock():
root_logger.warning("AlienCore is already running — exiting duplicate instance.")
sys.exit(0)
# ── Normal startup ────────────────────────────────────────────────────────
_run(firstrun=args.firstrun or cfg.is_first_run())
def _run(firstrun: bool = False):
"""Full AlienCore startup — hardware scan, tweaks, sensors, monitor, tray."""
# 0. Auth — load cached session, YubiKey dev-unlock, or show login
from core import auth as _auth
_auth.load_session()
if not _auth.is_logged_in():
if not _auth.try_dev_unlock():
_show_login()
if not _auth.is_logged_in():
# User closed the window without signing in — exit gracefully
logging.getLogger("aliencore").info("No session — exiting.")
return
# 0a. Trial-expiry hard lock. The login flow above might have just
# returned a session whose trial is already past 30 days (returning user
# who never bought); in that case we run a synchronous license refresh so
# we have the latest server-side license bits before deciding whether to
# paywall — otherwise a user who DID purchase but launches with stale
# cached state would see the paywall briefly before the background
# refresh corrects it.
if _auth.needs_paywall():
try:
_auth.refresh_license()
except Exception as e:
logging.getLogger("aliencore").debug(
"Pre-paywall refresh failed: %s", e)
# Loop the paywall: each pass shows the dialog, then re-checks. The user
# can sign out from inside the paywall, which routes us back through the
# login dialog → trial check → potentially paywall again. Capped at a
# small number of iterations to defend against pathological auth state.
for _ in range(3):
if not _auth.needs_paywall():
break
_show_paywall()
if _auth.is_licensed():
break # purchased (or YubiKey dev unlock) — continue startup
if not _auth.is_logged_in():
# Sign Out from inside the paywall — fall back to login flow
_show_login()
if not _auth.is_logged_in():
logging.getLogger("aliencore").info(
"No session after paywall sign-out — exiting.")
return
continue
# Still paywalled (user clicked Quit, or closed the window) — exit
logging.getLogger("aliencore").info(
"Trial expired and no license purchased — exiting.")
return
# Refresh license from server in the background (non-blocking)
_auth.refresh_session_async()
# 0b. Prewarm the lhm_bridge .NET subprocess on a background thread so its
# 2-3 s cold-start (CLR init + computer.Open) overlaps with hardware
# fingerprint and tweak application below. By the time the SensorThread
# makes its first poll, the bridge is already responding instantly and the
# sensor bar paints with real values instead of "---".
from core import lhm_manager as _lhm
_lhm.prewarm()
# 1. First-run GUI (blocks until user saves or closes)
if firstrun:
_show_first_run_gui()
# 1b. "What's new in v{VERSION}" — shown once after every upgrade.
# Fresh installs pass is_first_run=True so the welcome flow handles
# introductions and this dialog just records VERSION as seen.
try:
from gui.whats_new_dialog import show_if_updated as _show_whats_new
_show_whats_new(is_first_run=firstrun)
except Exception as e:
logging.getLogger("aliencore").debug("What's New dialog skipped: %s", e)
# 2. Sync startup registry entry with config
c = cfg.get()
from core import startup as _startup
_startup.sync(c["service"].get("start_with_windows", True))
# 3. Hardware fingerprint
# On a first run, the welcome dialog just refreshed the cache — skip the
# redundant rescan here.
force_refresh = (
False if firstrun
else c["service"].get("hardware_refresh_on_startup", True)
)
hw = hardware.build_profile(force_refresh=force_refresh)
# 3b. Configure boost tracker with detected CPU max frequency
from core import boost_tracker as _bt
_bt.configure(max_freq_mhz=hw.get("cpu", {}).get("max_freq_mhz", 0))
# 3c. Start sensor polling now (ahead of tweaks) so the SensorThread's
# first poll runs in parallel with baseline / profile tweak application.
# Combined with the lhm_bridge prewarm at step 0b, this means readings are
# populated before the bar window even paints.
sensors.start()
# 4. Apply baseline tweaks
tweaks.apply_baseline(hw, dry_run=False)
# 5. Apply initial profile tweaks (start at idle)
tweaks.apply_profile("idle", hw, dry_run=False)
# 6. Start AI watchdog (no-op if no API key configured)
from core import ai_manager
if c.get("ai", {}).get("api_key", "").strip():
ai_manager.start_watchdog()
# 7. Start learning engine
from core import learning
learning.start()
# 8. Start monitor loop
monitor.start(hw)
# 9. Start sensor bar (always-on-top floating display)
import threading as _threading
_threading.Thread(target=_start_bar, daemon=True, name="SensorBar").start()
# 9b. Start background update checker (non-blocking, 30s delayed first check)
from core import updater as _updater
_updater.start_background_check()
# 10. Start tray icon (blocks until Exit is chosen)
_start_tray(hw)
# ── Shutdown ──
logging.getLogger("aliencore").info("AlienCore shutting down.")
sensors.stop()
monitor.stop()
from core import ai_manager as _ai
_ai.stop_watchdog()
from core import lhm_manager
lhm_manager.stop()
# ─────────────────────────────────────────────────────────────────────────────
# First-run GUI
# ─────────────────────────────────────────────────────────────────────────────
def _show_login():
"""Spawn the login dialog as a separate subprocess.
Running the dialog in-process creates a tk.Tk() root in the main
AlienCore process and destroys it after sign-in. Once that happens,
the subsequent tk.Tk() roots that gui/bar.py (daemon thread) and
gui/tray.py (main thread) build for the sensor bar and tray menu
have undefined behaviour — on Windows the process dies inside the
Tcl/Tk C runtime with no Python traceback. Subprocess isolation
keeps the dialog's Tk root in its own process so the main one
never has to recreate Tk after a destroy. Same pattern as
settings_gui._open_login() and tray._open_ai_chat().
"""
import subprocess
if getattr(sys, "frozen", False):
argv = [sys.executable, "--login"]
cwd = os.path.dirname(os.path.abspath(sys.executable))
else:
argv = [sys.executable, os.path.join(BASE_DIR, "aliencore.py"), "--login"]
cwd = BASE_DIR
try:
subprocess.run(argv, cwd=cwd, creationflags=subprocess.CREATE_NO_WINDOW)
except Exception as e:
logging.getLogger("aliencore").warning("Login subprocess error: %s", e)
from core import auth as _auth
_auth.load_session()
# YubiKey dev-unlock is in-memory only and lives in whichever process
# detected it. If the user plugged the key in during the login dialog,
# the subprocess saw it but this process didn't — re-detect here.
if not _auth.is_logged_in():
_auth.try_dev_unlock()
def _show_paywall():
"""Spawn the trial-expired paywall as a separate subprocess.
Same Tk-isolation rationale as _show_login: a tk.Tk() created and
destroyed in the main process before later spawning gui/bar.py and
gui/tray.py crashes the Tcl C runtime on Windows. Subprocessing keeps
the paywall's Tk root in its own process so the parent's first Tk
root is the one the bar/tray build."""
import subprocess
if getattr(sys, "frozen", False):
argv = [sys.executable, "--paywall"]
cwd = os.path.dirname(os.path.abspath(sys.executable))
else:
argv = [sys.executable, os.path.join(BASE_DIR, "aliencore.py"),
"--paywall"]
cwd = BASE_DIR
try:
subprocess.run(argv, cwd=cwd, creationflags=subprocess.CREATE_NO_WINDOW)
except Exception as e:
logging.getLogger("aliencore").warning(
"Paywall subprocess error: %s", e)
# Reload session — purchase and sign-out write through to disk via the
# subprocess; the parent must re-read to see them. YubiKey dev-unlock is
# in-memory only (not persisted), so re-detect here unconditionally so
# the parent's _session ends up holding the dev override too.
from core import auth as _auth
_auth.load_session()
_auth.try_dev_unlock()
def _show_first_run_gui():
from gui.first_run_dialog import show as show_welcome
show_welcome()
cfg.reload()
# ─────────────────────────────────────────────────────────────────────────────
# Tray
# ─────────────────────────────────────────────────────────────────────────────
def _start_bar():
"""Start the always-on-top sensor bar."""
from gui.bar import start as bar_start
bar_start()
def _start_tray(hw: dict):
from gui import tray
import subprocess, threading
# Pre-warmed settings subprocess: built hidden, waits on a named event for
# the tray to signal "show". Eliminates per-click cold-start cost (process
# spawn + interpreter init + AlienCore imports + Tk window construction).
# Auto-reset event so a single SetEvent releases exactly one waiter (the
# prewarmed window). Created here up-front so the subprocess can OpenEventW
# by name immediately.
# restype=c_void_p so the 64-bit HANDLE isn't truncated to 32 bits.
_kernel32 = ctypes.windll.kernel32
_kernel32.CreateEventW.restype = ctypes.c_void_p
_kernel32.SetEvent.argtypes = [ctypes.c_void_p]
_show_event = _kernel32.CreateEventW(
None, False, False, "AlienCore_Settings_Show_v1")
_settings_proc = None # currently-prewarmed subprocess
_shown_proc = None # subprocess currently showing the window
_spawn_lock = threading.Lock()
# Frozen builds re-launch via sys.executable (= AlienCore.exe) directly —
# there's no aliencore.py on disk next to the bundle. Source builds
# invoke `python aliencore.py` as before.
if getattr(sys, "frozen", False):
_settings_argv = [sys.executable]
_settings_cwd = os.path.dirname(os.path.abspath(sys.executable))
else:
_settings_argv = [sys.executable, os.path.join(BASE_DIR, "aliencore.py")]
_settings_cwd = BASE_DIR
def _spawn_prewarm():
"""Spawn a hidden settings subprocess that waits for show signal."""
nonlocal _settings_proc
with _spawn_lock:
if _settings_proc is not None and _settings_proc.poll() is None:
return # already have one
try:
_settings_proc = subprocess.Popen(
_settings_argv + ["--settings", "--prewarm"],
cwd=_settings_cwd,
creationflags=subprocess.CREATE_NO_WINDOW,
)
except Exception as e:
logging.getLogger("aliencore").warning(
"Settings prewarm spawn failed: %s", e)
_settings_proc = None
def _watch_for_close_and_respawn(proc):
"""Wait for the shown subprocess to exit, then prewarm a fresh one."""
def _wait():
try:
proc.wait()
except Exception:
pass
_spawn_prewarm()
threading.Thread(target=_wait, daemon=True,
name="SettingsRespawnWatcher").start()
def on_settings_open():
"""Signal the prewarmed subprocess to show. Falls back to a cold spawn
if no prewarm exists (e.g. it crashed) — in that case the user pays the
usual cold cost once, and we prewarm a replacement after they close."""
nonlocal _settings_proc, _shown_proc
# If a previous click is still showing settings, don't open a second
if _shown_proc is not None and _shown_proc.poll() is None:
return
if _settings_proc is not None and _settings_proc.poll() is None:
# Hand the prewarm off to "shown" and fire the show event
_shown_proc = _settings_proc
_settings_proc = None
_kernel32.SetEvent(_show_event)
else:
# Prewarm missing — cold spawn (no --prewarm; opens immediately)
try:
_shown_proc = subprocess.Popen(
_settings_argv + ["--settings"],
cwd=_settings_cwd,
creationflags=subprocess.CREATE_NO_WINDOW,
)
except Exception as e:
logging.getLogger("aliencore").warning(
"Settings cold spawn failed: %s", e)
return
_watch_for_close_and_respawn(_shown_proc)
# Spawn the first prewarm shortly after tray init so we don't compete with
# the rest of startup for CPU. The user is unlikely to click Settings in
# the first few seconds anyway.
threading.Timer(2.0, _spawn_prewarm).start()
def on_quit():
nonlocal _settings_proc, _shown_proc
for p in (_settings_proc, _shown_proc):
if p is not None and p.poll() is None:
try:
p.terminate()
except Exception:
pass
# Release the named-event handle so test harnesses that import this
# module repeatedly don't accumulate kernel objects.
if _show_event:
try:
_kernel32.CloseHandle(_show_event)
except Exception:
pass
# Sensor bar lives on its own thread with its own Tk mainloop — if we
# don't tear it down explicitly its mainloop keeps the interpreter
# alive after the tray exits and the whole process hangs.
from gui import bar
bar.stop()
sensors.stop()
monitor.stop()
from core import lhm_manager
lhm_manager.stop()
tray.stop()
# Force immediate process termination. Python's normal interpreter
# shutdown waits on the Tk mainloop thread (a native Tcl_DoOneEvent
# call can't be interrupted even on a daemon thread), reaps every
# lingering Popen, and tears down COM apartments — cumulatively
# several seconds on some machines. We've already run our orderly
# cleanup above, so a hard exit here is safe and snappy.
os._exit(0)
tray.start(on_settings_open=on_settings_open, on_quit=on_quit)
def _on_settings_saved():
"""Called after user saves settings — reload config live."""
monitor.request_config_reload()
logging.getLogger("aliencore").info("Settings saved — config reloaded live.")
# ─────────────────────────────────────────────────────────────────────────────
# Argument parser
# ─────────────────────────────────────────────────────────────────────────────
def _parse_args():
p = argparse.ArgumentParser(
prog="aliencore",
description=f"AlienCore v{VERSION} — Adaptive System Optimizer"
)
p.add_argument("--firstrun", action="store_true", help="Force first-run settings GUI")
p.add_argument("--settings", action="store_true", help="Open settings GUI only")
p.add_argument("--prewarm", action="store_true",
help="Build settings window hidden; deiconify when tray signals show event")
p.add_argument("--login", action="store_true", help="Open login dialog only (subprocess)")
p.add_argument("--paywall", action="store_true", help="Open trial-expired paywall only (subprocess)")
p.add_argument("--ai-chat", action="store_true", help="Open AI chat window only (subprocess)")
p.add_argument("--dryrun", action="store_true", help="Show tweaks without applying")
p.add_argument("--restore", action="store_true", help="Restore system defaults")
# --no-elevate is source-mode-only: in frozen builds the embedded
# manifest sets requireAdministrator, so the OS enforces elevation
# at process creation and the flag is a no-op. Hiding it on frozen
# builds keeps --help honest and avoids advertising a flag that
# would only matter if a future build accidentally dropped uac_admin
# from the spec.
if not getattr(sys, "frozen", False):
p.add_argument("--no-elevate", action="store_true",
help="Skip the automatic UAC prompt at launch (source mode only)")
return p.parse_args()
def _should_auto_elevate(args) -> bool:
"""
True when the current invocation is a main/firstrun run that needs admin.
Excludes subprocess modes, diagnostic modes, and explicit opt-outs.
"""
if getattr(args, "no_elevate", False):
return False
if (args.settings or args.login or args.paywall or args.ai_chat
or args.dryrun or args.restore):
return False
return True
if __name__ == "__main__":
main()