huaweicloud-devkit 1.1.3-next.2 → 1.1.3-next.4

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.3-next.2",
4
+ "version": "1.1.3-next.4",
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",
@@ -55,6 +55,9 @@
55
55
  "markdownlint-cli2": "^0.23.2",
56
56
  "prettier": "^3.9.6"
57
57
  },
58
+ "overrides": {
59
+ "smol-toml": "1.8.0"
60
+ },
58
61
  "keywords": [
59
62
  "huaweicloud",
60
63
  "huawei-cloud",
@@ -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.3-next.2",
20
+ "version": "1.1.3-next.4",
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.3-next.2",
23
+ "version": "1.1.3-next.4",
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.3-next.2",
20
+ "version": "1.1.3-next.4",
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.3-next.2",
3
+ "version": "1.1.3-next.4",
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.3-next.2",
3
+ "version": "1.1.3-next.4",
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",
@@ -43,7 +43,7 @@ def load_policy():
43
43
  SECRET_READ_RE = re.compile("|".join(re.escape(op) for op in blocked_secrets), re.I)
44
44
  write_prefixes = policy.get("writeOperationPrefixes", [])
45
45
  if write_prefixes:
46
- WRITE_OPERATION_RE = re.compile(r"\b(" + "|".join(write_prefixes) + r")\w*", re.I)
46
+ WRITE_OPERATION_RE = re.compile(r"(^|[A-Za-z0-9])(" + "|".join(write_prefixes) + r")\w*", re.I)
47
47
  except Exception:
48
48
  pass
49
49
 
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.3-next.2",
5
+ "version": "1.1.3-next.4",
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.",
@@ -231,17 +231,40 @@
231
231
  "any": [
232
232
  {
233
233
  "field": "text",
234
- "regex": "hcloud\\s+\\w+\\s+(Delete\\w*|Detach\\w*|Remove\\w*|Dissociate\\w*|Unassign\\w*|Revoke\\w*)\\b"
234
+ "regex": "hcloud\\s+\\w+\\s+(\\w*Delete\\w*|\\w*Detach\\w*|\\w*Remove\\w*|Dissociate\\w*|Unassign\\w*|Revoke\\w*)\\b"
235
235
  },
236
236
  {
237
237
  "field": "text",
238
- "regex": "hcloud\\s+\\w+\\s+(BatchDelete\\w*|BatchRemove\\w*)\\b"
238
+ "regex": "hcloud\\s+\\w+\\s+(\\w*BatchDelete\\w*|\\w*BatchRemove\\w*)\\b"
239
+ }
240
+ ],
241
+ "none": [
242
+ {
243
+ "field": "text",
244
+ "regex": "DeleteProtection"
239
245
  }
240
246
  ]
241
247
  },
242
248
  "message": "This hcloud command will delete, detach, or remove cloud resources. The operation may be irreversible.",
243
249
  "remediation": "List the resources to be affected first, confirm with the user, and require explicit approval before executing any destructive operation."
244
250
  },
251
+ {
252
+ "id": "hwc-destructive-reset-operation",
253
+ "title": "Destructive resource reset operation",
254
+ "category": "destructive",
255
+ "severity": "warn",
256
+ "stages": ["command"],
257
+ "match": {
258
+ "any": [
259
+ {
260
+ "field": "text",
261
+ "regex": "hcloud\\s+\\w+\\s+(Batch)?Reset\\w*\\b"
262
+ }
263
+ ]
264
+ },
265
+ "message": "This hcloud command resets a cloud resource (password, metadata, or instance state). This may disrupt access or mutate the resource.",
266
+ "remediation": "Confirm the reset target with the user and require explicit approval before executing."
267
+ },
245
268
  {
246
269
  "id": "hwc-destructive-delete-cascade",
247
270
  "title": "Cascading cloud resource deletion",
@@ -21,16 +21,16 @@ Always run `hcloud <Service> <Operation> --help` before constructing commands to
21
21
 
22
22
  **Proactive by design**: any deployment intent ("部署一个博客", "搭一套环境", "买个服务器") automatically triggers stages 1-5 BEFORE anything is created. The user NEVER needs to ask for an architecture proposal, cost estimate, or risk warnings - they are built into the flow. Only stages 6-8 (create / verify / manage) wait for explicit user confirmation at the approval gates.
23
23
 
24
- | Stage | What happens | Hard rule |
25
- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
- | 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data |
27
- | 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html |
28
- | 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure |
29
- | 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names |
30
- | 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | Balance = 0 → the FIRST message must state: "您的余额为 0,按时长计费的资源(ECS/EIP/RDS 等)需要交保证金,金额约 1~5 元人民币不等。保证金与余额是两个概念——它是可退还的押金,且是代金券正常使用的前提"。官方解释(用户追问"为什么冻结保证金"时引用):https://support.huaweicloud.com/price-eip/faq_eip_0033.html (开通按需资源时冻结一定金额作为保证金;金额规则详见 https://support.huaweicloud.com/billing_faq/billing_faq_6200009.html )。充值入口:https://account.huaweicloud.com/usercenter/?region=cn-north-4#/userindex/balanceRecharge 。Remedy priority: (1) FIRST claim the one-time incentive voucher (huaweicloud_voucher_status → huaweicloud_voucher_claim) to offset usage fees; (2) then instruct the user to pay the refundable deposit (top-up via the link above) so the voucher applies and orders pass (otherwise `Ecs.7000`); (3) free-tier resources (FunctionGraph, small OBS) may proceed meanwhile without deposit. Balance > 0 → report how long it can sustain (balance ÷ monthly estimate). **权限降级(实测)**:IAM 用户无 `billing:balance:view` 时余额查询报 `CBC.0151` → 不阻塞流程,明确告知"余额无法程序化查询,请自行核对;若下单报 Ecs.7000 即余额/保证金不足",领券后继续。定价编码逐服务查证,见 `references/resource-catalog.md` § Pricing |
31
- | 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below |
32
- | 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error |
33
- | 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below |
24
+ | Stage | What happens | Hard rule |
25
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data |
27
+ | 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html |
28
+ | 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure |
29
+ | 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names |
30
+ | 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | Balance = 0 → the FIRST message must state: "您的余额为 0,按时长计费的资源(ECS/EIP/RDS 等)下单将失败(Ecs.7000),请先充值或使用免费额度"。充值入口:https://account.huaweicloud.com/usercenter/?region=cn-north-4#/userindex/balanceRecharge 。Remedy priority: (1) FIRST claim the one-time incentive voucher (huaweicloud_voucher_status → huaweicloud_voucher_claim) to offset usage fees; (2) then instruct the user to top up balance (via the link above) if pay-per-use orders are needed, otherwise orders fail with `Ecs.7000`; (3) free-tier resources (FunctionGraph, small OBS) proceed without any balance. Balance > 0 → report how long it can sustain (balance ÷ monthly estimate). **权限降级(实测)**:IAM 用户无 `billing:balance:view` 时余额查询报 `CBC.0151` → 不阻塞流程,明确告知"余额无法程序化查询,请自行核对;若下单报 Ecs.7000 即余额不足",领券后继续。定价编码逐服务查证,见 `references/resource-catalog.md` § Pricing |
31
+ | 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below |
32
+ | 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error |
33
+ | 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below |
34
34
 
35
35
  ## Provisioning Rules (Stage 6)
36
36
 
@@ -52,32 +52,32 @@ Execution: show the to-be-deleted list for final confirmation → delete in **re
52
52
 
53
53
  ## Critical Warnings
54
54
 
55
- | Trap | Why / Rule |
56
- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | Stale `securityToken` poisons every hcloud call | Symptom: every API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil still works. `configure set` refuses empty values and auth-sync never clears the field. Fix: `hcloud configure delete --cli-profile=default`, then re-run auth init with AK/SK only (no token) |
58
- | OBS writes bypass the REST write-prefix policy | `mb` / `cp` / `rm` / `chattri` are obsutil-style commands. ALWAYS route them through `huaweicloud_plan_cli_command` → approval → `huaweicloud_run_approved_command`. Never treat them as read-only |
59
- | `OBS mb` flag rules differ from cp/rm | `mb` does NOT accept `-f` (flag parse error) and REQUIRES explicit `-location=<region>` even with a regional endpoint (else `IllegalLocationConstraintException`) |
60
- | Duplicate `OBS mb` is idempotent success | Same-account same-name bucket creation returns success, not an error - never treat a second create as failure. OBS deletes are also idempotent (deleting a nonexistent object succeeds) |
61
- | Bucket ACL does not cascade to objects | Every uploaded object needs `-acl=public-read` at upload time (`-f` IS valid on cp/rm) |
62
- | OBS static website bucket name must equal the domain | e.g. bucket `www.example.com` for domain `www.example.com` |
63
- | VPC v3 API differs from older docs | v3 `CreateSecurityGroup` has NO `vpc_id` (account-scoped); `CreateSecurityGroupRule` uses `multiport=80` not `port_range_min/max`; `DeleteSubnet` requires `vpc_id` too. Always `--help` first |
64
- | Public exposure is auto-blocked | Risk rules deny `0.0.0.0/0` port rules at plan time. For test/dev use a narrower `remote_ip_prefix` (e.g. the VPC CIDR) or get user consent for a documented exception |
65
- | ECS CreateServers hidden required params | Besides flavor/image/nics/az it also needs `--server.vpcid` and `--server.root_volume.volumetype` - order submission fails without them |
66
- | FunctionGraph runtime/code enums are strict | runtime must be exact (`Node.js18.15`, `Python3.9`, `Go1.x`, ...); inline code is the PAIR `--package=default --code_type=inline` (`--package=inline` → `FSS.1006`); TIMER triggers use `--event_data.schedule_type=Rate --event_data.schedule=1m` (`@every` style → `FSS.1109`); DeleteFunction takes `--function_urn` WITHOUT `:latest` |
67
- | Keypairs: `NovaCreateKeypair`, not CreateKeypair | `ECS CreateKeypair` does NOT exist - use `hcloud ECS NovaCreateKeypair`; delete takes `--keypair_name` (not `--keypair.name`). The API-generated `private_key` is MASKED in tool output - for SSH access create the keypair in the console or import the user's own public key, then bind with `--server.key_name=<n>` |
68
- | Long inline args break MCP JSON | Values >~2KB inline (e.g. `--server.user_data=<base64 cloud-init>`) fail JSON parsing silently. Write a local JSON file and pass `--cli-jsonInput=<file>` (see `references/resource-catalog.md` header note) |
69
- | EIP quota fails at RUN stage, not at plan | `EIP.7905 Quota exceeded` appears only after plan+approval - pre-check quota during stage 5 (and count precisely: `ListPublicips --limit=50` truncates, a "50/50" read on a 59-EIP account is wrong). Released quotas may lag minutes before `EIP.7905` clears |
70
- | Zero balance blocks order submission | ECS creation fails with `Ecs.7000 Insufficient account balance` when cash + voucher accounts are 0. This is the stage-5 gate materialized: stop, mark `partial`, tell the user pay-by-duration resources need a deposit. Free-tier resources (FunctionGraph, small OBS) DO work at 0 balance - the gate applies to order-submission resources only |
71
- | CDN mainland acceleration requires ICP filing | Unregistered domains cannot go live on CDN - check before stage 3. CDN create ALSO requires domain ownership verification first (`CDN.00010185 Verify domain ownership failed` - user must add a DNS record or upload a verification file) |
72
- | CDN is a global service in KooCLI | CDN operations only accept `--cli-region=cn-north-1` or `ap-southeast-1` - never the resource region |
73
- | OBS static website hosting has NO CLI path | `chattri` only sets ACL/storage-class; obsutil has no website command. Options: console setup, or CDN OBS-origin with `--domain.sources.1.enable_obs_web_hosting=1`. Without website hosting, verify via direct object URLs (still public HTTP 200) |
74
- | DNS hosting location | `hcloud DNS ListPublicZones` decides: hosted in Huawei Cloud → auto-create CNAME record set; hosted elsewhere → output the CNAME value for manual setup at the user's DNS provider |
75
- | EIP bills when idle; stopped ECS still bills | Include in cost estimate; destroy unattached EIPs |
76
- | ECS delete defaults leak resources | `--delete_publicip` and `--delete_volume` are false by default - set `true` when tearing down |
77
- | EIP binding type is non-obvious | `--publicip.associate_instance_type=PORT` (not INSTANCE) |
78
- | State is session-only | Temp-dir file, dies with the session. Never promise cross-session deploy management |
79
- | State file must be UTF-8 WITHOUT BOM | PowerShell `Set-Content -Encoding utf8` adds a BOM that breaks JSON parsers - write state with Node or another BOM-less writer |
80
- | HTTPS needs a certificate | CDN serves plain HTTP by default; SSL cert (upload or SCM) required for HTTPS |
55
+ | Trap | Why / Rule |
56
+ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | Stale `securityToken` poisons every hcloud call | Symptom: every API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil still works. `configure set` refuses empty values and auth-sync never clears the field. Fix: `hcloud configure delete --cli-profile=default`, then re-run auth init with AK/SK only (no token) |
58
+ | OBS writes bypass the REST write-prefix policy | `mb` / `cp` / `rm` / `chattri` are obsutil-style commands. ALWAYS route them through `huaweicloud_plan_cli_command` → approval → `huaweicloud_run_approved_command`. Never treat them as read-only |
59
+ | `OBS mb` flag rules differ from cp/rm | `mb` does NOT accept `-f` (flag parse error) and REQUIRES explicit `-location=<region>` even with a regional endpoint (else `IllegalLocationConstraintException`) |
60
+ | Duplicate `OBS mb` is idempotent success | Same-account same-name bucket creation returns success, not an error - never treat a second create as failure. OBS deletes are also idempotent (deleting a nonexistent object succeeds) |
61
+ | Bucket ACL does not cascade to objects | Every uploaded object needs `-acl=public-read` at upload time (`-f` IS valid on cp/rm) |
62
+ | OBS static website bucket name must equal the domain | e.g. bucket `www.example.com` for domain `www.example.com` |
63
+ | VPC v3 API differs from older docs | v3 `CreateSecurityGroup` has NO `vpc_id` (account-scoped); `CreateSecurityGroupRule` uses `multiport=80` not `port_range_min/max`; `DeleteSubnet` requires `vpc_id` too. Always `--help` first |
64
+ | Public exposure is auto-blocked | Risk rules deny `0.0.0.0/0` port rules at plan time. For test/dev use a narrower `remote_ip_prefix` (e.g. the VPC CIDR) or get user consent for a documented exception |
65
+ | ECS CreateServers hidden required params | Besides flavor/image/nics/az it also needs `--server.vpcid` and `--server.root_volume.volumetype` - order submission fails without them |
66
+ | FunctionGraph runtime/code enums are strict | runtime must be exact (`Node.js18.15`, `Python3.9`, `Go1.x`, ...); inline code is the PAIR `--package=default --code_type=inline` (`--package=inline` → `FSS.1006`); TIMER triggers use `--event_data.schedule_type=Rate --event_data.schedule=1m` (`@every` style → `FSS.1109`); DeleteFunction takes `--function_urn` WITHOUT `:latest` |
67
+ | Keypairs: `NovaCreateKeypair`, not CreateKeypair | `ECS CreateKeypair` does NOT exist - use `hcloud ECS NovaCreateKeypair`; delete takes `--keypair_name` (not `--keypair.name`). The API-generated `private_key` is MASKED in tool output - for SSH access create the keypair in the console or import the user's own public key, then bind with `--server.key_name=<n>` |
68
+ | Long inline args break MCP JSON | Values >~2KB inline (e.g. `--server.user_data=<base64 cloud-init>`) fail JSON parsing silently. Write a local JSON file and pass `--cli-jsonInput=<file>` (see `references/resource-catalog.md` header note) |
69
+ | EIP quota fails at RUN stage, not at plan | `EIP.7905 Quota exceeded` appears only after plan+approval - pre-check quota during stage 5 (and count precisely: `ListPublicips --limit=50` truncates, a "50/50" read on a 59-EIP account is wrong). Released quotas may lag minutes before `EIP.7905` clears |
70
+ | Zero balance blocks order submission | ECS creation fails with `Ecs.7000 Insufficient account balance` when cash + voucher accounts are 0. This is the stage-5 gate materialized: stop, mark `partial`, tell the user pay-by-duration resources need a balance top-up. Free-tier resources (FunctionGraph, small OBS) DO work at 0 balance - the gate applies to order-submission resources only |
71
+ | CDN mainland acceleration requires ICP filing | Unregistered domains cannot go live on CDN - check before stage 3. CDN create ALSO requires domain ownership verification first (`CDN.00010185 Verify domain ownership failed` - user must add a DNS record or upload a verification file) |
72
+ | CDN is a global service in KooCLI | CDN operations only accept `--cli-region=cn-north-1` or `ap-southeast-1` - never the resource region |
73
+ | OBS static website hosting has NO CLI path | `chattri` only sets ACL/storage-class; obsutil has no website command. Options: console setup, or CDN OBS-origin with `--domain.sources.1.enable_obs_web_hosting=1`. Without website hosting, verify via direct object URLs (still public HTTP 200) |
74
+ | DNS hosting location | `hcloud DNS ListPublicZones` decides: hosted in Huawei Cloud → auto-create CNAME record set; hosted elsewhere → output the CNAME value for manual setup at the user's DNS provider |
75
+ | EIP bills when idle; stopped ECS still bills | Include in cost estimate; destroy unattached EIPs |
76
+ | ECS delete defaults leak resources | `--delete_publicip` and `--delete_volume` are false by default - set `true` when tearing down |
77
+ | EIP binding type is non-obvious | `--publicip.associate_instance_type=PORT` (not INSTANCE) |
78
+ | State is session-only | Temp-dir file, dies with the session. Never promise cross-session deploy management |
79
+ | State file must be UTF-8 WITHOUT BOM | PowerShell `Set-Content -Encoding utf8` adds a BOM that breaks JSON parsers - write state with Node or another BOM-less writer |
80
+ | HTTPS needs a certificate | CDN serves plain HTTP by default; SSL cert (upload or SCM) required for HTTPS |
81
81
 
82
82
  ## MCP Tools
83
83
 
@@ -43,7 +43,9 @@ export function globalCredentialsPath() {
43
43
  }
44
44
 
45
45
  export function obsConfigPath() {
46
- return join(baseHome(), '.obsutilconfig');
46
+ // obsutil reads its config from a fixed location (~/.obsutilconfig), independent
47
+ // of HUAWEICLOUD_HOME. HCLOUD_OBS_CONFIG_PATH exists solely for hermetic test injection.
48
+ return process.env.HCLOUD_OBS_CONFIG_PATH || join(homedir(), '.obsutilconfig');
47
49
  }
48
50
 
49
51
  export function readGlobalCredentials() {
@@ -16,8 +16,11 @@ import { redactSecrets } from '../safety-policy.mjs';
16
16
 
17
17
  export { hasRuntimeCredentials };
18
18
 
19
- function baseHome() {
20
- return process.env.HUAWEICLOUD_HOME || homedir();
19
+ export function kooCliConfigPath() {
20
+ // KooCLI keeps its config at a fixed location (~/.hcloud/config.json),
21
+ // independent of HUAWEICLOUD_HOME (that env only relocates devkit's own S1/S3).
22
+ // HCLOUD_CONFIG_PATH exists solely for hermetic test injection.
23
+ return process.env.HCLOUD_CONFIG_PATH || join(homedir(), '.hcloud', 'config.json');
21
24
  }
22
25
 
23
26
  export function fingerprint(ak, sk) {
@@ -38,7 +41,7 @@ export function isManualModified(path) {
38
41
  }
39
42
 
40
43
  export function readKooCliProfiles() {
41
- const configPath = join(baseHome(), '.hcloud', 'config.json');
44
+ const configPath = kooCliConfigPath();
42
45
  if (!existsSync(configPath)) return { error: 'KooCLI config not found' };
43
46
  try {
44
47
  const raw = JSON.parse(readFileSync(configPath, 'utf8'));
@@ -129,7 +132,7 @@ export function scanState() {
129
132
  store: 'S2-current',
130
133
  source: 'KooCLI current profile',
131
134
  fingerprint: currentFp,
132
- manualModified: isManualModified(kooCli.configPath || join(baseHome(), '.hcloud', 'config.json')),
135
+ manualModified: isManualModified(kooCli.configPath || kooCliConfigPath()),
133
136
  });
134
137
  }
135
138
  if (s1Fingerprint && s3Fingerprint && s1Fingerprint !== s3Fingerprint) {
@@ -36,7 +36,7 @@ import {
36
36
  getProxySettings,
37
37
  } from './proxy/proxy-config.mjs';
38
38
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
39
- import { queryDistTagsSync, determineTarget, semverCompare } from './update-check.mjs';
39
+ import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
40
40
  import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
41
41
  import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
42
42
 
@@ -3255,9 +3255,9 @@ function parseTarget() {
3255
3255
  process.exit(1);
3256
3256
  }
3257
3257
 
3258
- function checkForUpdate() {
3258
+ async function checkForUpdate() {
3259
3259
  if (pkgVersion === '0.0.0') return;
3260
- const distTags = queryDistTagsSync({ timeoutMs: 5000 });
3260
+ const distTags = await queryDistTagsFetch({ timeoutMs: 15000 });
3261
3261
  const target = determineTarget(pkgVersion, distTags ?? {});
3262
3262
  if (target && semverCompare(target, pkgVersion) > 0) {
3263
3263
  const tag = distTags.next && semverCompare(target, distTags.next) === 0 ? 'next' : 'latest';
@@ -3312,7 +3312,7 @@ async function cmdInstall() {
3312
3312
  }
3313
3313
 
3314
3314
  checkNode();
3315
- checkForUpdate();
3315
+ await checkForUpdate();
3316
3316
  const installFailures = [];
3317
3317
 
3318
3318
  function shouldInstall(name) {
@@ -4077,7 +4077,7 @@ async function cmdDoctor() {
4077
4077
  async function cmdUpdate() {
4078
4078
  console.log(BANNER);
4079
4079
  const target = parseTarget();
4080
- checkForUpdate();
4080
+ await checkForUpdate();
4081
4081
 
4082
4082
  if (target === 'opencode') {
4083
4083
  if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
@@ -19,7 +19,11 @@ const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = dirname(__filename);
20
20
  const PLUGIN_DIR = join(__dirname, '..', '..');
21
21
  const AGENT_TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
22
- const GLOBAL_TELEMETRY_DIR = join(homedir(), '.huaweicloud-devkit', 'telemetry');
22
+ const GLOBAL_TELEMETRY_DIR = join(
23
+ (process.env.HUAWEICLOUD_DEVKIT_HOME || '').trim() || homedir(),
24
+ '.huaweicloud-devkit',
25
+ 'telemetry',
26
+ );
23
27
 
24
28
  let PLUGIN_VERSION = '0.0.0';
25
29
  try {
@@ -6,6 +6,8 @@ import { join, dirname, resolve } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
+ import { fetchWithProxy } from './proxy/proxy-agent.mjs';
10
+
9
11
  const IS_WINDOWS = process.platform === 'win32';
10
12
  const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
11
13
  const NPX_BIN = IS_WINDOWS ? 'npx.cmd' : 'npx';
@@ -187,7 +189,33 @@ export function writeSkipState(file, dismissedVersion, { at = Date.now(), days =
187
189
  return state;
188
190
  }
189
191
 
190
- export function queryDistTagsSync({ timeoutMs = 5000, cwd } = {}) {
192
+ function debugLog(message) {
193
+ if (process.env.HUAWEICLOUD_DEVKIT_DEBUG === '1' || process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true') {
194
+ console.error(`[debug] ${message}`);
195
+ }
196
+ }
197
+
198
+ export function queryDistTagsFetch({ timeoutMs = 15000 } = {}) {
199
+ let registry = 'https://registry.npmjs.org';
200
+ if (process.env.HUAWEICLOUD_NPM_REGISTRY) {
201
+ registry = process.env.HUAWEICLOUD_NPM_REGISTRY.replace(/\/+$/, '');
202
+ }
203
+ const controller = new AbortController();
204
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
205
+ return fetchWithProxy(`${registry}/-/package/huaweicloud-devkit/dist-tags`)
206
+ .then((resp) => {
207
+ clearTimeout(timer);
208
+ if (!resp || !resp.ok) return null;
209
+ return resp.json().catch(() => null);
210
+ })
211
+ .catch((error) => {
212
+ clearTimeout(timer);
213
+ debugLog(`queryDistTagsFetch: ${error?.message || error}`);
214
+ return null;
215
+ });
216
+ }
217
+
218
+ export function queryDistTagsSync({ timeoutMs = 15000, cwd } = {}) {
191
219
  try {
192
220
  const result = spawnSync(NPM_BIN, ['view', 'huaweicloud-devkit', 'dist-tags', '--json'], {
193
221
  encoding: 'utf8',
@@ -195,14 +223,18 @@ export function queryDistTagsSync({ timeoutMs = 5000, cwd } = {}) {
195
223
  windowsHide: true,
196
224
  cwd,
197
225
  });
198
- if (result.status !== 0) return null;
226
+ if (result.status !== 0) {
227
+ debugLog(`queryDistTagsSync: npm view exited with status ${result.status}`);
228
+ return null;
229
+ }
199
230
  return parseDistTagsOutput(result.stdout);
200
- } catch {
231
+ } catch (error) {
232
+ debugLog(`queryDistTagsSync: ${error?.message || error}`);
201
233
  return null;
202
234
  }
203
235
  }
204
236
 
205
- export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
237
+ export function queryDistTags({ timeoutMs = 15000, cwd } = {}) {
206
238
  return new Promise((resolve) => {
207
239
  let child;
208
240
  try {
@@ -210,7 +242,8 @@ export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
210
242
  windowsHide: true,
211
243
  cwd,
212
244
  });
213
- } catch {
245
+ } catch (error) {
246
+ debugLog(`queryDistTags: ${error?.message || error}`);
214
247
  resolve(null);
215
248
  return;
216
249
  }
@@ -218,20 +251,26 @@ export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
218
251
  try {
219
252
  child.kill();
220
253
  } catch {}
254
+ debugLog(`queryDistTags: timed out after ${timeoutMs}ms`);
221
255
  resolve(null);
222
256
  }, timeoutMs);
223
257
  let stdout = '';
224
258
  child.stdout.on('data', (d) => {
225
259
  stdout += String(d);
226
260
  });
227
- child.on('error', () => {
261
+ child.on('error', (error) => {
228
262
  clearTimeout(timer);
263
+ debugLog(`queryDistTags: ${error?.message || error}`);
229
264
  resolve(null);
230
265
  });
231
266
  child.on('close', (code) => {
232
267
  clearTimeout(timer);
233
- if (code === 0) resolve(parseDistTagsOutput(stdout));
234
- else resolve(null);
268
+ if (code === 0) {
269
+ resolve(parseDistTagsOutput(stdout));
270
+ } else {
271
+ debugLog(`queryDistTags: npm view exited with code ${code}`);
272
+ resolve(null);
273
+ }
235
274
  });
236
275
  });
237
276
  }
@@ -250,19 +289,19 @@ export function invalidateUpdateCache() {
250
289
  lastHint = null;
251
290
  }
252
291
 
253
- function cacheValid() {
254
- return Boolean(cachedDistTags) && Date.now() - cachedAt <= TTL_MS;
292
+ function cacheValid(now = Date.now()) {
293
+ return Boolean(cachedDistTags) && now - cachedAt <= TTL_MS;
255
294
  }
256
295
 
257
- export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } = {}) {
296
+ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags, now = Date.now() } = {}) {
258
297
  if (process.env.HUAWEICLOUD_DEVKIT_SKIP_UPDATE === '1') {
259
- lastHint = judgeUpdate(current, null);
298
+ lastHint = judgeUpdate(current, null, undefined, now);
260
299
  return lastHint;
261
300
  }
262
301
  const skipState = readSkipState(resolveSkipFilePath());
263
- if (!cacheValid()) {
264
- if (!cachedDistTags && Date.now() - failedAt < FAIL_THROTTLE_MS) {
265
- lastHint = judgeUpdate(current, null, skipState);
302
+ if (!cacheValid(now)) {
303
+ if (!cachedDistTags && now - failedAt < FAIL_THROTTLE_MS) {
304
+ lastHint = judgeUpdate(current, null, skipState, now);
266
305
  return lastHint;
267
306
  }
268
307
  if (!inflightQuery) {
@@ -270,9 +309,9 @@ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } =
270
309
  .then((distTags) => {
271
310
  if (distTags) {
272
311
  cachedDistTags = distTags;
273
- cachedAt = Date.now();
312
+ cachedAt = now;
274
313
  } else {
275
- failedAt = Date.now();
314
+ failedAt = now;
276
315
  }
277
316
  return distTags;
278
317
  })
@@ -281,7 +320,7 @@ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } =
281
320
  });
282
321
  }
283
322
  const distTags = await inflightQuery;
284
- lastHint = judgeUpdate(current, distTags, skipState);
323
+ lastHint = judgeUpdate(current, distTags, skipState, now);
285
324
  return lastHint;
286
325
  }
287
326
  lastHint = judgeUpdate(current, cachedDistTags, skipState);