forgexa-cli 1.24.5__tar.gz → 1.25.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.
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/PKG-INFO +1 -1
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/__init__.py +1 -1
- forgexa_cli-1.25.0/forgexa_cli/_local_bind.py +83 -0
- forgexa_cli-1.25.0/forgexa_cli/agent_core.py +1771 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/daemon.py +897 -1532
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/main.py +142 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/SOURCES.txt +3 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/pyproject.toml +1 -1
- forgexa_cli-1.25.0/tests/test_local_bind_commands.py +189 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/README.md +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/setup.cfg +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.24.5 → forgexa_cli-1.25.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.25.0"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Forgexa IDE local-bind: CLI-side mapping file + git identity helpers.
|
|
2
|
+
|
|
3
|
+
Deliberately duplicates (does not import) the same logic already vendored
|
|
4
|
+
in backend/app/daemon.py (which itself duplicates
|
|
5
|
+
backend/app/services/git_url_normalize.py) — the CLI package and the
|
|
6
|
+
standalone daemon module are each meant to run independently, and importing
|
|
7
|
+
the full daemon.py module here would pull in its heavier import-time
|
|
8
|
+
environment checks for no benefit. Keep this file's mapping format and
|
|
9
|
+
normalize_repo_identity() in sync with daemon.py's versions if either ever
|
|
10
|
+
changes. See docs/designs/forgexa-ide-design.md §6.1/§6.2/§6.4.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def local_workspaces_mapping_path() -> Path:
|
|
22
|
+
return Path.home() / ".forgexa" / "local-workspaces.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_local_workspaces_mapping() -> dict:
|
|
26
|
+
path = local_workspaces_mapping_path()
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return {}
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
return data if isinstance(data, dict) else {}
|
|
32
|
+
except (OSError, ValueError):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def save_local_workspaces_mapping(mapping: dict) -> None:
|
|
37
|
+
"""Write the mapping atomically with OS-user-private permissions."""
|
|
38
|
+
path = local_workspaces_mapping_path()
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
tmp_path = path.with_name(path.name + ".tmp")
|
|
41
|
+
tmp_path.write_text(json.dumps(mapping, indent=2, sort_keys=True), encoding="utf-8")
|
|
42
|
+
if os.name != "nt":
|
|
43
|
+
os.chmod(tmp_path, 0o600)
|
|
44
|
+
tmp_path.replace(path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_SSH_SCHEME_RE = re.compile(r"^ssh://(?:[^@/]+@)?([^/:]+)(?::\d+)?/(.+?)(?:\.git)?/?$")
|
|
48
|
+
_HTTP_SCHEME_RE = re.compile(r"^https?://(?:[^@/]+@)?([^/:]+)(?::\d+)?/(.+?)(?:\.git)?/?$")
|
|
49
|
+
_SCP_LIKE_RE = re.compile(r"^[^@/]+@([^:/]+):(.+?)(?:\.git)?/?$")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_repo_identity(repo_url: str) -> str | None:
|
|
53
|
+
"""Canonical host-inclusive git remote identity (see module docstring)."""
|
|
54
|
+
repo_url_clean = (repo_url or "").strip()
|
|
55
|
+
if not repo_url_clean:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
match = _SSH_SCHEME_RE.match(repo_url_clean) or _HTTP_SCHEME_RE.match(repo_url_clean)
|
|
59
|
+
if not match and "@" in repo_url_clean and "://" not in repo_url_clean:
|
|
60
|
+
match = _SCP_LIKE_RE.match(repo_url_clean)
|
|
61
|
+
if not match:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
host = match.group(1).lower()
|
|
65
|
+
path = match.group(2).strip("/").lower()
|
|
66
|
+
path = re.sub(r"^scm/", "", path)
|
|
67
|
+
if not path:
|
|
68
|
+
return None
|
|
69
|
+
return f"{host}/{path}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def local_git_remote_identity(path: Path) -> str | None:
|
|
73
|
+
"""Best-effort `git remote get-url origin` + normalize, or None on any failure."""
|
|
74
|
+
try:
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
["git", "remote", "get-url", "origin"],
|
|
77
|
+
cwd=str(path), capture_output=True, text=True, timeout=10,
|
|
78
|
+
)
|
|
79
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
80
|
+
return None
|
|
81
|
+
if result.returncode != 0:
|
|
82
|
+
return None
|
|
83
|
+
return normalize_repo_identity(result.stdout.strip())
|