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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.9
3
+ Version: 1.14.11
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.14.9"
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.9"
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
- logger.warning(
2397
- "fetch branch %s failed for worktree %s: %s "
2398
- "(likely auth/SSH issue — will retry in sync loop)",
2399
- branch_name, ws_path, _pre_fe2,
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
- _last_sync_err = str(_pre_fe2)[:300]
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, which can fail with EROFS inside the daemon.
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 and mirror only the
4515
- small set of user config files it needs for headless execution.
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 (config_root, data_root, cache_root, state_root, claude_config_dir):
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.",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.9
3
+ Version: 1.14.11
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.14.9"
3
+ version = "1.14.11"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = { text = "MIT" }
File without changes
File without changes