forgexa-cli 1.14.10__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.10 → forgexa_cli-1.14.11}/PKG-INFO +1 -1
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli/daemon.py +42 -1
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli/main.py +61 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/pyproject.toml +1 -1
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/README.md +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.10 → forgexa_cli-1.14.11}/setup.cfg +0 -0
- {forgexa_cli-1.14.10 → 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] = []
|
|
@@ -4318,6 +4337,28 @@ class ProcessManager:
|
|
|
4318
4337
|
'Failed to run the query PRAGMA wal_checkpoint(PASSIVE)' errors and
|
|
4319
4338
|
exit code 1 when multiple tasks run simultaneously.
|
|
4320
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
|
+
|
|
4321
4362
|
cmd = [
|
|
4322
4363
|
agent.command, "run",
|
|
4323
4364
|
"--format", "json",
|
|
@@ -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
|