huaweicloud-devkit 1.1.0-next.7 → 1.1.0-next.9
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/README.md +20 -1
- package/README.zh-CN.md +20 -1
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +222 -48
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +274 -9
- package/plugins/huaweicloud-core/src/setup-cli.mjs +46 -0
- package/plugins/huaweicloud-core/src/tools.mjs +150 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Help AI coding agents use Huawei Cloud safely and accurately — a single integration that gives agents cloud knowledge, CLI tooling, and safety guardrails.
|
|
10
10
|
|
|
11
|
-
Supports OpenCode, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
11
|
+
Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
@@ -34,6 +34,23 @@ npx --yes huaweicloud-devkit uninstall --target opencode
|
|
|
34
34
|
rm -rf ~/.npm/_npx/ # Linux/macOS only; Windows path TBD
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Codex
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx --yes huaweicloud-devkit install --target codex
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Restart the Codex session** after installation.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx --yes huaweicloud-devkit doctor --target codex
|
|
47
|
+
npx --yes huaweicloud-devkit status --target codex
|
|
48
|
+
npx --yes huaweicloud-devkit update --target codex
|
|
49
|
+
npx --yes huaweicloud-devkit uninstall --target codex
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **Requires Codex CLI** — the `codex` command must be in PATH. If Codex is installed via WindowsApps (Microsoft Store), use `--target codex-desktop` instead. Run `codex --version` to verify CLI availability.
|
|
53
|
+
|
|
37
54
|
### CodeArts Agent
|
|
38
55
|
|
|
39
56
|
```bash
|
|
@@ -113,7 +130,9 @@ npx --yes huaweicloud-devkit update --target hermes
|
|
|
113
130
|
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
131
|
```
|
|
115
132
|
|
|
133
|
+
> **Uninstall notes**: On Linux, run `rm -rf ~/.npm/_npx/* && npm cache clean --force` after uninstall to ensure a clean slate. On Windows, close all Hermes sessions first to release file locks, then after uninstall check `%LOCALAPPDATA%\hermes\config.yaml` for YAML corruption and manually remove `%LOCALAPPDATA%\hermes\huaweicloud-plugins` if any files remain.
|
|
116
134
|
> **Safety hooks**: The installer configures Hermes shell hooks (`config.yaml` → `hooks.pre_tool_call`) to intercept unsafe terminal commands such as credential file reads, environment variable dumps, and unapproved `hcloud` write operations. Hermes shows a consent prompt the first time; approve it or set `hooks_auto_accept: true` in `config.yaml` to auto-accept.
|
|
135
|
+
> **MCP Python SDK**: The installer automatically installs the `mcp` Python package required by Hermes for MCP tool discovery. If you see `[FAIL] Hermes MCP Python SDK` in `doctor`, run `pip3 install mcp` manually.
|
|
117
136
|
> **Windows**: Hermes on Windows stores data under `%LOCALAPPDATA%\hermes` instead of `~/.hermes`. The installer detects this automatically.
|
|
118
137
|
|
|
119
138
|
### OpenClaw
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
帮助 AI 编码助手安全、准确地使用华为云——一站式集成云知识、CLI 工具和安全护栏。
|
|
10
10
|
|
|
11
|
-
支持 OpenCode、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
11
|
+
支持 OpenCode、Codex、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
12
12
|
|
|
13
13
|
## 前置条件
|
|
14
14
|
|
|
@@ -34,6 +34,23 @@ npx --yes huaweicloud-devkit uninstall --target opencode
|
|
|
34
34
|
rm -rf ~/.npm/_npx/ # 仅 Linux/macOS;Windows 路径待确认
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Codex
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx --yes huaweicloud-devkit install --target codex
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
安装后**重启 Codex 会话**。
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx --yes huaweicloud-devkit doctor --target codex
|
|
47
|
+
npx --yes huaweicloud-devkit status --target codex
|
|
48
|
+
npx --yes huaweicloud-devkit update --target codex
|
|
49
|
+
npx --yes huaweicloud-devkit uninstall --target codex
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> **需要 Codex CLI** — `codex` 命令必须在 PATH 中。若 Codex 通过 WindowsApps(Microsoft Store)安装,请使用 `--target codex-desktop` 替代。运行 `codex --version` 验证 CLI 可用性。
|
|
53
|
+
|
|
37
54
|
### CodeArts Agent(码道)
|
|
38
55
|
|
|
39
56
|
```bash
|
|
@@ -113,7 +130,9 @@ npx --yes huaweicloud-devkit update --target hermes
|
|
|
113
130
|
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
131
|
```
|
|
115
132
|
|
|
133
|
+
> **卸载说明**:Linux 上卸载后执行 `rm -rf ~/.npm/_npx/* && npm cache clean --force` 确保下次全新安装。Windows 上先关闭所有 Hermes 会话(释放文件锁),卸载后检查 `%LOCALAPPDATA%\hermes\config.yaml` 是否有 YAML 损坏,如有残留文件手动删除 `%LOCALAPPDATA%\hermes\huaweicloud-plugins`。
|
|
116
134
|
> **安全钩子(Safety hooks)**:安装器会在 `config.yaml` 中写入 shell hooks 配置(`hooks.pre_tool_call`),拦截不安全的终端命令,如读取凭据文件、导出环境变量、未审批的 `hcloud` 写操作。Hermes 首次使用时会弹出同意提示,可批准或设置 `hooks_auto_accept: true` 自动批准。
|
|
135
|
+
> **MCP Python SDK**:安装器会自动安装 Hermes 所需的 `mcp` Python 包。如果 doctor 显示 `[FAIL] Hermes MCP Python SDK`,手动执行 `pip3 install mcp`。
|
|
117
136
|
> **Windows**:Hermes 在 Windows 上数据存储在 `%LOCALAPPDATA%\hermes` 而非 `~/.hermes`,安装器会自动检测适配。
|
|
118
137
|
|
|
119
138
|
### OpenClaw
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.9",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.0-next.
|
|
20
|
+
"version": "1.1.0-next.9",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.0-next.
|
|
23
|
+
"version": "1.1.0-next.9",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.0-next.
|
|
20
|
+
"version": "1.1.0-next.9",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.9",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.9",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.0-next.
|
|
5
|
+
"version": "1.1.0-next.9",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -52,6 +52,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
52
52
|
| `huaweicloud_sandbox_exec_one_shot` | One-shot execution (fresh connection; best for long/heavy commands) |
|
|
53
53
|
| `huaweicloud_sandbox_upload_file` | Upload a local file into the sandbox (chunked base64 write + md5 verify) |
|
|
54
54
|
| `huaweicloud_sandbox_upload_project` | Upload a local project directory to sandbox (HTTP tunnel, tar.gz + extract) |
|
|
55
|
+
| `huaweicloud_sandbox_deploy_nginx` | Deploy nginx config with permissions fix and reload in one call |
|
|
56
|
+
| `huaweicloud_sandbox_deploy_check` | Run deployment completeness check (nginx, DevBridge, URL, QR if needed) |
|
|
55
57
|
| `huaweicloud_sandbox_close_session` | Close a persistent terminal session |
|
|
56
58
|
|
|
57
59
|
### Tool Selection Guide
|
|
@@ -63,6 +65,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
63
65
|
| `curl`, health checks, quick tests | Either — `exec_one_shot` preferred | Stateless, fast |
|
|
64
66
|
| Server startup (background) | `exec_with_session` | Need to `nohup ... &` then check output in same session |
|
|
65
67
|
| Deployment scripts | `exec_one_shot+shot` | Long script, fresh connection avoids session timeouts |
|
|
68
|
+
| nginx configuration | `deploy_nginx` | Auto-generates correct template + permissions + reload |
|
|
69
|
+
| Deployment completeness check | `deploy_check` | Verifies nginx, DevBridge, URL, QR before reporting success |
|
|
66
70
|
| Single file upload (<1MB) | `upload_file` | Base64 chunked, reliable for small files |
|
|
67
71
|
| Project directory upload (>1MB) | `upload_project` | HTTP tunnel, much faster than base64 for multi-file projects |
|
|
68
72
|
|
|
@@ -74,6 +78,12 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
74
78
|
|
|
75
79
|
**Session recovery**: if `exec_with_session` returns `session is not ready`, the WebSocket connection has dropped. Do NOT retry the same session — fall back to `exec_one_shot` for that command instead. To recover state (cd, env vars), reconstruct them explicitly in the one-shot command.
|
|
76
80
|
|
|
81
|
+
**Timeout recovery**: if `exec_one_shot` returns a timeout error, check whether partial output is available before declaring failure:
|
|
82
|
+
|
|
83
|
+
- For build commands: check `tail -30 /tmp/build.log` — the build may have completed but the tee pipe didn't flush before timeout
|
|
84
|
+
- For long scripts: split into independent `exec_one_shot` calls (max 5 sub-commands per call, 15s timeout per call)
|
|
85
|
+
- Do NOT retry the same composite command — split and retry individual steps
|
|
86
|
+
|
|
77
87
|
## Workflow
|
|
78
88
|
|
|
79
89
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
@@ -90,14 +100,22 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
90
100
|
|
|
91
101
|
```bash
|
|
92
102
|
# Remove stale nginx configs from previous deployments
|
|
93
|
-
sudo rm -f /etc/nginx/conf.d
|
|
103
|
+
sudo rm -f /etc/nginx/conf.d/*.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
|
|
94
104
|
# Remove stale DevBridge tunnels
|
|
95
105
|
devbridge delete-all 2>/dev/null || true
|
|
96
106
|
# Reload nginx to apply
|
|
97
107
|
sudo nginx -s reload 2>/dev/null || true
|
|
98
108
|
```
|
|
99
109
|
|
|
100
|
-
7. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
|
|
110
|
+
7. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox. After injection, credentials are also written to `/tmp/hw_creds.sh` for shell access:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Source credentials in any sandbox shell before using hcloud/devbridge:
|
|
114
|
+
source /tmp/hw_creds.sh 2>/dev/null && echo "Credentials loaded"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If `source /tmp/hw_creds.sh` returns empty, the credentials injection may not have completed — re-run `huaweicloud_sandbox_credentials`.
|
|
118
|
+
|
|
101
119
|
8. **Execute commands**: use `huaweicloud_sandbox_exec_with_session` for interactive work (cd, env setup), or `huaweicloud_sandbox_exec_one_shot` for long-running builds/installs. See [Tool Selection Guide](#tool-selection-guide).
|
|
102
120
|
9. **Upload project** (if deploying local code): `huaweicloud_sandbox_upload_project` to transfer a local project directory into `/workspace/<name>` — uses HTTP tunnel, much faster than base64 for multi-file projects. Use `huaweicloud_sandbox_upload_file` for single files.
|
|
103
121
|
10. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer
|
|
@@ -184,26 +202,48 @@ fi
|
|
|
184
202
|
export PATH=$PATH:$HOME/.huawei/bin # installer only writes ~/.bashrc; session shells do not re-source it
|
|
185
203
|
```
|
|
186
204
|
|
|
187
|
-
**Login** (non-interactive
|
|
205
|
+
**Login** (non-interactive; credentials injected by `huaweicloud_sandbox_credentials` are available at `/tmp/hw_creds.sh`):
|
|
188
206
|
|
|
189
207
|
```bash
|
|
190
|
-
|
|
208
|
+
source /tmp/hw_creds.sh 2>/dev/null
|
|
209
|
+
devbridge auth login --huaweicloud --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY"
|
|
191
210
|
```
|
|
192
211
|
|
|
193
212
|
- The `--huaweicloud` flag is required for AK/SK login; without it the CLI tries an interactive browser login, which fails in the sandbox.
|
|
194
|
-
-
|
|
213
|
+
- Credentials are stored in `/tmp/hw_creds.sh` (chmod 600) — source it before login, never echo the values.
|
|
214
|
+
- Verify with `devbridge auth status`. If `$HW_ACCESS_KEY` is empty, ensure `huaweicloud_sandbox_credentials` was called first.
|
|
195
215
|
|
|
196
216
|
**Expose** (run the web server and the tunnel in the background, then read the URL from the log; the app lives in the workspace mount, e.g. `/workspace/<repo-name>`):
|
|
197
217
|
|
|
198
218
|
```bash
|
|
199
|
-
# 0. Pre-cleanup:
|
|
219
|
+
# 0. Pre-cleanup: kill old processes and stale tunnels
|
|
220
|
+
pkill -f "devbridge host" 2>/dev/null || true
|
|
221
|
+
sleep 2
|
|
200
222
|
devbridge delete-all 2>/dev/null || true
|
|
201
223
|
|
|
202
224
|
# 1. Start tunnel
|
|
203
225
|
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
204
226
|
sleep 10 && cat /tmp/host.log
|
|
227
|
+
|
|
228
|
+
# 2. Extract tunnel URL and health-check before returning
|
|
229
|
+
TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
|
|
230
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
|
|
231
|
+
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then
|
|
232
|
+
echo "Tunnel verified: $TUNNEL_URL (HTTP $HTTP_CODE)"
|
|
233
|
+
else
|
|
234
|
+
echo "WARN: Tunnel URL unreachable (HTTP $HTTP_CODE). Rebuilding tunnel..."
|
|
235
|
+
pkill -f "devbridge host" && sleep 2
|
|
236
|
+
devbridge delete-all 2>/dev/null || true
|
|
237
|
+
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
238
|
+
sleep 10
|
|
239
|
+
TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
|
|
240
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
|
|
241
|
+
echo "Retry tunnel: $TUNNEL_URL (HTTP $HTTP_CODE)"
|
|
242
|
+
fi
|
|
205
243
|
```
|
|
206
244
|
|
|
245
|
+
**Never return a tunnel URL without verifying it first** — a stale URL (from a killed tunnel process) will silently fail. Always curl-check before giving the URL to the developer.
|
|
246
|
+
|
|
207
247
|
**Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:
|
|
208
248
|
|
|
209
249
|
```bash
|
|
@@ -325,27 +365,24 @@ If nginx cannot be installed, skip to Python HTTP server fallback (see `referenc
|
|
|
325
365
|
|
|
326
366
|
#### 3c: Verify remaining tools
|
|
327
367
|
|
|
328
|
-
Before installing project dependencies, verify the sandbox has the required runtime tools. Run
|
|
368
|
+
Before installing project dependencies, verify the sandbox has the required runtime tools. **Run each check as a separate `exec_one_shot` call with 15s timeout** — do not bundle all checks into one command. A single hung subcommand (e.g., `make --version` or `hugo version`) will timeout the entire check, blocking deployment:
|
|
329
369
|
|
|
330
|
-
```bash
|
|
331
|
-
# Core tools (expected pre-installed in sandbox image)
|
|
332
|
-
echo "=== Checking core tools ==="
|
|
333
|
-
if command -v node >/dev/null 2>&1; then node --version; else echo "MISSING: node"; fi
|
|
334
|
-
if command -v npm >/dev/null 2>&1; then npm --version; else echo "MISSING: npm"; fi
|
|
335
|
-
if command -v nginx >/dev/null 2>&1; then nginx -v 2>&1; else echo "MISSING: nginx"; fi
|
|
336
|
-
if command -v git >/dev/null 2>&1; then git --version; else echo "MISSING: git"; fi
|
|
337
|
-
if command -v python3 >/dev/null 2>&1; then python3 --version; else echo "MISSING: python3"; fi
|
|
338
|
-
if command -v curl >/dev/null 2>&1; then curl --version | head -1; else echo "MISSING: curl"; fi
|
|
339
|
-
if command -v wget >/dev/null 2>&1; then wget --version | head -1; else echo "MISSING: wget"; fi
|
|
340
|
-
if command -v make >/dev/null 2>&1; then make --version | head -1; else echo "MISSING: make"; fi
|
|
341
|
-
|
|
342
|
-
# Framework-specific tools (install on demand)
|
|
343
|
-
echo "=== Checking framework tools ==="
|
|
344
|
-
if command -v pnpm >/dev/null 2>&1; then pnpm --version; else echo "MISSING: pnpm"; fi
|
|
345
|
-
if command -v yarn >/dev/null 2>&1; then yarn --version; else echo "MISSING: yarn"; fi
|
|
346
|
-
if command -v hugo >/dev/null 2>&1; then hugo version; else echo "MISSING: hugo"; fi
|
|
347
|
-
if command -v devbridge >/dev/null 2>&1; then devbridge version; else echo "MISSING: devbridge"; fi
|
|
348
370
|
```
|
|
371
|
+
Check 1: node --version (timeout: 15s)
|
|
372
|
+
Check 2: npm --version (timeout: 15s)
|
|
373
|
+
Check 3: nginx -v 2>&1 (timeout: 15s)
|
|
374
|
+
Check 4: git --version (timeout: 15s)
|
|
375
|
+
Check 5: python3 --version (timeout: 15s)
|
|
376
|
+
Check 6: curl --version | head -1 (timeout: 15s)
|
|
377
|
+
Check 7: wget --version | head -1 (timeout: 15s)
|
|
378
|
+
Check 8: make --version | head -1 (timeout: 15s)
|
|
379
|
+
Check 9: pnpm --version (timeout: 15s)
|
|
380
|
+
Check 10: yarn --version (timeout: 15s)
|
|
381
|
+
Check 11: hugo version (timeout: 15s)
|
|
382
|
+
Check 12: devbridge version (timeout: 15s)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
For each check, parse the output: if stdout contains `MISSING:` or the tool wasn't found, install it. **Skip framework-specific tools not needed for the current project** (e.g., skip Hugo for React apps).
|
|
349
386
|
|
|
350
387
|
**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`. Use OS-aware commands:
|
|
351
388
|
|
|
@@ -395,6 +432,18 @@ cd /workspace/<dirname> && [ -d node_modules ] && echo "SKIP: node_modules exist
|
|
|
395
432
|
|
|
396
433
|
Wait for install to complete. For large projects on aarch64 sandboxes (1000+ packages), set `timeout_ms` to 180000 (3 min).
|
|
397
434
|
|
|
435
|
+
**Node version compatibility**: if `npm install` fails with native module errors (e.g., `rollup 4`, `@esbuild`, `node-gyp`), check the Node version:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
node -v
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Node v24+ uses musl-based binaries on some sandbox images, which may break native addons built for glibc. If native modules fail:
|
|
442
|
+
|
|
443
|
+
- Try `npm install --force` or `npm install --legacy-peer-deps`
|
|
444
|
+
- For rollup 4 projects, consider `npm install rollup@3` as fallback
|
|
445
|
+
- If webpack/rollup native addon errors persist, add `--ignore-scripts` then manually rebuild: `npm rebuild`
|
|
446
|
+
|
|
398
447
|
#### 4c: Build
|
|
399
448
|
|
|
400
449
|
**Timeout strategy by framework type:**
|
|
@@ -424,6 +473,20 @@ grep -r "outDir\|outputDir\|dest\|distDir" /workspace/<dirname>/.vitepress/confi
|
|
|
424
473
|
|
|
425
474
|
If a custom outDir is found, use that instead of the framework-detected default for all subsequent checks.
|
|
426
475
|
|
|
476
|
+
**Post-build output verification**: after a successful build, verify the actual `index.html` location. Framework-returned `outputDir` may be inaccurate (e.g., uni-app v3 framework: `dist`, actual: `dist/build/h5`):
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
# Find the real index.html after build
|
|
480
|
+
REAL_INDEX=$(find /workspace/<dirname>/<outputDir> -name "index.html" -type f 2>/dev/null | head -1)
|
|
481
|
+
if [ -n "$REAL_INDEX" ] && [ -f "$REAL_INDEX" ]; then
|
|
482
|
+
REAL_OUTDIR=$(dirname "$REAL_INDEX")
|
|
483
|
+
echo "Actual output dir: $REAL_OUTDIR"
|
|
484
|
+
# Use REAL_OUTDIR for nginx config instead of framework-reported outputDir
|
|
485
|
+
fi
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
If `REAL_OUTDIR` differs from `<outputDir>`, use `REAL_OUTDIR` for all subsequent steps (nginx config, port check, etc.).
|
|
489
|
+
|
|
427
490
|
**Post-timeout recovery**: if `exec_one_shot` returns a timeout error (Request timed out), do NOT fail immediately. First dump any captured build log, then check the output directory:
|
|
428
491
|
|
|
429
492
|
```bash
|
|
@@ -468,6 +531,62 @@ tail -5 /tmp/build.log 2>/dev/null
|
|
|
468
531
|
- For Hugo/static sites where `installCmd` is `null`, skip install entirely.
|
|
469
532
|
- For static sites where `buildCmd` is `null`, skip build entirely.
|
|
470
533
|
|
|
534
|
+
#### 4c-aux: Build Failure Response
|
|
535
|
+
|
|
536
|
+
Build failures fall into two categories. Handle them differently:
|
|
537
|
+
|
|
538
|
+
| Failure Type | Behavior |
|
|
539
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
540
|
+
| **Timeout** (timed out) | Check `/tmp/build.log` tail and output directory — build may have completed but `tee` pipe didn't flush. See Post-timeout recovery above. |
|
|
541
|
+
| **Non-zero exit code** | **STOP immediately.** The build engine explicitly rejected the output. Do NOT retry, modify source, or tweak env vars. Follow the procedure below. |
|
|
542
|
+
|
|
543
|
+
**When a build exits with non-zero exit code:**
|
|
544
|
+
|
|
545
|
+
1. **STOP** — do NOT retry, do NOT modify source code, do NOT change environment variables
|
|
546
|
+
2. **Extract the error** from `/tmp/build.log`:
|
|
547
|
+
```bash
|
|
548
|
+
tail -30 /tmp/build.log
|
|
549
|
+
```
|
|
550
|
+
3. **Present the failure to the developer** with:
|
|
551
|
+
- The app name and build command that failed
|
|
552
|
+
- The key error message (last meaningful lines from the build log)
|
|
553
|
+
- A brief diagnosis of the likely cause
|
|
554
|
+
4. **Offer fix options** (1-3 choices) and **wait for the developer to choose** before applying any fix:
|
|
555
|
+
- Never modify project source files (configs, scripts, etc.) without explicit approval
|
|
556
|
+
- If the fix requires editing source code, tell the developer what to change and where
|
|
557
|
+
5. **After the developer selects a fix**, apply it, then restart only the failed build — do NOT rebuild already-succeeded apps
|
|
558
|
+
|
|
559
|
+
| Rule | Rationale |
|
|
560
|
+
| ---------------------- | -------------------------------------------------------------- |
|
|
561
|
+
| No blind retry | Retrying without diagnosis wastes time and obscures real error |
|
|
562
|
+
| No silent source edits | Modifying project files without consent destroys trust |
|
|
563
|
+
| Developer decides fix | Different projects have different fix preferences |
|
|
564
|
+
| Only rebuild failed | Avoid redundant work in monorepo deployments |
|
|
565
|
+
|
|
566
|
+
**Monorepo-specific**: if one sub-app build fails while others succeed, report the failure immediately — do NOT delay until all builds complete. Continue other builds in parallel if possible, but notify the developer as soon as a failure is detected.
|
|
567
|
+
|
|
568
|
+
#### 4d: Fix Build Output Permissions
|
|
569
|
+
|
|
570
|
+
After a successful build, fix directory traverse permissions on the build output. Build tools (webpack, vite, uni-app) may create directories with restrictive permissions that block nginx from traversing to `index.html`. **Always resolve symlinks first** — `chmod` does not follow symlinks on Linux:
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
# Fix directory execute permissions for nginx traverse
|
|
574
|
+
REAL_OUTDIR="<outputDir>" # use actual output dir from post-build verification
|
|
575
|
+
PROJECT_ROOT="/workspace/<dirname>"
|
|
576
|
+
|
|
577
|
+
# Resolve symlinks — monorepo sub-apps may be symlinked
|
|
578
|
+
REAL_ROOT=$(readlink -f "$PROJECT_ROOT" 2>/dev/null || echo "$PROJECT_ROOT")
|
|
579
|
+
REL_OUTDIR=$(echo "$REAL_OUTDIR" | sed "s|$PROJECT_ROOT/||")
|
|
580
|
+
REAL_OUTDIR="${REAL_ROOT}/${REL_OUTDIR}"
|
|
581
|
+
|
|
582
|
+
# Fix permissions on resolved real paths
|
|
583
|
+
find "$REAL_ROOT" -path "*/${REAL_OUTDIR}" -prune -o -type d -exec chmod o+x {} \; 2>/dev/null || true
|
|
584
|
+
chmod -R o+rX "$REAL_ROOT" 2>/dev/null || true
|
|
585
|
+
chmod -R +x "$REAL_ROOT/node_modules/.bin" 2>/dev/null || true
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
This prevents the most common deployment failure: nginx 500 with `stat() ... Permission denied` caused by missing `o+x` on intermediate directories.
|
|
589
|
+
|
|
471
590
|
### Step 5: Port Availability Check
|
|
472
591
|
|
|
473
592
|
**Before configuring nginx or starting the app**, verify the target ports are free. Port conflicts from previous deployments cause silent failures:
|
|
@@ -518,17 +637,33 @@ If the port cannot be freed (different user/process), increment to the next avai
|
|
|
518
637
|
|
|
519
638
|
#### Configure Nginx
|
|
520
639
|
|
|
521
|
-
|
|
640
|
+
Use `huaweicloud_sandbox_deploy_nginx` to write the correct template, fix directory permissions, and reload nginx — all in one call:
|
|
641
|
+
|
|
642
|
+
```json
|
|
643
|
+
{
|
|
644
|
+
"nginx_type": "<nginxType>",
|
|
645
|
+
"port": <port>,
|
|
646
|
+
"project": "<dirname>",
|
|
647
|
+
"output_dir": "<outputDir>",
|
|
648
|
+
"node_port": <nodePort>,
|
|
649
|
+
"public_port": <publicPort>
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- `nginx_type` — `spa` (SPA/SSG/cross-platform), `proxy` (SSR), or `static` (Hugo/Hexo) from `detect_framework`
|
|
654
|
+
- `port` — listen port from framework detection
|
|
655
|
+
- `project` — project dir name under `/workspace`
|
|
656
|
+
- `output_dir` — build output dir relative to `/workspace/<project>`
|
|
657
|
+
- `node_port` — Node.js app port (required only when `nginx_type=proxy`)
|
|
658
|
+
- `public_port` — public listen port for SSR proxy (optional, defaults to `port`)
|
|
522
659
|
|
|
523
|
-
|
|
524
|
-
| --------- | ----------------------- | --------------------------- |
|
|
525
|
-
| `spa` | Template 1 (try_files) | SPA, SSG, cross-platform H5 |
|
|
526
|
-
| `proxy` | Template 2 (proxy_pass) | SSR (Next.js, Nuxt) |
|
|
527
|
-
| `static` | Template 3 (plain root) | Hugo, Hexo, static sites |
|
|
660
|
+
The tool automatically handles:
|
|
528
661
|
|
|
529
|
-
|
|
662
|
+
- Writing the correct nginx template (SPA try_files, SSR reverse proxy, or static)
|
|
663
|
+
- Fixing `o+x` directory traverse permissions on the project path
|
|
664
|
+
- Reloading nginx
|
|
530
665
|
|
|
531
|
-
|
|
666
|
+
If the tool returns `ok: false`, nginx may not be installed — fall back to Python HTTP server (see `references/nginx-templates.md`).
|
|
532
667
|
|
|
533
668
|
**Verify nginx is serving** — curl-check the app before proceeding to DevBridge:
|
|
534
669
|
|
|
@@ -538,18 +673,18 @@ curl -s -o /dev/null -w "nginx status: %{http_code}\n" http://localhost:<port>
|
|
|
538
673
|
|
|
539
674
|
If the status code is not 2xx/3xx:
|
|
540
675
|
|
|
541
|
-
- **403** —
|
|
676
|
+
- **403** — run `chmod -R o+rX /workspace/<project>/<outputDir>` and re-test
|
|
542
677
|
- **000 (connection refused)** — nginx not listening: check `sudo nginx -t` for config errors
|
|
543
678
|
- **Other** — check nginx error log: `sudo tail -20 /var/log/nginx/error.log`
|
|
544
679
|
|
|
545
680
|
> If `curl` is unavailable, check port via `lsof -i :<port>` or `netstat -tlnp | grep :<port>`
|
|
546
681
|
|
|
547
|
-
### Step 6: Start the App
|
|
682
|
+
### Step 6: Start the App [REQUIRED]
|
|
548
683
|
|
|
549
684
|
- **Static (SPA/SSG/cross-platform)**: nginx is already serving. Skip.
|
|
550
685
|
- **SSR**: run `<serveCmd>` via `exec_with_session` to start the Node process in background.
|
|
551
686
|
|
|
552
|
-
### Step 7: Expose via DevBridge
|
|
687
|
+
### Step 7: Expose via DevBridge [REQUIRED — deployment incomplete without this]
|
|
553
688
|
|
|
554
689
|
Follow the standard [Expose the deployed app](#expose-the-deployed-app-public-url) procedure. The app is already running on the detected port — only DevBridge tunnel setup is needed.
|
|
555
690
|
|
|
@@ -557,29 +692,64 @@ Use `exec_with_session` to background DevBridge. For SSR, DevBridge tunnels the
|
|
|
557
692
|
|
|
558
693
|
**Pre-flight**: always run `devbridge delete-all` before creating a new tunnel to prevent `10006: quota exceeded` from accumulated stale tunnels. If you still get quota error, list tunnels with `devbridge ls --all`, delete stale ones, and retry.
|
|
559
694
|
|
|
560
|
-
|
|
695
|
+
Extract the tunnel URL from DevBridge output. The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com`. **Return this URL to the developer as the deployment result.**
|
|
561
696
|
|
|
562
|
-
|
|
697
|
+
#### Cross-platform H5 QR code
|
|
563
698
|
|
|
564
|
-
**
|
|
699
|
+
**If `detect_framework` returned `type: "cross-platform"` (Taro, uni-app)**, after exposing the tunnel, generate a QR code for mobile scanning:
|
|
565
700
|
|
|
566
701
|
```bash
|
|
567
702
|
TUNNEL_URL="<extracted-tunnel-url>"
|
|
568
703
|
|
|
569
|
-
#
|
|
570
|
-
curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$TUNNEL_URL")" -o /workspace
|
|
571
|
-
chmod o+r /workspace
|
|
572
|
-
echo "QR code
|
|
704
|
+
# Generate QR inside nginx-served output directory so it is accessible via the tunnel URL
|
|
705
|
+
curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$TUNNEL_URL")" -o /workspace/<dirname>/<outputDir>/qr.png
|
|
706
|
+
chmod o+r /workspace/<dirname>/<outputDir>/qr.png
|
|
707
|
+
echo "QR code URL: ${TUNNEL_URL}/qr.png"
|
|
573
708
|
echo "Desktop URL: $TUNNEL_URL"
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**Do NOT use `qrencode -t ANSI256`** — terminal ANSI/ASCII QR codes have low precision and phone cameras cannot scan them. Also, never save the QR image outside the nginx root (e.g., `/workspace/qr.png`) — it must be inside the output directory so it is served by nginx alongside the app.
|
|
574
712
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
713
|
+
**After generating the QR code**, return both URLs to the developer:
|
|
714
|
+
|
|
715
|
+
```
|
|
716
|
+
桌面访问: <tunnel-url>
|
|
717
|
+
手机扫码: <tunnel-url>/qr.png
|
|
578
718
|
```
|
|
579
719
|
|
|
580
|
-
|
|
720
|
+
#### Deployment Completion Check [REQUIRED]
|
|
721
|
+
|
|
722
|
+
**Before reporting success, call `huaweicloud_sandbox_deploy_check`** to verify the deployment is complete:
|
|
723
|
+
|
|
724
|
+
```json
|
|
725
|
+
{
|
|
726
|
+
"port": <port>,
|
|
727
|
+
"project": "<dirname>",
|
|
728
|
+
"output_dir": "<outputDir>",
|
|
729
|
+
"framework_type": "<type>"
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The tool checks:
|
|
734
|
+
|
|
735
|
+
- **nginx_serving** — nginx responds with 2xx/3xx on the app port
|
|
736
|
+
- **output_dir** — build output directory exists and is non-empty
|
|
737
|
+
- **devbridge_tunnel** — DevBridge tunnel is active
|
|
738
|
+
- **tunnel_url_accessible** — tunnel URL returns 200/304
|
|
739
|
+
- **qr_code** (cross-platform only) — QR image exists in output dir
|
|
740
|
+
|
|
741
|
+
Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
|
|
742
|
+
|
|
743
|
+
**If `complete` is false, follow `nextStep` to resolve before reporting success.** Do not return a deployment URL until `complete: true`.
|
|
744
|
+
|
|
745
|
+
| Framework Type | Must Return |
|
|
746
|
+
| --------------------------- | ------------------------- |
|
|
747
|
+
| `cross-platform` | Desktop URL + QR code URL |
|
|
748
|
+
| `spa` / `ssg` / `static` | Desktop URL |
|
|
749
|
+
| `ssr` | Desktop URL |
|
|
750
|
+
| `monorepo` (cross-platform) | Desktop URL + QR code URL |
|
|
581
751
|
|
|
582
|
-
|
|
752
|
+
**If the framework is cross-platform and the QR code was not generated, the deployment is incomplete — go back and generate it before reporting success.**
|
|
583
753
|
|
|
584
754
|
## References
|
|
585
755
|
|
|
@@ -597,6 +767,8 @@ Return both the QR code and the URL to the developer. For cross-platform apps, m
|
|
|
597
767
|
| CLI PATH | The installer only writes `~/.bashrc`; run `export PATH=$PATH:$HOME/.huawei/bin` in the session before using `devbridge` |
|
|
598
768
|
| Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
|
|
599
769
|
| Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
|
|
770
|
+
| Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |
|
|
771
|
+
| Call deploy_check before success | Always call `huaweicloud_sandbox_deploy_check` before reporting deployment success. A green nginx status does not mean the tunnel is accessible — verify end-to-end with the tool. |
|
|
600
772
|
| Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
|
|
601
773
|
| Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide). |
|
|
602
774
|
| Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
|
|
@@ -607,6 +779,8 @@ Return both the QR code and the URL to the developer. For cross-platform apps, m
|
|
|
607
779
|
| Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
|
|
608
780
|
| Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
|
|
609
781
|
| Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
|
|
782
|
+
| Cross-platform needs QR code | When `detect_framework` returns `type: "cross-platform"` (Taro, uni-app), generating a QR code image is **mandatory** — the deployment is incomplete without it. Check the Deployment Completion Check table in Step 7. |
|
|
783
|
+
| Build fails do NOT auto-fix | When a build exits with non-zero exit code, STOP and present the error + fix options to the developer. Do not silently retry, modify configs, or change source files without explicit approval. See 4c-aux. |
|
|
610
784
|
|
|
611
785
|
## Node.js in the sandbox
|
|
612
786
|
|
|
@@ -299,15 +299,34 @@ async function createTarGz(localDir, exclude = []) {
|
|
|
299
299
|
mkdirSync(archiveDir, { recursive: true });
|
|
300
300
|
const archivePath = join(archiveDir, archiveName);
|
|
301
301
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
'
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
302
|
+
const hasGit = existsSync(join(localDir, '.git'));
|
|
303
|
+
if (hasGit) {
|
|
304
|
+
await execFileAsync('git', [
|
|
305
|
+
'-C',
|
|
306
|
+
localDir,
|
|
307
|
+
'archive',
|
|
308
|
+
'--format=tar.gz',
|
|
309
|
+
`--prefix=${basename(localDir)}/`,
|
|
310
|
+
`--output=${archivePath}`,
|
|
311
|
+
'HEAD',
|
|
312
|
+
]);
|
|
313
|
+
} else {
|
|
314
|
+
const args = [];
|
|
315
|
+
for (const pattern of exclude) {
|
|
316
|
+
if (pattern.startsWith('**/')) {
|
|
317
|
+
const base = pattern.slice(3);
|
|
318
|
+
for (let depth = 0; depth <= 4; depth++) {
|
|
319
|
+
const prefix = depth === 0 ? '' : '*/'.repeat(depth);
|
|
320
|
+
args.push('--exclude', `${prefix}${base}`);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
args.push('--exclude', pattern);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
|
|
327
|
+
await execFileAsync('tar', args);
|
|
328
|
+
}
|
|
329
|
+
|
|
311
330
|
return archivePath;
|
|
312
331
|
}
|
|
313
332
|
|
|
@@ -637,6 +656,22 @@ export async function uploadProjectWithSession(
|
|
|
637
656
|
username,
|
|
638
657
|
timeoutMs,
|
|
639
658
|
);
|
|
659
|
+
try {
|
|
660
|
+
await execWithSession(
|
|
661
|
+
workspaceId,
|
|
662
|
+
[
|
|
663
|
+
`REAL_PATH=$(readlink -f "${targetParentDir}/${projectName}" 2>/dev/null || echo "${targetParentDir}/${projectName}")`,
|
|
664
|
+
`if [ ! -d "$REAL_PATH" ] && [ -d "${targetParentDir}" ]; then`,
|
|
665
|
+
` REAL_PATH="${targetParentDir}/${projectName}"`,
|
|
666
|
+
`fi`,
|
|
667
|
+
`chmod -R o+rX "$REAL_PATH" 2>/dev/null || true`,
|
|
668
|
+
`find "$REAL_PATH" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
669
|
+
`find "$REAL_PATH" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
|
|
670
|
+
].join('\n'),
|
|
671
|
+
username,
|
|
672
|
+
15000,
|
|
673
|
+
);
|
|
674
|
+
} catch {}
|
|
640
675
|
}
|
|
641
676
|
|
|
642
677
|
try {
|
|
@@ -655,6 +690,236 @@ export async function uploadProjectWithSession(
|
|
|
655
690
|
};
|
|
656
691
|
}
|
|
657
692
|
|
|
693
|
+
export async function deployNginx(
|
|
694
|
+
workspaceId,
|
|
695
|
+
{ nginxType, port, project, outputDir, nodePort, publicPort, configName },
|
|
696
|
+
username = 'root',
|
|
697
|
+
timeoutMs = 30000,
|
|
698
|
+
) {
|
|
699
|
+
if (!workspaceId) {
|
|
700
|
+
throw new Error('sandbox deploy nginx: workspace_id is required.');
|
|
701
|
+
}
|
|
702
|
+
if (!nginxType || !port || !project || !outputDir) {
|
|
703
|
+
throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const projectPath = `/workspace/${project}`;
|
|
707
|
+
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
708
|
+
|
|
709
|
+
const resolveScript = `REAL_PROJECT=$(readlink -f "${projectPath}" 2>/dev/null || echo "${projectPath}")
|
|
710
|
+
REAL_OUTPUT="${outputPath}"
|
|
711
|
+
if [ "\${REAL_PROJECT}" != "${projectPath}" ]; then
|
|
712
|
+
REL_OUTPUT=$(echo "${outputDir}" | sed "s|${projectPath}/||")
|
|
713
|
+
REAL_OUTPUT="\${REAL_PROJECT}/\${REL_OUTPUT}"
|
|
714
|
+
fi`;
|
|
715
|
+
|
|
716
|
+
const templates = {
|
|
717
|
+
spa: `server {
|
|
718
|
+
listen ${port};
|
|
719
|
+
root ${outputPath};
|
|
720
|
+
index index.html;
|
|
721
|
+
|
|
722
|
+
location / {
|
|
723
|
+
try_files $uri /index.html;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
727
|
+
expires 1h;
|
|
728
|
+
add_header Cache-Control "public, immutable";
|
|
729
|
+
}
|
|
730
|
+
}`,
|
|
731
|
+
proxy: `server {
|
|
732
|
+
listen ${publicPort || port};
|
|
733
|
+
server_name _;
|
|
734
|
+
|
|
735
|
+
location / {
|
|
736
|
+
proxy_pass http://127.0.0.1:${nodePort};
|
|
737
|
+
proxy_http_version 1.1;
|
|
738
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
739
|
+
proxy_set_header Connection 'upgrade';
|
|
740
|
+
proxy_set_header Host $host;
|
|
741
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
742
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
743
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
744
|
+
proxy_cache_bypass $http_upgrade;
|
|
745
|
+
proxy_read_timeout 60s;
|
|
746
|
+
}
|
|
747
|
+
}`,
|
|
748
|
+
static: `server {
|
|
749
|
+
listen ${port};
|
|
750
|
+
root ${outputPath};
|
|
751
|
+
index index.html;
|
|
752
|
+
|
|
753
|
+
location / {
|
|
754
|
+
autoindex off;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
758
|
+
expires 1h;
|
|
759
|
+
add_header Cache-Control "public, immutable";
|
|
760
|
+
}
|
|
761
|
+
}`,
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const config = templates[nginxType];
|
|
765
|
+
if (!config) {
|
|
766
|
+
throw new Error(`sandbox deploy nginx: unknown nginxType "${nginxType}". Must be one of: spa, proxy, static`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const cmd = [
|
|
770
|
+
resolveScript,
|
|
771
|
+
`sudo mkdir -p /etc/nginx/conf.d`,
|
|
772
|
+
`sudo tee /etc/nginx/conf.d/${configName || 'app'}.conf > /dev/null << 'NGINX_EOF'`,
|
|
773
|
+
config,
|
|
774
|
+
`NGINX_EOF`,
|
|
775
|
+
`# Resolve symlinks for chmod (chmod does not follow symlinks on Linux)`,
|
|
776
|
+
`chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
|
|
777
|
+
`find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
778
|
+
`find "$REAL_PROJECT" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
|
|
779
|
+
`if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null; else sudo nginx; fi`,
|
|
780
|
+
].join('\n');
|
|
781
|
+
|
|
782
|
+
const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
|
|
783
|
+
|
|
784
|
+
let tunnelActive = false;
|
|
785
|
+
try {
|
|
786
|
+
const tunnelCheck = await execOneShot(
|
|
787
|
+
workspaceId,
|
|
788
|
+
'devbridge ls 2>/dev/null | grep -q "Tunnel URL" && echo "ACTIVE" || echo "INACTIVE"',
|
|
789
|
+
username,
|
|
790
|
+
10000,
|
|
791
|
+
);
|
|
792
|
+
tunnelActive = String(tunnelCheck.stdout || '').includes('ACTIVE');
|
|
793
|
+
} catch {}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
ok: result.exitCode === 0,
|
|
797
|
+
nginxType,
|
|
798
|
+
port: nginxType === 'proxy' ? publicPort || port : port,
|
|
799
|
+
outputPath,
|
|
800
|
+
projectPath,
|
|
801
|
+
exitCode: result.exitCode,
|
|
802
|
+
stdout: result.stdout,
|
|
803
|
+
nextStep: 'expose_via_devbridge',
|
|
804
|
+
warning: !tunnelActive
|
|
805
|
+
? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
|
|
806
|
+
: undefined,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export async function deployCheck(
|
|
811
|
+
workspaceId,
|
|
812
|
+
{ port, project, outputDir, frameworkType },
|
|
813
|
+
username = 'root',
|
|
814
|
+
timeoutMs = 30000,
|
|
815
|
+
) {
|
|
816
|
+
if (!workspaceId) {
|
|
817
|
+
throw new Error('sandbox deploy check: workspace_id is required.');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const projectPath = `/workspace/${project}`;
|
|
821
|
+
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
822
|
+
const isCrossPlatform = frameworkType === 'cross-platform';
|
|
823
|
+
|
|
824
|
+
const checkScript = [
|
|
825
|
+
`echo "=== DEPLOY CHECK ==="`,
|
|
826
|
+
`PASS=0`,
|
|
827
|
+
`TOTAL=0`,
|
|
828
|
+
``,
|
|
829
|
+
`TOTAL=$((TOTAL+1))`,
|
|
830
|
+
`if curl -s -o /dev/null -w "%{http_code}" http://localhost:${port} 2>/dev/null | grep -qE "^(2|3)"; then`,
|
|
831
|
+
` echo "nginx_serving:PASS (port ${port})"`,
|
|
832
|
+
` PASS=$((PASS+1))`,
|
|
833
|
+
`else`,
|
|
834
|
+
` echo "nginx_serving:FAIL"`,
|
|
835
|
+
`fi`,
|
|
836
|
+
``,
|
|
837
|
+
`TOTAL=$((TOTAL+1))`,
|
|
838
|
+
`if [ -d "${outputPath}" ] && ls -A "${outputPath}" 2>/dev/null | grep -q .; then`,
|
|
839
|
+
` echo "output_dir:PASS (${outputPath})"`,
|
|
840
|
+
` PASS=$((PASS+1))`,
|
|
841
|
+
`else`,
|
|
842
|
+
` echo "output_dir:FAIL (${outputPath} empty or missing)"`,
|
|
843
|
+
`fi`,
|
|
844
|
+
``,
|
|
845
|
+
`TOTAL=$((TOTAL+1))`,
|
|
846
|
+
`if devbridge ls 2>/dev/null | grep -q "Tunnel URL"; then`,
|
|
847
|
+
` echo "devbridge_tunnel:PASS"`,
|
|
848
|
+
` PASS=$((PASS+1))`,
|
|
849
|
+
`else`,
|
|
850
|
+
` echo "devbridge_tunnel:FAIL"`,
|
|
851
|
+
`fi`,
|
|
852
|
+
``,
|
|
853
|
+
`TOTAL=$((TOTAL+1))`,
|
|
854
|
+
`TUNNEL_URL=$(devbridge ls 2>/dev/null | grep -oP "Tunnel URL: \\Khttps://[^ ]+")`,
|
|
855
|
+
`if [ -n "$TUNNEL_URL" ]; then`,
|
|
856
|
+
` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
857
|
+
` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
|
|
858
|
+
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
|
|
859
|
+
` PASS=$((PASS+1))`,
|
|
860
|
+
` else`,
|
|
861
|
+
` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> HTTP $HTTP_CODE)"`,
|
|
862
|
+
` fi`,
|
|
863
|
+
`else`,
|
|
864
|
+
` echo "tunnel_url_accessible:FAIL (no tunnel URL)"`,
|
|
865
|
+
`fi`,
|
|
866
|
+
``,
|
|
867
|
+
`${
|
|
868
|
+
isCrossPlatform
|
|
869
|
+
? `
|
|
870
|
+
TOTAL=$((TOTAL+1))
|
|
871
|
+
if [ -f "${outputPath}/qr.png" ]; then
|
|
872
|
+
echo "qr_code:PASS"
|
|
873
|
+
PASS=$((PASS+1))
|
|
874
|
+
else
|
|
875
|
+
echo "qr_code:FAIL (cross-platform app missing QR code)"
|
|
876
|
+
fi
|
|
877
|
+
`
|
|
878
|
+
: ''
|
|
879
|
+
}`,
|
|
880
|
+
`echo "SCORE:\${PASS}/\${TOTAL}"`,
|
|
881
|
+
`[ "\${PASS}" = "\${TOTAL}" ] && echo "VERDICT:COMPLETE" || echo "VERDICT:INCOMPLETE"`,
|
|
882
|
+
]
|
|
883
|
+
.filter(Boolean)
|
|
884
|
+
.join('\n');
|
|
885
|
+
|
|
886
|
+
const result = await execOneShot(workspaceId, checkScript, username, timeoutMs);
|
|
887
|
+
const stdout = String(result.stdout || '');
|
|
888
|
+
const checks = {};
|
|
889
|
+
const lines = stdout.split('\n');
|
|
890
|
+
for (const line of lines) {
|
|
891
|
+
const m = line.match(/^(\w+):(PASS|FAIL)\b(.*)/);
|
|
892
|
+
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
893
|
+
}
|
|
894
|
+
const scoreMatch = stdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
895
|
+
const complete = /VERDICT:COMPLETE/.test(stdout);
|
|
896
|
+
|
|
897
|
+
const missing = [];
|
|
898
|
+
if (!complete) {
|
|
899
|
+
for (const [key, val] of Object.entries(checks)) {
|
|
900
|
+
if (val.status === 'FAIL') missing.push(key);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
ok: true,
|
|
906
|
+
complete,
|
|
907
|
+
checkType: isCrossPlatform ? 'cross-platform' : 'standard',
|
|
908
|
+
checks,
|
|
909
|
+
score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
|
|
910
|
+
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
911
|
+
nextStep: !complete
|
|
912
|
+
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
913
|
+
? 'expose_via_devbridge'
|
|
914
|
+
: missing.includes('nginx_serving')
|
|
915
|
+
? 'configure_nginx'
|
|
916
|
+
: missing.includes('qr_code')
|
|
917
|
+
? 'generate_qr_code'
|
|
918
|
+
: 'review_checks'
|
|
919
|
+
: 'complete',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
658
923
|
export async function closeSession(workspaceId, username) {
|
|
659
924
|
const key = `${workspaceId}:${username}`;
|
|
660
925
|
const session = sessions.get(key);
|
|
@@ -2098,6 +2098,41 @@ function removeHermesHooksConfigBlock() {
|
|
|
2098
2098
|
console.log(' Hooks config cleaned');
|
|
2099
2099
|
}
|
|
2100
2100
|
|
|
2101
|
+
function ensureHermesMcpSdk() {
|
|
2102
|
+
const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
|
|
2103
|
+
try {
|
|
2104
|
+
const r = spawnSync(pythonBin, ['-c', 'import mcp; print("ok")'], { encoding: 'utf8', timeout: 5000 });
|
|
2105
|
+
if (r.status === 0 && r.stdout.trim() === 'ok') {
|
|
2106
|
+
console.log(' MCP Python SDK: \x1b[32mAlready installed\x1b[0m');
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
} catch {}
|
|
2110
|
+
console.log(' Installing Hermes MCP Python SDK...');
|
|
2111
|
+
try {
|
|
2112
|
+
const pip = spawnSync(pythonBin, ['-m', 'pip', 'install', 'mcp', '--quiet'], { encoding: 'utf8', timeout: 60000 });
|
|
2113
|
+
if (pip.status === 0) {
|
|
2114
|
+
console.log(' MCP Python SDK: \x1b[32mInstalled\x1b[0m');
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
console.log(` MCP Python SDK: \x1b[33mInstall failed\x1b[0m`);
|
|
2118
|
+
console.log(` \x1b[33m Run manually: ${pythonBin} -m pip install mcp\x1b[0m`);
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
console.log(` MCP Python SDK: \x1b[33m${error.message}\x1b[0m`);
|
|
2121
|
+
console.log(` \x1b[33m Run manually: ${pythonBin} -m pip install mcp\x1b[0m`);
|
|
2122
|
+
}
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function hermesMcpSdkOk() {
|
|
2127
|
+
const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
|
|
2128
|
+
try {
|
|
2129
|
+
const r = spawnSync(pythonBin, ['-c', 'import mcp; print("ok")'], { encoding: 'utf8', timeout: 5000 });
|
|
2130
|
+
return r.status === 0 && r.stdout.trim() === 'ok';
|
|
2131
|
+
} catch {
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2101
2136
|
async function installHermes() {
|
|
2102
2137
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2103
2138
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
@@ -2121,6 +2156,7 @@ async function installHermes() {
|
|
|
2121
2156
|
if (!skipMcp) ensureHermesMcpConfig();
|
|
2122
2157
|
ensureHermesHooksConfig();
|
|
2123
2158
|
if (!skipMcp) installRuntimeDeps(pluginDest);
|
|
2159
|
+
if (!skipMcp) ensureHermesMcpSdk();
|
|
2124
2160
|
}
|
|
2125
2161
|
|
|
2126
2162
|
async function updateHermes() {
|
|
@@ -2141,6 +2177,7 @@ async function updateHermes() {
|
|
|
2141
2177
|
console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
|
|
2142
2178
|
ensureHermesMcpConfig();
|
|
2143
2179
|
ensureHermesHooksConfig();
|
|
2180
|
+
ensureHermesMcpSdk();
|
|
2144
2181
|
mkdirSync(pluginDest, { recursive: true });
|
|
2145
2182
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2146
2183
|
installRuntimeDeps(pluginDest);
|
|
@@ -2184,6 +2221,7 @@ function hermesStatus() {
|
|
|
2184
2221
|
console.log(
|
|
2185
2222
|
` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2186
2223
|
);
|
|
2224
|
+
console.log(` MCP Python SDK: ${hermesMcpSdkOk() ? '\x1b[32mReady\x1b[0m' : '\x1b[31mMissing\x1b[0m'}`);
|
|
2187
2225
|
let skillCount = 0;
|
|
2188
2226
|
if (existsSync(skillsDir)) {
|
|
2189
2227
|
skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
@@ -2766,6 +2804,14 @@ async function cmdDoctor() {
|
|
|
2766
2804
|
mcpCfgTarget ? `Found in ${mcpCfgTarget} config` : 'Run: npx huaweicloud-devkit install',
|
|
2767
2805
|
);
|
|
2768
2806
|
|
|
2807
|
+
// Hermes MCP Python SDK (only checked when Hermes config is found)
|
|
2808
|
+
if (
|
|
2809
|
+
mcpCfgTarget === 'Hermes Agent' ||
|
|
2810
|
+
(existsSync(hermesConfigFile()) && readFileSync(hermesConfigFile(), 'utf8').includes('mcp_servers'))
|
|
2811
|
+
) {
|
|
2812
|
+
check('Hermes MCP Python SDK', hermesMcpSdkOk(), 'Run: pip3 install mcp');
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2769
2815
|
// hcloud CLI
|
|
2770
2816
|
const hcloudBin = findHcloudBin() || process.env.HCLOUD_BIN || 'hcloud';
|
|
2771
2817
|
const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], {
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
closeSession,
|
|
17
17
|
uploadFileWithSession,
|
|
18
18
|
uploadProjectWithSession,
|
|
19
|
+
deployNginx,
|
|
20
|
+
deployCheck,
|
|
19
21
|
getCurrentWorkspaceId,
|
|
20
22
|
setWorkspaceId,
|
|
21
23
|
} from './sandbox/session-manager.mjs';
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
hdkitVoucherStatus,
|
|
28
30
|
hdkitVoucherClaim,
|
|
29
31
|
} from './sandbox/hdkitservice-api.mjs';
|
|
32
|
+
import { getCredentials } from './sandbox/hwlink-api.mjs';
|
|
30
33
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
31
34
|
import {
|
|
32
35
|
readGlobalCredentials,
|
|
@@ -586,6 +589,72 @@ export const TOOL_DEFINITIONS = [
|
|
|
586
589
|
},
|
|
587
590
|
},
|
|
588
591
|
},
|
|
592
|
+
{
|
|
593
|
+
name: 'huaweicloud_sandbox_deploy_nginx',
|
|
594
|
+
description:
|
|
595
|
+
'Deploy an nginx configuration on the sandbox and reload. Takes nginxType, port, project, outputDir from framework detection and writes the correct template (SPA try_files, SSR reverse proxy, or static). Also fixes directory traverse permissions on the project path. Use this instead of manually constructing nginx config — it handles permissions, template selection, and reload in one call.',
|
|
596
|
+
inputSchema: {
|
|
597
|
+
type: 'object',
|
|
598
|
+
required: ['nginx_type', 'port', 'project', 'output_dir'],
|
|
599
|
+
properties: {
|
|
600
|
+
nginx_type: {
|
|
601
|
+
type: 'string',
|
|
602
|
+
description:
|
|
603
|
+
'Nginx config type from framework detection: spa (try_files fallback for SPA/SSG/cross-platform), proxy (reverse proxy for SSR), or static (plain root for Hugo/Hexo).',
|
|
604
|
+
enum: ['spa', 'proxy', 'static'],
|
|
605
|
+
},
|
|
606
|
+
port: { type: 'number', description: 'Listen port (from framework detection).' },
|
|
607
|
+
project: { type: 'string', description: 'Project directory name under /workspace, e.g. movie-ticket.' },
|
|
608
|
+
output_dir: {
|
|
609
|
+
type: 'string',
|
|
610
|
+
description: 'Build output directory relative to /workspace/<project>, e.g. dist/build/h5.',
|
|
611
|
+
},
|
|
612
|
+
node_port: { type: 'number', description: 'Node.js app port for SSR (required when nginx_type=proxy).' },
|
|
613
|
+
public_port: { type: 'number', description: 'Public listen port for SSR proxy (optional, defaults to port).' },
|
|
614
|
+
config_name: {
|
|
615
|
+
type: 'string',
|
|
616
|
+
description:
|
|
617
|
+
'Config file name (without .conf suffix). Defaults to app. Use distinct names (e.g. admin, docs) for multi-app deployments to avoid config overwrites.',
|
|
618
|
+
},
|
|
619
|
+
workspace_id: {
|
|
620
|
+
type: 'string',
|
|
621
|
+
description:
|
|
622
|
+
'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
|
|
623
|
+
},
|
|
624
|
+
username: { type: 'string', description: 'Login username (default: root)' },
|
|
625
|
+
timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: 'huaweicloud_sandbox_deploy_check',
|
|
631
|
+
description:
|
|
632
|
+
'Run a deployment completeness check on the sandbox. Verifies nginx is serving, output directory exists, DevBridge tunnel is active and accessible, and QR code exists (cross-platform). Returns a score and nextStep to fix any missing items. Call this at the end of a deployment workflow to confirm everything is working before reporting success.',
|
|
633
|
+
inputSchema: {
|
|
634
|
+
type: 'object',
|
|
635
|
+
required: ['port', 'project', 'output_dir'],
|
|
636
|
+
properties: {
|
|
637
|
+
port: { type: 'number', description: 'App listen port (from framework detection).' },
|
|
638
|
+
project: { type: 'string', description: 'Project directory name under /workspace.' },
|
|
639
|
+
output_dir: {
|
|
640
|
+
type: 'string',
|
|
641
|
+
description: 'Build output directory relative to /workspace/<project>, e.g. dist/build/h5.',
|
|
642
|
+
},
|
|
643
|
+
framework_type: {
|
|
644
|
+
type: 'string',
|
|
645
|
+
description: 'Framework type from detect_framework. Set to cross-platform for QR code check.',
|
|
646
|
+
enum: ['spa', 'ssr', 'ssg', 'cross-platform', 'monorepo', 'static'],
|
|
647
|
+
},
|
|
648
|
+
workspace_id: {
|
|
649
|
+
type: 'string',
|
|
650
|
+
description:
|
|
651
|
+
'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
|
|
652
|
+
},
|
|
653
|
+
username: { type: 'string', description: 'Login username (default: root)' },
|
|
654
|
+
timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
},
|
|
589
658
|
{
|
|
590
659
|
name: 'huaweicloud_sandbox_check_user',
|
|
591
660
|
description:
|
|
@@ -824,6 +893,59 @@ export async function callTool(name, args = {}) {
|
|
|
824
893
|
},
|
|
825
894
|
);
|
|
826
895
|
}
|
|
896
|
+
case 'huaweicloud_sandbox_deploy_nginx': {
|
|
897
|
+
if (!args.nginx_type || !args.port || !args.project || !args.output_dir) {
|
|
898
|
+
throw new Error('nginx_type, port, project, and output_dir are required.');
|
|
899
|
+
}
|
|
900
|
+
const sandboxWsId7 = args.workspace_id || getCurrentWorkspaceId();
|
|
901
|
+
if (!sandboxWsId7) {
|
|
902
|
+
throw new Error(
|
|
903
|
+
'workspace_id is required. No sandbox connected — call huaweicloud_sandbox_connect first, ' +
|
|
904
|
+
'or set HW_WORKSPACE_ID environment variable before starting the agent.',
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const sandboxUser7 = args.username || 'root';
|
|
908
|
+
const sandboxTimeout7 = args.timeout_ms || 30000;
|
|
909
|
+
return await deployNginx(
|
|
910
|
+
sandboxWsId7,
|
|
911
|
+
{
|
|
912
|
+
nginxType: args.nginx_type,
|
|
913
|
+
port: args.port,
|
|
914
|
+
project: args.project,
|
|
915
|
+
outputDir: args.output_dir,
|
|
916
|
+
nodePort: args.node_port,
|
|
917
|
+
publicPort: args.public_port,
|
|
918
|
+
configName: args.config_name,
|
|
919
|
+
},
|
|
920
|
+
sandboxUser7,
|
|
921
|
+
sandboxTimeout7,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
case 'huaweicloud_sandbox_deploy_check': {
|
|
925
|
+
if (!args.port || !args.project || !args.output_dir) {
|
|
926
|
+
throw new Error('port, project, and output_dir are required.');
|
|
927
|
+
}
|
|
928
|
+
const sandboxWsId8 = args.workspace_id || getCurrentWorkspaceId();
|
|
929
|
+
if (!sandboxWsId8) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
'workspace_id is required. No sandbox connected — call huaweicloud_sandbox_connect first, ' +
|
|
932
|
+
'or set HW_WORKSPACE_ID environment variable before starting the agent.',
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const sandboxUser8 = args.username || 'root';
|
|
936
|
+
const sandboxTimeout8 = args.timeout_ms || 30000;
|
|
937
|
+
return await deployCheck(
|
|
938
|
+
sandboxWsId8,
|
|
939
|
+
{
|
|
940
|
+
port: args.port,
|
|
941
|
+
project: args.project,
|
|
942
|
+
outputDir: args.output_dir,
|
|
943
|
+
frameworkType: args.framework_type,
|
|
944
|
+
},
|
|
945
|
+
sandboxUser8,
|
|
946
|
+
sandboxTimeout8,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
827
949
|
case 'huaweicloud_sandbox_check_user':
|
|
828
950
|
return await hdkitCheckUser();
|
|
829
951
|
case 'huaweicloud_sandbox_sign_agreement':
|
|
@@ -833,11 +955,37 @@ export async function callTool(name, args = {}) {
|
|
|
833
955
|
const devStageId = connectResult?.dev_stage_id || connectResult?.devStageId;
|
|
834
956
|
if (devStageId) {
|
|
835
957
|
setWorkspaceId(devStageId);
|
|
958
|
+
try {
|
|
959
|
+
await execOneShot(devStageId, 'devbridge delete-all 2>/dev/null || true', 'root', 15000);
|
|
960
|
+
} catch {}
|
|
836
961
|
}
|
|
837
962
|
return connectResult;
|
|
838
963
|
}
|
|
839
|
-
case 'huaweicloud_sandbox_credentials':
|
|
840
|
-
|
|
964
|
+
case 'huaweicloud_sandbox_credentials': {
|
|
965
|
+
const credResult = await hdkitCredentials(args.session_id, args.dev_stage_id, args.enable_sts !== false);
|
|
966
|
+
const sandboxWsIdCred = args.dev_stage_id || getCurrentWorkspaceId();
|
|
967
|
+
if (sandboxWsIdCred) {
|
|
968
|
+
try {
|
|
969
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
970
|
+
const credsScript = [
|
|
971
|
+
`export HW_ACCESS_KEY='${ak}'`,
|
|
972
|
+
`export HW_SECRET_KEY='${sk}'`,
|
|
973
|
+
securitytoken ? `export HW_SECURITY_TOKEN='${securitytoken}'` : '',
|
|
974
|
+
securitytoken ? `export X_HW_SECURITY_TOKEN='${securitytoken}'` : '',
|
|
975
|
+
]
|
|
976
|
+
.filter(Boolean)
|
|
977
|
+
.join('\n');
|
|
978
|
+
const credsFile = '/tmp/hw_creds.sh';
|
|
979
|
+
await execOneShot(
|
|
980
|
+
sandboxWsIdCred,
|
|
981
|
+
`cat > ${credsFile} << 'HWCREDS_EOF'\n${credsScript}\nHWCREDS_EOF\nchmod 600 ${credsFile}`,
|
|
982
|
+
'root',
|
|
983
|
+
15000,
|
|
984
|
+
);
|
|
985
|
+
} catch {}
|
|
986
|
+
}
|
|
987
|
+
return credResult;
|
|
988
|
+
}
|
|
841
989
|
case 'huaweicloud_voucher_status':
|
|
842
990
|
return await hdkitVoucherStatus(args.domain_id);
|
|
843
991
|
case 'huaweicloud_voucher_claim':
|