@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.
@@ -6,186 +6,229 @@ agent: build
6
6
 
7
7
  ---
8
8
 
9
- ## Step 0: 强制安装 dx CLI
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
- pnpm add -g @ranger1/dx@latest && dx initial
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
- ## Step 1: 并行检测
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
- **同时执行以下 5 Bash 调用(真正并行):**
32
+ # 安装/升级 dx 到最新(幂等)
33
+ pnpm add -g @ranger1/dx@latest
22
34
 
23
- ```bash
24
- # 批次 1: CLI 版本检测
25
- echo "=== CLI_VERSIONS ===";
26
- echo "opencode:" && (which opencode && opencode --version 2>/dev/null || echo "NOT_FOUND");
27
- echo "dx:" && (which dx && dx --version 2>/dev/null || echo "NOT_FOUND");
28
- echo "rg:" && (which rg && rg --version 2>/dev/null | head -n 1 || echo "NOT_FOUND");
29
- echo "agent-browser:" && (which agent-browser && agent-browser --version 2>/dev/null || echo "NOT_FOUND");
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
- ```bash
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
- ```bash
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
- ```bash
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
- ```bash
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
- ## Step 2: 输出报告
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
- opencode | <状态> | <版本>
68
- dx | <状态> | <版本>
69
- rg | <状态> | <版本>
70
- python3 | <状态> | <版本>
71
- python(软链接) | <状态> | <版本>
72
- AGENTS.md | <状态> | -
73
- oh-my-opencode 插件 | <状态> | -
74
- opencode-openai-codex-auth 插件 | <状态> | -
75
- agent-browser | <状态> | <版本>
76
- attach(全局配置写入) | <状态> | -
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 3: 统一处理缺失项
82
-
83
- **如检测到任何缺失项,统一询问一次:**
84
-
85
- `AskUserQuestion`: 检测到以下缺失项,是否自动安装/配置所有?
86
-
87
- 确认后按顺序处理(需要直接执行对应安装/配置命令,不要只输出提示):
130
+ ## Step 2: 只问一次(缺失/升级)
88
131
 
89
- ### 3.1 opencode CLI 未安装
132
+ 如果出现 `NEED_FIX`,只问一次:是否一键安装 + 升级到最新版本(包含插件配置 attach)。
90
133
 
91
- 执行安装:
134
+ `AskUserQuestion`: 检测到缺失/未就绪项,是否一键修复并升级到最新版本?
92
135
 
93
- ```bash
94
- # brew 优先
95
- brew install opencode || npm install -g opencode
96
- ```
136
+ 选项:
97
137
 
98
- ### 3.2 AGENTS.md 未找到
138
+ - 一键修复(Recommended)
139
+ - 跳过(只输出检测表)
99
140
 
100
- 提示用户:
101
-
102
- - AGENTS.md 文件不存在,OpenCode 需要此文件作为项目指令入口
103
- - 建议创建或检查文件路径
141
+ ---
104
142
 
105
- ### 3.3 rg(ripgrep)未安装
143
+ ## Step 3: 一键修复(安装 + 升级到最新)
106
144
 
107
- 执行安装(幂等:已安装会跳过;缺少包管理器会报错并提示手动安装):
145
+ 确认后直接执行以下脚本(幂等;尽量走包管理器升级;插件用 attach 配置确保可自动安装/更新):
108
146
 
109
147
  ```bash
110
- set -e
148
+ set -euo pipefail
111
149
 
112
- if command -v rg >/dev/null 2>&1; then
113
- rg --version 2>/dev/null | head -n 1 || true
114
- exit 0
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
- if command -v brew >/dev/null 2>&1; then
118
- brew install ripgrep
119
- elif command -v apt-get >/dev/null 2>&1; then
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
- rg --version 2>/dev/null | head -n 1 || true
129
- ```
130
-
131
- ### 3.6 agent-browser 未安装
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
- ```bash
144
- # macOS (Homebrew)
145
- brew install python
146
- ```
164
+ # dx(始终升级到最新)
165
+ pnpm add -g @ranger1/dx@latest
147
166
 
148
- ### 3.6.2 python 命令缺失(需要软链接到 python3)
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
- 如果 `python` 不存在但 `python3` 存在,执行:
171
+ # OpenCode 模板(确保 opencode_attach.py 存在)
172
+ if [ ! -f "$HOME/.opencode/commands/opencode_attach.py" ]; then
173
+ dx initial
174
+ fi
151
175
 
152
- ```bash
153
- set -e
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 python >/dev/null 2>&1; then
156
- python --version
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
- PY3="$(command -v python3 2>/dev/null || true)"
161
- if [ -z "$PY3" ]; then
162
- echo "python3 NOT_FOUND"
163
- exit 1
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
- PY_DIR="$(dirname "$PY3")"
167
- if [ -w "$PY_DIR" ]; then
168
- ln -sf "$PY3" "$PY_DIR/python"
169
- echo "linked: $PY_DIR/python -> $PY3"
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
- ln -sf "$PY3" "$HOME/.local/bin/python"
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
- python --version
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
- ### 3.9 自动 attach(推荐)
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
- 执行 attach(会自动覆盖/新建对应节点,其它不动,并生成备份文件):
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
- ```bash
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 output = `${prefix} ${message}`
89
+ const safeMessage = sanitizeForLog(message)
90
+ const output = `${prefix} ${safeMessage}`
52
91
  console.log(output)
53
- this.writeLog('info', message)
92
+ this.writeLog('info', safeMessage)
54
93
  }
55
94
 
56
95
  success(message) {
57
- const output = `✅ ${message}`
96
+ const safeMessage = sanitizeForLog(message)
97
+ const output = `✅ ${safeMessage}`
58
98
  console.log(output)
59
- this.writeLog('success', message)
99
+ this.writeLog('success', safeMessage)
60
100
  }
61
101
 
62
102
  warn(message) {
63
- const output = `⚠️ ${message}`
103
+ const safeMessage = sanitizeForLog(message)
104
+ const output = `⚠️ ${safeMessage}`
64
105
  console.log(output)
65
- this.writeLog('warn', message)
106
+ this.writeLog('warn', safeMessage)
66
107
  }
67
108
 
68
109
  error(message) {
69
- const output = `❌ ${message}`
110
+ const safeMessage = sanitizeForLog(message)
111
+ const output = `❌ ${safeMessage}`
70
112
  console.log(output)
71
- this.writeLog('error', message)
113
+ this.writeLog('error', safeMessage)
72
114
  }
73
115
 
74
116
  debug(message) {
75
117
  if (this.logLevel === 'debug') {
76
- const output = `🐛 ${message}`
118
+ const safeMessage = sanitizeForLog(message)
119
+ const output = `🐛 ${safeMessage}`
77
120
  console.log(output)
78
- this.writeLog('debug', message)
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} ${message}`)
133
+ console.log(`🚀 ${prefix} ${safeMessage}`)
89
134
  console.log(separator)
90
135
 
91
- this.writeLog('step', `${prefix} ${message}`)
136
+ this.writeLog('step', `${prefix} ${safeMessage}`)
92
137
  }
93
138
 
94
139
  // 进度显示
95
140
  progress(message) {
96
- process.stdout.write(`⌛ ${message}...`)
97
- this.writeLog('progress', `开始: ${message}`)
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
- console.log(`💻 执行: ${command}`)
108
- this.writeLog('command', command)
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
- console.log(`\n${headers.join('\t')}`)
122
- console.log('-'.repeat(headers.join('\t').length))
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
- console.log(` ${service}: http://localhost:${port} ${url ? `(${url})` : ''}`)
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()}] ${message}\n`
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' })
@@ -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 => logger.error(` - ${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
- logger.info(curlCmd)
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
- 'vercel',
87
- 'ls',
88
- `--token=${token}`,
89
- orgId ? `--scope=${orgId}` : '',
90
- '--json',
91
- ].filter(Boolean).join(' ')
92
-
93
- const output = execSync(cmd, { encoding: 'utf8' })
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
  // 根据环境筛选部署
@@ -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 => logger.error(` - ${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 buildCmd = [
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
- buildCmd.push('--prod')
244
+ buildArgs.push('--prod')
151
245
  }
152
246
 
153
247
  if (orgId) {
154
- buildCmd.push(`--scope=${orgId}`)
248
+ buildArgs.push('--scope', orgId)
155
249
  }
156
250
 
157
- execSync(buildCmd.join(' '), {
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 deployCmd = [
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
- deployCmd.push('--prod')
260
+ baseDeployArgs.push('--prod')
177
261
  }
178
262
 
179
263
  if (orgId) {
180
- deployCmd.push(`--scope=${orgId}`)
264
+ baseDeployArgs.push('--scope', orgId)
181
265
  }
182
266
 
183
- execSync(deployCmd.join(' '), {
184
- stdio: 'inherit',
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {