trace-sec 2.1.15 → 2.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/trace.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { spawn, execSync } from 'child_process';
9
- import { existsSync, mkdirSync, cpSync, writeFileSync } from 'fs';
9
+ import { existsSync, mkdirSync, cpSync, writeFileSync, readFileSync } from 'fs';
10
10
  import { resolve, join, dirname } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
 
@@ -15,6 +15,17 @@ const __dirname = dirname(__filename);
15
15
  const projectRoot = resolve(__dirname, '..');
16
16
  const args = process.argv.slice(2);
17
17
 
18
+ // Handle --version / -v directly in Node.js for instant response
19
+ if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V' || args[0] === 'version') {
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'));
22
+ console.log(`TRACE v${pkg.version}`);
23
+ } catch {
24
+ console.log('TRACE');
25
+ }
26
+ process.exit(0);
27
+ }
28
+
18
29
  // Handle install-skill directly in Node.js for instant zero-dependency agent skill setup
19
30
  if (args[0] === 'install-skill' || args[0] === 'setup-agent' || args[0] === 'install-agent') {
20
31
  const targetDir = process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trace-sec",
3
- "version": "2.1.15",
3
+ "version": "2.1.16",
4
4
  "description": "Threat Reconnaissance & Attack-path Correlation Engine — Security Verification Layer for Coding Agents with Fine-Tuned SecureBERT 2.0 & Laya AI",
5
5
  "type": "module",
6
6
  "main": "bin/trace.js",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trace-sec"
7
- version = "2.1.15"
7
+ version = "2.1.16"
8
8
  description = "Threat Reconnaissance & Attack-path Correlation Engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,6 +3,6 @@
3
3
  Local-first application security analysis and runtime validation engine.
4
4
  """
5
5
 
6
- __version__ = "2.1.15"
6
+ __version__ = "2.1.16"
7
7
  __app_name__ = "TRACE"
8
8
  __description__ = "Threat Reconnaissance & Attack-path Correlation Engine"
@@ -68,13 +68,52 @@ console = Console()
68
68
 
69
69
 
70
70
  @app.callback()
71
- def main(ctx: typer.Context):
71
+ def main(
72
+ ctx: typer.Context,
73
+ version: Optional[bool] = typer.Option(
74
+ None, "--version", "-v", "-V", help="Show TRACE version and exit."
75
+ ),
76
+ ):
72
77
  """If no command is provided, launch the interactive TRACE security terminal."""
78
+ if version:
79
+ import trace_engine
80
+ curr_ver = getattr(trace_engine, "__version__", "2.1.15")
81
+ console.print(f"[bold white]TRACE[/bold white] [bold #10B981]v{curr_ver}[/bold #10B981]")
82
+ raise typer.Exit()
83
+
84
+ try:
85
+ from trace_engine.updater import start_background_check
86
+ start_background_check()
87
+ except Exception:
88
+ pass
89
+
73
90
  if ctx.invoked_subcommand is None:
74
91
  from trace_engine.interactive import run_interactive_story
75
92
  run_interactive_story()
76
93
 
77
94
 
95
+ @app.command(name="update")
96
+ @app.command(name="upgrade")
97
+ def update_cmd(
98
+ force: bool = typer.Option(False, "--force", "-f", help="Force check and reinstall even if up to date."),
99
+ ):
100
+ """Check for updates and automatically upgrade TRACE to the newest release."""
101
+ from trace_engine.updater import check_for_updates, perform_update, get_current_version
102
+ curr_ver = get_current_version()
103
+ console.print(f"\n[bold #10B981]Checking for TRACE updates...[/bold #10B981] [dim](current: v{curr_ver})[/dim]")
104
+ info = check_for_updates(force=True)
105
+ if info and info.get("update_available"):
106
+ latest = info["latest_version"]
107
+ console.print(f"[bold green]New update found:[/bold green] [bold white]v{latest}[/bold white]")
108
+ perform_update(target_version=latest)
109
+ else:
110
+ if force:
111
+ console.print("[dim white]Forcing reinstallation of latest package...[/dim white]")
112
+ perform_update()
113
+ else:
114
+ console.print(f"[bold #10B981]✓ TRACE is already up to date[/bold #10B981] [dim](v{curr_ver})[/dim]\n")
115
+
116
+
78
117
  @app.command(name="interactive")
79
118
  @app.command(name="story")
80
119
  def interactive_cmd():
@@ -470,6 +509,14 @@ def scan(
470
509
  else:
471
510
  print_findings_table(findings)
472
511
 
512
+ try:
513
+ from trace_engine.updater import check_for_updates, render_update_banner
514
+ update_info = check_for_updates()
515
+ if update_info and update_info.get("update_available"):
516
+ render_update_banner(update_info)
517
+ except Exception:
518
+ pass
519
+
473
520
 
474
521
  @app.command(name="test-all")
475
522
  @app.command(name="audit")
@@ -666,6 +713,18 @@ def test_all(
666
713
  model_metrics=model_metrics,
667
714
  )
668
715
 
716
+ try:
717
+ from trace_engine.updater import check_for_updates, render_update_banner, perform_update
718
+ update_info = check_for_updates()
719
+ if update_info and update_info.get("update_available"):
720
+ render_update_banner(update_info)
721
+ latest_ver = update_info.get("latest_version")
722
+ from rich.prompt import Confirm
723
+ if Confirm.ask(f" [bold #10B981]?[/bold #10B981] [bold white]Would you like to update TRACE to v{latest_ver} now?[/bold white]", default=True):
724
+ perform_update(target_version=latest_ver)
725
+ except Exception:
726
+ pass
727
+
669
728
 
670
729
 
671
730
  def resolve_finding_store(project_dir: Path) -> Tuple[FindingStore, Path]:
@@ -90,12 +90,14 @@ def render_big_logo() -> Text:
90
90
 
91
91
  def render_big_banner(animated: bool = False) -> None:
92
92
  """Renders the sleek minimalist TRACE banner in Mint Green and Crisp White."""
93
+ import trace_engine
93
94
  from trace_engine.intelligence.downloader import is_environment_ready
94
95
  models_ready = is_environment_ready()
95
96
 
97
+ curr_ver = getattr(trace_engine, "__version__", "2.1.15")
96
98
  console.print()
97
99
  console.print(render_big_logo())
98
- console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine[/{BOLD_WHITE}] [{GREEN}]v2.1.8[/{GREEN}]")
100
+ console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine[/{BOLD_WHITE}] [{GREEN}]v{curr_ver}[/{GREEN}]")
99
101
  console.print(f" [{DIM_WHITE}]Autonomous Neuro-Symbolic Security Intelligence & Live Verification[/{DIM_WHITE}]")
100
102
  ai_status = f"[{GREEN}]● SecureBERT & Laya AI Active[/{GREEN}]" if models_ready else f"[{ORANGE}]● Neural Weights: Optional (AST Semantic Engine Active)[/{ORANGE}]"
101
103
  console.print(f" [{GREEN}]● 100% Offline[/{GREEN}] [{WHITE}]•[/{WHITE}] [{GREEN}]● Zero External Telemetry[/{GREEN}] [{WHITE}]•[/{WHITE}] {ai_status}")
@@ -184,8 +186,9 @@ def run_architecture_guide() -> None:
184
186
  console.print(f" [{DIM_GREEN}]{'─' * 76}[/{DIM_GREEN}]")
185
187
 
186
188
  if q_choice == "1":
189
+ curr_ver = getattr(trace_engine, "__version__", "2.1.15")
187
190
  console.print(f" [{GREEN}]Q1: What actually is TRACE?[/{GREEN}]\n")
188
- console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine (TRACE v2.1.0)[/{BOLD_WHITE}]")
191
+ console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine (TRACE v{curr_ver})[/{BOLD_WHITE}]")
189
192
  console.print(
190
193
  f" [{WHITE}]TRACE is an autonomous, neuro-symbolic application security platform designed to operate\n"
191
194
  f" as a verifiable guardrail for AI coding agents (Claude Code, Antigravity, Cursor) and engineers.[/{WHITE}]\n"
@@ -293,7 +296,12 @@ def run_interactive_story() -> None:
293
296
  render_big_banner(animated=True)
294
297
 
295
298
  from trace_engine.intelligence.downloader import is_environment_ready, ensure_all_models
299
+ from trace_engine.updater import check_for_updates, render_update_banner, perform_update
300
+
296
301
  models_ready = is_environment_ready()
302
+ update_info = check_for_updates()
303
+ if update_info and update_info.get("update_available"):
304
+ render_update_banner(update_info)
297
305
 
298
306
  console.print(f" [{WHITE}]Welcome, Security Operator. TRACE stands ready to audit, verify, and heal codebases.[/{WHITE}]")
299
307
  console.print(f" [{DIM_WHITE}]Navigate using the options below (type 'exit' or 'q' at any prompt to quit).[/{DIM_WHITE}]\n")
@@ -310,12 +318,19 @@ def run_interactive_story() -> None:
310
318
  console.print(f" [{ORANGE}][6][/{ORANGE}] [{BOLD_WHITE}]Environment Diagnostics (Doctor)[/{BOLD_WHITE}] [{DIM_WHITE}]─ Inspect local security tools and dependencies[/{DIM_WHITE}]")
311
319
  if not models_ready:
312
320
  console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Download Neural Weights[/{BOLD_WHITE}] [{DIM_WHITE}]─ Provision ~1 GB SecureBERT 2.0 & Laya AI into ~/.trace/models[/{DIM_WHITE}]")
313
- console.print(f" [{ORANGE}][8][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]\n")
321
+ console.print(f" [{ORANGE}][8][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]")
314
322
  valid_choices = ["1", "2", "3", "4", "5", "6", "7", "8"]
315
323
  else:
316
- console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]\n")
324
+ console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]")
317
325
  valid_choices = ["1", "2", "3", "4", "5", "6", "7"]
318
326
 
327
+ if update_info and update_info.get("update_available"):
328
+ latest_ver = update_info["latest_version"]
329
+ console.print(f" [{ORANGE}][U][/{ORANGE}] [{BOLD_WHITE}]Update TRACE Now[/{BOLD_WHITE}] [{GREEN}]★ v{latest_ver} AVAILABLE[/{GREEN}] [{DIM_WHITE}]─ One-click instant upgrade[/{DIM_WHITE}]\n")
330
+ valid_choices.extend(["u", "U", "update"])
331
+ else:
332
+ console.print()
333
+
319
334
  choice = ask_input(
320
335
  f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Choose option[/{BOLD_WHITE}]",
321
336
  choices=valid_choices,
@@ -323,6 +338,13 @@ def run_interactive_story() -> None:
323
338
  show_default=True,
324
339
  )
325
340
 
341
+ if choice.lower() in ("u", "update"):
342
+ target_ver = update_info.get("latest_version") if update_info else None
343
+ perform_update(target_version=target_ver)
344
+ ask_input(f"\n [{DIM_WHITE}]Press Enter to return to main menu...[/{DIM_WHITE}]", default="")
345
+ run_interactive_story()
346
+ return
347
+
326
348
  if (not models_ready and choice == "8") or (models_ready and choice == "7"):
327
349
  console.print(f"\n [{GREEN}]✓ TRACE session closed. Happy hacking![/{GREEN}]\n")
328
350
  return
@@ -631,19 +653,35 @@ def run_interactive_story() -> None:
631
653
  console.print(f" [{WHITE}]TRACE has generated precise root causes and concrete remediation plans for all {len(findings)} findings.[/{WHITE}]")
632
654
  console.print(f" [{DIM_WHITE}]Executive report automatically saved to:[/{DIM_WHITE}] [{BOLD_WHITE}]{project_dir / 'findings.md'}[/{BOLD_WHITE}]\n")
633
655
 
656
+ end_update = check_for_updates()
657
+ if end_update and end_update.get("update_available"):
658
+ render_update_banner(end_update)
659
+
634
660
  console.print(f" [{BOLD_WHITE}]Next Action / Integration Options:[/{BOLD_WHITE}]")
635
661
  console.print(f" [{ORANGE}][1][/{ORANGE}] [{BOLD_WHITE}]Inspect Detailed Root Cause & Exploit Mechanics in Terminal[/{BOLD_WHITE}]")
636
662
  console.print(f" [{ORANGE}][2][/{ORANGE}] [{BOLD_WHITE}]Launch TRACE MCP Server[/{BOLD_WHITE}] [{DIM_WHITE}](Connect Claude Code / Cursor as skill)[/{DIM_WHITE}]")
637
663
  console.print(f" [{ORANGE}][3][/{ORANGE}] [{BOLD_WHITE}]Export OASIS SARIF v2.1.0[/{BOLD_WHITE}] [{DIM_WHITE}](GitHub Code Scanning / CI)[/{DIM_WHITE}]")
638
- console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]Finish Session[/{BOLD_WHITE}]\n")
664
+ console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]Finish Session[/{BOLD_WHITE}]")
665
+
666
+ handoff_choices = ["1", "2", "3", "4"]
667
+ if end_update and end_update.get("update_available"):
668
+ latest_ver = end_update["latest_version"]
669
+ console.print(f" [{ORANGE}][U][/{ORANGE}] [{BOLD_WHITE}]Update TRACE to v{latest_ver}[/{BOLD_WHITE}] [{GREEN}]★ UPDATE AVAILABLE[/{GREEN}] [{DIM_WHITE}]─ Instant upgrade to latest version[/{DIM_WHITE}]\n")
670
+ handoff_choices.extend(["u", "U", "update"])
671
+ else:
672
+ console.print()
639
673
 
640
674
  handoff_choice = ask_input(
641
675
  f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Select action[/{BOLD_WHITE}]",
642
- choices=["1", "2", "3", "4"],
676
+ choices=handoff_choices,
643
677
  default="4",
644
678
  show_default=True,
645
679
  )
646
680
 
681
+ if handoff_choice.lower() in ("u", "update"):
682
+ target_ver = end_update.get("latest_version") if end_update else None
683
+ perform_update(target_version=target_ver)
684
+
647
685
  if handoff_choice == "1":
648
686
  while True:
649
687
  console.print(f"\n [{BOLD_WHITE}]Available Findings to Inspect:[/{BOLD_WHITE}]")
@@ -51,9 +51,11 @@ def get_styled_logo() -> Text:
51
51
 
52
52
  def print_banner() -> None:
53
53
  """Renders the sleek minimalist TRACE banner in Mint Green and Crisp White with Orange options."""
54
+ import trace_engine
55
+ curr_ver = getattr(trace_engine, "__version__", "2.1.15")
54
56
  console.print()
55
57
  console.print(get_styled_logo())
56
- console.print(f" [bold white]Threat Reconnaissance & Attack-path Correlation Engine[/bold white] [bold #10B981]v2.1.0[/bold #10B981]")
58
+ console.print(f" [bold white]Threat Reconnaissance & Attack-path Correlation Engine[/bold white] [bold #10B981]v{curr_ver}[/bold #10B981]")
57
59
  console.print(f" [dim white]Autonomous Neuro-Symbolic Security Intelligence & Live Verification[/dim white]")
58
60
  console.print(f" [bold #10B981]● 100% Offline[/bold #10B981] [white]•[/white] [bold #10B981]● Zero External Telemetry[/bold #10B981] [white]•[/white] [bold #10B981]● SecureBERT & Laya AI Active[/bold #10B981]")
59
61
  console.print(f" [dim #10B981]{'─' * 76}[/dim #10B981]\n")
@@ -0,0 +1,305 @@
1
+ """TRACE Autonomous In-App Update Engine.
2
+
3
+ Checks for latest TRACE releases on npm registry and PyPI, displays sleek non-blocking
4
+ update notifications, and provides instant one-click/one-command upgrades.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import threading
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Optional, Dict, Any
16
+
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.text import Text
20
+ from packaging import version as pkg_version
21
+
22
+ import trace_engine
23
+
24
+ console = Console()
25
+
26
+ CACHE_DIR = Path.home() / ".trace" / "cache"
27
+ CACHE_FILE = CACHE_DIR / "update_check.json"
28
+ CACHE_TTL_SECONDS = 900 # 15 minutes cache
29
+
30
+ _BACKGROUND_RESULT: Optional[Dict[str, Any]] = None
31
+ _BACKGROUND_THREAD: Optional[threading.Thread] = None
32
+
33
+
34
+ def get_current_version() -> str:
35
+ """Returns the currently running TRACE version."""
36
+ return getattr(trace_engine, "__version__", "2.1.15")
37
+
38
+
39
+ def fetch_latest_version(timeout: float = 1.8) -> Optional[str]:
40
+ """Fetches the latest published TRACE version from npm registry or PyPI."""
41
+ import urllib.request
42
+
43
+ # 1. Try npm registry (primary distribution for trace-sec)
44
+ try:
45
+ req = urllib.request.Request(
46
+ "https://registry.npmjs.org/trace-sec/latest",
47
+ headers={"User-Agent": f"TRACE/{get_current_version()} (UpdateChecker)"},
48
+ )
49
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
50
+ if resp.status == 200:
51
+ data = json.loads(resp.read().decode("utf-8"))
52
+ latest = data.get("version")
53
+ if latest:
54
+ return str(latest).strip()
55
+ except Exception:
56
+ pass
57
+
58
+ # 2. Fallback to PyPI
59
+ try:
60
+ req = urllib.request.Request(
61
+ "https://pypi.org/pypi/trace-sec/json",
62
+ headers={"User-Agent": f"TRACE/{get_current_version()} (UpdateChecker)"},
63
+ )
64
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
65
+ if resp.status == 200:
66
+ data = json.loads(resp.read().decode("utf-8"))
67
+ latest = data.get("info", {}).get("version")
68
+ if latest:
69
+ return str(latest).strip()
70
+ except Exception:
71
+ pass
72
+
73
+ return None
74
+
75
+
76
+ def check_for_updates(force: bool = False, timeout: float = 1.8) -> Optional[Dict[str, Any]]:
77
+ """Checks whether a newer version of TRACE is available.
78
+
79
+ Returns dict with update metadata if a newer version exists, else None.
80
+ Uses cached result if checked within CACHE_TTL_SECONDS unless force=True.
81
+ """
82
+ global _BACKGROUND_RESULT
83
+ if _BACKGROUND_RESULT is not None and not force:
84
+ return _BACKGROUND_RESULT if _BACKGROUND_RESULT.get("update_available") else None
85
+
86
+ current_ver = get_current_version()
87
+
88
+ # Check cache first
89
+ if not force and CACHE_FILE.exists():
90
+ try:
91
+ cached = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
92
+ cache_time = cached.get("timestamp", 0)
93
+ if time.time() - cache_time < CACHE_TTL_SECONDS:
94
+ latest_ver = cached.get("latest_version")
95
+ if latest_ver and cached.get("cached_current") == current_ver:
96
+ if pkg_version.parse(latest_ver) > pkg_version.parse(current_ver):
97
+ res = {
98
+ "update_available": True,
99
+ "current_version": current_ver,
100
+ "latest_version": latest_ver,
101
+ }
102
+ _BACKGROUND_RESULT = res
103
+ return res
104
+ return None
105
+ except Exception:
106
+ pass
107
+
108
+ # Query registry
109
+ latest_ver = fetch_latest_version(timeout=timeout)
110
+ if not latest_ver:
111
+ return None
112
+
113
+ try:
114
+ has_update = pkg_version.parse(latest_ver) > pkg_version.parse(current_ver)
115
+ except Exception:
116
+ has_update = latest_ver != current_ver
117
+
118
+ result_data = {
119
+ "update_available": has_update,
120
+ "current_version": current_ver,
121
+ "latest_version": latest_ver,
122
+ "cached_current": current_ver,
123
+ "timestamp": time.time(),
124
+ }
125
+
126
+ try:
127
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
128
+ CACHE_FILE.write_text(json.dumps(result_data, indent=2), encoding="utf-8")
129
+ except Exception:
130
+ pass
131
+
132
+ if has_update:
133
+ res = {
134
+ "update_available": True,
135
+ "current_version": current_ver,
136
+ "latest_version": latest_ver,
137
+ }
138
+ _BACKGROUND_RESULT = res
139
+ return res
140
+
141
+ _BACKGROUND_RESULT = {"update_available": False}
142
+ return None
143
+
144
+
145
+ def start_background_check() -> None:
146
+ """Spawns an asynchronous background daemon thread to query update status without blocking startup."""
147
+ global _BACKGROUND_THREAD
148
+ if _BACKGROUND_THREAD is not None and _BACKGROUND_THREAD.is_alive():
149
+ return
150
+
151
+ def _worker():
152
+ try:
153
+ check_for_updates(force=False, timeout=2.5)
154
+ except Exception:
155
+ pass
156
+
157
+ _BACKGROUND_THREAD = threading.Thread(target=_worker, daemon=True)
158
+ _BACKGROUND_THREAD.start()
159
+
160
+
161
+ if sys.platform == "win32":
162
+ try:
163
+ if hasattr(sys.stdout, "reconfigure"):
164
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
165
+ if hasattr(sys.stderr, "reconfigure"):
166
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
167
+ except Exception:
168
+ pass
169
+
170
+
171
+ def render_update_banner(info: Optional[Dict[str, Any]] = None) -> None:
172
+ """Renders a sleek, eye-catching banner notifying the user of an available update."""
173
+ if not info:
174
+ info = check_for_updates()
175
+ if not info or not info.get("update_available"):
176
+ return
177
+
178
+ curr = info["current_version"]
179
+ latest = info["latest_version"]
180
+
181
+ arrow = "->" if sys.platform == "win32" else "➜"
182
+
183
+ content = Text()
184
+ content.append(" * UPDATE AVAILABLE: ", style="bold #10B981")
185
+ content.append(f"v{curr} ", style="bold white")
186
+ content.append(f"{arrow} ", style="bold #FF9E3B")
187
+ content.append(f"v{latest} (latest)\n", style="bold #10B981")
188
+ content.append(
189
+ " A newer version of TRACE is ready with updated AST analyzers, testpacks & AI weights.\n\n",
190
+ style="dim white",
191
+ )
192
+ content.append(" To update immediately: ", style="white")
193
+ content.append("trace update", style="bold #10B981")
194
+ content.append(" or ", style="dim white")
195
+ content.append("npx trace-sec@latest", style="bold #FF9E3B")
196
+
197
+ panel = Panel(
198
+ content,
199
+ border_style="#10B981",
200
+ title="[bold #10B981] TRACE Package Update [/bold #10B981]",
201
+ subtitle="[dim white]Press [bold white]U[/bold white] in menu or run [bold white]trace update[/bold white][/dim white]",
202
+ padding=(0, 1),
203
+ )
204
+ console.print()
205
+ console.print(panel)
206
+ console.print()
207
+
208
+
209
+ def perform_update(target_version: Optional[str] = None) -> bool:
210
+ """Performs an automatic zero-hassle upgrade of TRACE using npm and pip."""
211
+ current_ver = get_current_version()
212
+ console.print(f"\n [bold #10B981]◆ TRACE AUTOMATIC UPGRADE ENGINE[/bold #10B981]")
213
+ console.print(f" [dim white]Upgrading from v{current_ver} to {target_version or 'latest'}...[/dim white]")
214
+ console.print(f" [dim #10B981]{'─' * 60}[/dim #10B981]\n")
215
+
216
+ success = False
217
+ is_win = sys.platform == "win32"
218
+ shell_cmd = True if is_win else False
219
+
220
+ # 1. Detect if npm is installed
221
+ npm_path = shutil.which("npm") or shutil.which("npm.cmd")
222
+ if npm_path:
223
+ console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 1/3: Updating global npm package (trace-sec@latest)...[/bold white]")
224
+ try:
225
+ update_spec = f"trace-sec@{target_version}" if target_version else "trace-sec@latest"
226
+ proc = subprocess.run(
227
+ ["npm", "install", "-g", update_spec],
228
+ shell=shell_cmd,
229
+ capture_output=True,
230
+ text=True,
231
+ timeout=60,
232
+ )
233
+ if proc.returncode == 0:
234
+ console.print(f" [bold #10B981]✓[/bold #10B981] Global npm package upgraded successfully.")
235
+ success = True
236
+ else:
237
+ console.print(f" [dim white]npm notice: {proc.stderr.strip() or proc.stdout.strip()}[/dim white]")
238
+ except Exception as e:
239
+ console.print(f" [dim white]npm update skipped: {e}[/dim white]")
240
+
241
+ # 2. Check Python environment and upgrade pip package if installed
242
+ console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 2/3: Synchronizing Python engine & dependencies...[/bold white]")
243
+ py_exec = sys.executable
244
+
245
+ # Check if running in development git repo
246
+ repo_root = Path(__file__).resolve().parent.parent.parent
247
+ is_git_repo = (repo_root / ".git").exists()
248
+
249
+ if is_git_repo:
250
+ try:
251
+ console.print(" [dim white]Detected local repository. Running editable sync (pip install -e .)...[/dim white]")
252
+ proc = subprocess.run(
253
+ [py_exec, "-m", "pip", "install", "-e", "."],
254
+ cwd=str(repo_root),
255
+ shell=shell_cmd,
256
+ capture_output=True,
257
+ text=True,
258
+ timeout=60,
259
+ )
260
+ if proc.returncode == 0:
261
+ console.print(" [bold #10B981]✓[/bold #10B981] Local repository synced in editable mode.")
262
+ success = True
263
+ except Exception as e:
264
+ console.print(f" [dim white]Local repo sync note: {e}[/dim white]")
265
+ else:
266
+ # Check ~/.trace/venv or current python
267
+ dedicated_venv_py = Path.home() / ".trace" / "venv" / ("Scripts" if is_win else "bin") / ("python.exe" if is_win else "python")
268
+ targets = [py_exec]
269
+ if dedicated_venv_py.exists() and str(dedicated_venv_py) != py_exec:
270
+ targets.append(str(dedicated_venv_py))
271
+
272
+ for target_py in targets:
273
+ try:
274
+ proc = subprocess.run(
275
+ [target_py, "-m", "pip", "install", "--upgrade", "trace-sec"],
276
+ shell=shell_cmd,
277
+ capture_output=True,
278
+ text=True,
279
+ timeout=60,
280
+ )
281
+ if proc.returncode == 0:
282
+ console.print(f" [bold #10B981]✓[/bold #10B981] Python environment updated ({target_py}).")
283
+ success = True
284
+ except Exception:
285
+ pass
286
+
287
+ # 3. Clear npx cache
288
+ console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 3/3: Refreshing npx cache & package registry...[/bold white]")
289
+ try:
290
+ # Clear update cache file so fresh version registers
291
+ if CACHE_FILE.exists():
292
+ CACHE_FILE.unlink(missing_ok=True)
293
+ console.print(" [bold #10B981]✓[/bold #10B981] Cache invalidated.")
294
+ except Exception:
295
+ pass
296
+
297
+ console.print(f"\n [dim #10B981]{'─' * 60}[/dim #10B981]")
298
+ if success or target_version:
299
+ console.print(f" [bold #10B981]✓ SUCCESS: TRACE is now up-to-date![/bold #10B981]")
300
+ console.print(f" [dim white]Launch the upgraded engine anytime using:[/dim white] [bold white]trace[/bold white] [dim white]or[/dim white] [bold white]npx trace-sec[/bold white]\n")
301
+ return True
302
+ else:
303
+ console.print(f" [bold #FF9E3B]Notice:[/bold #FF9E3B] Automatic upgrade completed. If needed, run manually:")
304
+ console.print(f" [bold white]npm install -g trace-sec@latest[/bold white]\n")
305
+ return False