@ranger1/dx 0.1.43 → 0.1.45
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.
- package/@opencode/commands/doctor.md +175 -132
- package/lib/logger.js +73 -22
- package/lib/telegram-webhook.js +24 -12
- package/lib/vercel-deploy.js +112 -28
- package/package.json +1 -1
|
@@ -6,186 +6,229 @@ agent: build
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## Step 0:
|
|
9
|
+
## Step 0: Bootstrap(dx + 模板 + pnpm)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
目标:
|
|
12
|
+
|
|
13
|
+
- dx 安装/升级到最新
|
|
14
|
+
- 刷新 `~/.opencode/commands/*`(确保 `opencode_attach.py` 可用)
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
|
-
|
|
15
|
-
```
|
|
17
|
+
set -euo pipefail
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
# 必要前提:node + corepack(用于 pnpm)
|
|
20
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
21
|
+
echo "ERROR: node NOT_FOUND (need Node.js >= 20)"
|
|
22
|
+
echo "macOS: brew install node"
|
|
23
|
+
echo "Debian/Ubuntu: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
if ! command -v pnpm >/dev/null 2>&1; then
|
|
28
|
+
corepack enable >/dev/null 2>&1 || true
|
|
29
|
+
corepack prepare pnpm@latest --activate
|
|
30
|
+
fi
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
# 安装/升级 dx 到最新(幂等)
|
|
33
|
+
pnpm add -g @ranger1/dx@latest
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
# 备份后刷新模板(避免覆盖导致不可回退)
|
|
36
|
+
ts="$(date +%Y%m%d%H%M%S)"
|
|
37
|
+
if [ -d "$HOME/.opencode/commands" ]; then
|
|
38
|
+
cp -a "$HOME/.opencode/commands" "$HOME/.opencode/commands.bak.${ts}" 2>/dev/null || true
|
|
39
|
+
fi
|
|
40
|
+
if [ -d "$HOME/.opencode/agents" ]; then
|
|
41
|
+
cp -a "$HOME/.opencode/agents" "$HOME/.opencode/agents.bak.${ts}" 2>/dev/null || true
|
|
42
|
+
fi
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
# 批次 2: 项目文件检测
|
|
34
|
-
echo "=== PROJECT_FILES ===";
|
|
35
|
-
echo "AGENTS.md:" && (test -f AGENTS.md && echo "FOUND" || echo "NOT_FOUND");
|
|
44
|
+
dx initial
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
# 批次 3: OpenCode 插件检测
|
|
40
|
-
# 注意:插件名可能带版本号(如 @1.3.0),使用模糊匹配
|
|
41
|
-
echo "=== OPENCODE_PLUGINS ===";
|
|
42
|
-
echo "oh-my-opencode:" && (opencode plugin list 2>/dev/null | grep -q 'oh-my-opencode' && echo "INSTALLED" || echo "NOT_INSTALLED");
|
|
43
|
-
echo "opencode-openai-codex-auth:" && (opencode plugin list 2>/dev/null | grep -q 'opencode-openai-codex-auth' && echo "INSTALLED" || echo "NOT_INSTALLED");
|
|
44
|
-
```
|
|
47
|
+
---
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
# 批次 4: attach 配置(统一)
|
|
48
|
-
echo "=== OPENCODE_ATTACH ===";
|
|
49
|
-
echo "attach:" && (python3 ~/.opencode/commands/opencode_attach.py --dry-run >/dev/null 2>&1 && echo "READY" || echo "NOT_READY");
|
|
50
|
-
```
|
|
49
|
+
## Step 1: 快速检测(单次 Bash)
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
# 批次 5: Python 检测(python3 + python 软链接)
|
|
54
|
-
echo "=== PYTHON ===";
|
|
55
|
-
echo "python3:" && (which python3 && python3 --version 2>/dev/null || echo "NOT_FOUND");
|
|
56
|
-
echo "python:" && (which python && python --version 2>/dev/null || echo "NOT_FOUND");
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
51
|
+
目标:一次 Bash 输出完整状态表,减少 tool 调用与 token。
|
|
60
52
|
|
|
61
|
-
|
|
53
|
+
```bash
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
os="$(uname -s 2>/dev/null || echo unknown)"
|
|
57
|
+
pm="none"
|
|
58
|
+
if command -v brew >/dev/null 2>&1; then pm="brew"; fi
|
|
59
|
+
if command -v apt-get >/dev/null 2>&1; then pm="apt"; fi
|
|
60
|
+
|
|
61
|
+
ver() {
|
|
62
|
+
# usage: ver <bin> <cmd>
|
|
63
|
+
b="$1"; shift
|
|
64
|
+
if command -v "$b" >/dev/null 2>&1; then
|
|
65
|
+
("$@" 2>/dev/null | head -n 1) || true
|
|
66
|
+
else
|
|
67
|
+
echo "NOT_FOUND"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
has_agents="NOT_FOUND"; [ -f AGENTS.md ] && has_agents="FOUND"
|
|
72
|
+
|
|
73
|
+
dx_v="$(ver dx dx --version)"
|
|
74
|
+
opencode_v="$(ver opencode opencode --version)"
|
|
75
|
+
rg_v="$(ver rg rg --version)"
|
|
76
|
+
agent_browser_v="$(ver agent-browser agent-browser --version)"
|
|
77
|
+
py3_v="$(ver python3 python3 --version)"
|
|
78
|
+
py_v="$(ver python python --version)"
|
|
79
|
+
|
|
80
|
+
attach_status="NOT_READY"
|
|
81
|
+
if command -v python3 >/dev/null 2>&1 && [ -f "$HOME/.opencode/commands/opencode_attach.py" ]; then
|
|
82
|
+
python3 "$HOME/.opencode/commands/opencode_attach.py" --dry-run >/dev/null 2>&1 && attach_status="READY" || true
|
|
83
|
+
fi
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
# 插件以“配置是否就绪”为准(真正安装由 opencode 启动时自动完成)
|
|
86
|
+
cfg_opencode="$HOME/.config/opencode/opencode.json"
|
|
87
|
+
plug_oh="NOT_CONFIGURED"
|
|
88
|
+
plug_codex="NOT_CONFIGURED"
|
|
89
|
+
plug_antigravity="NOT_CONFIGURED"
|
|
90
|
+
if [ -f "$cfg_opencode" ]; then
|
|
91
|
+
grep -q 'oh-my-opencode' "$cfg_opencode" && plug_oh="CONFIGURED" || true
|
|
92
|
+
grep -q 'opencode-openai-codex-auth' "$cfg_opencode" && plug_codex="CONFIGURED" || true
|
|
93
|
+
grep -q 'opencode-antigravity-auth' "$cfg_opencode" && plug_antigravity="CONFIGURED" || true
|
|
94
|
+
fi
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
echo "OS: ${os} | PM: ${pm}"
|
|
97
|
+
echo
|
|
98
|
+
printf '%-34s | %-12s | %s\n' "tool" "status" "version"
|
|
99
|
+
printf '%-34s | %-12s | %s\n' "opencode" "$( [ "$opencode_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$opencode_v"
|
|
100
|
+
printf '%-34s | %-12s | %s\n' "dx" "$( [ "$dx_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$dx_v"
|
|
101
|
+
printf '%-34s | %-12s | %s\n' "rg" "$( [ "$rg_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$rg_v"
|
|
102
|
+
printf '%-34s | %-12s | %s\n' "agent-browser" "$( [ "$agent_browser_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$agent_browser_v"
|
|
103
|
+
printf '%-34s | %-12s | %s\n' "python3" "$( [ "$py3_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$py3_v"
|
|
104
|
+
printf '%-34s | %-12s | %s\n' "python (softlink)" "$( [ "$py_v" = NOT_FOUND ] && echo MISSING || echo OK )" "$py_v"
|
|
105
|
+
printf '%-34s | %-12s | %s\n' "AGENTS.md" "$has_agents" "-"
|
|
106
|
+
printf '%-34s | %-12s | %s\n' "attach (global config)" "$attach_status" "-"
|
|
107
|
+
printf '%-34s | %-12s | %s\n' "plugin: oh-my-opencode" "$plug_oh" "-"
|
|
108
|
+
printf '%-34s | %-12s | %s\n' "plugin: opencode-openai-codex-auth" "$plug_codex" "-"
|
|
109
|
+
printf '%-34s | %-12s | %s\n' "plugin: opencode-antigravity-auth" "$plug_antigravity" "-"
|
|
110
|
+
|
|
111
|
+
missing=0
|
|
112
|
+
for x in "$opencode_v" "$dx_v" "$rg_v" "$agent_browser_v" "$py3_v"; do
|
|
113
|
+
[ "$x" = NOT_FOUND ] && missing=1
|
|
114
|
+
done
|
|
115
|
+
[ "$attach_status" != READY ] && missing=1
|
|
116
|
+
for x in "$plug_oh" "$plug_codex" "$plug_antigravity"; do
|
|
117
|
+
[ "$x" != CONFIGURED ] && missing=1
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
echo
|
|
121
|
+
if [ "$missing" = 0 ]; then
|
|
122
|
+
echo "OK: all dependencies ready"
|
|
123
|
+
else
|
|
124
|
+
echo "NEED_FIX: missing or not-ready items detected"
|
|
125
|
+
fi
|
|
77
126
|
```
|
|
78
127
|
|
|
79
128
|
---
|
|
80
129
|
|
|
81
|
-
## Step
|
|
82
|
-
|
|
83
|
-
**如检测到任何缺失项,统一询问一次:**
|
|
84
|
-
|
|
85
|
-
`AskUserQuestion`: 检测到以下缺失项,是否自动安装/配置所有?
|
|
86
|
-
|
|
87
|
-
确认后按顺序处理(需要直接执行对应安装/配置命令,不要只输出提示):
|
|
130
|
+
## Step 2: 只问一次(缺失/升级)
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
如果出现 `NEED_FIX`,只问一次:是否一键安装 + 升级到最新版本(包含插件配置 attach)。
|
|
90
133
|
|
|
91
|
-
|
|
134
|
+
`AskUserQuestion`: 检测到缺失/未就绪项,是否一键修复并升级到最新版本?
|
|
92
135
|
|
|
93
|
-
|
|
94
|
-
# brew 优先
|
|
95
|
-
brew install opencode || npm install -g opencode
|
|
96
|
-
```
|
|
136
|
+
选项:
|
|
97
137
|
|
|
98
|
-
|
|
138
|
+
- 一键修复(Recommended)
|
|
139
|
+
- 跳过(只输出检测表)
|
|
99
140
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- AGENTS.md 文件不存在,OpenCode 需要此文件作为项目指令入口
|
|
103
|
-
- 建议创建或检查文件路径
|
|
141
|
+
---
|
|
104
142
|
|
|
105
|
-
|
|
143
|
+
## Step 3: 一键修复(安装 + 升级到最新)
|
|
106
144
|
|
|
107
|
-
|
|
145
|
+
确认后直接执行以下脚本(幂等;尽量走包管理器升级;插件用 attach 配置确保可自动安装/更新):
|
|
108
146
|
|
|
109
147
|
```bash
|
|
110
|
-
set -
|
|
148
|
+
set -euo pipefail
|
|
111
149
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
fi
|
|
150
|
+
os="$(uname -s 2>/dev/null || echo unknown)"
|
|
151
|
+
has_brew=0; command -v brew >/dev/null 2>&1 && has_brew=1
|
|
152
|
+
has_apt=0; command -v apt-get >/dev/null 2>&1 && has_apt=1
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
sudo apt-get update
|
|
121
|
-
sudo apt-get install -y ripgrep
|
|
122
|
-
else
|
|
123
|
-
echo "ripgrep (rg) NOT_FOUND, and no supported package manager (brew/apt-get) detected"
|
|
124
|
-
echo "Please install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation"
|
|
125
|
-
exit 1
|
|
154
|
+
need_sudo=0
|
|
155
|
+
if [ "$has_apt" = 1 ] && command -v sudo >/dev/null 2>&1; then
|
|
156
|
+
need_sudo=1
|
|
126
157
|
fi
|
|
127
158
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
执行安装:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
npm install -g agent-browser && agent-browser install
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 3.6.1 python3 未安装
|
|
140
|
-
|
|
141
|
-
执行安装:
|
|
159
|
+
if ! command -v pnpm >/dev/null 2>&1; then
|
|
160
|
+
corepack enable >/dev/null 2>&1 || true
|
|
161
|
+
corepack prepare pnpm@latest --activate
|
|
162
|
+
fi
|
|
142
163
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
brew install python
|
|
146
|
-
```
|
|
164
|
+
# dx(始终升级到最新)
|
|
165
|
+
pnpm add -g @ranger1/dx@latest
|
|
147
166
|
|
|
148
|
-
|
|
167
|
+
if ! command -v dx >/dev/null 2>&1; then
|
|
168
|
+
echo "WARN: dx still NOT_FOUND (check PATH for pnpm global bin: pnpm bin -g)"
|
|
169
|
+
fi
|
|
149
170
|
|
|
150
|
-
|
|
171
|
+
# OpenCode 模板(确保 opencode_attach.py 存在)
|
|
172
|
+
if [ ! -f "$HOME/.opencode/commands/opencode_attach.py" ]; then
|
|
173
|
+
dx initial
|
|
174
|
+
fi
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
|
|
176
|
+
# opencode CLI
|
|
177
|
+
if [ "$os" = "Darwin" ] && [ "$has_brew" = 1 ]; then
|
|
178
|
+
brew update >/dev/null
|
|
179
|
+
brew tap anomalyco/tap >/dev/null 2>&1 || true
|
|
180
|
+
brew install anomalyco/tap/opencode >/dev/null 2>&1 || brew upgrade opencode >/dev/null 2>&1 || true
|
|
181
|
+
else
|
|
182
|
+
# 官方支持 npm/bun/pnpm;这里统一用 pnpm
|
|
183
|
+
pnpm add -g opencode-ai@latest
|
|
184
|
+
fi
|
|
154
185
|
|
|
155
|
-
if command -v
|
|
156
|
-
|
|
157
|
-
exit 0
|
|
186
|
+
if ! command -v opencode >/dev/null 2>&1; then
|
|
187
|
+
echo "WARN: opencode still NOT_FOUND (check PATH for pnpm global bin: pnpm bin -g)"
|
|
158
188
|
fi
|
|
159
189
|
|
|
160
|
-
|
|
161
|
-
if [
|
|
162
|
-
|
|
163
|
-
|
|
190
|
+
# ripgrep
|
|
191
|
+
if [ "$has_brew" = 1 ]; then
|
|
192
|
+
brew install ripgrep >/dev/null 2>&1 || brew upgrade ripgrep >/dev/null 2>&1 || true
|
|
193
|
+
elif [ "$has_apt" = 1 ] && [ "$need_sudo" = 1 ]; then
|
|
194
|
+
sudo apt-get update -y >/dev/null
|
|
195
|
+
sudo apt-get install -y ripgrep
|
|
196
|
+
else
|
|
197
|
+
echo "WARN: no brew/apt-get; skip ripgrep auto-install"
|
|
164
198
|
fi
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
if [
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
# python3 (+ python 软链接尽量走系统包)
|
|
201
|
+
if [ "$has_brew" = 1 ]; then
|
|
202
|
+
brew install python >/dev/null 2>&1 || brew upgrade python >/dev/null 2>&1 || true
|
|
203
|
+
elif [ "$has_apt" = 1 ] && [ "$need_sudo" = 1 ]; then
|
|
204
|
+
sudo apt-get update -y >/dev/null
|
|
205
|
+
sudo apt-get install -y python3 python3-venv python3-pip python-is-python3
|
|
170
206
|
else
|
|
171
|
-
|
|
172
|
-
echo "linked: $HOME/.local/bin/python -> $PY3"
|
|
173
|
-
echo "NOTE: ensure $HOME/.local/bin is in PATH"
|
|
207
|
+
echo "WARN: no brew/apt-get; skip python auto-install"
|
|
174
208
|
fi
|
|
175
209
|
|
|
176
|
-
|
|
177
|
-
|
|
210
|
+
# agent-browser(安装/升级 + 安装 Chromium)
|
|
211
|
+
if [ "$os" = "Darwin" ] && [ "$has_brew" = 1 ]; then
|
|
212
|
+
brew install agent-browser >/dev/null 2>&1 || brew upgrade agent-browser >/dev/null 2>&1 || true
|
|
213
|
+
else
|
|
214
|
+
pnpm add -g agent-browser@latest
|
|
215
|
+
fi
|
|
178
216
|
|
|
179
|
-
|
|
217
|
+
if command -v agent-browser >/dev/null 2>&1; then
|
|
218
|
+
agent-browser install >/dev/null 2>&1 || agent-browser install --with-deps
|
|
219
|
+
fi
|
|
180
220
|
|
|
181
|
-
|
|
221
|
+
# attach(写入 ~/.config/opencode/*.json;自动备份 .bak.*)
|
|
222
|
+
if command -v python3 >/dev/null 2>&1 && [ -f "$HOME/.opencode/commands/opencode_attach.py" ]; then
|
|
223
|
+
python3 "$HOME/.opencode/commands/opencode_attach.py"
|
|
224
|
+
else
|
|
225
|
+
echo "ERROR: python3/opencode_attach.py NOT_READY"
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
182
228
|
|
|
183
|
-
|
|
184
|
-
python3 ~/.opencode/commands/opencode_attach.py
|
|
229
|
+
echo "DONE"
|
|
185
230
|
```
|
|
186
231
|
|
|
187
|
-
|
|
188
|
-
|
|
189
232
|
---
|
|
190
233
|
|
|
191
234
|
## 输出格式
|
|
@@ -203,4 +246,4 @@ python3 ~/.opencode/commands/opencode_attach.py
|
|
|
203
246
|
```
|
|
204
247
|
|
|
205
248
|
**修复完成后:**
|
|
206
|
-
|
|
249
|
+
重复执行 Step 1,输出最终状态表格,确认所有项目均为 OK/READY/CONFIGURED。
|
package/lib/logger.js
CHANGED
|
@@ -5,6 +5,44 @@ function resolveProjectRoot() {
|
|
|
5
5
|
return process.env.DX_PROJECT_ROOT || process.cwd()
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function sanitizeForLog(input) {
|
|
9
|
+
let text = input == null ? '' : String(input)
|
|
10
|
+
|
|
11
|
+
// CLI token args (vercel)
|
|
12
|
+
text = text.replace(/--token=("[^"]*"|'[^']*'|[^\s]+)/gi, '--token=***')
|
|
13
|
+
text = text.replace(/--token\s+("[^"]*"|'[^']*'|[^\s]+)/gi, '--token ***')
|
|
14
|
+
|
|
15
|
+
// Env style secrets
|
|
16
|
+
text = text.replace(/\bVERCEL_TOKEN=([^\s]+)/g, 'VERCEL_TOKEN=***')
|
|
17
|
+
text = text.replace(/\bTELEGRAM_BOT_TOKEN=([^\s]+)/g, 'TELEGRAM_BOT_TOKEN=***')
|
|
18
|
+
text = text.replace(
|
|
19
|
+
/\bTELEGRAM_BOT_WEBHOOK_SECRET=([^\s]+)/g,
|
|
20
|
+
'TELEGRAM_BOT_WEBHOOK_SECRET=***',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Authorization bearer
|
|
24
|
+
text = text.replace(
|
|
25
|
+
/Authorization:\s*Bearer\s+([^\s]+)/gi,
|
|
26
|
+
'Authorization: Bearer ***',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// JSON-ish token fields
|
|
30
|
+
text = text.replace(/"token"\s*:\s*"[^"]*"/gi, '"token":"***"')
|
|
31
|
+
text = text.replace(
|
|
32
|
+
/("secret_token"\s*:\s*")([^"]*)(")/gi,
|
|
33
|
+
'$1***$3',
|
|
34
|
+
)
|
|
35
|
+
text = text.replace(/\bsecret_token=([^\s&]+)/gi, 'secret_token=***')
|
|
36
|
+
|
|
37
|
+
// Telegram bot token in URLs
|
|
38
|
+
text = text.replace(
|
|
39
|
+
/api\.telegram\.org\/bot([^/\s]+)(\/|$)/gi,
|
|
40
|
+
'api.telegram.org/bot***$2',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return text
|
|
44
|
+
}
|
|
45
|
+
|
|
8
46
|
// 处理输出管道被关闭导致的 EPIPE 错误,避免进程在清理阶段崩溃
|
|
9
47
|
try {
|
|
10
48
|
const handleBrokenPipe = err => {
|
|
@@ -48,34 +86,39 @@ export class Logger {
|
|
|
48
86
|
|
|
49
87
|
// 基础日志方法
|
|
50
88
|
info(message, prefix = '🚀') {
|
|
51
|
-
const
|
|
89
|
+
const safeMessage = sanitizeForLog(message)
|
|
90
|
+
const output = `${prefix} ${safeMessage}`
|
|
52
91
|
console.log(output)
|
|
53
|
-
this.writeLog('info',
|
|
92
|
+
this.writeLog('info', safeMessage)
|
|
54
93
|
}
|
|
55
94
|
|
|
56
95
|
success(message) {
|
|
57
|
-
const
|
|
96
|
+
const safeMessage = sanitizeForLog(message)
|
|
97
|
+
const output = `✅ ${safeMessage}`
|
|
58
98
|
console.log(output)
|
|
59
|
-
this.writeLog('success',
|
|
99
|
+
this.writeLog('success', safeMessage)
|
|
60
100
|
}
|
|
61
101
|
|
|
62
102
|
warn(message) {
|
|
63
|
-
const
|
|
103
|
+
const safeMessage = sanitizeForLog(message)
|
|
104
|
+
const output = `⚠️ ${safeMessage}`
|
|
64
105
|
console.log(output)
|
|
65
|
-
this.writeLog('warn',
|
|
106
|
+
this.writeLog('warn', safeMessage)
|
|
66
107
|
}
|
|
67
108
|
|
|
68
109
|
error(message) {
|
|
69
|
-
const
|
|
110
|
+
const safeMessage = sanitizeForLog(message)
|
|
111
|
+
const output = `❌ ${safeMessage}`
|
|
70
112
|
console.log(output)
|
|
71
|
-
this.writeLog('error',
|
|
113
|
+
this.writeLog('error', safeMessage)
|
|
72
114
|
}
|
|
73
115
|
|
|
74
116
|
debug(message) {
|
|
75
117
|
if (this.logLevel === 'debug') {
|
|
76
|
-
const
|
|
118
|
+
const safeMessage = sanitizeForLog(message)
|
|
119
|
+
const output = `🐛 ${safeMessage}`
|
|
77
120
|
console.log(output)
|
|
78
|
-
this.writeLog('debug',
|
|
121
|
+
this.writeLog('debug', safeMessage)
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
|
|
@@ -84,17 +127,20 @@ export class Logger {
|
|
|
84
127
|
const prefix = stepNumber ? `步骤 ${stepNumber}:` : '执行:'
|
|
85
128
|
const separator = '=================================='
|
|
86
129
|
|
|
130
|
+
const safeMessage = sanitizeForLog(message)
|
|
131
|
+
|
|
87
132
|
console.log(`\n${separator}`)
|
|
88
|
-
console.log(`🚀 ${prefix} ${
|
|
133
|
+
console.log(`🚀 ${prefix} ${safeMessage}`)
|
|
89
134
|
console.log(separator)
|
|
90
135
|
|
|
91
|
-
this.writeLog('step', `${prefix} ${
|
|
136
|
+
this.writeLog('step', `${prefix} ${safeMessage}`)
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
// 进度显示
|
|
95
140
|
progress(message) {
|
|
96
|
-
|
|
97
|
-
|
|
141
|
+
const safeMessage = sanitizeForLog(message)
|
|
142
|
+
process.stdout.write(`⌛ ${safeMessage}...`)
|
|
143
|
+
this.writeLog('progress', `开始: ${safeMessage}`)
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
progressDone() {
|
|
@@ -104,8 +150,9 @@ export class Logger {
|
|
|
104
150
|
|
|
105
151
|
// 命令执行日志
|
|
106
152
|
command(command) {
|
|
107
|
-
|
|
108
|
-
|
|
153
|
+
const safeCommand = sanitizeForLog(command)
|
|
154
|
+
console.log(`💻 执行: ${safeCommand}`)
|
|
155
|
+
this.writeLog('command', safeCommand)
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
// 分隔符
|
|
@@ -118,15 +165,16 @@ export class Logger {
|
|
|
118
165
|
if (data.length === 0) return
|
|
119
166
|
|
|
120
167
|
if (headers.length > 0) {
|
|
121
|
-
|
|
122
|
-
console.log(
|
|
168
|
+
const safeHeaders = headers.map(h => sanitizeForLog(h))
|
|
169
|
+
console.log(`\n${safeHeaders.join('\t')}`)
|
|
170
|
+
console.log('-'.repeat(safeHeaders.join('\t').length))
|
|
123
171
|
}
|
|
124
172
|
|
|
125
173
|
data.forEach(row => {
|
|
126
174
|
if (Array.isArray(row)) {
|
|
127
|
-
console.log(row.join('\t'))
|
|
175
|
+
console.log(row.map(cell => sanitizeForLog(cell)).join('\t'))
|
|
128
176
|
} else {
|
|
129
|
-
console.log(row)
|
|
177
|
+
console.log(sanitizeForLog(row))
|
|
130
178
|
}
|
|
131
179
|
})
|
|
132
180
|
console.log()
|
|
@@ -136,7 +184,9 @@ export class Logger {
|
|
|
136
184
|
ports(portInfo) {
|
|
137
185
|
console.log('\n📡 服务端口信息:')
|
|
138
186
|
portInfo.forEach(({ service, port, url }) => {
|
|
139
|
-
|
|
187
|
+
const safeService = sanitizeForLog(service)
|
|
188
|
+
const safeUrl = url ? sanitizeForLog(url) : ''
|
|
189
|
+
console.log(` ${safeService}: http://localhost:${port} ${safeUrl ? `(${safeUrl})` : ''}`)
|
|
140
190
|
})
|
|
141
191
|
console.log()
|
|
142
192
|
}
|
|
@@ -146,8 +196,9 @@ export class Logger {
|
|
|
146
196
|
if (!this.enableFile) return
|
|
147
197
|
|
|
148
198
|
try {
|
|
199
|
+
const safeMessage = sanitizeForLog(message)
|
|
149
200
|
const timestamp = this.formatTimestamp()
|
|
150
|
-
const logLine = `[${timestamp}] [${level.toUpperCase()}] ${
|
|
201
|
+
const logLine = `[${timestamp}] [${level.toUpperCase()}] ${safeMessage}\n`
|
|
151
202
|
|
|
152
203
|
const logFile = join(this.logDir, `ai-cli-${new Date().toISOString().split('T')[0]}.log`)
|
|
153
204
|
writeFileSync(logFile, logLine, { flag: 'a', encoding: 'utf8' })
|
package/lib/telegram-webhook.js
CHANGED
|
@@ -22,7 +22,9 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
22
22
|
|
|
23
23
|
if (missingVars.length > 0) {
|
|
24
24
|
logger.error('缺少以下 Telegram Bot 环境变量:')
|
|
25
|
-
missingVars.forEach(v =>
|
|
25
|
+
missingVars.forEach(v => {
|
|
26
|
+
logger.error(` - ${v}`)
|
|
27
|
+
})
|
|
26
28
|
logger.warn('跳过 Webhook 配置,请手动设置')
|
|
27
29
|
return
|
|
28
30
|
}
|
|
@@ -67,8 +69,15 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
67
69
|
}
|
|
68
70
|
else {
|
|
69
71
|
logger.error(`Telegram Webhook 设置失败: ${result.description}`)
|
|
70
|
-
logger.info('
|
|
71
|
-
|
|
72
|
+
logger.info('请手动执行以下命令(不要把明文 token/secret 写进日志):')
|
|
73
|
+
const manualPayload = JSON.stringify({
|
|
74
|
+
url: webhookUrl,
|
|
75
|
+
secret_token: '<YOUR_WEBHOOK_SECRET>',
|
|
76
|
+
drop_pending_updates: false,
|
|
77
|
+
})
|
|
78
|
+
logger.info(
|
|
79
|
+
`curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" -H "Content-Type: application/json" -d '${manualPayload}' --silent`,
|
|
80
|
+
)
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
catch (error) {
|
|
@@ -82,15 +91,18 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
82
91
|
*/
|
|
83
92
|
async function getLatestDeploymentUrl(projectId, orgId, token, environment) {
|
|
84
93
|
try {
|
|
85
|
-
const cmd = [
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
const cmd = ['vercel', 'ls', orgId ? `--scope=${orgId}` : '', '--json']
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ')
|
|
97
|
+
|
|
98
|
+
const output = execSync(cmd, {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
// 不通过 CLI args 传递 token,避免出现在错误信息/日志中
|
|
103
|
+
VERCEL_TOKEN: token,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
94
106
|
const deployments = JSON.parse(output)
|
|
95
107
|
|
|
96
108
|
// 根据环境筛选部署
|
package/lib/vercel-deploy.js
CHANGED
|
@@ -1,11 +1,105 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
1
|
+
import { execSync, spawn } from 'node:child_process'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { envManager } from './env.js'
|
|
4
|
-
import { execManager } from './exec.js'
|
|
5
4
|
import { logger } from './logger.js'
|
|
6
5
|
|
|
7
6
|
const ALLOWED_ENVIRONMENTS = ['development', 'staging', 'production']
|
|
8
7
|
|
|
8
|
+
function collectErrorText(err) {
|
|
9
|
+
const parts = []
|
|
10
|
+
if (err?.message) parts.push(String(err.message))
|
|
11
|
+
if (err?.stderr) parts.push(String(err.stderr))
|
|
12
|
+
if (err?.stdout) parts.push(String(err.stdout))
|
|
13
|
+
return parts.join('\n')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isMissingFilesError(err) {
|
|
17
|
+
const text = collectErrorText(err)
|
|
18
|
+
return (
|
|
19
|
+
text.includes('missing_files') ||
|
|
20
|
+
text.includes('Missing files') ||
|
|
21
|
+
text.includes('code":"missing_files"')
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runVercel(args, options = {}) {
|
|
26
|
+
const { env, cwd } = options
|
|
27
|
+
const MAX_CAPTURE = 20000
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn('vercel', args, {
|
|
31
|
+
cwd: cwd || process.cwd(),
|
|
32
|
+
env: env || process.env,
|
|
33
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
let stdout = ''
|
|
37
|
+
let stderr = ''
|
|
38
|
+
|
|
39
|
+
const append = (current, chunk) => {
|
|
40
|
+
const next = current + chunk
|
|
41
|
+
return next.length > MAX_CAPTURE ? next.slice(-MAX_CAPTURE) : next
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
child.stdout.on('data', data => {
|
|
45
|
+
process.stdout.write(data)
|
|
46
|
+
stdout = append(stdout, data.toString('utf8'))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
child.stderr.on('data', data => {
|
|
50
|
+
process.stderr.write(data)
|
|
51
|
+
stderr = append(stderr, data.toString('utf8'))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
child.on('error', error => {
|
|
55
|
+
error.stdout = stdout
|
|
56
|
+
error.stderr = stderr
|
|
57
|
+
reject(error)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
child.on('close', code => {
|
|
61
|
+
if (code === 0) return resolve({ code, stdout, stderr })
|
|
62
|
+
const error = new Error(`vercel ${args[0] || ''} 失败 (exit ${code})`)
|
|
63
|
+
error.code = code
|
|
64
|
+
error.stdout = stdout
|
|
65
|
+
error.stderr = stderr
|
|
66
|
+
reject(error)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function deployPrebuiltWithFallback(options) {
|
|
72
|
+
const {
|
|
73
|
+
baseArgs,
|
|
74
|
+
env,
|
|
75
|
+
cwd,
|
|
76
|
+
run = runVercel,
|
|
77
|
+
cleanupArchiveParts = () => {
|
|
78
|
+
try {
|
|
79
|
+
execSync('rm -f .vercel/source.tgz.part*', { stdio: 'ignore' })
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
onMissingFiles = () => {
|
|
85
|
+
logger.warn('检测到 missing_files,自动使用 --archive=tgz 重试一次')
|
|
86
|
+
},
|
|
87
|
+
} = options || {}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await run(baseArgs, { env, cwd })
|
|
91
|
+
return { usedArchive: false }
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (!isMissingFilesError(e)) throw e
|
|
94
|
+
onMissingFiles(e)
|
|
95
|
+
cleanupArchiveParts()
|
|
96
|
+
const archiveArgs = baseArgs.slice()
|
|
97
|
+
archiveArgs.splice(2, 0, '--archive=tgz')
|
|
98
|
+
await run(archiveArgs, { env, cwd })
|
|
99
|
+
return { usedArchive: true }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
9
103
|
export async function deployToVercel(target, options = {}) {
|
|
10
104
|
const { environment = 'staging' } = options
|
|
11
105
|
|
|
@@ -63,7 +157,9 @@ export async function deployToVercel(target, options = {}) {
|
|
|
63
157
|
// 如果有缺失变量,统一报错并退出
|
|
64
158
|
if (missingVars.length > 0) {
|
|
65
159
|
logger.error('缺少以下 Vercel 环境变量:')
|
|
66
|
-
missingVars.forEach(v =>
|
|
160
|
+
missingVars.forEach(v => {
|
|
161
|
+
logger.error(` - ${v}`)
|
|
162
|
+
})
|
|
67
163
|
logger.info('')
|
|
68
164
|
logger.info('请在 .env.<env>.local 中配置这些变量,例如:')
|
|
69
165
|
logger.info(' VERCEL_TOKEN=<your-vercel-token>')
|
|
@@ -120,6 +216,9 @@ export async function deployToVercel(target, options = {}) {
|
|
|
120
216
|
envVars.VERCEL_ORG_ID = orgId
|
|
121
217
|
}
|
|
122
218
|
|
|
219
|
+
// 不通过 CLI args 传递 token,避免出现在错误信息/日志中
|
|
220
|
+
envVars.VERCEL_TOKEN = token
|
|
221
|
+
|
|
123
222
|
// 绕过 Vercel Git author 权限检查:临时修改最新 commit 的 author
|
|
124
223
|
const authorEmail = process.env.VERCEL_GIT_COMMIT_AUTHOR_EMAIL
|
|
125
224
|
let originalAuthor = null
|
|
@@ -138,52 +237,37 @@ export async function deployToVercel(target, options = {}) {
|
|
|
138
237
|
try {
|
|
139
238
|
// 第一步:本地构建
|
|
140
239
|
logger.step(`本地构建 ${t} (${environment})`)
|
|
141
|
-
const
|
|
142
|
-
'vercel build',
|
|
143
|
-
`--local-config="${configPath}"`,
|
|
144
|
-
'--yes',
|
|
145
|
-
`--token=${token}`,
|
|
146
|
-
]
|
|
240
|
+
const buildArgs = ['build', '--local-config', configPath, '--yes']
|
|
147
241
|
|
|
148
242
|
// staging 和 production 环境需要 --prod 标志,确保构建产物与部署环境匹配
|
|
149
243
|
if (environment === 'staging' || environment === 'production') {
|
|
150
|
-
|
|
244
|
+
buildArgs.push('--prod')
|
|
151
245
|
}
|
|
152
246
|
|
|
153
247
|
if (orgId) {
|
|
154
|
-
|
|
248
|
+
buildArgs.push('--scope', orgId)
|
|
155
249
|
}
|
|
156
250
|
|
|
157
|
-
|
|
158
|
-
stdio: 'inherit',
|
|
159
|
-
cwd: process.cwd(),
|
|
160
|
-
env: envVars,
|
|
161
|
-
})
|
|
251
|
+
await runVercel(buildArgs, { env: envVars, cwd: process.cwd() })
|
|
162
252
|
logger.success(`${t} 本地构建成功`)
|
|
163
253
|
|
|
164
254
|
// 第二步:上传预构建产物
|
|
165
255
|
logger.step(`部署 ${t} 到 Vercel (${environment})`)
|
|
166
|
-
const
|
|
167
|
-
'vercel deploy',
|
|
168
|
-
'--prebuilt',
|
|
169
|
-
`--local-config="${configPath}"`,
|
|
170
|
-
'--yes',
|
|
171
|
-
`--token=${token}`,
|
|
172
|
-
]
|
|
256
|
+
const baseDeployArgs = ['deploy', '--prebuilt', '--local-config', configPath, '--yes']
|
|
173
257
|
|
|
174
258
|
// staging 和 production 环境都添加 --prod 标志以绑定固定域名
|
|
175
259
|
if (environment === 'staging' || environment === 'production') {
|
|
176
|
-
|
|
260
|
+
baseDeployArgs.push('--prod')
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
if (orgId) {
|
|
180
|
-
|
|
264
|
+
baseDeployArgs.push('--scope', orgId)
|
|
181
265
|
}
|
|
182
266
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
cwd: process.cwd(),
|
|
267
|
+
await deployPrebuiltWithFallback({
|
|
268
|
+
baseArgs: baseDeployArgs,
|
|
186
269
|
env: envVars,
|
|
270
|
+
cwd: process.cwd(),
|
|
187
271
|
})
|
|
188
272
|
logger.success(`${t} 部署成功`)
|
|
189
273
|
|