@ranger1/dx 0.1.32 → 0.1.33

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.
@@ -44,3 +44,27 @@ python3 ~/.opencode/agents/pr_context.py --pr <PR_NUMBER> --round <ROUND>
44
44
  - **失败/异常时**:
45
45
  - 若脚本 stdout 已输出合法 JSON(包含 `error` 或其他字段)→ 仍然**原样返回该 JSON**。
46
46
  - 若脚本未输出合法 JSON / 退出异常 → 仅输出一行 JSON:`{"error":"PR_CONTEXT_AGENT_FAILED"}`(必要时可加 `detail` 字段)。
47
+
48
+ ## GitHub 认证校验(重要)
49
+
50
+ 脚本会在调用 `gh repo view/gh pr view` 之前校验 GitHub CLI 已认证。
51
+
52
+ - 为了避免 `gh auth status` 在“其他 host(例如 enterprise)认证异常”时误判,脚本会优先从 `git remote origin` 推断 host,并使用:
53
+ - `gh auth status --hostname <host>`
54
+ - 推断失败时默认使用 `github.com`。
55
+
56
+ 可能出现的错误:
57
+
58
+ - `{"error":"GH_CLI_NOT_FOUND"}`:找不到 `gh` 命令(PATH 内未安装/不可执行)
59
+ - 处理:安装 GitHub CLI:https://cli.github.com/
60
+ - `{"error":"GH_NOT_AUTHENTICATED"}`:当前 repo 的 host 未认证
61
+ - 处理:`gh auth login --hostname <host>`
62
+
63
+ 本地排查命令(在同一个 shell 环境运行):
64
+
65
+ ```bash
66
+ git remote get-url origin
67
+ gh auth status
68
+ gh auth status --hostname github.com
69
+ env | grep '^GH_'
70
+ ```
@@ -38,6 +38,30 @@ python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
38
38
  - 若脚本 stdout 已输出合法 JSON(包含 `error` 或其他字段)→ 仍然**原样返回该 JSON**。
39
39
  - 若脚本未输出合法 JSON / 退出异常 → 仅输出一行 JSON:`{"error":"PR_PRECHECK_AGENT_FAILED"}`(必要时可加 `detail` 字段)。
40
40
 
41
+ ## GitHub 认证校验(重要)
42
+
43
+ 脚本会在执行 `gh pr view/checkout` 之前校验 GitHub CLI 已认证。
44
+
45
+ - 为了避免 `gh auth status` 在“其他 host(例如 enterprise)认证异常”时误判,脚本会优先从 `git remote origin` 推断 host,并使用:
46
+ - `gh auth status --hostname <host>`
47
+ - 推断失败时默认使用 `github.com`。
48
+
49
+ 可能出现的错误:
50
+
51
+ - `{"error":"GH_CLI_NOT_FOUND"}`:找不到 `gh` 命令(PATH 内未安装/不可执行)
52
+ - 处理:安装 GitHub CLI:https://cli.github.com/
53
+ - `{"error":"GH_NOT_AUTHENTICATED"}`:当前 repo 的 host 未认证
54
+ - 处理:`gh auth login --hostname <host>`
55
+
56
+ 本地排查命令(在同一个 shell 环境运行):
57
+
58
+ ```bash
59
+ git remote get-url origin
60
+ gh auth status
61
+ gh auth status --hostname github.com
62
+ env | grep '^GH_'
63
+ ```
64
+
41
65
  ## 仅当出现 merge 冲突时怎么处理
42
66
 
43
67
  当脚本输出 `{"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}` 时:
@@ -9,8 +9,10 @@ import argparse
9
9
  import hashlib
10
10
  import json
11
11
  import os
12
+ import re
12
13
  import subprocess
13
14
  import sys
15
+ from urllib.parse import urlparse
14
16
  from pathlib import Path
15
17
 
16
18
 
@@ -24,8 +26,41 @@ def _json_out(obj):
24
26
 
25
27
 
26
28
  def _run_capture(cmd):
27
- p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
28
- return p.returncode, p.stdout, p.stderr
29
+ try:
30
+ p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
31
+ return p.returncode, p.stdout, p.stderr
32
+ except FileNotFoundError as e:
33
+ return 127, "", str(e)
34
+
35
+
36
+ def _detect_git_remote_host():
37
+ # Best-effort parse from origin remote.
38
+ rc, origin_url, _ = _run_capture(["git", "remote", "get-url", "origin"])
39
+ if rc != 0:
40
+ rc, origin_url, _ = _run_capture(["git", "config", "--get", "remote.origin.url"])
41
+ if rc != 0:
42
+ return None
43
+
44
+ url = (origin_url or "").strip()
45
+ if not url:
46
+ return None
47
+
48
+ # Examples:
49
+ # - git@github.com:owner/repo.git
50
+ # - ssh://git@github.company.com/owner/repo.git
51
+ # - https://github.com/owner/repo.git
52
+ if url.startswith("git@"): # SCP-like syntax
53
+ m = re.match(r"^git@([^:]+):", url)
54
+ return m.group(1) if m else None
55
+
56
+ if url.startswith("ssh://") or url.startswith("https://") or url.startswith("http://"):
57
+ try:
58
+ parsed = urlparse(url)
59
+ return parsed.hostname
60
+ except Exception:
61
+ return None
62
+
63
+ return None
29
64
 
30
65
 
31
66
  def _clip(s, n):
@@ -95,9 +130,31 @@ def main(argv):
95
130
  _json_out({"error": "NOT_A_GIT_REPO"})
96
131
  return 1
97
132
 
98
- if subprocess.run(["gh", "auth", "status"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
99
- _json_out({"error": "GH_NOT_AUTHENTICATED"})
133
+ host = _detect_git_remote_host() or "github.com"
134
+ rc, gh_out, gh_err = _run_capture(["gh", "auth", "status", "--hostname", host])
135
+ if rc == 127:
136
+ _json_out({
137
+ "error": "GH_CLI_NOT_FOUND",
138
+ "detail": "gh not found in PATH",
139
+ "suggestion": "Install GitHub CLI: https://cli.github.com/",
140
+ })
100
141
  return 1
142
+ if rc != 0:
143
+ # If host detection is wrong, a global check might still succeed.
144
+ rc2, gh_out2, gh_err2 = _run_capture(["gh", "auth", "status"])
145
+ if rc2 == 0:
146
+ pass
147
+ else:
148
+ detail = (gh_err or gh_out or "").strip()
149
+ if len(detail) > 4000:
150
+ detail = detail[-4000:]
151
+ _json_out({
152
+ "error": "GH_NOT_AUTHENTICATED",
153
+ "host": host,
154
+ "detail": detail,
155
+ "suggestion": f"Run: gh auth login --hostname {host}",
156
+ })
157
+ return 1
101
158
 
102
159
  rc, owner_repo, _ = _run_capture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
103
160
  owner_repo = owner_repo.strip() if rc == 0 else ""
@@ -18,10 +18,19 @@ import re
18
18
  import secrets
19
19
  import subprocess
20
20
  import sys
21
+ from urllib.parse import urlparse
21
22
  from pathlib import Path
22
23
 
23
24
 
24
25
  def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
26
+ try:
27
+ return _run(cmd, cwd=cwd, stdout_path=stdout_path, stderr_path=stderr_path)
28
+ except FileNotFoundError as e:
29
+ # Match common shell semantics for "command not found".
30
+ return 127
31
+
32
+
33
+ def _run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
25
34
  if stdout_path and stderr_path and stdout_path == stderr_path:
26
35
  with open(stdout_path, "wb") as f:
27
36
  p = subprocess.run(cmd, cwd=cwd, stdout=f, stderr=f)
@@ -45,8 +54,42 @@ def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
45
54
 
46
55
 
47
56
  def run_capture(cmd, *, cwd=None):
48
- p = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
49
- return p.returncode, p.stdout, p.stderr
57
+ try:
58
+ p = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
59
+ return p.returncode, p.stdout, p.stderr
60
+ except FileNotFoundError as e:
61
+ return 127, "", str(e)
62
+
63
+
64
+ def _detect_git_remote_host():
65
+ # Best-effort parse from origin remote.
66
+ rc, origin_url, _ = run_capture(["git", "remote", "get-url", "origin"])
67
+ if rc != 0:
68
+ rc, origin_url, _ = run_capture(["git", "config", "--get", "remote.origin.url"])
69
+ if rc != 0:
70
+ return None
71
+
72
+ url = (origin_url or "").strip()
73
+ if not url:
74
+ return None
75
+
76
+ # Examples:
77
+ # - git@github.com:owner/repo.git
78
+ # - ssh://git@github.company.com/owner/repo.git
79
+ # - https://github.com/owner/repo.git
80
+ if url.startswith("git@"): # SCP-like syntax
81
+ # git@host:owner/repo(.git)
82
+ m = re.match(r"^git@([^:]+):", url)
83
+ return m.group(1) if m else None
84
+
85
+ if url.startswith("ssh://") or url.startswith("https://") or url.startswith("http://"):
86
+ try:
87
+ parsed = urlparse(url)
88
+ return parsed.hostname
89
+ except Exception:
90
+ return None
91
+
92
+ return None
50
93
 
51
94
 
52
95
  def tail_text(path, max_lines=200, max_chars=12000):
@@ -101,9 +144,25 @@ def main():
101
144
  print(json.dumps({"error": "NOT_A_GIT_REPO"}))
102
145
  return 1
103
146
 
104
- rc = run(["gh", "auth", "status"])
147
+ host = _detect_git_remote_host() or "github.com"
148
+ rc, gh_out, gh_err = run_capture(["gh", "auth", "status", "--hostname", host])
149
+ if rc == 127:
150
+ print(json.dumps({
151
+ "error": "GH_CLI_NOT_FOUND",
152
+ "detail": "gh not found in PATH",
153
+ "suggestion": "Install GitHub CLI: https://cli.github.com/",
154
+ }))
155
+ return 1
105
156
  if rc != 0:
106
- print(json.dumps({"error": "GH_NOT_AUTHENTICATED"}))
157
+ detail = (gh_err or gh_out or "").strip()
158
+ if len(detail) > 4000:
159
+ detail = detail[-4000:]
160
+ print(json.dumps({
161
+ "error": "GH_NOT_AUTHENTICATED",
162
+ "host": host,
163
+ "detail": detail,
164
+ "suggestion": f"Run: gh auth login --hostname {host}",
165
+ }))
107
166
  return 1
108
167
 
109
168
  rc, pr_json, _ = run_capture(["gh", "pr", "view", pr, "--json", "headRefName,baseRefName,mergeable"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {