huaweicloud-devkit 1.1.0-next.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/README.zh-CN.md +45 -2
- package/package.json +3 -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/hooks/huaweicloud-safety.py +29 -12
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +291 -53
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
- package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
- package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
- package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +365 -11
- package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
- package/plugins/huaweicloud-core/src/tools.mjs +184 -14
|
@@ -18,19 +18,19 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
|
|
|
18
18
|
|
|
19
19
|
## Critical Warnings
|
|
20
20
|
|
|
21
|
-
| Trap | Why
|
|
22
|
-
| ------------------------------------- |
|
|
23
|
-
| VPC CIDR cannot change | Once set, VPC CIDR block is immutable
|
|
24
|
-
| Security group stateful | SG rules are stateful. Return traffic auto-allowed
|
|
25
|
-
| Network ACL stateless | ACL rules must allow both inbound AND outbound
|
|
26
|
-
| EIP bills when idle | Unbound EIP still charges. Release when unused
|
|
27
|
-
| Subnet AZ binding | Subnet tied to single AZ. Cross-AZ needs multiple subnets
|
|
28
|
-
| EIP PER type needs `--bandwidth.name` | PER bandwidth requires explicit name; `--help` marks it optional but it's required
|
|
29
|
-
| **VPC params need nested prefix** | KooCLI 7.x VPC API uses `--vpc.<param>`, `--subnet.<param>`, `--security_group.<param>`. Example: `--vpc.name=xxx` NOT `--name=xxx`
|
|
30
|
-
| **Security group needs no vpc_id** | VPC v3 API `CreateSecurityGroup` does NOT accept `vpc_id`. Security groups are region-level, not VPC-bound
|
|
31
|
-
| Subnet DNS empty → ECS no DNS | DNS params (`--subnet.primary_dns`, `--subnet.secondary_dns`) marked optional but empty default breaks cloud-init domain resolution — `yum`/`apt` installs fail silently. Always set both. Common DNS
|
|
32
|
-
| SCP blocks 0.0.0.0/0 SG rules | If `CreateSecurityGroupRule` with `--remote_ip_prefix=0.0.0.0/0` fails with `SYS.0403`, an org-level SCP policy is denying wide-open rules. Narrow to a specific CIDR range (e.g., your office IP) instead
|
|
33
|
-
| **VPC tags use `*` separator** | VPC tag format: `--vpc.tags.1=env*test` (asterisk between key and value). NOT `--vpc.tags.1.key=env` (ECS-style). This is different from ECS `--server.metadata.key=value`
|
|
21
|
+
| Trap | Why |
|
|
22
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| VPC CIDR cannot change | Once set, VPC CIDR block is immutable |
|
|
24
|
+
| Security group stateful | SG rules are stateful. Return traffic auto-allowed |
|
|
25
|
+
| Network ACL stateless | ACL rules must allow both inbound AND outbound |
|
|
26
|
+
| EIP bills when idle | Unbound EIP still charges. Release when unused |
|
|
27
|
+
| Subnet AZ binding | Subnet tied to single AZ. Cross-AZ needs multiple subnets |
|
|
28
|
+
| EIP PER type needs `--bandwidth.name` | PER bandwidth requires explicit name; `--help` marks it optional but it's required |
|
|
29
|
+
| **VPC params need nested prefix** | KooCLI 7.x VPC API uses `--vpc.<param>`, `--subnet.<param>`, `--security_group.<param>`. Example: `--vpc.name=xxx` NOT `--name=xxx` |
|
|
30
|
+
| **Security group needs no vpc_id** | VPC v3 API `CreateSecurityGroup` does NOT accept `vpc_id`. Security groups are region-level, not VPC-bound |
|
|
31
|
+
| Subnet DNS empty → ECS no DNS | DNS params (`--subnet.primary_dns`, `--subnet.secondary_dns`) marked optional but empty default breaks cloud-init domain resolution — `yum`/`apt` installs fail silently. Always set both. Common DNS (primary / secondary): cn-north-4 (100.125.1.250 / 100.125.129.250), cn-north-1 (100.125.1.250 / 100.125.21.250), cn-east-3 (100.125.1.250 / 100.125.64.250), cn-south-1 (100.125.1.250 / 100.125.136.29), ap-southeast-3 (100.125.1.250 / 100.125.128.250). Authoritative per region: `hcloud DNS ListNameServers --type=private --region=<r>` |
|
|
32
|
+
| SCP blocks 0.0.0.0/0 SG rules | If `CreateSecurityGroupRule` with `--remote_ip_prefix=0.0.0.0/0` fails with `SYS.0403`, an org-level SCP policy is denying wide-open rules. Narrow to a specific CIDR range (e.g., your office IP) instead |
|
|
33
|
+
| **VPC tags use `*` separator** | VPC tag format: `--vpc.tags.1=env*test` (asterisk between key and value). NOT `--vpc.tags.1.key=env` (ECS-style). This is different from ECS `--server.metadata.key=value` |
|
|
34
34
|
|
|
35
35
|
## Common Workflows
|
|
36
36
|
|
|
@@ -43,25 +43,25 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
|
|
|
43
43
|
| SG rule | hcloud VPC CreateSecurityGroupRule --security_group_rule.security_group_id=<id> --security_group_rule.direction=<direction> --security_group_rule.protocol=<protocol> --security_group_rule.multiport=<port> --security_group_rule.remote_ip_prefix=<cidr> | references/security-group.md |
|
|
44
44
|
| Create EIP | hcloud EIP CreatePublicip --publicip.type=<type> --bandwidth.size=<size> --bandwidth.share_type=<share-type> --bandwidth.name=<name> | Run `hcloud EIP CreatePublicip --help` to confirm valid type values per region |
|
|
45
45
|
| Bind EIP to ECS | hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT | Get port ID from `hcloud ECS ListServersDetails --server_id=<id>` → `OS-EXT-IPS:port_id` |
|
|
46
|
-
| Unbind EIP | hcloud EIP
|
|
46
|
+
| Unbind EIP | hcloud EIP DisassociatePublicips --publicip_id=<id> | references/eip.md |
|
|
47
47
|
| Delete EIP | hcloud EIP DeletePublicip --publicip_id=<id> | references/eip.md |
|
|
48
48
|
| List EIPs | hcloud EIP ListPublicips | |
|
|
49
49
|
| NAT gateway | hcloud NAT CreateNatGateway --nat.name=<name> --nat.spec=<spec> --router_id=<vpc-id> --internal_network_id=<subnet-id> | Run `hcloud NAT CreateNatGateway --help` for available spec values |
|
|
50
50
|
|
|
51
51
|
## Troubleshooting
|
|
52
52
|
|
|
53
|
-
| Error | Root Cause -> Fix
|
|
54
|
-
| -------------------------------------------------- |
|
|
55
|
-
| Cannot reach instance | SG missing rule or no EIP -> Add SG rule / Bind EIP
|
|
56
|
-
| Subnet CIDR conflict | Overlapping with existing subnets -> Choose non-overlapping CIDR
|
|
57
|
-
| NAT gateway no internet | Route table missing default route -> Add 0.0.0.0/0 via NAT
|
|
58
|
-
| EIP quota exceeded | Check actual quota: `hcloud EIP ListPublicips --cli-region=<r>` to see current usage. Default varies by account (typically 5-10)
|
|
59
|
-
| EIP.7905 | Run `hcloud EIP ListPublicips --cli-region=<r>` first to check current usage
|
|
60
|
-
| VPC.0301: Bandwidth name invalid | PER type requires `--bandwidth.name`, even though `--help` marks it optional
|
|
61
|
-
| EIP has no public IP after binding | May need AddIngressEipV2 for ELB-type resources (see huawei-apig)
|
|
62
|
-
| ECS cloud-init fails silently (port 80/443 closed) | Subnet likely has no DNS. Check `hcloud VPC ShowSubnet --subnet_id=<id>` → `dnsList` empty? Rebuild subnet with `--subnet.primary_dns=<dns1> --subnet.secondary_dns=<dns2>`. DNS addresses per region: `hcloud
|
|
63
|
-
| VPC.0209: Subnet still used | Subnet has dependent resources (ECS/RDS) — delete instances first, then subnet
|
|
64
|
-
| SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction. If SSH is blocked, bootstrap via cloud-init user_data instead: see `huawei-ecs` — no SSH needed
|
|
53
|
+
| Error | Root Cause -> Fix |
|
|
54
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| Cannot reach instance | SG missing rule or no EIP -> Add SG rule / Bind EIP |
|
|
56
|
+
| Subnet CIDR conflict | Overlapping with existing subnets -> Choose non-overlapping CIDR |
|
|
57
|
+
| NAT gateway no internet | Route table missing default route -> Add 0.0.0.0/0 via NAT |
|
|
58
|
+
| EIP quota exceeded | Check actual quota: `hcloud EIP ListPublicips --cli-region=<r>` to see current usage. Default varies by account (typically 5-10) |
|
|
59
|
+
| EIP.7905 | Run `hcloud EIP ListPublicips --cli-region=<r>` first to check current usage |
|
|
60
|
+
| VPC.0301: Bandwidth name invalid | PER type requires `--bandwidth.name`, even though `--help` marks it optional |
|
|
61
|
+
| EIP has no public IP after binding | May need AddIngressEipV2 for ELB-type resources (see huawei-apig) |
|
|
62
|
+
| ECS cloud-init fails silently (port 80/443 closed) | Subnet likely has no DNS. Check `hcloud VPC ShowSubnet --subnet_id=<id>` → `dnsList` empty? Rebuild subnet with `--subnet.primary_dns=<dns1> --subnet.secondary_dns=<dns2>`. DNS addresses per region: `hcloud DNS ListNameServers --type=private --region=<r>` |
|
|
63
|
+
| VPC.0209: Subnet still used | Subnet has dependent resources (ECS/RDS) — delete instances first, then subnet |
|
|
64
|
+
| SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction. If SSH is blocked, bootstrap via cloud-init user_data instead: see `huawei-ecs` — no SSH needed |
|
|
65
65
|
|
|
66
66
|
## Security Considerations
|
|
67
67
|
|
|
@@ -6,11 +6,17 @@ Subnets without DNS configuration will fail cloud-init. Use these DNS addresses:
|
|
|
6
6
|
|
|
7
7
|
| Region | Primary DNS | Secondary DNS |
|
|
8
8
|
| -------------- | ------------- | --------------- |
|
|
9
|
-
| cn-north-1 | 100.125.1.250 | 100.125.
|
|
9
|
+
| cn-north-1 | 100.125.1.250 | 100.125.21.250 |
|
|
10
10
|
| cn-north-4 | 100.125.1.250 | 100.125.129.250 |
|
|
11
|
-
| cn-east-3 | 100.125.1.250 | 100.125.
|
|
12
|
-
| cn-south-1 | 100.125.1.250 | 100.125.
|
|
13
|
-
| ap-southeast-3 | 100.125.1.250 | 100.125.
|
|
11
|
+
| cn-east-3 | 100.125.1.250 | 100.125.64.250 |
|
|
12
|
+
| cn-south-1 | 100.125.1.250 | 100.125.136.29 |
|
|
13
|
+
| ap-southeast-3 | 100.125.1.250 | 100.125.128.250 |
|
|
14
|
+
|
|
15
|
+
Get the authoritative values for any region at runtime (preferred over this table):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
hcloud DNS ListNameServers --type=private --region=<region>
|
|
19
|
+
```
|
|
14
20
|
|
|
15
21
|
**Subnet create with DNS**:
|
|
16
22
|
|
|
@@ -21,8 +27,8 @@ hcloud VPC CreateSubnet \
|
|
|
21
27
|
--subnet.cidr=10.50.1.0/24 \
|
|
22
28
|
--subnet.gateway_ip=10.50.1.1 \
|
|
23
29
|
--subnet.availability_zone=<az> \
|
|
24
|
-
--subnet.dnsList.1
|
|
25
|
-
--subnet.dnsList.2
|
|
30
|
+
--subnet.dnsList.1=<dns1> \
|
|
31
|
+
--subnet.dnsList.2=<dns2>
|
|
26
32
|
```
|
|
27
33
|
|
|
28
34
|
## Nested Prefix Summary
|
|
@@ -29,8 +29,8 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
|
|
|
29
29
|
|
|
30
30
|
| Task | Operation |
|
|
31
31
|
| ------------------- | --------------------------------------------------------------- |
|
|
32
|
-
| List WAF instances | `
|
|
33
|
-
| List policies | `
|
|
32
|
+
| List WAF instances | `ListCompositeHosts --cli-region=<r> --project_id=<p>` |
|
|
33
|
+
| List policies | `ListPolicy --cli-region=<r> --project_id=<p>` |
|
|
34
34
|
| Create custom rule | `BatchCreateCustomRule --cli-region=<r> --project_id=<p>` |
|
|
35
35
|
| Create IP blacklist | `BatchCreateWhiteblackipRule --cli-region=<r> --project_id=<p>` |
|
|
36
36
|
| Create CC rule | `BatchCreateCcRule --cli-region=<r> --project_id=<p>` |
|
|
@@ -39,8 +39,11 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
|
|
|
39
39
|
### AAD
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
hcloud AAD
|
|
42
|
+
# List AAD instances (requires --cli-region=cn-north-4 or ap-southeast-1)
|
|
43
|
+
hcloud AAD ListInstance --cli-region=<r> --project_id=<p>
|
|
44
|
+
|
|
45
|
+
# List DDoS attack events
|
|
46
|
+
hcloud AAD ListDDoSAttackEvent --cli-region=<r> --project_id=<p>
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
## Troubleshooting
|
|
@@ -125,13 +125,13 @@ hcloud <Service> <Op> --cli-debug=true
|
|
|
125
125
|
|
|
126
126
|
Credentials are resolved in this order (highest priority first):
|
|
127
127
|
|
|
128
|
-
| Priority | Source
|
|
129
|
-
| -------- |
|
|
130
|
-
| 1 | Runtime credentials
|
|
131
|
-
| 2 | Environment variables
|
|
132
|
-
| 3 | CodeArts
|
|
133
|
-
| 4 | Global config file
|
|
134
|
-
| 5 | KooCLI profile
|
|
128
|
+
| Priority | Source | Mechanism | Persistence |
|
|
129
|
+
| -------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------- |
|
|
130
|
+
| 1 | Runtime credentials | `huaweicloud_auth_init` tool | Memory (cleared on MCP restart) |
|
|
131
|
+
| 2 | Environment variables | `HW_ACCESS_KEY` / `HW_SECRET_KEY` | MCP process lifetime |
|
|
132
|
+
| 3 | CodeArts / CodeArts Work | `.codeartsdoer/mcp/mcp_settings.json` (project → user) or `.codeartswork/mcp/mcp_settings.json` (user) | File |
|
|
133
|
+
| 4 | Global config file | `~/.config/huaweicloud/credentials.json` | Permanent |
|
|
134
|
+
| 5 | KooCLI profile | `~/.hcloud/config.json` (KooCLI only) | Permanent |
|
|
135
135
|
|
|
136
136
|
When switching accounts within the same Agent session, use `huaweicloud_auth_init` to set runtime credentials. This overrides all other sources for the current MCP process.
|
|
137
137
|
|
|
@@ -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']);
|
|
@@ -179,6 +186,11 @@ function atomcodeRegistered() {
|
|
|
179
186
|
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
function openclawRegistered() {
|
|
190
|
+
const cfg = readJsonSafe(join(baseHome(), '.agents', 'huaweicloud-plugins', '.mcp.json'));
|
|
191
|
+
return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
|
|
192
|
+
}
|
|
193
|
+
|
|
182
194
|
function hermesHome() {
|
|
183
195
|
if (process.env.HERMES_HOME) return process.env.HERMES_HOME;
|
|
184
196
|
// Hermes on Windows stores under LOCALAPPDATA, not ~/.hermes
|
|
@@ -208,10 +220,12 @@ export function getAgentRegistrationStatuses(target = 'all') {
|
|
|
208
220
|
if (agent === 'codex-desktop') configured = codexDesktopRegistered();
|
|
209
221
|
if (agent === 'codex') configured = codexCliRegistered();
|
|
210
222
|
if (agent === 'codearts') configured = codeartsRegistered();
|
|
223
|
+
if (agent === 'codearts-work') configured = codeartsWorkRegistered();
|
|
211
224
|
if (agent === 'workbuddy') configured = workbuddyRegistered();
|
|
212
225
|
if (agent === 'dsh') configured = dshRegistered();
|
|
213
226
|
if (agent === 'officeace') configured = officeaceRegistered();
|
|
214
227
|
if (agent === 'hermes') configured = hermesRegistered();
|
|
228
|
+
if (agent === 'openclaw') configured = openclawRegistered();
|
|
215
229
|
if (agent === 'atomcode') configured = atomcodeRegistered();
|
|
216
230
|
result.agents[agent] = { configured };
|
|
217
231
|
}
|
|
@@ -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
|
|
|
@@ -54,9 +54,42 @@ export function syncAuth(target = 'all') {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
let hcloud = { ok: false, message: 'KooCLI not installed' };
|
|
58
|
+
if (hcloudInstalled()) {
|
|
59
|
+
const bin = process.env.HCLOUD_BIN || 'hcloud';
|
|
60
|
+
const r = spawnSync(
|
|
61
|
+
bin,
|
|
62
|
+
[
|
|
63
|
+
'configure',
|
|
64
|
+
'set',
|
|
65
|
+
`--cli-access-key=${credentials.ak}`,
|
|
66
|
+
`--cli-secret-key=${credentials.sk}`,
|
|
67
|
+
`--cli-region=${credentials.region || ''}`,
|
|
68
|
+
],
|
|
69
|
+
{
|
|
70
|
+
shell: false,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
stdio: 'pipe',
|
|
73
|
+
timeout: 30000,
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
if (r.status === 0) {
|
|
77
|
+
hcloud = { ok: true, message: 'KooCLI config synced' };
|
|
78
|
+
} else {
|
|
79
|
+
hcloud = {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: 'KooCLI config sync failed',
|
|
82
|
+
error: String(r.stderr || '')
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 240),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
return {
|
|
58
90
|
ok: true,
|
|
59
91
|
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
92
|
+
hcloud,
|
|
60
93
|
credentialsConfigured: true,
|
|
61
94
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
62
95
|
note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
|
|
@@ -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,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
2
6
|
|
|
3
7
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
4
8
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
@@ -6,6 +10,46 @@ import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
|
6
10
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
11
|
const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
|
|
8
12
|
const DEFAULT_MAX_RETRIES = 1;
|
|
13
|
+
const APPROVAL_TTL_MS = 5 * 60_000;
|
|
14
|
+
const LARGE_OUTPUT_THRESHOLD = 50_000;
|
|
15
|
+
const OUTPUT_DIR = join('/tmp', 'huaweicloud-devkit');
|
|
16
|
+
|
|
17
|
+
const approvalStore = new Map();
|
|
18
|
+
|
|
19
|
+
export function createApprovalToken(rawArgs) {
|
|
20
|
+
const token = randomUUID();
|
|
21
|
+
approvalStore.set(token, { rawArgs, createdAt: Date.now() });
|
|
22
|
+
if (approvalStore.size % 20 === 0) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for (const [k, v] of approvalStore) {
|
|
25
|
+
if (now - v.createdAt > APPROVAL_TTL_MS) approvalStore.delete(k);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function consumeApprovalToken(token) {
|
|
32
|
+
const entry = approvalStore.get(token);
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) {
|
|
35
|
+
approvalStore.delete(token);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
approvalStore.delete(token);
|
|
39
|
+
return entry.rawArgs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveLargeOutput(rawStdout) {
|
|
43
|
+
if (rawStdout.length <= LARGE_OUTPUT_THRESHOLD) return null;
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
46
|
+
const filePath = join(OUTPUT_DIR, `output-${Date.now()}.json`);
|
|
47
|
+
writeFileSync(filePath, rawStdout, { encoding: 'utf8' });
|
|
48
|
+
return filePath;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
9
53
|
|
|
10
54
|
export function planHcloudCommand(args, options = {}) {
|
|
11
55
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
|
|
@@ -26,6 +70,7 @@ export function planHcloudCommand(args, options = {}) {
|
|
|
26
70
|
executableBlock: redactOutput(command),
|
|
27
71
|
warnings,
|
|
28
72
|
classification,
|
|
73
|
+
approvalToken: createApprovalToken(normalizedArgs),
|
|
29
74
|
safeToRun: classification.decision === 'allow',
|
|
30
75
|
};
|
|
31
76
|
}
|
|
@@ -53,12 +98,22 @@ export async function runHcloud(args, options = {}) {
|
|
|
53
98
|
throw new Error('Unreachable retry state.');
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
function discoverHcloudPath() {
|
|
102
|
+
if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
|
|
103
|
+
const candidates =
|
|
104
|
+
process.platform === 'win32'
|
|
105
|
+
? [join(homedir(), 'hcloud', 'hcloud.exe')]
|
|
106
|
+
: [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
|
|
107
|
+
return candidates.find((c) => existsSync(c)) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
function runHcloudOnce(plan, options) {
|
|
57
111
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
58
112
|
const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
|
|
59
|
-
const executable = options.executable || options.env?.HCLOUD_BIN ||
|
|
113
|
+
const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
|
|
60
114
|
const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
|
|
61
115
|
const cwd = options.cwd || undefined;
|
|
116
|
+
const stdin = options.stdin ?? 'y\n';
|
|
62
117
|
|
|
63
118
|
return new Promise((resolve) => {
|
|
64
119
|
const proxySettings = getProxySettings();
|
|
@@ -78,11 +133,11 @@ function runHcloudOnce(plan, options) {
|
|
|
78
133
|
...options.env,
|
|
79
134
|
},
|
|
80
135
|
});
|
|
81
|
-
if (
|
|
82
|
-
if (typeof
|
|
83
|
-
|
|
136
|
+
if (stdin) {
|
|
137
|
+
if (typeof stdin === 'function') {
|
|
138
|
+
stdin(child.stdin);
|
|
84
139
|
} else {
|
|
85
|
-
child.stdin.write(String(
|
|
140
|
+
child.stdin.write(String(stdin));
|
|
86
141
|
child.stdin.end();
|
|
87
142
|
}
|
|
88
143
|
}
|
|
@@ -99,6 +154,13 @@ function runHcloudOnce(plan, options) {
|
|
|
99
154
|
clearTimeout(timer);
|
|
100
155
|
clearTimeout(forceTimer);
|
|
101
156
|
clearTimeout(settleTimer);
|
|
157
|
+
if (result.stdout && String(result.stdout).length > LARGE_OUTPUT_THRESHOLD) {
|
|
158
|
+
const outputFile = saveLargeOutput(stdout);
|
|
159
|
+
if (outputFile) {
|
|
160
|
+
result.outputFile = outputFile;
|
|
161
|
+
result.stdout = String(result.stdout).slice(0, 2000) + `\n...(truncated, full output saved to ${outputFile})`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
102
164
|
resolve(result);
|
|
103
165
|
}
|
|
104
166
|
|
|
@@ -49,6 +49,29 @@ for (const base of [pluginRoot, packageRoot]) {
|
|
|
49
49
|
let buffer = Buffer.alloc(0);
|
|
50
50
|
let useContentLengthFraming = true;
|
|
51
51
|
|
|
52
|
+
// Keep the event loop alive after stdin is closed (Windows Hermes workaround).
|
|
53
|
+
// Node.js exits when no active handles remain; the stdin 'data' listener is
|
|
54
|
+
// the only handle. On Windows, Hermes may close the stdin pipe after the
|
|
55
|
+
// initial handshake, causing the process to exit silently (exit 0).
|
|
56
|
+
//
|
|
57
|
+
// When stdin closes, start a keepalive timer. When stdout also closes (normal
|
|
58
|
+
// shutdown signal from OfficeAce or other agents), clear the timer and exit.
|
|
59
|
+
let keepAlive = null;
|
|
60
|
+
function onStdinClose() {
|
|
61
|
+
if (keepAlive) return;
|
|
62
|
+
keepAlive = setInterval(() => {}, 60000);
|
|
63
|
+
}
|
|
64
|
+
function onStdoutClose() {
|
|
65
|
+
if (keepAlive) {
|
|
66
|
+
clearInterval(keepAlive);
|
|
67
|
+
keepAlive = null;
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 0;
|
|
70
|
+
}
|
|
71
|
+
stdin.on('close', onStdinClose);
|
|
72
|
+
stdin.on('end', onStdinClose);
|
|
73
|
+
stdout.on('close', onStdoutClose);
|
|
74
|
+
|
|
52
75
|
stdin.on('data', (chunk) => {
|
|
53
76
|
buffer = Buffer.concat([buffer, chunk]);
|
|
54
77
|
readFrames();
|
|
@@ -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
|
|