forgexa-cli 1.14.9__tar.gz → 1.14.11__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.14.9 → forgexa_cli-1.14.11}/PKG-INFO +1 -1
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli/daemon.py +80 -10
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli/main.py +61 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/pyproject.toml +1 -1
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/README.md +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/setup.cfg +0 -0
- {forgexa_cli-1.14.9 → forgexa_cli-1.14.11}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.14.
|
|
2
|
+
__version__ = "1.14.11"
|
|
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
523
523
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
524
524
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
525
525
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
526
|
-
DAEMON_VERSION = "1.14.
|
|
526
|
+
DAEMON_VERSION = "1.14.11"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -939,6 +939,25 @@ def _daemon_extract_claude_output(raw: str) -> str:
|
|
|
939
939
|
return raw
|
|
940
940
|
|
|
941
941
|
|
|
942
|
+
# OpenCode requires v1.4.0+ for --dangerously-skip-permissions (added 2026-04-08).
|
|
943
|
+
# Without it, headless opencode run blocks on permission prompts and exits with code 1.
|
|
944
|
+
_OPENCODE_MIN_VERSION = (1, 4, 0)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _parse_semver(v: str) -> tuple[int, int, int]:
|
|
948
|
+
"""Parse a semver string into a comparable (major, minor, patch) tuple.
|
|
949
|
+
|
|
950
|
+
Strips a leading 'v' and ignores pre-release/build suffixes so that
|
|
951
|
+
'1.4.0-beta.1' and 'v1.4.0' both return (1, 4, 0).
|
|
952
|
+
Returns (0, 0, 0) when the string cannot be parsed.
|
|
953
|
+
"""
|
|
954
|
+
import re as _re
|
|
955
|
+
m = _re.match(r"^v?(\d+)\.(\d+)\.(\d+)", v.strip())
|
|
956
|
+
if m:
|
|
957
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
958
|
+
return (0, 0, 0)
|
|
959
|
+
|
|
960
|
+
|
|
942
961
|
def _daemon_extract_opencode_output(raw: str) -> str:
|
|
943
962
|
"""Extract text content from OpenCode JSONL output."""
|
|
944
963
|
parts: list[str] = []
|
|
@@ -2393,12 +2412,28 @@ class WorkspaceManager:
|
|
|
2393
2412
|
cwd=ws_path, timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
|
|
2394
2413
|
)
|
|
2395
2414
|
except RuntimeError as _pre_fe2:
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2415
|
+
_sync_err_str = str(_pre_fe2)
|
|
2416
|
+
_sync_err_lower_pre = _sync_err_str.lower()
|
|
2417
|
+
_is_not_found_pre = (
|
|
2418
|
+
"couldn't find remote ref" in _sync_err_lower_pre
|
|
2419
|
+
or "\u65e0\u6cd5\u627e\u5230\u8fdc\u7a0b\u5f15\u7528" in _sync_err_lower_pre
|
|
2420
|
+
or "remote ref does not exist" in _sync_err_lower_pre
|
|
2400
2421
|
)
|
|
2401
|
-
|
|
2422
|
+
if _is_not_found_pre:
|
|
2423
|
+
# Branch doesn't exist on remote — expected for fresh analysis.
|
|
2424
|
+
# Log at INFO, not WARNING, so operators aren't misled.
|
|
2425
|
+
logger.info(
|
|
2426
|
+
"fetch branch %s not found on remote for worktree %s "
|
|
2427
|
+
"(branch was deleted or never created — normal for fresh analysis)",
|
|
2428
|
+
branch_name, ws_path,
|
|
2429
|
+
)
|
|
2430
|
+
else:
|
|
2431
|
+
logger.warning(
|
|
2432
|
+
"fetch branch %s failed for worktree %s: %s "
|
|
2433
|
+
"(likely auth/SSH issue — will retry in sync loop)",
|
|
2434
|
+
branch_name, ws_path, _pre_fe2,
|
|
2435
|
+
)
|
|
2436
|
+
_last_sync_err = _sync_err_str[:300]
|
|
2402
2437
|
_branch_fetch_ok = False
|
|
2403
2438
|
|
|
2404
2439
|
if fresh_start and not _branch_fetch_ok:
|
|
@@ -4302,6 +4337,28 @@ class ProcessManager:
|
|
|
4302
4337
|
'Failed to run the query PRAGMA wal_checkpoint(PASSIVE)' errors and
|
|
4303
4338
|
exit code 1 when multiple tasks run simultaneously.
|
|
4304
4339
|
"""
|
|
4340
|
+
# --dangerously-skip-permissions was introduced in opencode v1.4.0 (2026-04-08).
|
|
4341
|
+
# Older versions silently ignore the flag but then block on permission prompts
|
|
4342
|
+
# in headless mode (no TTY), eventually exiting with code 1 after a long wait.
|
|
4343
|
+
# Fail fast here so the error is immediately actionable.
|
|
4344
|
+
_oc_ver = _parse_semver(agent.version)
|
|
4345
|
+
if _oc_ver < _OPENCODE_MIN_VERSION:
|
|
4346
|
+
_min_str = ".".join(str(x) for x in _OPENCODE_MIN_VERSION)
|
|
4347
|
+
return TaskResult(
|
|
4348
|
+
status="failed",
|
|
4349
|
+
exit_code=-1,
|
|
4350
|
+
stdout="",
|
|
4351
|
+
stderr="",
|
|
4352
|
+
error=(
|
|
4353
|
+
f"OpenCode {agent.version} is too old for headless execution. "
|
|
4354
|
+
f"Version {_min_str}+ is required: --dangerously-skip-permissions "
|
|
4355
|
+
f"(which auto-approves permission prompts in non-interactive mode) "
|
|
4356
|
+
f"was added in v{_min_str} on 2026-04-08. "
|
|
4357
|
+
f"Upgrade with: opencode upgrade"
|
|
4358
|
+
),
|
|
4359
|
+
failure_code="agent_version_incompatible",
|
|
4360
|
+
)
|
|
4361
|
+
|
|
4305
4362
|
cmd = [
|
|
4306
4363
|
agent.command, "run",
|
|
4307
4364
|
"--format", "json",
|
|
@@ -4509,23 +4566,35 @@ class ProcessManager:
|
|
|
4509
4566
|
The daemon service runs with ProtectSystem=strict and only allows
|
|
4510
4567
|
writes under ~/.forgexa plus the workspace root. Claude Code writes
|
|
4511
4568
|
config, debug logs, cache, and session state under XDG paths and
|
|
4512
|
-
~/.claude by default,
|
|
4569
|
+
~/.claude by default, and newer builds may still touch $HOME/.claude.json
|
|
4570
|
+
even when XDG paths are set. That can fail with EROFS inside the daemon.
|
|
4513
4571
|
|
|
4514
|
-
Point Claude at writable daemon-owned directories
|
|
4515
|
-
|
|
4572
|
+
Point Claude at writable daemon-owned directories, redirect HOME to
|
|
4573
|
+
the same writable sandbox, and mirror only the small set of user
|
|
4574
|
+
config files it needs for headless execution.
|
|
4516
4575
|
"""
|
|
4517
4576
|
sandbox_root = Path.home() / ".forgexa" / "claude-home"
|
|
4518
4577
|
config_root = sandbox_root / "config"
|
|
4519
4578
|
data_root = sandbox_root / "data"
|
|
4520
4579
|
cache_root = sandbox_root / "cache"
|
|
4521
4580
|
state_root = sandbox_root / "state"
|
|
4581
|
+
home_claude_dir = sandbox_root / ".claude"
|
|
4522
4582
|
claude_config_dir = config_root / "claude"
|
|
4523
4583
|
|
|
4524
|
-
for path in (
|
|
4584
|
+
for path in (
|
|
4585
|
+
config_root,
|
|
4586
|
+
data_root,
|
|
4587
|
+
cache_root,
|
|
4588
|
+
state_root,
|
|
4589
|
+
home_claude_dir,
|
|
4590
|
+
claude_config_dir,
|
|
4591
|
+
):
|
|
4525
4592
|
path.mkdir(parents=True, exist_ok=True)
|
|
4526
4593
|
|
|
4527
4594
|
source_files = [
|
|
4595
|
+
(Path.home() / ".claude.json", sandbox_root / ".claude.json"),
|
|
4528
4596
|
(Path.home() / ".claude.json", claude_config_dir / ".claude.json"),
|
|
4597
|
+
(Path.home() / ".claude" / "settings.json", home_claude_dir / "settings.json"),
|
|
4529
4598
|
(Path.home() / ".claude" / "settings.json", claude_config_dir / "settings.json"),
|
|
4530
4599
|
]
|
|
4531
4600
|
for source, dest in source_files:
|
|
@@ -4539,6 +4608,7 @@ class ProcessManager:
|
|
|
4539
4608
|
)
|
|
4540
4609
|
|
|
4541
4610
|
env = {
|
|
4611
|
+
"HOME": str(sandbox_root),
|
|
4542
4612
|
"XDG_CONFIG_HOME": str(config_root),
|
|
4543
4613
|
"XDG_DATA_HOME": str(data_root),
|
|
4544
4614
|
"XDG_CACHE_HOME": str(cache_root),
|
|
@@ -73,6 +73,63 @@ except ImportError:
|
|
|
73
73
|
_SERVER_URL_OVERRIDE: str | None = None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
# ── Banner ────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
_LOGO_ART = """\
|
|
79
|
+
███████╗ ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗
|
|
80
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝╚██╗██╔╝██╔══██╗
|
|
81
|
+
█████╗ ██║ ██║██████╔╝██║ ███╗█████╗ ╚███╔╝ ███████║
|
|
82
|
+
██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔██╗ ██╔══██║
|
|
83
|
+
██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗██╔╝ ██╗██║ ██║
|
|
84
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _supports_color() -> bool:
|
|
88
|
+
"""Return True when stdout can render ANSI colors."""
|
|
89
|
+
if not sys.stdout.isatty():
|
|
90
|
+
return False
|
|
91
|
+
if os.environ.get("NO_COLOR"):
|
|
92
|
+
return False
|
|
93
|
+
if os.environ.get("TERM") in ("dumb", ""):
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _brand_color() -> str:
|
|
99
|
+
"""Return the best available ANSI escape for Forgexa brand blue (#096dd9).
|
|
100
|
+
|
|
101
|
+
Priority:
|
|
102
|
+
1. 24-bit truecolor — exact match (COLORTERM=truecolor|24bit)
|
|
103
|
+
2. Bold blue — 16-color fallback
|
|
104
|
+
"""
|
|
105
|
+
colorterm = os.environ.get("COLORTERM", "").lower()
|
|
106
|
+
if colorterm in ("truecolor", "24bit"):
|
|
107
|
+
return "\033[38;2;9;109;217m" # #096dd9 exact
|
|
108
|
+
return "\033[1;34m" # bold blue — 16-color fallback
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _print_banner() -> None:
|
|
112
|
+
"""Print the Forgexa logo + tagline.
|
|
113
|
+
|
|
114
|
+
Shown only on bare invocation and top-level --help/-h.
|
|
115
|
+
Respects NO_COLOR, TERM=dumb, and non-TTY environments.
|
|
116
|
+
"""
|
|
117
|
+
from forgexa_cli import __version__
|
|
118
|
+
|
|
119
|
+
if _supports_color():
|
|
120
|
+
purple = _brand_color()
|
|
121
|
+
reset = "\033[0m"
|
|
122
|
+
logo = f"{purple}{_LOGO_ART}{reset}"
|
|
123
|
+
tagline = f" {purple}AI-Driven Software Factory · v{__version__}{reset}"
|
|
124
|
+
else:
|
|
125
|
+
logo = _LOGO_ART
|
|
126
|
+
tagline = f" AI-Driven Software Factory · v{__version__}"
|
|
127
|
+
|
|
128
|
+
print(logo)
|
|
129
|
+
print(tagline)
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
|
|
76
133
|
# ── Config file helpers (~/.forgexa/config) ──
|
|
77
134
|
|
|
78
135
|
def _config_path() -> Path:
|
|
@@ -1396,6 +1453,10 @@ def main() -> None:
|
|
|
1396
1453
|
|
|
1397
1454
|
active_url = _api_url() # resolved before parsing (for help text)
|
|
1398
1455
|
|
|
1456
|
+
# Show logo on bare invocation or top-level --help / -h
|
|
1457
|
+
if len(sys.argv) == 1 or (len(sys.argv) >= 2 and sys.argv[1] in ("-h", "--help")):
|
|
1458
|
+
_print_banner()
|
|
1459
|
+
|
|
1399
1460
|
parser = argparse.ArgumentParser(
|
|
1400
1461
|
prog="forgexa",
|
|
1401
1462
|
description="Forgexa CLI — communicates with the Forgexa server via REST API.",
|
|
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
|