huaweicloud-devkit 1.1.0-next.13 → 1.1.0-next.15
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 +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 +68 -5
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +8 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
- package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +13 -1
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +80 -11
- package/plugins/huaweicloud-core/src/setup-cli.mjs +224 -30
- package/plugins/huaweicloud-core/src/tools.mjs +12 -6
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.15",
|
|
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.15",
|
|
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.15",
|
|
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.15",
|
|
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.15",
|
|
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.15",
|
|
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.15",
|
|
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.",
|
|
@@ -95,10 +95,15 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
95
95
|
2. **Real-name verification only** (`HDKIT_NOT_REALNAME`): tell the developer once, "Huawei Cloud requires real-name verification before using the sandbox — please complete it in the Huawei Cloud console (实名认证)." and stop — do not retry `connect` in a loop
|
|
96
96
|
3. **Sign agreement only** (`HDKIT_NOT_AGREEMENT`): **STOP and do NOT sign on your own.** Ask the developer: "Huawei Cloud sandbox requires signing the latest developer service agreement. May I sign it for you?" Then **wait for the developer to explicitly agree** (e.g. "签署" / "确认" / "sign it"). Only after explicit consent call `huaweicloud_sandbox_sign_agreement` and return its result (`signed`/`signedCount`) to the developer. **Never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent.** Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
|
|
97
97
|
4. **Both missing** (`HDKIT_NOT_REALNAME_AND_AGREEMENT`): present **both** requirements together in one message — the real-name verification steps (console, step 2) **and** the agreement-signing request (step 3, wait for explicit consent) — so the developer can complete both at once
|
|
98
|
-
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
|
|
99
|
-
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs
|
|
98
|
+
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`. The `source` parameter identifies the calling agent (valid values: `'CLI'`, `'WEb'`, `'VSCODE'`, `'CURSO'`, `'WEBIDE'`, etc. — case-sensitive). The `git` parameter (with `repo_url`, `repo_name`, `target_path`) is accepted but does NOT auto-clone the repository — always clone manually.
|
|
99
|
+
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs, DevBridge tunnels, and stale web processes from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
+
# Kill stale Node.js web processes from previous deployments
|
|
103
|
+
pkill -9 -f "next-server" 2>/dev/null || true
|
|
104
|
+
pkill -9 -f "next start" 2>/dev/null || true
|
|
105
|
+
pkill -9 -f "nuxt" 2>/dev/null || true
|
|
106
|
+
sleep 1
|
|
102
107
|
# Remove stale nginx configs from previous deployments
|
|
103
108
|
sudo rm -f /etc/nginx/conf.d/*.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
|
|
104
109
|
# Remove stale DevBridge tunnels
|
|
@@ -202,7 +207,7 @@ fi
|
|
|
202
207
|
export PATH=$PATH:$HOME/.huawei/bin # installer only writes ~/.bashrc; session shells do not re-source it
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
**Login** (non-interactive; credentials
|
|
210
|
+
**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:
|
|
206
211
|
|
|
207
212
|
```bash
|
|
208
213
|
source /tmp/hw_creds.sh 2>/dev/null
|
|
@@ -349,6 +354,16 @@ if command -v apt-get >/dev/null 2>&1; then echo "PKG_MGR=apt"; elif command -v
|
|
|
349
354
|
|
|
350
355
|
Use the detected `PKG_MGR` for all package installations below.
|
|
351
356
|
|
|
357
|
+
**Architecture awareness**: the sandbox runs Linux aarch64 (ARM64). Native binaries built on x64 (Windows/macOS Intel) will not execute. Always install dependencies and build inside the sandbox. For projects with native addons (Taro `@swc/core`, Prisma, `esbuild`, `node-gyp`), local x64 pre-build + upload of `dist/` output is a viable alternative when sandbox builds fail.
|
|
358
|
+
|
|
359
|
+
**GitCode SSL**: if `git clone` from GitCode fails with SSL certificate errors:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
git config --global http.sslVerify false
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Then retry the clone. This is a known sandbox environment limitation.
|
|
366
|
+
|
|
352
367
|
#### 3b: Install nginx (before project upload)
|
|
353
368
|
|
|
354
369
|
```bash
|
|
@@ -422,6 +437,15 @@ echo "NODE_ENV=${NODE_ENV:-development}"
|
|
|
422
437
|
|
|
423
438
|
This must run via `exec_with_session` so the exported variables persist for subsequent build commands in the same session.
|
|
424
439
|
|
|
440
|
+
**Prisma / ORM compatibility**: Prisma CLI (`prisma generate`, `prisma db push`, `prisma migrate`) only reads `.env` by default, NOT `.env.local` or `.env.development`. If the project uses `.env.local` for `DATABASE_URL`, link it before any Prisma command:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
# Prisma requires .env (not .env.local) — symlink if needed
|
|
444
|
+
if [ -f .env.local ] && [ ! -f .env ]; then ln -sf .env.local .env 2>/dev/null || cp .env.local .env; fi
|
|
445
|
+
# Then re-source
|
|
446
|
+
set -a && source .env 2>/dev/null; set +a
|
|
447
|
+
```
|
|
448
|
+
|
|
425
449
|
#### 4b: Install Dependencies
|
|
426
450
|
|
|
427
451
|
Use `exec_one_shot` for install (no shared state needed). Skip if `node_modules` already exists:
|
|
@@ -444,6 +468,18 @@ Node v24+ uses musl-based binaries on some sandbox images, which may break nativ
|
|
|
444
468
|
- For rollup 4 projects, consider `npm install rollup@3` as fallback
|
|
445
469
|
- If webpack/rollup native addon errors persist, add `--ignore-scripts` then manually rebuild: `npm rebuild`
|
|
446
470
|
|
|
471
|
+
**Prisma / database initialization**: if `prisma/schema.prisma` exists in the project, initialize the database after install and before build. Prisma Client generation (`prisma generate`) is usually handled by `postinstall`, but `prisma db push` (SQLite) or `prisma migrate deploy` (PostgreSQL/MySQL) must be run manually:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
cd /workspace/<dirname>
|
|
475
|
+
if [ -f prisma/schema.prisma ]; then
|
|
476
|
+
echo "Prisma schema detected — initializing database..."
|
|
477
|
+
npx prisma db push --skip-generate 2>/dev/null || npx prisma migrate deploy 2>/dev/null || echo "WARN: skip db init"
|
|
478
|
+
fi
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
> `--skip-generate` avoids redundant generation when `postinstall` already ran `prisma generate`. For SQLite, `DATABASE_URL="file:./dev.db"` must be set in `.env`/`.env.local` before this step.
|
|
482
|
+
|
|
447
483
|
#### 4c: Build
|
|
448
484
|
|
|
449
485
|
**Timeout strategy by framework type:**
|
|
@@ -587,6 +623,17 @@ chmod -R +x "$REAL_ROOT/node_modules/.bin" 2>/dev/null || true
|
|
|
587
623
|
|
|
588
624
|
This prevents the most common deployment failure: nginx 500 with `stat() ... Permission denied` caused by missing `o+x` on intermediate directories.
|
|
589
625
|
|
|
626
|
+
#### 4e: Write Deployment Fingerprint
|
|
627
|
+
|
|
628
|
+
After a successful build, write a deployment fingerprint into the output directory. This enables `deploy_check` to verify the nginx-served content belongs to the current deployment (not a stale process from an earlier session):
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
echo "deployed-$(date +%s)-<dirname>" > /workspace/<dirname>/<outputDir>/.deploy_fingerprint
|
|
632
|
+
chmod o+r /workspace/<dirname>/<outputDir>/.deploy_fingerprint 2>/dev/null || true
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
> For SSR proxy type, nginx does not serve static files from `.next` — the fingerprint check will gracefully SKIP in `deploy_check`. The fingerprint is still written as a deployment marker for debugging.
|
|
636
|
+
|
|
590
637
|
### Step 5: Port Availability Check
|
|
591
638
|
|
|
592
639
|
**Before configuring nginx or starting the app**, verify the target ports are free. Port conflicts from previous deployments cause silent failures:
|
|
@@ -654,7 +701,7 @@ Use `huaweicloud_sandbox_deploy_nginx` to write the correct template, fix direct
|
|
|
654
701
|
- `port` — listen port from framework detection
|
|
655
702
|
- `project` — project dir name under `/workspace`
|
|
656
703
|
- `output_dir` — build output dir relative to `/workspace/<project>`
|
|
657
|
-
- `node_port` — Node.js app port
|
|
704
|
+
- `node_port` — Node.js app port for SSR proxy. Defaults to `<port> + 1` if omitted, and the result includes `nodePort` so you know which port to bind the Node process to.
|
|
658
705
|
- `public_port` — public listen port for SSR proxy (optional, defaults to `port`)
|
|
659
706
|
|
|
660
707
|
The tool automatically handles:
|
|
@@ -682,7 +729,21 @@ If the status code is not 2xx/3xx:
|
|
|
682
729
|
### Step 6: Start the App [REQUIRED]
|
|
683
730
|
|
|
684
731
|
- **Static (SPA/SSG/cross-platform)**: nginx is already serving. Skip.
|
|
685
|
-
- **SSR**:
|
|
732
|
+
- **SSR**: `PORT=<nodePort>` prefix is REQUIRED before `<serveCmd>`. nginx `proxy_pass` targets `<nodePort>`, not `<port>` — the two must differ. `deployNginx` returns `nodePort` in its result (defaults to `<port> + 1` for proxy type).
|
|
733
|
+
|
|
734
|
+
**Runtime environment variables**: SSR apps often need `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET`, etc. at runtime. Before starting, verify env vars from Step 4a are still loaded, and re-source `.env` if the shell session was reset:
|
|
735
|
+
|
|
736
|
+
```bash
|
|
737
|
+
cd /workspace/<dirname>
|
|
738
|
+
# Re-load env vars (SSR apps need them at runtime, not just build time)
|
|
739
|
+
if [ -f .env ]; then set -a && source .env 2>/dev/null; set +a; echo "Loaded .env"; fi
|
|
740
|
+
if [ -f .env.local ]; then set -a && source .env.local 2>/dev/null; set +a; echo "Loaded .env.local"; fi
|
|
741
|
+
# Verify critical vars
|
|
742
|
+
echo "DATABASE_URL=${DATABASE_URL:-<NOT SET>}"
|
|
743
|
+
echo "NEXTAUTH_URL=${NEXTAUTH_URL:-<NOT SET>}"
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Then start with `PORT=<nodePort> <serveCmd>` via `exec_with_session` to run the Node process in background.
|
|
686
747
|
|
|
687
748
|
### Step 7: Expose via DevBridge [REQUIRED — deployment incomplete without this]
|
|
688
749
|
|
|
@@ -771,6 +832,8 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
|
|
|
771
832
|
| 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. |
|
|
772
833
|
| Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
|
|
773
834
|
| 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). |
|
|
835
|
+
| 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. |
|
|
836
|
+
| 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. |
|
|
774
837
|
| Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
|
|
775
838
|
| Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
|
|
776
839
|
| 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 |
|
|
@@ -42,6 +42,7 @@ sudo tee /etc/nginx/conf.d/app.conf > /dev/null << 'NGINX_EOF'
|
|
|
42
42
|
server {
|
|
43
43
|
listen <publicPort>;
|
|
44
44
|
server_name _;
|
|
45
|
+
large_client_header_buffers 4 32k;
|
|
45
46
|
|
|
46
47
|
location / {
|
|
47
48
|
proxy_pass http://127.0.0.1:<nodePort>;
|
|
@@ -54,6 +55,9 @@ server {
|
|
|
54
55
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
55
56
|
proxy_cache_bypass $http_upgrade;
|
|
56
57
|
proxy_read_timeout 60s;
|
|
58
|
+
proxy_buffer_size 128k;
|
|
59
|
+
proxy_buffers 4 256k;
|
|
60
|
+
proxy_busy_buffers_size 256k;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
NGINX_EOF
|
|
@@ -9,6 +9,7 @@ export const SUPPORTED_AGENT_TARGETS = [
|
|
|
9
9
|
'codex',
|
|
10
10
|
'codex-desktop',
|
|
11
11
|
'codearts',
|
|
12
|
+
'codearts-work',
|
|
12
13
|
'workbuddy',
|
|
13
14
|
'dsh',
|
|
14
15
|
'officeace',
|
|
@@ -78,6 +79,12 @@ function codeartsRegistered() {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function codeartsWorkRegistered() {
|
|
83
|
+
const path = join(baseHome(), '.codeartswork', 'mcp', 'mcp_settings.json');
|
|
84
|
+
const cfg = readJsonSafe(path);
|
|
85
|
+
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
function workbuddyRegistered() {
|
|
82
89
|
const cfg = readJsonSafe(join(baseHome(), '.workbuddy', 'mcp.json'));
|
|
83
90
|
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
@@ -208,6 +215,7 @@ export function getAgentRegistrationStatuses(target = 'all') {
|
|
|
208
215
|
if (agent === 'codex-desktop') configured = codexDesktopRegistered();
|
|
209
216
|
if (agent === 'codex') configured = codexCliRegistered();
|
|
210
217
|
if (agent === 'codearts') configured = codeartsRegistered();
|
|
218
|
+
if (agent === 'codearts-work') configured = codeartsWorkRegistered();
|
|
211
219
|
if (agent === 'workbuddy') configured = workbuddyRegistered();
|
|
212
220
|
if (agent === 'dsh') configured = dshRegistered();
|
|
213
221
|
if (agent === 'officeace') configured = officeaceRegistered();
|
|
@@ -123,7 +123,11 @@ export function getParentCwd() {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function isCodeArtsContext() {
|
|
126
|
-
return
|
|
126
|
+
return (
|
|
127
|
+
existsSync(join(process.cwd(), '.codeartsdoer')) ||
|
|
128
|
+
existsSync(join(homedir(), '.codeartsdoer')) ||
|
|
129
|
+
existsSync(join(homedir(), '.codeartswork'))
|
|
130
|
+
);
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function readCodeArtsCredentials() {
|
|
@@ -154,6 +158,31 @@ function readCodeArtsCredentials() {
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
// CodeArts Work — user-level only
|
|
162
|
+
{
|
|
163
|
+
const path = join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
|
|
164
|
+
try {
|
|
165
|
+
if (existsSync(path)) {
|
|
166
|
+
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
167
|
+
const server = config?.mcpServers?.['huaweicloud-devkit'];
|
|
168
|
+
if (server?.env) {
|
|
169
|
+
const ak = server.env.HW_ACCESS_KEY;
|
|
170
|
+
const sk = server.env.HW_SECRET_KEY;
|
|
171
|
+
if (ak && sk) {
|
|
172
|
+
return {
|
|
173
|
+
ak,
|
|
174
|
+
sk,
|
|
175
|
+
securityToken: server.env.HW_SECURITY_TOKEN || '',
|
|
176
|
+
region: server.env.HW_REGION || server.env.HUAWEICLOUD_REGION || '',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// mcp_settings.json missing or invalid — skip
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
157
186
|
return null;
|
|
158
187
|
}
|
|
159
188
|
|
|
@@ -157,10 +157,13 @@ function readPkg(projectPath) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function detectPackageManager(projectPath) {
|
|
160
|
+
function detectPackageManager(projectPath, pkg) {
|
|
161
161
|
if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
162
162
|
if (existsSync(join(projectPath, 'yarn.lock'))) return 'yarn';
|
|
163
163
|
if (existsSync(join(projectPath, 'package-lock.json'))) return 'npm';
|
|
164
|
+
if (existsSync(join(projectPath, 'bun.lockb'))) return 'bun';
|
|
165
|
+
const declared = (pkg?.packageManager || '').split('@')[0];
|
|
166
|
+
if (declared && ['pnpm', 'yarn', 'npm', 'bun'].includes(declared)) return declared;
|
|
164
167
|
return 'npm';
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -234,10 +237,24 @@ function frameworkResult(fw, pm, projectPath) {
|
|
|
234
237
|
return patchCommands({ ...fw, packageManager: pm, rootDir: projectPath }, pm);
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
function readVitepressOutDir(projectPath) {
|
|
241
|
+
const configFiles = ['config.mts', 'config.ts', 'config.mjs', 'config.js'];
|
|
242
|
+
for (const cf of configFiles) {
|
|
243
|
+
const configPath = join(projectPath, '.vitepress', cf);
|
|
244
|
+
if (!existsSync(configPath)) continue;
|
|
245
|
+
try {
|
|
246
|
+
const content = readFileSync(configPath, 'utf8');
|
|
247
|
+
const match = content.match(/outDir\s*:\s*['"]([^'"]+)['"]/);
|
|
248
|
+
if (match) return match[1];
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
237
254
|
export function detectFramework(projectPath) {
|
|
238
255
|
const pkg = readPkg(projectPath);
|
|
239
256
|
const deps = collectDeps(pkg);
|
|
240
|
-
const pm = detectPackageManager(projectPath);
|
|
257
|
+
const pm = detectPackageManager(projectPath, pkg);
|
|
241
258
|
|
|
242
259
|
if (
|
|
243
260
|
existsSync(join(projectPath, 'pnpm-workspace.yaml')) ||
|
|
@@ -287,7 +304,12 @@ export function detectFramework(projectPath) {
|
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
if (existsSync(join(projectPath, '.vitepress'))) {
|
|
290
|
-
|
|
307
|
+
const outDir = readVitepressOutDir(projectPath);
|
|
308
|
+
return frameworkResult(
|
|
309
|
+
outDir ? { ...FRAMEWORKS.vitepress, outputDir: outDir } : FRAMEWORKS.vitepress,
|
|
310
|
+
pm,
|
|
311
|
+
projectPath,
|
|
312
|
+
);
|
|
291
313
|
}
|
|
292
314
|
|
|
293
315
|
if (
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
|
|
3
6
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
4
7
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
@@ -53,10 +56,19 @@ export async function runHcloud(args, options = {}) {
|
|
|
53
56
|
throw new Error('Unreachable retry state.');
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
function discoverHcloudPath() {
|
|
60
|
+
if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
|
|
61
|
+
const candidates =
|
|
62
|
+
process.platform === 'win32'
|
|
63
|
+
? [join(homedir(), 'hcloud', 'hcloud.exe')]
|
|
64
|
+
: [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
|
|
65
|
+
return candidates.find((c) => existsSync(c)) || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
function runHcloudOnce(plan, options) {
|
|
57
69
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
58
70
|
const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
|
|
59
|
-
const executable = options.executable || options.env?.HCLOUD_BIN ||
|
|
71
|
+
const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
|
|
60
72
|
const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
|
|
61
73
|
const cwd = options.cwd || undefined;
|
|
62
74
|
|
|
@@ -7,6 +7,12 @@ const HDKIT_BASE_URL =
|
|
|
7
7
|
async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
8
8
|
const { ak, sk, securitytoken } = getCredentials();
|
|
9
9
|
|
|
10
|
+
if (!ak || !sk) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Huawei Cloud credentials are not configured. ' +
|
|
13
|
+
'Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
|
|
14
|
+
);
|
|
15
|
+
}
|
|
10
16
|
const headers = {
|
|
11
17
|
'Content-Type': 'application/json',
|
|
12
18
|
'X-HW-AK': ak,
|
|
@@ -49,10 +55,12 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (!resp.ok) {
|
|
52
|
-
const
|
|
53
|
-
|
|
58
|
+
const code = data.code || `HTTP_${resp.status}`;
|
|
59
|
+
const trace = data.traceId ? ` [trace: ${data.traceId}]` : '';
|
|
60
|
+
const err = new Error(`${code}: ${data.message || 'hdkitservice error'}${trace}`);
|
|
61
|
+
err.code = code;
|
|
54
62
|
err.status = resp.status;
|
|
55
|
-
err.traceId = data.traceId;
|
|
63
|
+
err.traceId = data.traceId;
|
|
56
64
|
throw err;
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -694,7 +694,7 @@ export async function deployNginx(
|
|
|
694
694
|
workspaceId,
|
|
695
695
|
{ nginxType, port, project, outputDir, nodePort, publicPort, configName },
|
|
696
696
|
username = 'root',
|
|
697
|
-
timeoutMs =
|
|
697
|
+
timeoutMs = 60000,
|
|
698
698
|
) {
|
|
699
699
|
if (!workspaceId) {
|
|
700
700
|
throw new Error('sandbox deploy nginx: workspace_id is required.');
|
|
@@ -703,6 +703,53 @@ export async function deployNginx(
|
|
|
703
703
|
throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
const nginxCheck = await execOneShot(
|
|
707
|
+
workspaceId,
|
|
708
|
+
'command -v nginx >/dev/null 2>&1 && echo "INSTALLED" || echo "MISSING"',
|
|
709
|
+
username,
|
|
710
|
+
10000,
|
|
711
|
+
);
|
|
712
|
+
if (!String(nginxCheck.stdout || '').includes('INSTALLED')) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
'sandbox deploy nginx: nginx is not installed. Install it first:\n' +
|
|
715
|
+
' Detect OS: source /etc/os-release && echo $ID\n' +
|
|
716
|
+
' apt: sudo apt-get update -qq && sudo apt-get install -y -qq nginx\n' +
|
|
717
|
+
' yum: sudo yum install -y nginx\n' +
|
|
718
|
+
' dnf: sudo dnf install -y nginx\n' +
|
|
719
|
+
'Alternatively, skip nginx and use Python HTTP server (see nginx-templates.md).',
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const listenPort = publicPort || port;
|
|
724
|
+
const basePort = nginxType === 'proxy' ? listenPort : port;
|
|
725
|
+
|
|
726
|
+
let targetPort = basePort;
|
|
727
|
+
let portWarning;
|
|
728
|
+
const maxPortAttempts = 10;
|
|
729
|
+
for (let offset = 0; offset < maxPortAttempts; offset += 1) {
|
|
730
|
+
targetPort = basePort + offset;
|
|
731
|
+
try {
|
|
732
|
+
const portCheck = await execOneShot(
|
|
733
|
+
workspaceId,
|
|
734
|
+
`ss -tlnp 2>/dev/null | grep -q ":${targetPort} " && echo "IN_USE" || echo "FREE"`,
|
|
735
|
+
username,
|
|
736
|
+
10000,
|
|
737
|
+
);
|
|
738
|
+
if (!String(portCheck.stdout || '').includes('IN_USE')) break;
|
|
739
|
+
if (offset === 0) {
|
|
740
|
+
portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
|
|
741
|
+
}
|
|
742
|
+
} catch {}
|
|
743
|
+
if (offset === maxPortAttempts - 1) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`sandbox deploy nginx: all ports ${basePort}-${basePort + maxPortAttempts - 1} are in use. Free a port and try again.`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const effectiveNodePort =
|
|
751
|
+
nginxType === 'proxy' ? (nodePort && nodePort !== listenPort ? nodePort : listenPort + 1) : undefined;
|
|
752
|
+
|
|
706
753
|
const projectPath = `/workspace/${project}`;
|
|
707
754
|
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
708
755
|
|
|
@@ -715,7 +762,7 @@ fi`;
|
|
|
715
762
|
|
|
716
763
|
const templates = {
|
|
717
764
|
spa: `server {
|
|
718
|
-
listen ${
|
|
765
|
+
listen ${targetPort};
|
|
719
766
|
root ${outputPath};
|
|
720
767
|
index index.html;
|
|
721
768
|
|
|
@@ -729,11 +776,12 @@ fi`;
|
|
|
729
776
|
}
|
|
730
777
|
}`,
|
|
731
778
|
proxy: `server {
|
|
732
|
-
listen ${
|
|
779
|
+
listen ${listenPort};
|
|
733
780
|
server_name _;
|
|
781
|
+
large_client_header_buffers 4 32k;
|
|
734
782
|
|
|
735
783
|
location / {
|
|
736
|
-
proxy_pass http://127.0.0.1:${
|
|
784
|
+
proxy_pass http://127.0.0.1:${effectiveNodePort};
|
|
737
785
|
proxy_http_version 1.1;
|
|
738
786
|
proxy_set_header Upgrade $http_upgrade;
|
|
739
787
|
proxy_set_header Connection 'upgrade';
|
|
@@ -743,10 +791,13 @@ fi`;
|
|
|
743
791
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
744
792
|
proxy_cache_bypass $http_upgrade;
|
|
745
793
|
proxy_read_timeout 60s;
|
|
794
|
+
proxy_buffer_size 128k;
|
|
795
|
+
proxy_buffers 4 256k;
|
|
796
|
+
proxy_busy_buffers_size 256k;
|
|
746
797
|
}
|
|
747
798
|
}`,
|
|
748
799
|
static: `server {
|
|
749
|
-
listen ${
|
|
800
|
+
listen ${targetPort};
|
|
750
801
|
root ${outputPath};
|
|
751
802
|
index index.html;
|
|
752
803
|
|
|
@@ -769,14 +820,14 @@ fi`;
|
|
|
769
820
|
const cmd = [
|
|
770
821
|
resolveScript,
|
|
771
822
|
`sudo mkdir -p /etc/nginx/conf.d`,
|
|
772
|
-
`sudo tee /etc/nginx/conf.d/${configName ||
|
|
823
|
+
`sudo tee /etc/nginx/conf.d/${configName || project}.conf > /dev/null << 'NGINX_EOF'`,
|
|
773
824
|
config,
|
|
774
825
|
`NGINX_EOF`,
|
|
775
826
|
`# Resolve symlinks for chmod (chmod does not follow symlinks on Linux)`,
|
|
776
827
|
`chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
|
|
777
828
|
`find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
778
829
|
`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`,
|
|
830
|
+
`if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null || { sudo killall -9 nginx 2>/dev/null; sleep 1; sudo nginx; }; else sudo nginx; fi`,
|
|
780
831
|
].join('\n');
|
|
781
832
|
|
|
782
833
|
const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
|
|
@@ -795,15 +846,17 @@ fi`;
|
|
|
795
846
|
return {
|
|
796
847
|
ok: result.exitCode === 0,
|
|
797
848
|
nginxType,
|
|
798
|
-
port:
|
|
849
|
+
port: targetPort,
|
|
850
|
+
nodePort: effectiveNodePort || undefined,
|
|
799
851
|
outputPath,
|
|
800
852
|
projectPath,
|
|
801
853
|
exitCode: result.exitCode,
|
|
802
854
|
stdout: result.stdout,
|
|
803
855
|
nextStep: 'expose_via_devbridge',
|
|
804
|
-
warning:
|
|
805
|
-
|
|
806
|
-
|
|
856
|
+
warning:
|
|
857
|
+
(!tunnelActive
|
|
858
|
+
? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
|
|
859
|
+
: portWarning) || undefined,
|
|
807
860
|
};
|
|
808
861
|
}
|
|
809
862
|
|
|
@@ -843,6 +896,22 @@ export async function deployCheck(
|
|
|
843
896
|
`fi`,
|
|
844
897
|
``,
|
|
845
898
|
`TOTAL=$((TOTAL+1))`,
|
|
899
|
+
`FINGERPRINT_FILE="${outputPath}/.deploy_fingerprint"`,
|
|
900
|
+
`FINGERPRINT_EXPECTED=$(cat "$FINGERPRINT_FILE" 2>/dev/null)`,
|
|
901
|
+
`if [ -n "$FINGERPRINT_EXPECTED" ]; then`,
|
|
902
|
+
` FINGERPRINT_ACTUAL=$(curl -s http://localhost:${port}/.deploy_fingerprint 2>/dev/null)`,
|
|
903
|
+
` if [ "$FINGERPRINT_EXPECTED" = "$FINGERPRINT_ACTUAL" ]; then`,
|
|
904
|
+
` echo "content_verified:PASS"`,
|
|
905
|
+
` PASS=$((PASS+1))`,
|
|
906
|
+
` else`,
|
|
907
|
+
` echo "content_verified:FAIL (fingerprint mismatch — nginx may be serving stale content from a previous deployment)"`,
|
|
908
|
+
` fi`,
|
|
909
|
+
`else`,
|
|
910
|
+
` echo "content_verified:SKIP (no fingerprint file)"`,
|
|
911
|
+
` TOTAL=$((TOTAL-1))`,
|
|
912
|
+
`fi`,
|
|
913
|
+
``,
|
|
914
|
+
`TOTAL=$((TOTAL+1))`,
|
|
846
915
|
`if devbridge ls 2>/dev/null | grep -q "Tunnel URL"; then`,
|
|
847
916
|
` echo "devbridge_tunnel:PASS"`,
|
|
848
917
|
` PASS=$((PASS+1))`,
|
|
@@ -105,6 +105,17 @@ function codeartsPluginsDir() {
|
|
|
105
105
|
return join(homedir(), '.codeartsdoer', 'huaweicloud-plugins');
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// CodeArts Work (CodeArts Space) — user-level only
|
|
109
|
+
function codeartsWorkSkillsDir() {
|
|
110
|
+
return join(homedir(), '.codeartswork', 'skills');
|
|
111
|
+
}
|
|
112
|
+
function codeartsWorkMcpSettingsFile() {
|
|
113
|
+
return join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
|
|
114
|
+
}
|
|
115
|
+
function codeartsWorkPluginsDir() {
|
|
116
|
+
return join(homedir(), '.codeartswork', 'huaweicloud-plugins');
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
function workbuddySkillsDir() {
|
|
109
120
|
return join(homedir(), '.workbuddy', 'skills');
|
|
110
121
|
}
|
|
@@ -1248,6 +1259,146 @@ function codeartsStatus() {
|
|
|
1248
1259
|
}
|
|
1249
1260
|
}
|
|
1250
1261
|
|
|
1262
|
+
// --- CodeArts Work (CodeArts Space) ---
|
|
1263
|
+
|
|
1264
|
+
function registerCodeartsWorkMcp() {
|
|
1265
|
+
const configPath = codeartsWorkMcpSettingsFile();
|
|
1266
|
+
const mcpPath = join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
1267
|
+
const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
|
|
1268
|
+
const hcloudBin = findHcloudBin();
|
|
1269
|
+
if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
|
|
1270
|
+
let config = {};
|
|
1271
|
+
if (existsSync(configPath)) {
|
|
1272
|
+
try {
|
|
1273
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1274
|
+
} catch {
|
|
1275
|
+
console.log(
|
|
1276
|
+
` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`,
|
|
1277
|
+
);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const existing = config.mcpServers?.['huaweicloud-devkit'];
|
|
1281
|
+
if (
|
|
1282
|
+
existing &&
|
|
1283
|
+
existing.command === 'node' &&
|
|
1284
|
+
Array.isArray(existing.args) &&
|
|
1285
|
+
existing.args[0] === mcpPath &&
|
|
1286
|
+
existing.timeout === 300000
|
|
1287
|
+
) {
|
|
1288
|
+
console.log(` MCP config unchanged: ${configPath}`);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
config.mcpServers = config.mcpServers || {};
|
|
1293
|
+
config.mcpServers['huaweicloud-devkit'] = {
|
|
1294
|
+
command: 'node',
|
|
1295
|
+
args: [mcpPath],
|
|
1296
|
+
env,
|
|
1297
|
+
enabled: true,
|
|
1298
|
+
timeout: 300000,
|
|
1299
|
+
};
|
|
1300
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1301
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1302
|
+
console.log(` MCP config updated: ${configPath}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function installCodeArtsWork() {
|
|
1306
|
+
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
1307
|
+
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
1308
|
+
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
1309
|
+
|
|
1310
|
+
copyDir(skillsSrc, codeartsWorkSkillsDir());
|
|
1311
|
+
console.log(` Skills -> ${codeartsWorkSkillsDir()}`);
|
|
1312
|
+
|
|
1313
|
+
const pluginDest = codeartsWorkPluginsDir();
|
|
1314
|
+
copyDir(srcDir, join(pluginDest, 'src'));
|
|
1315
|
+
console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
|
|
1316
|
+
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
1317
|
+
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
1318
|
+
|
|
1319
|
+
registerCodeartsWorkMcp();
|
|
1320
|
+
installRuntimeDeps(pluginDest);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function updateCodeArtsWork() {
|
|
1324
|
+
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
1325
|
+
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
1326
|
+
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
1327
|
+
const pluginDest = codeartsWorkPluginsDir();
|
|
1328
|
+
|
|
1329
|
+
copyDir(skillsSrc, codeartsWorkSkillsDir());
|
|
1330
|
+
console.log(` Skills updated -> ${codeartsWorkSkillsDir()}`);
|
|
1331
|
+
copyDir(srcDir, join(pluginDest, 'src'));
|
|
1332
|
+
console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
|
|
1333
|
+
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
1334
|
+
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
1335
|
+
registerCodeartsWorkMcp();
|
|
1336
|
+
mkdirSync(pluginDest, { recursive: true });
|
|
1337
|
+
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
1338
|
+
installRuntimeDeps(pluginDest);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function uninstallCodeArtsWork() {
|
|
1342
|
+
const skillsDir = codeartsWorkSkillsDir();
|
|
1343
|
+
if (existsSync(skillsDir)) {
|
|
1344
|
+
let removed = 0;
|
|
1345
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1346
|
+
if (entry.name.startsWith('huawei')) {
|
|
1347
|
+
removeIfExists(join(skillsDir, entry.name));
|
|
1348
|
+
removed++;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (removeIfExists(codeartsWorkPluginsDir())) {
|
|
1355
|
+
console.log(' Removed MCP server and safety policy');
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const configPath = codeartsWorkMcpSettingsFile();
|
|
1359
|
+
if (existsSync(configPath)) {
|
|
1360
|
+
let config = {};
|
|
1361
|
+
try {
|
|
1362
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1363
|
+
} catch {}
|
|
1364
|
+
if (config.mcpServers?.['huaweicloud-devkit']) {
|
|
1365
|
+
delete config.mcpServers['huaweicloud-devkit'];
|
|
1366
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
1367
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1368
|
+
console.log(` Config cleaned: ${configPath}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function codeartsWorkStatus() {
|
|
1374
|
+
const pluginDir = codeartsWorkPluginsDir();
|
|
1375
|
+
console.log(
|
|
1376
|
+
` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
1377
|
+
);
|
|
1378
|
+
console.log(
|
|
1379
|
+
` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
1380
|
+
);
|
|
1381
|
+
let skillCount = 0;
|
|
1382
|
+
if (existsSync(codeartsWorkSkillsDir())) {
|
|
1383
|
+
skillCount = readdirSync(codeartsWorkSkillsDir(), { withFileTypes: true }).filter(
|
|
1384
|
+
(d) => d.isDirectory() && d.name.startsWith('huawei'),
|
|
1385
|
+
).length;
|
|
1386
|
+
}
|
|
1387
|
+
console.log(
|
|
1388
|
+
` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
|
|
1389
|
+
);
|
|
1390
|
+
if (existsSync(codeartsWorkMcpSettingsFile())) {
|
|
1391
|
+
try {
|
|
1392
|
+
const config = JSON.parse(readFileSync(codeartsWorkMcpSettingsFile(), 'utf8'));
|
|
1393
|
+
console.log(
|
|
1394
|
+
` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`,
|
|
1395
|
+
);
|
|
1396
|
+
} catch {
|
|
1397
|
+
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1251
1402
|
// Returns true when the config file was written, false when it was already correct.
|
|
1252
1403
|
function ensureWorkbuddyMcpConfig() {
|
|
1253
1404
|
const configPath = workbuddyMcpConfigFile();
|
|
@@ -2307,6 +2458,7 @@ function autoDetectTarget() {
|
|
|
2307
2458
|
['opencode', () => existsSync(join(homedir(), '.config', 'opencode'))],
|
|
2308
2459
|
['codex-desktop', () => existsSync(join(homedir(), '.codex'))],
|
|
2309
2460
|
['codearts', () => existsSync(join(homedir(), '.codeartsdoer'))],
|
|
2461
|
+
['codearts-work', () => existsSync(join(homedir(), '.codeartswork'))],
|
|
2310
2462
|
['workbuddy', () => existsSync(join(homedir(), '.workbuddy'))],
|
|
2311
2463
|
[
|
|
2312
2464
|
'dsh',
|
|
@@ -2359,6 +2511,10 @@ async function cmdInstall() {
|
|
|
2359
2511
|
console.log('\n[CodeArts]');
|
|
2360
2512
|
await installCodeArts();
|
|
2361
2513
|
}
|
|
2514
|
+
if (target === 'codearts-work' || target === 'all') {
|
|
2515
|
+
console.log('\n[CodeArts Work]');
|
|
2516
|
+
await installCodeArtsWork();
|
|
2517
|
+
}
|
|
2362
2518
|
if (target === 'workbuddy' || target === 'all') {
|
|
2363
2519
|
console.log('\n[WorkBuddy]');
|
|
2364
2520
|
await installWorkBuddy();
|
|
@@ -2410,23 +2566,25 @@ async function cmdInstall() {
|
|
|
2410
2566
|
const appName =
|
|
2411
2567
|
target === 'codearts'
|
|
2412
2568
|
? 'CodeArts'
|
|
2413
|
-
: target === '
|
|
2414
|
-
? '
|
|
2415
|
-
: target === 'codex'
|
|
2416
|
-
? 'Codex'
|
|
2417
|
-
: target === '
|
|
2418
|
-
? '
|
|
2419
|
-
: target === '
|
|
2420
|
-
? '
|
|
2421
|
-
: target === '
|
|
2422
|
-
? '
|
|
2423
|
-
: target === '
|
|
2424
|
-
? '
|
|
2425
|
-
: target === '
|
|
2426
|
-
? '
|
|
2427
|
-
: target === '
|
|
2428
|
-
? '
|
|
2429
|
-
:
|
|
2569
|
+
: target === 'codearts-work'
|
|
2570
|
+
? 'CodeArts Work'
|
|
2571
|
+
: target === 'codex-desktop'
|
|
2572
|
+
? 'Codex Desktop'
|
|
2573
|
+
: target === 'codex'
|
|
2574
|
+
? 'Codex'
|
|
2575
|
+
: target === 'workbuddy'
|
|
2576
|
+
? 'WorkBuddy'
|
|
2577
|
+
: target === 'dsh'
|
|
2578
|
+
? 'DSH'
|
|
2579
|
+
: target === 'officeace'
|
|
2580
|
+
? 'OfficeAce'
|
|
2581
|
+
: target === 'hermes'
|
|
2582
|
+
? 'Hermes Agent'
|
|
2583
|
+
: target === 'openclaw'
|
|
2584
|
+
? 'OpenClaw'
|
|
2585
|
+
: target === 'atomcode'
|
|
2586
|
+
? 'AtomCode'
|
|
2587
|
+
: '当前 agent';
|
|
2430
2588
|
const pad = ' '.repeat(24 - appName.length);
|
|
2431
2589
|
if (target === 'officeace') {
|
|
2432
2590
|
console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════════╗`);
|
|
@@ -2480,17 +2638,19 @@ async function cmdInstall() {
|
|
|
2480
2638
|
? dshPluginsDir()
|
|
2481
2639
|
: target === 'codearts'
|
|
2482
2640
|
? codeartsPluginsDir()
|
|
2483
|
-
: target === '
|
|
2484
|
-
?
|
|
2485
|
-
: target === '
|
|
2486
|
-
?
|
|
2487
|
-
: target === '
|
|
2488
|
-
?
|
|
2489
|
-
: target === '
|
|
2490
|
-
?
|
|
2491
|
-
: target === '
|
|
2492
|
-
?
|
|
2493
|
-
:
|
|
2641
|
+
: target === 'codearts-work'
|
|
2642
|
+
? codeartsWorkPluginsDir()
|
|
2643
|
+
: target === 'workbuddy'
|
|
2644
|
+
? workbuddyPluginsDir()
|
|
2645
|
+
: target === 'officeace'
|
|
2646
|
+
? officeacePluginsDir()
|
|
2647
|
+
: target === 'openclaw'
|
|
2648
|
+
? codexDesktopPluginsDir()
|
|
2649
|
+
: target === 'atomcode'
|
|
2650
|
+
? atomcodePluginsDir()
|
|
2651
|
+
: target === 'codex-desktop'
|
|
2652
|
+
? codexDesktopPluginsDir()
|
|
2653
|
+
: opencodePluginsDir();
|
|
2494
2654
|
mkdirSync(markerDir, { recursive: true });
|
|
2495
2655
|
writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
|
|
2496
2656
|
if (target === 'opencode' || target === 'all') {
|
|
@@ -2499,6 +2659,9 @@ async function cmdInstall() {
|
|
|
2499
2659
|
if (target === 'codearts' || target === 'all') {
|
|
2500
2660
|
console.log('Or describe your Huawei Cloud task in CodeArts');
|
|
2501
2661
|
}
|
|
2662
|
+
if (target === 'codearts-work' || target === 'all') {
|
|
2663
|
+
console.log('Or describe your Huawei Cloud task in CodeArts Work');
|
|
2664
|
+
}
|
|
2502
2665
|
if (target === 'codex' || target === 'all') {
|
|
2503
2666
|
console.log('Or mention @huaweicloud-core in Codex');
|
|
2504
2667
|
}
|
|
@@ -2532,6 +2695,10 @@ async function cmdUninstall() {
|
|
|
2532
2695
|
console.log('\n[CodeArts]');
|
|
2533
2696
|
uninstallCodeArts();
|
|
2534
2697
|
}
|
|
2698
|
+
if (target === 'codearts-work' || target === 'all') {
|
|
2699
|
+
console.log('\n[CodeArts Work]');
|
|
2700
|
+
uninstallCodeArtsWork();
|
|
2701
|
+
}
|
|
2535
2702
|
if (target === 'workbuddy' || target === 'all') {
|
|
2536
2703
|
console.log('\n[WorkBuddy]');
|
|
2537
2704
|
uninstallWorkBuddy();
|
|
@@ -2596,6 +2763,10 @@ async function cmdStatus() {
|
|
|
2596
2763
|
console.log('\n[CodeArts]');
|
|
2597
2764
|
codeartsStatus();
|
|
2598
2765
|
}
|
|
2766
|
+
if (target === 'codearts-work' || target === 'all') {
|
|
2767
|
+
console.log('\n[CodeArts Work]');
|
|
2768
|
+
codeartsWorkStatus();
|
|
2769
|
+
}
|
|
2599
2770
|
if (target === 'workbuddy' || target === 'all') {
|
|
2600
2771
|
console.log('\n[WorkBuddy]');
|
|
2601
2772
|
workbuddyStatus();
|
|
@@ -2674,6 +2845,7 @@ async function cmdDoctor() {
|
|
|
2674
2845
|
const opencodePluginDir = opencodePluginsDir();
|
|
2675
2846
|
const codexPluginDir = codexDesktopPluginsDir();
|
|
2676
2847
|
const codeartsPluginDir = codeartsPluginsDir();
|
|
2848
|
+
const codeartsWorkPluginDir = codeartsWorkPluginsDir();
|
|
2677
2849
|
const workbuddyPluginDir = workbuddyPluginsDir();
|
|
2678
2850
|
const dshPluginDir = dshPluginsDir();
|
|
2679
2851
|
const officeacePluginDir = officeacePluginsDir();
|
|
@@ -2683,6 +2855,7 @@ async function cmdDoctor() {
|
|
|
2683
2855
|
existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2684
2856
|
existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2685
2857
|
existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2858
|
+
existsSync(join(codeartsWorkPluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2686
2859
|
existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2687
2860
|
existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs')) ||
|
|
2688
2861
|
existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs')) ||
|
|
@@ -2727,6 +2900,7 @@ async function cmdDoctor() {
|
|
|
2727
2900
|
existsSync(join(opencodePluginDir, 'safety', 'policy.json')) ||
|
|
2728
2901
|
existsSync(join(codexPluginDir, 'safety', 'policy.json')) ||
|
|
2729
2902
|
existsSync(join(codeartsPluginDir, 'safety', 'policy.json')) ||
|
|
2903
|
+
existsSync(join(codeartsWorkPluginDir, 'safety', 'policy.json')) ||
|
|
2730
2904
|
existsSync(join(workbuddyPluginDir, 'safety', 'policy.json')) ||
|
|
2731
2905
|
existsSync(join(dshPluginDir, 'safety', 'policy.json')) ||
|
|
2732
2906
|
existsSync(join(officeacePluginDir, 'safety', 'policy.json')) ||
|
|
@@ -2875,6 +3049,7 @@ async function cmdDoctor() {
|
|
|
2875
3049
|
opencodeSkillsDir(),
|
|
2876
3050
|
codexDesktopSkillsDir(),
|
|
2877
3051
|
codeartsSkillsDir(),
|
|
3052
|
+
codeartsWorkSkillsDir(),
|
|
2878
3053
|
workbuddySkillsDir(),
|
|
2879
3054
|
dshSkillsDir(),
|
|
2880
3055
|
officeaceSkillsDir(),
|
|
@@ -2934,6 +3109,7 @@ async function cmdDoctor() {
|
|
|
2934
3109
|
{ path: join(codexDesktopPluginsDir(), '.installed'), name: 'Codex Desktop' },
|
|
2935
3110
|
{ path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
|
|
2936
3111
|
{ path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
|
|
3112
|
+
{ path: join(codeartsWorkPluginsDir(), '.installed'), name: 'CodeArts Work' },
|
|
2937
3113
|
{ path: join(dshPluginsDir(), '.installed'), name: 'DSH' },
|
|
2938
3114
|
{ path: join(officeacePluginsDir(), '.installed'), name: 'OfficeAce' },
|
|
2939
3115
|
{ path: join(atomcodePluginsDir(), '.installed'), name: 'AtomCode' },
|
|
@@ -3006,6 +3182,18 @@ async function cmdUpdate() {
|
|
|
3006
3182
|
return;
|
|
3007
3183
|
}
|
|
3008
3184
|
|
|
3185
|
+
if (target === 'codearts-work') {
|
|
3186
|
+
if (!existsSync(join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
3187
|
+
console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
console.log('[CodeArts Work]');
|
|
3191
|
+
await updateCodeArtsWork();
|
|
3192
|
+
console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
|
|
3193
|
+
console.log(`\x1b[33mMCP 工具在重启 CodeArts Work 会话后才生效。\x1b[0m`);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3009
3197
|
if (target === 'workbuddy') {
|
|
3010
3198
|
if (!existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
3011
3199
|
console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
|
|
@@ -3095,6 +3283,11 @@ async function cmdUpdate() {
|
|
|
3095
3283
|
await updateCodeArts();
|
|
3096
3284
|
updatedAny = true;
|
|
3097
3285
|
}
|
|
3286
|
+
if (existsSync(join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
3287
|
+
console.log('\n[CodeArts Work]');
|
|
3288
|
+
await updateCodeArtsWork();
|
|
3289
|
+
updatedAny = true;
|
|
3290
|
+
}
|
|
3098
3291
|
if (existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
|
|
3099
3292
|
console.log('\n[WorkBuddy]');
|
|
3100
3293
|
await updateWorkBuddy();
|
|
@@ -3620,7 +3813,7 @@ async function main() {
|
|
|
3620
3813
|
default:
|
|
3621
3814
|
console.log(BANNER);
|
|
3622
3815
|
console.log(
|
|
3623
|
-
'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|officeace|hermes|openclaw|atomcode|all>]\n',
|
|
3816
|
+
'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|codearts-work|workbuddy|dsh|officeace|hermes|openclaw|atomcode|all>]\n',
|
|
3624
3817
|
);
|
|
3625
3818
|
console.log('Commands:');
|
|
3626
3819
|
console.log(' install Install skills, MCP server, safety policy');
|
|
@@ -3635,12 +3828,13 @@ async function main() {
|
|
|
3635
3828
|
console.log(' help Show this help');
|
|
3636
3829
|
console.log('\nOptions:');
|
|
3637
3830
|
console.log(
|
|
3638
|
-
' --target Target agent: opencode (default), codex, codearts, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
|
|
3831
|
+
' --target Target agent: opencode (default), codex, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
|
|
3639
3832
|
);
|
|
3640
3833
|
console.log('\nExamples:');
|
|
3641
3834
|
console.log(' npx huaweicloud-devkit install');
|
|
3642
3835
|
console.log(' npx huaweicloud-devkit install --target codex');
|
|
3643
3836
|
console.log(' npx huaweicloud-devkit install --target codearts');
|
|
3837
|
+
console.log(' npx huaweicloud-devkit install --target codearts-work');
|
|
3644
3838
|
console.log(' npx huaweicloud-devkit install --target workbuddy');
|
|
3645
3839
|
console.log(' npx huaweicloud-devkit install --target dsh');
|
|
3646
3840
|
console.log(' npx huaweicloud-devkit install --target officeace');
|
|
@@ -48,6 +48,10 @@ function codeartsSkillsDir() {
|
|
|
48
48
|
const home = homedir();
|
|
49
49
|
return join(home, '.codeartsdoer', 'skills');
|
|
50
50
|
}
|
|
51
|
+
function codeartsWorkSkillsDir() {
|
|
52
|
+
const home = homedir();
|
|
53
|
+
return join(home, '.codeartswork', 'skills');
|
|
54
|
+
}
|
|
51
55
|
function workbuddySkillsDir() {
|
|
52
56
|
const home = homedir();
|
|
53
57
|
return join(home, '.workbuddy', 'skills');
|
|
@@ -126,6 +130,7 @@ function resolveSkillsRoot() {
|
|
|
126
130
|
SKILLS_ROOT_DEV,
|
|
127
131
|
dshSkillsDir(),
|
|
128
132
|
codeartsSkillsDir(),
|
|
133
|
+
codeartsWorkSkillsDir(),
|
|
129
134
|
opencodeSkillsDir(),
|
|
130
135
|
workbuddySkillsDir(),
|
|
131
136
|
officeaceSkillsRoot(),
|
|
@@ -449,7 +454,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
449
454
|
target: {
|
|
450
455
|
type: 'string',
|
|
451
456
|
description:
|
|
452
|
-
'Agent target to check: opencode, codex, codex-desktop, codearts, workbuddy, dsh, officeace, hermes, openclaw, atomcode, or all (default).',
|
|
457
|
+
'Agent target to check: opencode, codex, codex-desktop, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, or all (default).',
|
|
453
458
|
},
|
|
454
459
|
},
|
|
455
460
|
},
|
|
@@ -464,7 +469,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
464
469
|
target: {
|
|
465
470
|
type: 'string',
|
|
466
471
|
description:
|
|
467
|
-
'Agent target to report after sync: opencode, codex, codex-desktop, codearts, workbuddy, dsh, officeace, hermes, or all (default).',
|
|
472
|
+
'Agent target to report after sync: opencode, codex, codex-desktop, codearts, codearts-work, workbuddy, dsh, officeace, hermes, or all (default).',
|
|
468
473
|
},
|
|
469
474
|
},
|
|
470
475
|
},
|
|
@@ -552,7 +557,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
552
557
|
'Workspace ID from huaweicloud_sandbox_connect return value. Required - must be passed explicitly when HW_WORKSPACE_ID is not set.',
|
|
553
558
|
},
|
|
554
559
|
username: { type: 'string', description: 'Login username (default: root)' },
|
|
555
|
-
timeout_ms: { type: 'number', description: '
|
|
560
|
+
timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 60000)' },
|
|
556
561
|
},
|
|
557
562
|
},
|
|
558
563
|
},
|
|
@@ -614,7 +619,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
614
619
|
config_name: {
|
|
615
620
|
type: 'string',
|
|
616
621
|
description:
|
|
617
|
-
'Config file name (without .conf suffix). Defaults to
|
|
622
|
+
'Config file name (without .conf suffix). Defaults to the project name, ensuring each project gets its own config. Override with distinct names (e.g. admin, docs) for sub-app deployments.',
|
|
618
623
|
},
|
|
619
624
|
workspace_id: {
|
|
620
625
|
type: 'string',
|
|
@@ -905,7 +910,7 @@ export async function callTool(name, args = {}) {
|
|
|
905
910
|
);
|
|
906
911
|
}
|
|
907
912
|
const sandboxUser7 = args.username || 'root';
|
|
908
|
-
const sandboxTimeout7 = args.timeout_ms ||
|
|
913
|
+
const sandboxTimeout7 = args.timeout_ms || 60000;
|
|
909
914
|
return await deployNginx(
|
|
910
915
|
sandboxWsId7,
|
|
911
916
|
{
|
|
@@ -962,7 +967,8 @@ export async function callTool(name, args = {}) {
|
|
|
962
967
|
return connectResult;
|
|
963
968
|
}
|
|
964
969
|
case 'huaweicloud_sandbox_credentials': {
|
|
965
|
-
const
|
|
970
|
+
const devStageId = args.dev_stage_id || getCurrentWorkspaceId();
|
|
971
|
+
const credResult = await hdkitCredentials(args.session_id, devStageId, args.enable_sts !== false);
|
|
966
972
|
const sandboxWsIdCred = args.dev_stage_id || getCurrentWorkspaceId();
|
|
967
973
|
if (sandboxWsIdCred) {
|
|
968
974
|
try {
|