huaweicloud-devkit 1.1.6-next.0 → 1.1.6-next.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.6-next.0",
4
+ "version": "1.1.6-next.1",
5
5
  "kooCliVersion": "7.2.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -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.6-next.0",
20
+ "version": "1.1.6-next.1",
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.6-next.0",
23
+ "version": "1.1.6-next.1",
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.6-next.0",
20
+ "version": "1.1.6-next.1",
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.6-next.0",
3
+ "version": "1.1.6-next.1",
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.6-next.0",
3
+ "version": "1.1.6-next.1",
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.6-next.0",
5
+ "version": "1.1.6-next.1",
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.",
@@ -18,8 +18,10 @@ Domain expertise for Huawei Cloud CloudDeploy. Covers application creation, depl
18
18
 
19
19
  | Trap | Why |
20
20
  | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | Service name is `CodeArtsDeploy` | `hcloud CloudDeploy` returns `[USE_ERROR]不支持的服务名称`. The KooCLI service name is **CodeArtsDeploy** (product name CloudDeploy). Run `hcloud --help` to verify. |
22
+ | Wrong operation names fail | The per-action names historically assumed (Start/List/Create/Delete + `Task`/`Tasks`) do NOT exist — KooCLI returns `[USE_ERROR]不支持的operation`. Use the verified names in Common Workflows. |
23
+ | `project_id` is the CodeArts project ID | Not the IAM project. If CodeArts is not enabled for the account, calls fail with `Deploy.00016902 项目不存在`. |
21
24
  | **Flyway SQL dialect mismatch (H2 dev → MySQL prod)** | Spring Boot apps commonly develop with H2 in-memory DB, then deploy to RDS MySQL. Flyway migrations using H2-specific syntax (e.g. `DATEADD`, `CHARACTER_LENGTH`, `BOOLEAN`) silently succeed on H2 but fail on MySQL. Before deploying, audit `V*__*.sql` migration files: replace `DATEADD` with `DATE_ADD`, `BOOLEAN` with `TINYINT(1)`, remove `characterEncoding=utf8mb4` from Spring Boot datasource URL (KooCLI RDS CreateInstance sets charset at the instance level). Use `Flyway.validate-on-migrate=true` in CI to catch dialect issues early. |
22
- | Service name may be `CodeArtsDeploy` | hcloud service name for deployment may be `CodeArtsDeploy` instead of `CloudDeploy`. Run `hcloud --help` to verify |
23
25
  | Deployment hosts need agent | Install CloudDeploy agent on target hosts first |
24
26
  | Task must reference application first | Create application before task |
25
27
  | Artifact source defaults to OBS | Most deployment tasks pull artifacts from OBS. Ensure bucket and object exist |
@@ -27,22 +29,25 @@ Domain expertise for Huawei Cloud CloudDeploy. Covers application creation, depl
27
29
 
28
30
  ## Common Workflows
29
31
 
30
- | Task | Operation |
31
- | ---------------------- | ---------------------------------------------------------------- |
32
- | Create application | `CreateApp --name=<n> --platform=<p> --cli-region=<r>` |
33
- | Create deployment task | `CreateTask --name=<n> --app_id=<id> --artifact_source_type=OBS` |
34
- | Start deployment | `StartTask --task_id=<id>` |
35
- | List tasks | `ListTasks --app_id=<id>` |
36
- | Delete task | `DeleteTask --task_id=<id>` |
32
+ | Task | Operation(省略参数用 `<placeholder>`,参数以 `hcloud CodeArtsDeploy <Op> --help` 为准) |
33
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | List applications | `ListAllApp --cli-region=<r> --project_id=<id> --page=1 --size=10`(`ListDeployTasks` 已由云侧于 2024-09-30 弃用,推荐本接口) |
35
+ | Show app detail | `ShowAppDetailById --cli-region=<r> --app_id=<id>` |
36
+ | Create application | `CreateApp --cli-region=<r> --name=<n> --create_type=template --project_id=<id> --is_draft=false`(`--create_type` 仅有 `template` 一个值) |
37
+ | Create deployment task | `CreateDeployTaskByTemplate --cli-region=<r> --template_id=<id> --task_name=<n>`(deprecated since 2024-09-30 — `--help` recommends `CreateApp`) |
38
+ | Start deployment | `StartDeployTask --cli-region=<r> --task_id=<id>` |
39
+ | Delete task | `DeleteDeployTask --cli-region=<r> --task_id=<id>`(deprecated since 2024-09-30 — `--help` recommends `DeleteApplication --app_id=<id>`) |
37
40
 
38
41
  ## Troubleshooting
39
42
 
40
- | Error | Fix |
41
- | ------------------ | ----------------------------------------------------------------------------- |
42
- | Agent offline | Check agent service on target host, network connectivity, firewall rules |
43
- | Deployment timeout | Check artifact size, increase task timeout, verify target host resources |
44
- | Artifact not found | Verify OBS bucket and object path, check artifact permissions |
45
- | Permission denied | Verify IAM roles for deployment: `CodeArtsDeploy FullAccess` or custom policy |
43
+ | Error | Fix |
44
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
45
+ | Agent offline | Check agent service on target host, network connectivity, firewall rules |
46
+ | Deployment timeout | Check artifact size, increase task timeout, verify target host resources |
47
+ | Artifact not found | Verify OBS bucket and object path, check artifact permissions |
48
+ | Permission denied | Verify IAM roles for deployment: `CodeArtsDeploy FullAccess` or custom policy |
49
+ | `Deploy.00016902` 项目不存在 | CodeArts not enabled or wrong project type: use the **CodeArts project ID** (found in CodeArts console), not the IAM project ID; enable CodeArts first |
50
+ | `APIGW.0301` Incorrect IAM authentication information | Credentials lack CodeArtsDeploy access — use AK/SK with CodeArtsDeploy permissions (STS scoped credentials may be rejected) |
46
51
 
47
52
  ## Security
48
53
 
@@ -222,29 +222,84 @@ Expose a deployed web app to a public URL and return that URL to the developer.
222
222
 
223
223
  **User-facing language rule**: the tunnel mechanism below is an internal implementation detail. Never mention "DevBridge", "tunnel", or the tunnel CLI to the developer. Tell the developer only: "正在生成访问地址..." while exposing, then hand over the final URL.
224
224
 
225
- **Install** (internal tooling; skip if `devbridge version` already works):
225
+ ### Step 0: Ensure DevBridge CLI 0.2.x (install or upgrade in place)
226
+
227
+ **Check the version first.** Sandboxes created before Sep 2026 ship devbridge 0.1.13, whose hardcoded gateway (`cn-north-4-bridge.myhuaweicloud.com`) was migrated and now serves a 「服务已迁移」 placeholder page — 0.1.x can no longer connect:
226
228
 
227
229
  ```bash
228
- if command -v devbridge >/dev/null 2>&1; then
229
- echo "devbridge already installed"
230
- else
231
- curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh | bash
230
+ export PATH="$HOME/.huawei/bin:$PATH" # PREPEND — appending lets a stale image binary in /usr/local/bin win
231
+ devbridge version 2>/dev/null || echo "not installed"
232
+ ```
233
+
234
+ - `0.2.x-release` → continue to Step 1.
235
+ - `0.1.x-release` or not installed → upgrade in place (replaces the binary — keep NO second copy):
236
+
237
+ ```bash
238
+ # Official installer (GitHub can be slow/flaky from the sandbox — force HTTP/1.1 and retry all errors)
239
+ curl -fsSL --http1.1 --retry 3 --retry-all-errors --max-time 120 \
240
+ https://github.com/huaweicloud/devspace-devbridge/releases/latest/download/install.sh \
241
+ -o /tmp/devbridge-install.sh
242
+
243
+ # Preferred: install via the GitCode mirror (domestic CDN, much faster).
244
+ # Extract the version baked into the installer, then point -u at the matching GitCode tag.
245
+ # Note: GitCode repo is named "devbrige" (missing 'd') — this is the actual repo name, not a typo.
246
+ # GitHub repo is "devspace-devbridge" (correct spelling). Do NOT "fix" the GitCode URL.
247
+ DB_TARGET=$(grep -m1 'DEFAULT_VERSION=' /tmp/devbridge-install.sh | cut -d'"' -f2)
248
+ if ! bash /tmp/devbridge-install.sh -s -u "https://gitcode.com/CloudDeveloperDepartment/devbrige/releases/download/${DB_TARGET}" -v "${DB_TARGET}"; then
249
+ bash /tmp/devbridge-install.sh -s # fallback: GitHub (baked-in URL)
250
+ fi
251
+ export PATH="$HOME/.huawei/bin:$PATH" # PREPEND (session shells do not re-source ~/.bashrc)
252
+ devbridge version # must print 0.2.x-release
253
+
254
+ # Remove the stale image-installed binary so only ONE version remains.
255
+ # (Image sandboxes ship 0.1.x at /usr/local/bin/devbridge; the installer writes ~/.huawei/bin.
256
+ # Compare against the explicit ~/.huawei/bin path — never `command -v`, which can resolve
257
+ # to the stale binary itself when PATH order differs.)
258
+ if [ -x "$HOME/.huawei/bin/devbridge" ] && [ -f /usr/local/bin/devbridge ] && [ "$(readlink -f /usr/local/bin/devbridge)" != "$(readlink -f "$HOME/.huawei/bin/devbridge")" ]; then
259
+ rm -f /usr/local/bin/devbridge && echo "stale /usr/local/bin/devbridge removed"
232
260
  fi
233
- export PATH=$PATH:$HOME/.huawei/bin # installer only writes ~/.bashrc; session shells do not re-source it
234
261
  ```
235
262
 
236
- **Login** (non-interactive; credentials from `huaweicloud_sandbox_credentials` are available via `/tmp/hw_creds.sh`). If `source /tmp/hw_creds.sh` returns empty, the credentials injection has expired (sandbox session reconnection resets them) — re-run `huaweicloud_sandbox_credentials` first:
263
+ - `-s` (silent) is **required** in the sandbox — without it the installer blocks reading `/dev/tty`.
264
+ - **Old tunnels do not survive the upgrade**: 0.1.x-created tunnels are not registered on the s2 gateway — always rebuild the tunnel after upgrading (the expose flow below does this via pre-cleanup).
265
+ - If `10006: quota exceeded` appears after upgrading, stale pre-upgrade tunnels still count against the quota — `devbridge delete-all` and retry.
266
+
267
+ ### Step 1: Authenticate with an API Key (0.2.x removed AK/SK login)
268
+
269
+ devbridge 0.2.x removed `--access-key/--secret-key/--huaweicloud`. The only non-interactive auth is a DevBridge API Key. AK/SK (`/tmp/hw_creds.sh`) remains valid for hcloud, but not for devbridge.
270
+
271
+ **Check, then login if the key exists** (the API Key is a long-lived account-level credential stored in its own file `/tmp/hw_api_key`, separate from the temporary AK/SK in `/tmp/hw_creds.sh` — never echo its value):
237
272
 
238
273
  ```bash
239
274
  source /tmp/hw_creds.sh 2>/dev/null
240
- devbridge auth login --huaweicloud --access-key "$HW_ACCESS_KEY" --secret-key "$HW_SECRET_KEY"
275
+ source /tmp/hw_api_key 2>/dev/null
276
+ if [ -n "$HW_API_KEY" ]; then
277
+ devbridge auth login --api-key "$HW_API_KEY" && devbridge auth status
278
+ else
279
+ echo "NO_API_KEY"
280
+ fi
241
281
  ```
242
282
 
243
- - The `--huaweicloud` flag is required for AK/SK login; without it the CLI tries an interactive browser login, which fails in the sandbox.
244
- - Credentials are stored in `/tmp/hw_creds.sh` (chmod 600) — source it before login, never echo the values.
245
- - Verify with `devbridge auth status`. If `$HW_ACCESS_KEY` is empty, ensure `huaweicloud_sandbox_credentials` was called first.
283
+ **If `NO_API_KEY`** — STOP and guide the developer through creating one (wait for the key before continuing):
284
+
285
+ 1. **Why (one sentence)**: "沙箱的地址生成服务已升级到 0.2.x,新版本仅支持 API Key 登录(官方变更)。API Key 按账号管理,创建一次长期可用、所有沙箱通用。"
286
+ 2. **Where (exact steps)**: open https://devstation.connect.huaweicloud.com/space/devbridge/apikey → 登录控制台 → 选择 DevBridge 场景 → 点击"创建"。**完整值仅在创建时展示一次,立即复制**(`devbridge_` 开头)。
287
+ 3. **How to hand it over** (present both, recommend the first):
288
+ - **Recommended**: 在本地终端执行 `export HW_API_KEY=<粘贴Key>`(或写入 shell profile),然后告知"已设置" — Key 不经过对话记录;随后 agent 重新调用 `huaweicloud_sandbox_credentials`(本地 `HW_API_KEY` 会被自动透传注入沙箱)。
289
+ - **Alternative**: 直接把 Key 粘贴在对话中 — agent 通过 `huaweicloud_sandbox_credentials` 的 `api_key` 参数注入,**绝不回显、不复述、不写入日志**。
290
+ 4. **Security**: 不要提交到代码库或分享;怀疑泄露时在同页面删除并重建(1 分钟)。
246
291
 
247
- **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>`):
292
+ **Failure paths**:
293
+
294
+ | Symptom | Guidance |
295
+ | -------------------------------------- | ---------------------------------------------------- |
296
+ | `auth login` returns 401/403 | Key 已被删除或禁用 — 引导在同页面删除旧 Key 重新创建 |
297
+ | 无法访问 API Key 管理页面 | 账号无该页面权限 — 引导联系账号管理员处理 |
298
+ | Key 带空格/换行/缺失 `devbridge_` 前缀 | 引导重新完整复制粘贴 |
299
+
300
+ ### Step 2: Expose
301
+
302
+ 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>`:
248
303
 
249
304
  ```bash
250
305
  # 0. Pre-cleanup: kill old processes and stale tunnels
@@ -273,13 +328,15 @@ else
273
328
  fi
274
329
  ```
275
330
 
276
- **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.
331
+ **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. Note: a migrated gateway serves its placeholder page with **HTTP 200** — a bare status check is not enough when diagnosing "200 but wrong content".
277
332
 
278
333
  **Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:
279
334
 
280
335
  ```bash
281
336
  # Step A: List all tunnels (both active and stale)
282
- devbridge list -j
337
+ # 0.2.x: `devbridge list` (plain table; data rows start with the 8-char tunnel ID)
338
+ # 0.1.x (legacy): `devbridge list -j` (JSON)
339
+ devbridge list
283
340
  # Step B: Remove all stale tunnels
284
341
  devbridge delete-all
285
342
  # Step C: Retry tunnel creation
@@ -289,10 +346,10 @@ sleep 10 && cat /tmp/host.log
289
346
 
290
347
  This eliminates the most common deployment failure — historical tunnels from previous sessions accumulating past the max=10 quota.
291
348
 
292
- - The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com` (from the `Tunnel URL:` line).
349
+ - The public URL has the form `https://<id>-<port>.devbridge-s2.hwtunnel.com` (from the `Tunnel URL:` line; 8-char lowercase base32 id). The pre-migration bridge domain `cn-north-4-bridge.myhuaweicloud.com` is dead — treat any URL on it as broken and never hand one to the developer.
293
350
  - **Return this URL to the developer as the deployment result link.** Keep the host process running (do not close the session before handing over the URL).
294
351
  - Tunnel `description` (`-d`) accepts only Chinese characters, letters, and digits (0-64). Symbols such as `-`/`_`/spaces are rejected (`Invalid tunnel description`).
295
- - Internal docs: https://huaweicloud.github.io/devspace-devbridge/
352
+ - Official docs: https://huaweicloud.github.io/devspace-devbridge/
296
353
 
297
354
  **No local downgrade**: if the tunnel tooling cannot be installed in the sandbox, STOP and report a generic error ("无法生成访问地址") without technical detail. Never install it on the developer's local machine — a local install would defeat the purpose of sandbox deployment.
298
355
 
@@ -446,7 +503,7 @@ For each check, parse the output: if stdout contains `MISSING:` or the tool wasn
446
503
  | pnpm | `npm i -g pnpm` | Same |
447
504
  | yarn | `npm i -g yarn` | Same |
448
505
  | Hugo | `curl -fsSL https://github.com/gohugoio/hugo/releases/download/v0.140.0/hugo_extended_0.140.0_linux-amd64.tar.gz -o /tmp/hugo.tar.gz && sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo && rm /tmp/hugo.tar.gz` | Same |
449
- | DevBridge | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin` | Same |
506
+ | DevBridge | Follow [Step 0 of "Expose the deployed app"](#step-0-ensure-devbridge-cli-02x-install-or-upgrade-in-place) — checks version, installs or upgrades in place via the official installer | Same |
450
507
 
451
508
  **If Node.js is missing**, install it first — all build workflows depend on it. Stop and report to the developer if Node.js installation fails.
452
509
 
@@ -782,13 +839,15 @@ If the status code is not 2xx/3xx:
782
839
 
783
840
  ### Step 7: Expose via DevBridge [REQUIRED — deployment incomplete without this]
784
841
 
842
+ When `deploy_check` returns `nextStep: expose_via_devbridge`, it also returns an executable `remediation` string — run those devbridge commands in the sandbox (after `source /tmp/hw_creds.sh`). Until a tunnel exists, `deploy_check` returns no `publicUrl`; do not fabricate a URL.
843
+
785
844
  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.
786
845
 
787
846
  Use `exec_with_session` to background DevBridge. For SSR, DevBridge tunnels the nginx public port (not the Node port directly).
788
847
 
789
- **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 list -j`, delete stale ones, and retry.
848
+ **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 list` (0.2.x plain table; 0.1.x legacy `-j` JSON), delete stale ones, and retry.
790
849
 
791
- 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.**
850
+ Extract the tunnel URL from DevBridge output. The public URL has the form `https://<id>-<port>.devbridge-s2.hwtunnel.com`. **Return this URL to the developer as the deployment result.**
792
851
 
793
852
  #### Cross-platform H5 QR code
794
853
 
@@ -830,8 +889,8 @@ The tool checks:
830
889
 
831
890
  - **nginx_serving** — nginx responds with 2xx/3xx on the app port
832
891
  - **output_dir** — build output directory exists and is non-empty
833
- - **devbridge_tunnel** — DevBridge tunnel is active
834
- - **tunnel_url_accessible** — tunnel URL returns 200/304
892
+ - **devbridge_tunnel** — DevBridge tunnel is active (version-aware: 0.1.x JSON / 0.2.x table)
893
+ - **tunnel_url_accessible** — tunnel URL returns 200/304 with real content; a gateway migration placeholder page (「服务已迁移」 with HTTP 200) counts as FAIL
835
894
  - **qr_code** (cross-platform only) — QR image exists in output dir
836
895
 
837
896
  Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
@@ -854,32 +913,36 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
854
913
 
855
914
  ## Critical Warnings
856
915
 
857
- | Trap | Why |
858
- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
859
- | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
860
- | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
861
- | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
862
- | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
863
- | Login needs `--huaweicloud` | `devbridge auth login --access-key/--secret-key` without `--huaweicloud` falls back to interactive browser login, which fails in the sandbox |
864
- | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH=$PATH:$HOME/.huawei/bin` in the session before using `devbridge` |
865
- | 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 |
866
- | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
867
- | 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. |
868
- | 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. |
869
- | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
870
- | 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). |
871
- | SSR nginx/Node ports must differ | nginx `proxy_pass` targets `<nodePort>`, not `<port>`. `deploy_nginx` auto-defaults `nodePort` to `<port>+1` — always start the Node process with `PORT=<nodePort>` to match. Same-port = EADDRINUSE. |
872
- | HTTP 200 ≠ correct content | A green HTTP check does not guarantee the right project is serving — old processes from a previous session bound to the same port will still return 200. `deploy_check` verifies the deployment fingerprint to catch this. |
873
- | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
874
- | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
875
- | Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
876
- | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
877
- | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
878
- | 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") |
879
- | 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. |
880
- | 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. |
881
- | 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. |
882
- | 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. |
916
+ | Trap | Why |
917
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
918
+ | Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
919
+ | Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
920
+ | Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
921
+ | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
922
+ | devbridge 0.2.x needs an API Key | 0.2.x removed AK/SK login (`--access-key/--secret-key/--huaweicloud` are gone). Login with `--api-key "$HW_API_KEY"` from `/tmp/hw_api_key` (long-lived credential, stored separately from `/tmp/hw_creds.sh`). If missing, guide the developer to create one (see Step 1 of "Expose the deployed app") |
923
+ | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
924
+ | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
925
+ | 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 |
926
+ | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
927
+ | 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. |
928
+ | 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. |
929
+ | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
930
+ | 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). |
931
+ | SSR nginx/Node ports must differ | nginx `proxy_pass` targets `<nodePort>`, not `<port>`. `deploy_nginx` auto-defaults `nodePort` to `<port>+1` — always start the Node process with `PORT=<nodePort>` to match. Same-port = EADDRINUSE. |
932
+ | HTTP 200 ≠ correct content | A green HTTP check does not guarantee the right project is serving — old processes from a previous session bound to the same port will still return 200. `deploy_check` verifies the deployment fingerprint to catch this. |
933
+ | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
934
+ | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
935
+ | Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
936
+ | Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
937
+ | Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
938
+ | 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") |
939
+ | 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. |
940
+ | 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. |
941
+ | 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. |
942
+ | 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. |
943
+ | Tunnel description: no spaces/hyphens | `devbridge create <name> -d 'hello world'` fails with `Invalid tunnel description: only Chinese characters, digits, letters, length 0-64`. The message is misleading — the real rule is no spaces/hyphens. Omit `-d` or use bare letters/digits. |
944
+ | Use `host`, never `connect` | `devbridge connect` is the sender side (for on-prem machines dialing out). For public preview it loops `Connection failed, retrying...` forever with no hint. Always use `devbridge host <tunnelId> -p <port>`. |
945
+ | Port drift detaches the tunnel | When deploy_nginx auto-increments to a free port, an existing tunnel stays bound to the old port — re-bind: `devbridge port create <tunnelId> -p <newPort> --protocol http -a` and restart `devbridge host`. deploy_nginx emits a warning when this happens. |
883
946
 
884
947
  ## Node.js in the sandbox
885
948
 
@@ -896,11 +959,12 @@ node --version
896
959
 
897
960
  ## Environment Variables
898
961
 
899
- | Variable | Required | Description |
900
- | ----------------------- | -------- | --------------------------------------------------------------- |
901
- | `HW_ACCESS_KEY` | Yes | Huawei Cloud AK |
902
- | `HW_SECRET_KEY` | Yes | Huawei Cloud SK |
903
- | `HW_SECURITY_TOKEN` | No | STS security token |
904
- | `HW_WORKSPACE_ID` | No | Default workspace ID |
905
- | `HDKITSERVICE_ENDPOINT` | No | hdkitservice API endpoint (default: devkit.huaweicloud.com) |
906
- | `HWLINK_ENDPOINT` | No | DevStation API endpoint (default: devstation.myhuaweicloud.com) |
962
+ | Variable | Required | Description |
963
+ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
964
+ | `HW_ACCESS_KEY` | Yes | Huawei Cloud AK |
965
+ | `HW_SECRET_KEY` | Yes | Huawei Cloud SK |
966
+ | `HW_SECURITY_TOKEN` | No | STS security token |
967
+ | `HW_API_KEY` | No | DevBridge API Key (`devbridge_...`) — required for devbridge 0.2.x tunnel login; injected into the sandbox via `huaweicloud_sandbox_credentials` (`api_key` param or local env) |
968
+ | `HW_WORKSPACE_ID` | No | Default workspace ID |
969
+ | `HDKITSERVICE_ENDPOINT` | No | hdkitservice API endpoint (default: devkit.huaweicloud.com) |
970
+ | `HWLINK_ENDPOINT` | No | DevStation API endpoint (default: devstation.myhuaweicloud.com) |
@@ -75,10 +75,10 @@ These tools should be available in the sandbox image. Verify with the pre-flight
75
75
 
76
76
  Tools that may need installation in the sandbox before building:
77
77
 
78
- | Tool | Trigger | Install Command |
79
- | --------- | ------------------------------------ | ------------------------------------------------------------------------------------ |
80
- | yarn | `yarn.lock` detected | `npm i -g yarn` |
81
- | Hugo | `hugo.toml` / `config.toml` detected | Download Hugo extended binary (see below) |
82
- | DevBridge | Tunnel exposure step | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash` |
78
+ | Tool | Trigger | Install Command |
79
+ | --------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
80
+ | yarn | `yarn.lock` detected | `npm i -g yarn` |
81
+ | Hugo | `hugo.toml` / `config.toml` detected | Download Hugo extended binary (see below) |
82
+ | DevBridge | Tunnel exposure step | Official installer with version check + in-place upgrade (see SKILL.md "Expose the deployed app" Step 0) |
83
83
 
84
84
  > Hugo download: `curl -fsSL https://github.com/gohugoio/hugo/releases/download/v0.140.0/hugo_extended_0.140.0_linux-amd64.tar.gz -o /tmp/hugo.tar.gz && sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo && rm /tmp/hugo.tar.gz`
@@ -59,6 +59,24 @@ export async function dispatch(method, params, opts = {}) {
59
59
  }
60
60
 
61
61
  if (method === 'tools/call') {
62
+ const tool = TOOL_DEFINITIONS.find((t) => t.name === params.name);
63
+ if (!tool) {
64
+ // JSON-RPC 2.0: an unknown tool name is a client-side parameter error,
65
+ // not a server fault (#704 D9-2).
66
+ const unknownToolError = new Error(`Unknown tool: ${params.name}`);
67
+ unknownToolError.code = -32602;
68
+ throw unknownToolError;
69
+ }
70
+ const missing = (tool.inputSchema?.required || []).filter(
71
+ (key) => !params.arguments || !Object.hasOwn(params.arguments, key),
72
+ );
73
+ if (missing.length > 0) {
74
+ const invalidParamsError = new Error(
75
+ `Invalid params: missing required field(s) ${missing.map((key) => JSON.stringify(key)).join(', ')} for tool "${params.name}".`,
76
+ );
77
+ invalidParamsError.code = -32602;
78
+ throw invalidParamsError;
79
+ }
62
80
  const result = await callTool(params.name, params.arguments || {});
63
81
  const decorated = _decorateResult(sessionId, params.name, result);
64
82
  return {
@@ -128,7 +128,13 @@ function runStdioServer() {
128
128
  useContentLengthFraming = false;
129
129
  const line = buffer.subarray(0, lf).toString('utf8').trim();
130
130
  buffer = buffer.subarray(lf + 1);
131
- if (line) void handleMessage(JSON.parse(line));
131
+ if (line) {
132
+ try {
133
+ void handleMessage(JSON.parse(line));
134
+ } catch {
135
+ writeParseError();
136
+ }
137
+ }
132
138
  continue;
133
139
  }
134
140
 
@@ -149,11 +155,21 @@ function runStdioServer() {
149
155
  if (buffer.length < bodyEnd) return false;
150
156
  const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
151
157
  buffer = buffer.subarray(bodyEnd);
152
- void handleMessage(JSON.parse(body));
158
+ try {
159
+ void handleMessage(JSON.parse(body));
160
+ } catch {
161
+ writeParseError();
162
+ }
153
163
  return true;
154
164
  }
155
165
 
156
166
  async function handleMessage(message) {
167
+ // Valid JSON that is not an object (null / array / string) is an invalid
168
+ // request per JSON-RPC 2.0 — reply -32600 rather than silently dropping it.
169
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
170
+ writeJsonRpcError(-32600, 'Invalid Request');
171
+ return;
172
+ }
157
173
  if (!Object.hasOwn(message, 'id')) {
158
174
  if (message.method === 'notifications/initialized') return;
159
175
  return;
@@ -181,4 +197,16 @@ function runStdioServer() {
181
197
  stdout.write(json + '\n');
182
198
  }
183
199
  }
200
+
201
+ function writeJsonRpcError(code, message) {
202
+ writeMessage({
203
+ jsonrpc: '2.0',
204
+ id: null,
205
+ error: { code, message },
206
+ });
207
+ }
208
+
209
+ function writeParseError() {
210
+ writeJsonRpcError(-32700, 'Parse error');
211
+ }
184
212
  }
@@ -22,9 +22,18 @@ import { trackSandboxConnect, trackSandboxDisconnect } from '../telemetry/teleme
22
22
 
23
23
  const execFileAsync = promisify(execFile);
24
24
 
25
+ // Public tunnel URL domain for the DevBridge s2 gateway. The pre-migration Huawei Cloud
26
+ // bridge domain was retired in Sep 2026 and now serves a 「服务已迁移」 placeholder page with
27
+ // HTTP 200 — never construct tunnel URLs from it.
28
+ const DEVBRIDGE_TUNNEL_DOMAIN = 'devbridge-s2.hwtunnel.com';
29
+ // Migration placeholder page marker — a tunnel URL returning this body must be treated as unreachable.
30
+ const DEVBRIDGE_MIGRATION_MARKER = '服务已迁移';
31
+
25
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
33
  export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
27
34
 
35
+ export const TUNNEL_URL_PATTERN = /TUNNEL_URL:(https:\/\/[A-Za-z0-9_-]+-\d+\.devbridge-s2\.hwtunnel\.com)/;
36
+
28
37
  let currentWorkspaceId = process.env.HW_WORKSPACE_ID || null;
29
38
 
30
39
  function getCurrentWorkspaceId() {
@@ -180,6 +189,24 @@ export function splitBase64Chunks(base64, chunkSize = UPLOAD_CHUNK_SIZE) {
180
189
  return chunks;
181
190
  }
182
191
 
192
+ export function formatPortConflictWarning(basePort, targetPort) {
193
+ return targetPort !== basePort ? `Port ${basePort} is in use — auto-assigned port ${targetPort}` : undefined;
194
+ }
195
+
196
+ export function formatPortDriftWarning(basePort, targetPort) {
197
+ if (targetPort === basePort) return undefined;
198
+ return `Port ${basePort} was occupied — nginx now listens on port ${targetPort}. Any DevBridge tunnel bound to port ${basePort} is detached: run "devbridge port create <tunnelId> -p ${targetPort} --protocol http -a" and restart "devbridge host" for the new port.`;
199
+ }
200
+
201
+ export function formatProxyPortWarning(basePort, targetPort) {
202
+ if (targetPort === basePort) return undefined;
203
+ return `Port ${basePort} is in use — the proxy template still listens on port ${basePort}: auto-increment does not apply to proxy configs, so nginx may fail to bind. Free the port or deploy a static/spa build instead.`;
204
+ }
205
+
206
+ export function buildExposeRemediation(port) {
207
+ return `In the sandbox: source /tmp/hw_creds.sh; source /tmp/hw_api_key 2>/dev/null; devbridge delete-all; devbridge create <name>; devbridge port create <tunnelId> -p ${port} --protocol http -a; nohup devbridge host <tunnelId> -p ${port} > /tmp/host.log 2>&1 & If deploy_nginx reported a different (auto-incremented) port in its "port" field, use THAT port instead of the one shown here. Full procedure in huawei-sandbox skill, Step 7 (Expose via DevBridge).`;
208
+ }
209
+
183
210
  export async function uploadFileWithSession(workspaceId, localPath, remotePath, username = 'root', timeoutMs = 30000) {
184
211
  if (!existsSync(localPath)) {
185
212
  throw new Error(`sandbox upload: local file not found: ${localPath}`);
@@ -728,7 +755,6 @@ export async function deployNginx(
728
755
  const basePort = nginxType === 'proxy' ? listenPort : port;
729
756
 
730
757
  let targetPort = basePort;
731
- let portWarning;
732
758
  const maxPortAttempts = 10;
733
759
  for (let offset = 0; offset < maxPortAttempts; offset += 1) {
734
760
  targetPort = basePort + offset;
@@ -740,9 +766,6 @@ export async function deployNginx(
740
766
  10000,
741
767
  );
742
768
  if (!String(portCheck.stdout || '').includes('IN_USE')) break;
743
- if (offset === 0) {
744
- portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
745
- }
746
769
  } catch {}
747
770
  if (offset === maxPortAttempts - 1) {
748
771
  throw new Error(
@@ -840,9 +863,11 @@ fi`;
840
863
 
841
864
  let tunnelActive = false;
842
865
  try {
866
+ // Version-aware: devbridge 0.1.x exposes JSON via `list -j`; 0.2.x removed -j and
867
+ // prints a table whose data rows start with the 8-char base32 tunnel ID.
843
868
  const tunnelCheck = await execOneShot(
844
869
  workspaceId,
845
- 'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
870
+ '(devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' || devbridge list 2>/dev/null | grep -Eq \'^[a-z2-7]{8}[[:space:]]\') && echo "ACTIVE" || echo "INACTIVE"',
846
871
  username,
847
872
  10000,
848
873
  );
@@ -860,9 +885,17 @@ fi`;
860
885
  stdout: result.stdout,
861
886
  nextStep: 'expose_via_devbridge',
862
887
  warning:
863
- (!tunnelActive
864
- ? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
865
- : portWarning) || undefined,
888
+ [
889
+ !tunnelActive
890
+ ? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
891
+ : undefined,
892
+ nginxType === 'proxy'
893
+ ? formatProxyPortWarning(basePort, targetPort)
894
+ : formatPortConflictWarning(basePort, targetPort),
895
+ tunnelActive && nginxType !== 'proxy' ? formatPortDriftWarning(basePort, targetPort) : undefined,
896
+ ]
897
+ .filter(Boolean)
898
+ .join(' ') || undefined,
866
899
  };
867
900
  }
868
901
 
@@ -918,7 +951,13 @@ export async function deployCheck(
918
951
  `fi`,
919
952
  ``,
920
953
  `TOTAL=$((TOTAL+1))`,
954
+ `DB_TUNNEL_ACTIVE=0`,
921
955
  `if devbridge list -j 2>/dev/null | grep -q '"tunnelId"'; then`,
956
+ ` DB_TUNNEL_ACTIVE=1`,
957
+ `elif devbridge list 2>/dev/null | grep -Eq '^[a-z2-7]{8}[[:space:]]'; then`,
958
+ ` DB_TUNNEL_ACTIVE=1`,
959
+ `fi`,
960
+ `if [ "$DB_TUNNEL_ACTIVE" = "1" ]; then`,
922
961
  ` echo "devbridge_tunnel:PASS"`,
923
962
  ` PASS=$((PASS+1))`,
924
963
  `else`,
@@ -927,18 +966,29 @@ export async function deployCheck(
927
966
  ``,
928
967
  `TOTAL=$((TOTAL+1))`,
929
968
  `TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
930
- `TUNNEL_URL="https://\${TUNNEL_ID}-${port}.cn-north-4-bridge.myhuaweicloud.com"`,
931
- `if [ -n "$TUNNEL_ID" ] && [ -n "$TUNNEL_URL" ]; then`,
932
- ` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
933
- ` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
934
- ` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
935
- ` PASS=$((PASS+1))`,
936
- ` else`,
937
- ` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> HTTP $HTTP_CODE)"`,
938
- ` fi`,
969
+ `if [ -z "$TUNNEL_ID" ]; then`,
970
+ ` TUNNEL_ID=$(devbridge list 2>/dev/null | grep -E '^[a-z2-7]{8}[[:space:]]' | awk '{print $1}' | head -1)`,
971
+ `fi`,
972
+ `TUNNEL_URL="https://\${TUNNEL_ID}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN}"`,
973
+ `probe_tunnel() {`,
974
+ ` local url="$1" code`,
975
+ ` code=$(curl -s -o /tmp/.dc_tunnel_body -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000")`,
976
+ ` [ "$code" = "000" ] && return 1`,
977
+ ` # A migrated gateway serves a placeholder page with HTTP 200 — treat it as unreachable.`,
978
+ ` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then return 1; fi`,
979
+ ` rm -f /tmp/.dc_tunnel_body`,
980
+ ` [ "$code" = "200" ] || [ "$code" = "304" ] || return 1`,
981
+ ` return 0`,
982
+ `}`,
983
+ `if [ -n "$TUNNEL_ID" ] && probe_tunnel "$TUNNEL_URL"; then`,
984
+ ` echo "tunnel_url_accessible:PASS ($TUNNEL_URL)"`,
985
+ ` PASS=$((PASS+1))`,
986
+ `elif [ -n "$TUNNEL_ID" ]; then`,
987
+ ` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> unreachable, tunnel not found, or migration placeholder page)"`,
939
988
  `else`,
940
- ` echo "tunnel_url_accessible:FAIL (no tunnel URL)"`,
989
+ ` echo "tunnel_url_accessible:FAIL (no tunnel)"`,
941
990
  `fi`,
991
+ `rm -f /tmp/.dc_tunnel_body 2>/dev/null || true`,
942
992
  ``,
943
993
  `${`
944
994
  TOTAL=$((TOTAL+1))
@@ -999,7 +1049,7 @@ fi
999
1049
  if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
1000
1050
  }
1001
1051
  const scoreMatch = cleanStdout.match(/SCORE:(\d+)\/(\d+)/);
1002
- const tunnelMatch = cleanStdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
1052
+ const tunnelMatch = cleanStdout.match(TUNNEL_URL_PATTERN);
1003
1053
  const complete = /VERDICT:COMPLETE/.test(cleanStdout);
1004
1054
 
1005
1055
  const missing = [];
@@ -1015,6 +1065,18 @@ fi
1015
1065
  ? 'Check output parsing failed — individual check results could not be extracted. See rawOutput for details.'
1016
1066
  : undefined;
1017
1067
 
1068
+ const nextStepValue = !complete
1069
+ ? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
1070
+ ? 'expose_via_devbridge'
1071
+ : missing.includes('nginx_serving')
1072
+ ? 'configure_nginx'
1073
+ : missing.includes('qr_code')
1074
+ ? 'generate_qr_code'
1075
+ : parseWarning
1076
+ ? 'review_raw_output'
1077
+ : 'review_checks'
1078
+ : 'complete';
1079
+
1018
1080
  return {
1019
1081
  ok: true,
1020
1082
  complete,
@@ -1025,17 +1087,8 @@ fi
1025
1087
  missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
1026
1088
  parseWarning,
1027
1089
  rawOutput: parseWarning ? stdout.trim() : undefined,
1028
- nextStep: !complete
1029
- ? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
1030
- ? 'expose_via_devbridge'
1031
- : missing.includes('nginx_serving')
1032
- ? 'configure_nginx'
1033
- : missing.includes('qr_code')
1034
- ? 'generate_qr_code'
1035
- : parseWarning
1036
- ? 'review_raw_output'
1037
- : 'review_checks'
1038
- : 'complete',
1090
+ nextStep: nextStepValue,
1091
+ remediation: nextStepValue === 'expose_via_devbridge' ? buildExposeRemediation(port) : undefined,
1039
1092
  };
1040
1093
  }
1041
1094
 
@@ -189,6 +189,10 @@ function officeaceCapabilitiesDir() {
189
189
  if (isUsableOfficeaceRoot(configRoot)) return configRoot;
190
190
  const markerRoot = readOfficeaceRootMarker();
191
191
  if (markerRoot) return markerRoot;
192
+ // Skip system probes (registry / Program Files) so automated environments can
193
+ // keep OfficeAce detection hermetic — those probes can leak a real OfficeAce
194
+ // install into a sandboxed/fake HOME (#654).
195
+ if (process.env.HUAWEICLOUD_DEVKIT_SKIP_OFFICEACE_DETECT === '1') return null;
192
196
  const regDir = readOfficeaceRegistryInstallDir();
193
197
  if (regDir) {
194
198
  const dir = join(regDir, '.office-claw');
@@ -587,7 +591,10 @@ function installRuntimeDeps(pluginsDir) {
587
591
  function removeIfExists(p) {
588
592
  if (existsSync(p)) {
589
593
  try {
590
- rmSync(p, { recursive: true, force: true });
594
+ // Retry transient EBUSY/EPERM/ENOTEMPTY (locked files on Windows) before
595
+ // giving up — uninstall should not leave a skill behind just because a
596
+ // handle was momentarily open (#556).
597
+ rmSync(p, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
591
598
  return true;
592
599
  } catch (error) {
593
600
  console.log(` \x1b[33m[WARN]\x1b[0m Could not remove ${p}: ${error.message}`);
@@ -846,8 +853,7 @@ function uninstallOpenCode() {
846
853
  if (existsSync(skills)) {
847
854
  for (const entry of readdirSync(skills, { withFileTypes: true })) {
848
855
  if (entry.name.startsWith('huawei')) {
849
- removeIfExists(join(skills, entry.name));
850
- removed++;
856
+ if (removeIfExists(join(skills, entry.name))) removed++;
851
857
  }
852
858
  }
853
859
  console.log(` Removed ${removed} skills`);
@@ -858,8 +864,7 @@ function uninstallOpenCode() {
858
864
  if (existsSync(commands)) {
859
865
  for (const entry of readdirSync(commands, { withFileTypes: true })) {
860
866
  if (entry.name.startsWith('huawei')) {
861
- removeIfExists(join(commands, entry.name));
862
- cmdRemoved++;
867
+ if (removeIfExists(join(commands, entry.name))) cmdRemoved++;
863
868
  }
864
869
  }
865
870
  if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
@@ -885,8 +890,7 @@ function pruneStale(targetDir, sourceDir) {
885
890
  for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
886
891
  if (!entry.name.startsWith('huawei')) continue;
887
892
  if (!sourceNames.has(entry.name)) {
888
- removeIfExists(join(targetDir, entry.name));
889
- removed++;
893
+ if (removeIfExists(join(targetDir, entry.name))) removed++;
890
894
  }
891
895
  }
892
896
  return removed;
@@ -1033,8 +1037,7 @@ function uninstallOpenClaw() {
1033
1037
  if (existsSync(skillsDir)) {
1034
1038
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1035
1039
  if (entry.name.startsWith('huawei')) {
1036
- removeIfExists(join(skillsDir, entry.name));
1037
- removed++;
1040
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1038
1041
  }
1039
1042
  }
1040
1043
  console.log(` Removed ${removed} skills`);
@@ -1045,8 +1048,7 @@ function uninstallOpenClaw() {
1045
1048
  if (existsSync(cmdDir)) {
1046
1049
  for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
1047
1050
  if (entry.name.startsWith('huawei')) {
1048
- removeIfExists(join(cmdDir, entry.name));
1049
- cmdRemoved++;
1051
+ if (removeIfExists(join(cmdDir, entry.name))) cmdRemoved++;
1050
1052
  }
1051
1053
  }
1052
1054
  if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
@@ -1192,8 +1194,7 @@ function uninstallCodexDesktop() {
1192
1194
  if (existsSync(skillsDir)) {
1193
1195
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1194
1196
  if (entry.name.startsWith('huawei')) {
1195
- removeIfExists(join(skillsDir, entry.name));
1196
- removed++;
1197
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1197
1198
  }
1198
1199
  }
1199
1200
  console.log(` Removed ${removed} skills`);
@@ -1204,8 +1205,7 @@ function uninstallCodexDesktop() {
1204
1205
  if (existsSync(cmdDir)) {
1205
1206
  for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
1206
1207
  if (entry.name.startsWith('huawei')) {
1207
- removeIfExists(join(cmdDir, entry.name));
1208
- cmdRemoved++;
1208
+ if (removeIfExists(join(cmdDir, entry.name))) cmdRemoved++;
1209
1209
  }
1210
1210
  }
1211
1211
  if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
@@ -1349,8 +1349,7 @@ function uninstallCodeArts() {
1349
1349
  if (!existsSync(skillsDir)) continue;
1350
1350
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1351
1351
  if (entry.name.startsWith('huawei')) {
1352
- removeIfExists(join(skillsDir, entry.name));
1353
- removed++;
1352
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1354
1353
  }
1355
1354
  }
1356
1355
  }
@@ -1510,8 +1509,7 @@ function uninstallCodeArtsWork() {
1510
1509
  let removed = 0;
1511
1510
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1512
1511
  if (entry.name.startsWith('huawei')) {
1513
- removeIfExists(join(skillsDir, entry.name));
1514
- removed++;
1512
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1515
1513
  }
1516
1514
  }
1517
1515
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -1798,8 +1796,7 @@ function uninstallWorkBuddy() {
1798
1796
  if (existsSync(skillsDir)) {
1799
1797
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1800
1798
  if (entry.name.startsWith('huawei')) {
1801
- removeIfExists(join(skillsDir, entry.name));
1802
- removed++;
1799
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1803
1800
  }
1804
1801
  }
1805
1802
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -1835,9 +1832,10 @@ function uninstallWorkBuddy() {
1835
1832
  if (settings.hooks?.PostToolUse) {
1836
1833
  const before = settings.hooks.PostToolUse.length;
1837
1834
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((e) => e?.matcher !== '*');
1838
- if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
1835
+ const after = settings.hooks.PostToolUse.length;
1836
+ if (after === 0) delete settings.hooks.PostToolUse;
1839
1837
  if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
1840
- if (before !== settings.hooks.PostToolUse?.length) {
1838
+ if (before !== after) {
1841
1839
  writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
1842
1840
  console.log(` PostToolUse hook removed from ${settingsPath}`);
1843
1841
  }
@@ -1988,8 +1986,7 @@ function uninstallAtomCode() {
1988
1986
  if (existsSync(skillsDir)) {
1989
1987
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1990
1988
  if (entry.name.startsWith('huawei')) {
1991
- removeIfExists(join(skillsDir, entry.name));
1992
- removed++;
1989
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
1993
1990
  }
1994
1991
  }
1995
1992
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -2303,8 +2300,7 @@ function uninstallDsh() {
2303
2300
  if (existsSync(skillsDir)) {
2304
2301
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
2305
2302
  if (entry.name.startsWith('huawei')) {
2306
- removeIfExists(join(skillsDir, entry.name));
2307
- removed++;
2303
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
2308
2304
  }
2309
2305
  }
2310
2306
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -2445,8 +2441,7 @@ function uninstallOfficeAce() {
2445
2441
  if (existsSync(skillsDir)) {
2446
2442
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
2447
2443
  if (entry.name.startsWith('huawei')) {
2448
- removeIfExists(join(skillsDir, entry.name));
2449
- removed++;
2444
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
2450
2445
  }
2451
2446
  }
2452
2447
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -3029,8 +3024,7 @@ function uninstallHermes() {
3029
3024
  if (existsSync(skillsDir)) {
3030
3025
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
3031
3026
  if (entry.name.startsWith('huawei')) {
3032
- removeIfExists(join(skillsDir, entry.name));
3033
- removed++;
3027
+ if (removeIfExists(join(skillsDir, entry.name))) removed++;
3034
3028
  }
3035
3029
  }
3036
3030
  if (removed > 0) console.log(` Removed ${removed} skills`);
@@ -4171,7 +4165,7 @@ async function cmdDoctor() {
4171
4165
  console.log('\x1b[33mFix failures above, then restart your session.\x1b[0m');
4172
4166
  }
4173
4167
  if (fail === 0 && mcpConfigured) {
4174
- console.log('\n\x1b[32mAll checks passed.\x1b[0m Restart your session, then describe your Huawei Cloud task');
4168
+ console.log('\n\x1b[32mAll checks passed.\x1b[0m You can now describe your Huawei Cloud task');
4175
4169
  }
4176
4170
 
4177
4171
  // Detect "installed but not restarted" — check all supported agents
@@ -671,7 +671,7 @@ export const TOOL_DEFINITIONS = [
671
671
  {
672
672
  name: 'huaweicloud_sandbox_deploy_nginx',
673
673
  description:
674
- '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.',
674
+ '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. If the requested port is already in use, the actual port is auto-incremented and returned in "port" alongside a warning.',
675
675
  inputSchema: {
676
676
  type: 'object',
677
677
  required: ['nginx_type', 'port', 'project', 'output_dir'],
@@ -708,7 +708,7 @@ export const TOOL_DEFINITIONS = [
708
708
  {
709
709
  name: 'huaweicloud_sandbox_deploy_check',
710
710
  description:
711
- '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.',
711
+ '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, nextStep, and (when the DevBridge tunnel is missing) an executable "remediation" command string. Call this at the end of a deployment workflow to confirm everything is working before reporting success.',
712
712
  inputSchema: {
713
713
  type: 'object',
714
714
  required: ['port', 'project', 'output_dir'],
@@ -784,7 +784,7 @@ export const TOOL_DEFINITIONS = [
784
784
  {
785
785
  name: 'huaweicloud_sandbox_credentials',
786
786
  description:
787
- 'Configure temporary AK/SK for a sandbox via hdkitservice. Validates the current AK/SK against IAM before injecting (invalid SK is rejected here instead of failing later with APIGW.0301 during exec), then injects temporary credentials into the sandbox. The sandbox must be in RUNNING state.',
787
+ 'Configure temporary AK/SK for a sandbox via hdkitservice. Validates the current AK/SK against IAM before injecting (invalid SK is rejected here instead of failing later with APIGW.0301 during exec), then injects temporary credentials into the sandbox. Also injects an optional DevBridge API Key — required for devbridge 0.2.x tunnel exposure because 0.2.x removed AK/SK login. The API Key is a long-lived account-level credential, so it is stored in a separate file (/tmp/hw_api_key, 0600) from the temporary AK/SK (/tmp/hw_creds.sh). Source of truth: local HW_API_KEY env first, then the api_key param. The sandbox must be in RUNNING state.',
788
788
  inputSchema: {
789
789
  type: 'object',
790
790
  properties: {
@@ -796,6 +796,11 @@ export const TOOL_DEFINITIONS = [
796
796
  description:
797
797
  'Region used for IAM credential validation and project_id resolution (defaults to the configured region)',
798
798
  },
799
+ api_key: {
800
+ type: 'string',
801
+ description:
802
+ 'DevBridge API Key (devbridge_...), injected into the sandbox as HW_API_KEY for devbridge 0.2.x auth. The local HW_API_KEY environment variable takes precedence over this param (preferred delivery — keeps the long-lived key out of the conversation). Users create one at https://devstation.connect.huaweicloud.com/space/devbridge/apikey (full value shown once at creation). Required for exposing web apps via devbridge 0.2.x; if missing, guide the user through creating one.',
803
+ },
799
804
  },
800
805
  },
801
806
  },
@@ -1448,6 +1453,13 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1448
1453
  }
1449
1454
  const credResult = await hdkitCredentials(args.session_id, devStageId, args.enable_sts !== false);
1450
1455
  const sandboxWsIdCred = args.dev_stage_id || getCurrentWorkspaceId();
1456
+ // The DevBridge API Key is a LONG-LIVED account-level credential (no expiry, manual
1457
+ // revocation only) — unlike the temporary STS AK/SK. It is stored in its own file
1458
+ // (/tmp/hw_api_key, 0600) so an accidental dump of /tmp/hw_creds.sh never exposes it,
1459
+ // and the local HW_API_KEY env takes precedence over the tool param so the key can be
1460
+ // delivered without entering the conversation.
1461
+ const apiKey = process.env.HW_API_KEY || args.api_key || '';
1462
+ const apiKeyFile = '/tmp/hw_api_key';
1451
1463
  if (sandboxWsIdCred) {
1452
1464
  try {
1453
1465
  const { ak, sk, securitytoken } = getCredentials();
@@ -1468,12 +1480,31 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1468
1480
  15000,
1469
1481
  );
1470
1482
  await execWithSession(sandboxWsIdCred, `source ${credsFile} && echo "CREDS_SOURCED"`, 'root', 15000);
1483
+ if (apiKey) {
1484
+ await execOneShot(
1485
+ sandboxWsIdCred,
1486
+ `cat > ${apiKeyFile} << 'HWAPIKEY_EOF'\nexport HW_API_KEY='${apiKey}'\nHWAPIKEY_EOF\nchmod 600 ${apiKeyFile}`,
1487
+ 'root',
1488
+ 15000,
1489
+ );
1490
+ } else {
1491
+ // Refresh with no key → drop any stale copy, same semantics as the creds file rewrite.
1492
+ await execOneShot(sandboxWsIdCred, `rm -f ${apiKeyFile}`, 'root', 15000);
1493
+ }
1471
1494
  } catch {}
1472
1495
  }
1473
1496
  const result = {
1474
1497
  ...credResult,
1475
1498
  credentialValidation: validation.warning ? 'passed-with-warning' : 'passed',
1476
1499
  };
1500
+ if (sandboxWsIdCred) result.apiKeyInjected = Boolean(apiKey);
1501
+ if (apiKey) {
1502
+ result.apiKeyHint =
1503
+ 'DevBridge API Key written to /tmp/hw_api_key (0600, kept separate from the temporary AK/SK in /tmp/hw_creds.sh — it is a long-lived account-level credential). devbridge 0.2.x uses it via: source /tmp/hw_api_key && devbridge auth login --api-key "$HW_API_KEY". Never echo it into logs.';
1504
+ } else {
1505
+ result.apiKeyHint =
1506
+ 'No DevBridge API Key provided — devbridge 0.2.x cannot log in with AK/SK. To expose web apps, ask the user for an API Key (created at https://devstation.connect.huaweicloud.com/space/devbridge/apikey) and re-run with api_key, or set the local HW_API_KEY environment variable (preferred — keeps the key out of the conversation).';
1507
+ }
1477
1508
  if (validation.projectId) result.projectId = validation.projectId;
1478
1509
  if (validation.warning) result.warning = validation.warning;
1479
1510
  if (validation.skipped) result.warning = validation.error;
@@ -7,6 +7,7 @@ import { homedir } from 'node:os';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { fetchWithProxy } from './proxy/proxy-agent.mjs';
10
+ import { SUPPORTED_AGENT_TARGETS } from './auth/agent-registration.mjs';
10
11
 
11
12
  const IS_WINDOWS = process.platform === 'win32';
12
13
  const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
@@ -259,6 +260,9 @@ export function queryDistTagsSync({ timeoutMs = 15000, cwd } = {}) {
259
260
  timeout: timeoutMs,
260
261
  windowsHide: true,
261
262
  cwd,
263
+ // Windows + Node 22: spawning `npm.cmd` directly throws EINVAL (CVE-2024-27980
264
+ // mitigation); shell:true routes the .cmd through cmd.exe like other spawns (#643).
265
+ shell: true,
262
266
  });
263
267
  if (result.status !== 0) {
264
268
  debugLog(`queryDistTagsSync: npm view exited with status ${result.status}`);
@@ -278,6 +282,7 @@ export function queryDistTags({ timeoutMs = 15000, cwd } = {}) {
278
282
  child = spawn(NPM_BIN, ['view', 'huaweicloud-devkit', 'dist-tags', '--json'], {
279
283
  windowsHide: true,
280
284
  cwd,
285
+ shell: true,
281
286
  });
282
287
  } catch (error) {
283
288
  debugLog(`queryDistTags: ${error?.message || error}`);
@@ -401,6 +406,13 @@ export async function upgradePackage({ target = 'all', version = 'latest' } = {}
401
406
  if (version !== 'latest') {
402
407
  return { success: false, error: 'version 参数仅支持 latest。目标版本由插件自动判定。' };
403
408
  }
409
+ // The target is interpolated into an npx command spawned with shell:true;
410
+ // reject anything outside the known agent set + 'all' so a crafted value
411
+ // cannot be interpreted as shell operators (review #717).
412
+ const targetStr = String(target);
413
+ if (targetStr !== 'all' && !SUPPORTED_AGENT_TARGETS.includes(targetStr)) {
414
+ return { success: false, error: `不支持的升级目标:${targetStr}` };
415
+ }
404
416
  const { doQuery = queryDistTags, spawnFn = defaultSpawn } = options;
405
417
  // Tests inject currentVersion explicitly - the repo package.json version changes
406
418
  // between prerelease and stable lines, which must not flip the upgrade-tag logic.
@@ -431,7 +443,7 @@ export async function upgradePackage({ target = 'all', version = 'latest' } = {}
431
443
  const command = ['--yes', `huaweicloud-devkit@${tag}`, 'update', '--target', String(target)];
432
444
  let execResult;
433
445
  try {
434
- execResult = spawnFn(NPX_BIN, command, { encoding: 'utf8', timeout: 300000, windowsHide: true });
446
+ execResult = spawnFn(NPX_BIN, command, { encoding: 'utf8', timeout: 300000, windowsHide: true, shell: true });
435
447
  } catch (error) {
436
448
  return {
437
449
  success: false,