yncli 1.0.8__tar.gz → 1.1.0__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.
- {yncli-1.0.8 → yncli-1.1.0}/PKG-INFO +1 -1
- {yncli-1.0.8 → yncli-1.1.0}/pyproject.toml +1 -1
- {yncli-1.0.8 → yncli-1.1.0}/setup.py +1 -1
- {yncli-1.0.8 → yncli-1.1.0}/yncli/main.py +11 -0
- yncli-1.1.0/yncli/mention_resolver.py +88 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tui.py +42 -1
- yncli-1.1.0/yncli/update_checker.py +120 -0
- yncli-1.1.0/yncli/version.py +1 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/PKG-INFO +1 -1
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/SOURCES.txt +1 -0
- yncli-1.0.8/yncli/update_checker.py +0 -86
- yncli-1.0.8/yncli/version.py +0 -1
- {yncli-1.0.8 → yncli-1.1.0}/README.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/setup.cfg +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/__init__.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/agent.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/architect.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/code_review.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/debug_oracle.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/debugger.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/polyglot.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/refactor_master.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/security_auditor.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/system_architect.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/test_runner.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/ultrabrain.md +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/clean_text.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/client.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/config.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/language_detector.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/skills_manager.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/system_prompt.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/__init__.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/file_tools.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/polyglot_tools.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/search_tools.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/system_tools.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/workspace_memory.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli/workspace_scanner.py +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/dependency_links.txt +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/entry_points.txt +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/requires.txt +0 -0
- {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/top_level.txt +0 -0
|
@@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8") if
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="yncli",
|
|
9
|
-
version="1.0
|
|
9
|
+
version="1.1.0",
|
|
10
10
|
description="Autonomous Polyglot AI Coding Agent & TUI",
|
|
11
11
|
long_description=long_description,
|
|
12
12
|
long_description_content_type="text/markdown",
|
|
@@ -74,8 +74,19 @@ def main():
|
|
|
74
74
|
action="store_true",
|
|
75
75
|
help="List available models and exit"
|
|
76
76
|
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--update", "-u",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Check and automatically update yncli to the latest version"
|
|
81
|
+
)
|
|
77
82
|
|
|
78
83
|
args = parser.parse_args()
|
|
84
|
+
|
|
85
|
+
if args.update:
|
|
86
|
+
from yncli.update_checker import perform_self_update
|
|
87
|
+
perform_self_update()
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
79
90
|
config = load_config()
|
|
80
91
|
|
|
81
92
|
base_url = config.get("base_url")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Tuple, List, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_file_mentions(prompt: str, workspace_dir: str = ".") -> Tuple[str, List[Dict[str, str]]]:
|
|
8
|
+
"""
|
|
9
|
+
Scans the prompt for @file or @folder mentions (e.g. @app/Http/Controllers/UserController.php or @index.html).
|
|
10
|
+
Loads the contents of referenced files and appends them as explicit contextual attachments.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
(cleaned_prompt, list_of_attached_files)
|
|
14
|
+
"""
|
|
15
|
+
cwd = Path(workspace_dir).resolve()
|
|
16
|
+
|
|
17
|
+
# Matches patterns like @filename.ext or @"path with spaces/file.ext" or @path/to/file
|
|
18
|
+
mention_pattern = re.compile(r'@(?:"([^"]+)"|([a-zA-Z0-9_\-\.\/\\]+))')
|
|
19
|
+
|
|
20
|
+
matches = mention_pattern.findall(prompt)
|
|
21
|
+
if not matches:
|
|
22
|
+
return prompt, []
|
|
23
|
+
|
|
24
|
+
attached_files = []
|
|
25
|
+
seen_paths = set()
|
|
26
|
+
|
|
27
|
+
for m_quoted, m_unquoted in matches:
|
|
28
|
+
rel_path = (m_quoted or m_unquoted).strip()
|
|
29
|
+
if not rel_path:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
clean_path = rel_path.replace("\\", "/")
|
|
33
|
+
if clean_path in seen_paths:
|
|
34
|
+
continue
|
|
35
|
+
seen_paths.add(clean_path)
|
|
36
|
+
|
|
37
|
+
target_file = cwd / rel_path
|
|
38
|
+
if target_file.exists() and target_file.is_file():
|
|
39
|
+
try:
|
|
40
|
+
# Read content up to 100KB
|
|
41
|
+
size = target_file.stat().st_size
|
|
42
|
+
if size < 150000:
|
|
43
|
+
with open(target_file, "r", encoding="utf-8", errors="replace") as fp:
|
|
44
|
+
content = fp.read()
|
|
45
|
+
attached_files.append({
|
|
46
|
+
"path": clean_path,
|
|
47
|
+
"content": content,
|
|
48
|
+
"lines": len(content.splitlines()),
|
|
49
|
+
"size_bytes": size
|
|
50
|
+
})
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
elif not target_file.exists():
|
|
54
|
+
# Try searching in subdirectories
|
|
55
|
+
for root, _, files in os.walk(cwd):
|
|
56
|
+
for f in files:
|
|
57
|
+
if f.lower() == Path(rel_path).name.lower():
|
|
58
|
+
found = Path(root) / f
|
|
59
|
+
try:
|
|
60
|
+
rel_found = found.relative_to(cwd).as_posix()
|
|
61
|
+
if rel_found not in seen_paths and found.stat().st_size < 150000:
|
|
62
|
+
seen_paths.add(rel_found)
|
|
63
|
+
with open(found, "r", encoding="utf-8", errors="replace") as fp:
|
|
64
|
+
content = fp.read()
|
|
65
|
+
attached_files.append({
|
|
66
|
+
"path": rel_found,
|
|
67
|
+
"content": content,
|
|
68
|
+
"lines": len(content.splitlines()),
|
|
69
|
+
"size_bytes": found.stat().st_size
|
|
70
|
+
})
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if not attached_files:
|
|
76
|
+
return prompt, []
|
|
77
|
+
|
|
78
|
+
# Build enriched context block
|
|
79
|
+
enrichment_blocks = ["\n\n### [USER ATTACHED @FILES CONTEXT]:"]
|
|
80
|
+
for item in attached_files:
|
|
81
|
+
enrichment_blocks.append(
|
|
82
|
+
f"\n--- ATTACHED FILE: @{item['path']} ({item['lines']} lines) ---\n"
|
|
83
|
+
f"{item['content']}\n"
|
|
84
|
+
f"--- END OF @{item['path']} ---"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
enriched_prompt = prompt + "\n" + "\n".join(enrichment_blocks)
|
|
88
|
+
return enriched_prompt, attached_files
|
|
@@ -50,6 +50,7 @@ class TerminalUI:
|
|
|
50
50
|
from prompt_toolkit.history import FileHistory
|
|
51
51
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
52
52
|
from prompt_toolkit.styles import Style
|
|
53
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
53
54
|
|
|
54
55
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
56
|
history_file = CONFIG_DIR / "history.txt"
|
|
@@ -57,11 +58,37 @@ class TerminalUI:
|
|
|
57
58
|
prompt_style = Style.from_dict({
|
|
58
59
|
"prompt": "#38bdf8",
|
|
59
60
|
"arrow": "#a855f7",
|
|
61
|
+
"completion-menu.completion": "bg:#1e1e2e #cdd6f4",
|
|
62
|
+
"completion-menu.completion.current": "bg:#313244 #89b4fa bold",
|
|
63
|
+
"completion-menu.meta": "bg:#181825 #a6adc8",
|
|
60
64
|
})
|
|
61
65
|
|
|
66
|
+
class MentionCompleter(Completer):
|
|
67
|
+
def __init__(self, workspace_memory):
|
|
68
|
+
self.workspace_memory = workspace_memory
|
|
69
|
+
|
|
70
|
+
def get_completions(self, document, complete_event):
|
|
71
|
+
text = document.text_before_cursor
|
|
72
|
+
# Check if user just typed '@' or is typing a path after '@'
|
|
73
|
+
if "@" in text:
|
|
74
|
+
last_at = text.rfind("@")
|
|
75
|
+
word = text[last_at + 1:]
|
|
76
|
+
# Offer completion for files in workspace tree
|
|
77
|
+
for fpath in self.workspace_memory.file_tree:
|
|
78
|
+
if fpath.lower().startswith(word.lower()) or word.lower() in fpath.lower():
|
|
79
|
+
yield Completion(
|
|
80
|
+
fpath,
|
|
81
|
+
start_position=-len(word),
|
|
82
|
+
display=f"@{fpath}",
|
|
83
|
+
display_meta="file"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
completer = MentionCompleter(self.agent.workspace_memory)
|
|
87
|
+
|
|
62
88
|
self.session = PromptSession(
|
|
63
89
|
history=FileHistory(str(history_file)),
|
|
64
90
|
auto_suggest=AutoSuggestFromHistory(),
|
|
91
|
+
completer=completer,
|
|
65
92
|
style=prompt_style
|
|
66
93
|
)
|
|
67
94
|
except Exception:
|
|
@@ -353,6 +380,11 @@ class TerminalUI:
|
|
|
353
380
|
self.console.print(table)
|
|
354
381
|
return True
|
|
355
382
|
|
|
383
|
+
elif cmd in ("/update", "/upgrade"):
|
|
384
|
+
from yncli.update_checker import perform_self_update
|
|
385
|
+
perform_self_update()
|
|
386
|
+
return True
|
|
387
|
+
|
|
356
388
|
elif cmd == "/save":
|
|
357
389
|
fname = args[0] if args else f"chat_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
|
|
358
390
|
lines = [f"# yncli export - {datetime.datetime.now().isoformat()}\n\n"]
|
|
@@ -454,6 +486,15 @@ class TerminalUI:
|
|
|
454
486
|
is_thinking = False
|
|
455
487
|
step_history: List[str] = []
|
|
456
488
|
|
|
489
|
+
from yncli.mention_resolver import resolve_file_mentions
|
|
490
|
+
resolved_prompt, attached_files = resolve_file_mentions(user_prompt, workspace_dir=self.agent.workspace_dir)
|
|
491
|
+
|
|
492
|
+
if attached_files:
|
|
493
|
+
pills = []
|
|
494
|
+
for af in attached_files:
|
|
495
|
+
pills.append(f"[bold cyan]@{af['path']}[/bold cyan] [dim]({af['lines']} baris)[/dim]")
|
|
496
|
+
self.console.print(f"[dim]📎 Menyertakan file:[/dim] {', '.join(pills)}")
|
|
497
|
+
|
|
457
498
|
def _update_live_display(current_status: str) -> None:
|
|
458
499
|
# Maintain max 3 recent steps
|
|
459
500
|
display_lines = list(step_history[-2:]) + [f"[bold cyan]●[/bold cyan] {current_status}"]
|
|
@@ -520,7 +561,7 @@ class TerminalUI:
|
|
|
520
561
|
_update_live_display(f"[bold #38bdf8][ - ][/bold #38bdf8] Menganalisis instruksi ({self.agent.model})...")
|
|
521
562
|
try:
|
|
522
563
|
res = self.agent.run_turn(
|
|
523
|
-
user_prompt=
|
|
564
|
+
user_prompt=resolved_prompt,
|
|
524
565
|
on_thinking=on_thinking,
|
|
525
566
|
on_content=on_content,
|
|
526
567
|
on_tool_start=on_tool_start,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
from yncli.version import __version__
|
|
9
|
+
from yncli.config import CONFIG_DIR
|
|
10
|
+
|
|
11
|
+
CACHE_FILE = CONFIG_DIR / "update_cache.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_version_tuple(v: str):
|
|
15
|
+
"""Parse version string into tuple of ints for accurate semver comparison."""
|
|
16
|
+
try:
|
|
17
|
+
clean = v.strip().lstrip("v").split("-")[0]
|
|
18
|
+
return tuple(int(x) for x in clean.split(".") if x.isdigit())
|
|
19
|
+
except Exception:
|
|
20
|
+
return (0, 0, 0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_latest_version_live(timeout_sec: float = 1.2) -> Optional[str]:
|
|
24
|
+
"""Queries PyPI directly to get the absolute latest published version."""
|
|
25
|
+
try:
|
|
26
|
+
import requests
|
|
27
|
+
resp = requests.get("https://pypi.org/pypi/yncli/json", timeout=timeout_sec)
|
|
28
|
+
if resp.status_code == 200:
|
|
29
|
+
data = resp.json()
|
|
30
|
+
latest = data.get("info", {}).get("version")
|
|
31
|
+
if latest:
|
|
32
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
CACHE_FILE.write_text(json.dumps({
|
|
34
|
+
"last_checked": time.time(),
|
|
35
|
+
"latest_version": latest
|
|
36
|
+
}, indent=2), encoding="utf-8")
|
|
37
|
+
return latest
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_cached_update() -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""Returns update info from cache if available."""
|
|
45
|
+
try:
|
|
46
|
+
if CACHE_FILE.exists():
|
|
47
|
+
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
48
|
+
latest_str = data.get("latest_version")
|
|
49
|
+
if latest_str:
|
|
50
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
51
|
+
latest_tuple = parse_version_tuple(latest_str)
|
|
52
|
+
if latest_tuple > curr_tuple:
|
|
53
|
+
return {
|
|
54
|
+
"current": __version__,
|
|
55
|
+
"latest": latest_str
|
|
56
|
+
}
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_for_updates_fast(timeout_sec: float = 1.2) -> Optional[Dict[str, Any]]:
|
|
63
|
+
"""
|
|
64
|
+
Checks for updates immediately on every startup with a fast 1.2s timeout.
|
|
65
|
+
Falls back to cached update if offline or timeout.
|
|
66
|
+
"""
|
|
67
|
+
# 1. Try fast live fetch
|
|
68
|
+
latest_live = fetch_latest_version_live(timeout_sec=timeout_sec)
|
|
69
|
+
if latest_live:
|
|
70
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
71
|
+
latest_tuple = parse_version_tuple(latest_live)
|
|
72
|
+
if latest_tuple > curr_tuple:
|
|
73
|
+
return {
|
|
74
|
+
"current": __version__,
|
|
75
|
+
"latest": latest_live
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 2. Fallback to cache if network is slow/offline
|
|
79
|
+
return get_cached_update()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def perform_self_update() -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Direct self-updater: Runs pip install --upgrade --no-cache-dir yncli
|
|
85
|
+
and optionally npm install -g @yanzyuyu/yncli
|
|
86
|
+
"""
|
|
87
|
+
print(f"\n[YNCLI] Memeriksa versi terbaru dari PyPI...")
|
|
88
|
+
latest = fetch_latest_version_live(timeout_sec=5.0)
|
|
89
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
90
|
+
latest_tuple = parse_version_tuple(latest) if latest else curr_tuple
|
|
91
|
+
|
|
92
|
+
print(f"[YNCLI] Versi saat ini: v{__version__}")
|
|
93
|
+
if latest:
|
|
94
|
+
print(f"[YNCLI] Versi terbaru: v{latest}")
|
|
95
|
+
|
|
96
|
+
if latest and latest_tuple <= curr_tuple:
|
|
97
|
+
print("\n[OK] YNCLI Anda sudah menggunakan versi paling baru!")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
print("\n[YNCLI] Mengunduh dan menginstal pembaruan...")
|
|
101
|
+
|
|
102
|
+
# 1. Update Python module via pip
|
|
103
|
+
pip_cmd = [
|
|
104
|
+
sys.executable, "-m", "pip", "install",
|
|
105
|
+
"--upgrade", "--no-cache-dir",
|
|
106
|
+
"--disable-pip-version-check",
|
|
107
|
+
f"yncli=={latest}" if latest else "yncli"
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
res = subprocess.run(pip_cmd)
|
|
111
|
+
if res.returncode == 0:
|
|
112
|
+
print("\n[SUCCESS] YNCLI berhasil diperbarui ke versi terbaru!")
|
|
113
|
+
print("Silakan jalankan 'yncli' kembali untuk menikmati fitur baru.\n")
|
|
114
|
+
# Clear update cache
|
|
115
|
+
CACHE_FILE.unlink(missing_ok=True)
|
|
116
|
+
return True
|
|
117
|
+
else:
|
|
118
|
+
print("\n[ERROR] Gagal memperbarui via pip. Coba jalankan manual:")
|
|
119
|
+
print(f"pip install --upgrade --no-cache-dir yncli\n")
|
|
120
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
import threading
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
|
|
7
|
-
from yncli.version import __version__
|
|
8
|
-
from yncli.config import CONFIG_DIR
|
|
9
|
-
|
|
10
|
-
CACHE_FILE = CONFIG_DIR / "update_cache.json"
|
|
11
|
-
CHECK_INTERVAL_SECONDS = 2 * 3600 # 2 hours
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def parse_version_tuple(v: str):
|
|
15
|
-
"""Parse version string into tuple of ints for accurate semver comparison."""
|
|
16
|
-
try:
|
|
17
|
-
clean = v.strip().lstrip("v").split("-")[0]
|
|
18
|
-
return tuple(int(x) for x in clean.split(".") if x.isdigit())
|
|
19
|
-
except Exception:
|
|
20
|
-
return (0, 0, 0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _fetch_latest_version_pypi(timeout_sec: float = 1.0) -> Optional[str]:
|
|
24
|
-
try:
|
|
25
|
-
import requests
|
|
26
|
-
resp = requests.get("https://pypi.org/pypi/yncli/json", timeout=timeout_sec)
|
|
27
|
-
if resp.status_code == 200:
|
|
28
|
-
data = resp.json()
|
|
29
|
-
return data.get("info", {}).get("version")
|
|
30
|
-
except Exception:
|
|
31
|
-
pass
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_cached_update() -> Optional[Dict[str, Any]]:
|
|
36
|
-
"""Returns cached update info if available and latest > current."""
|
|
37
|
-
try:
|
|
38
|
-
if CACHE_FILE.exists():
|
|
39
|
-
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
40
|
-
latest_str = data.get("latest_version")
|
|
41
|
-
if latest_str:
|
|
42
|
-
curr_tuple = parse_version_tuple(__version__)
|
|
43
|
-
latest_tuple = parse_version_tuple(latest_str)
|
|
44
|
-
if latest_tuple > curr_tuple:
|
|
45
|
-
return {
|
|
46
|
-
"current": __version__,
|
|
47
|
-
"latest": latest_str
|
|
48
|
-
}
|
|
49
|
-
except Exception:
|
|
50
|
-
pass
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def check_for_updates_fast(timeout_sec: float = 1.0) -> Optional[Dict[str, Any]]:
|
|
55
|
-
"""Checks cache first, and if not cached or expired, queries PyPI quickly."""
|
|
56
|
-
cached = get_cached_update()
|
|
57
|
-
if cached:
|
|
58
|
-
return cached
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
now = time.time()
|
|
62
|
-
last_checked = 0
|
|
63
|
-
if CACHE_FILE.exists():
|
|
64
|
-
try:
|
|
65
|
-
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
66
|
-
last_checked = data.get("last_checked", 0)
|
|
67
|
-
except Exception:
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
if (now - last_checked) > CHECK_INTERVAL_SECONDS:
|
|
71
|
-
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
latest = _fetch_latest_version_pypi(timeout_sec=timeout_sec)
|
|
73
|
-
if latest:
|
|
74
|
-
cache_data = {
|
|
75
|
-
"last_checked": now,
|
|
76
|
-
"latest_version": latest
|
|
77
|
-
}
|
|
78
|
-
CACHE_FILE.write_text(json.dumps(cache_data, indent=2), encoding="utf-8")
|
|
79
|
-
curr_tuple = parse_version_tuple(__version__)
|
|
80
|
-
latest_tuple = parse_version_tuple(latest)
|
|
81
|
-
if latest_tuple > curr_tuple:
|
|
82
|
-
return {"current": __version__, "latest": latest}
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
85
|
-
|
|
86
|
-
return None
|
yncli-1.0.8/yncli/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.8"
|
|
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
|
|
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
|