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 +12 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/trace_engine/__init__.py +1 -1
- package/src/trace_engine/cli.py +60 -1
- package/src/trace_engine/interactive.py +44 -6
- package/src/trace_engine/output/terminal.py +3 -1
- package/src/trace_engine/updater.py +305 -0
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.
|
|
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
package/src/trace_engine/cli.py
CHANGED
|
@@ -68,13 +68,52 @@ console = Console()
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
@app.callback()
|
|
71
|
-
def main(
|
|
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}]
|
|
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
|
|
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}]
|
|
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}]
|
|
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}]
|
|
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=
|
|
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]
|
|
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
|