huaweicloud-devkit 1.1.8 → 1.1.9-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/integrations/opencode/package-lock.json +24 -0
  4. package/integrations/opencode/package.json +10 -0
  5. package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
  6. package/package.json +2 -2
  7. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  12. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  13. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -0
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +16 -2
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +22 -0
  16. package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +118 -0
  17. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +3 -3
  18. package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +36 -36
  19. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -2
  20. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +30 -3
  21. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +35 -6
  22. package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +1 -0
  23. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +2 -2
  24. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +6 -6
  25. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +16 -9
  26. package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +37 -2
  27. package/plugins/huaweicloud-core/src/auth/credentials.mjs +14 -27
  28. package/plugins/huaweicloud-core/src/auth/guidance.mjs +112 -0
  29. package/plugins/huaweicloud-core/src/auth/reconcile.mjs +36 -2
  30. package/plugins/huaweicloud-core/src/auth/service.mjs +54 -46
  31. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +32 -4
  32. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +26 -6
  33. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +120 -4
  34. package/plugins/huaweicloud-core/src/setup-cli.mjs +111 -8
  35. package/plugins/huaweicloud-core/src/tools.mjs +23 -3
@@ -11,6 +11,8 @@ import {
11
11
  import { dirname, join } from 'node:path';
12
12
  import { homedir } from 'node:os';
13
13
 
14
+ import { buildOnboardingSteps, makeError } from './guidance.mjs';
15
+
14
16
  // Verify a credential file ended up with 0600. On Windows-mounted drives inside WSL
15
17
  // (drvfs/9p) chmod is silently ignored, so the file can be world-readable (0777).
16
18
  // Native Windows has no POSIX modes (statSync always reports 0666), so skip the check there.
@@ -203,7 +205,8 @@ export function writeObsConfig(credentials = {}) {
203
205
  const sk = String(credentials.sk || '');
204
206
  const securityToken = String(credentials.securityToken || '');
205
207
  if (!region || !ak || !sk) {
206
- throw new Error('region, ak, and sk are required to write OBS config');
208
+ // #900: unified error shape from the shared ERROR_TABLE.
209
+ throw makeError('HDKIT_CRED_REGION_MISSING');
207
210
  }
208
211
  const path = obsConfigPath();
209
212
  const endpoint = credentials.endpoint || `https://obs.${region}.myhuaweicloud.com`;
@@ -263,43 +266,25 @@ export function resolveCredentials(options = {}) {
263
266
 
264
267
  if (!ak || !sk) {
265
268
  if (options.allowMissing) return null;
266
- const err = new Error(
267
- 'Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
268
- );
269
- err.code = 'HDKIT_CRED_MISSING';
270
- // Lightweight onboarding hint. HDKIT_CRED_MISSING fires when no credential
271
- // resolved: with env empty/masked and S1 absent, the scenario is 3 by
272
- // default. Scenario 4 (import a config-provided account) is only reachable
273
- // in the narrow case where CodeArts S4 offers real creds that the resolver
274
- // still couldn't adopt (e.g. CODEARTS_PROJECT_DIR set but the marker dir is
275
- // absent). Full guidance comes from getAuthStatus(); keep this branch cheap.
269
+ // #900: unified guidance — one steps builder shared with computeOnboarding,
270
+ // and the error shape from the single ERROR_TABLE. Zero IO beyond the
271
+ // readCodeArtsCredentials() probe we already did above for scenario routing.
276
272
  const codeartsCreds = readCodeArtsCredentials();
277
273
  const injectedAvailable = present(codeartsCreds?.ak) && present(codeartsCreds?.sk);
274
+ const codearts = isCodeArtsContext();
275
+ const err = makeError('HDKIT_CRED_MISSING');
278
276
  err.onboarding = injectedAvailable
279
277
  ? {
280
278
  scenario: 4,
281
279
  reason: 'import-injected',
282
280
  message: '检测到配置中已有有效账号,可导入为正式凭证。',
283
- steps: isCodeArtsContext()
284
- ? [{ action: 'auth_switch', args: { mode: 'mcp-config', action: 'persist' }, label: '导入配置中的账号' }]
285
- : [
286
- {
287
- action: 'auth_switch',
288
- args: { mode: 'import', action: 'persist' },
289
- label: '通过 creds-import.json 导入',
290
- },
291
- ],
281
+ steps: buildOnboardingSteps(4, { codearts }),
292
282
  }
293
283
  : {
294
284
  scenario: 3,
295
285
  reason: 's1-missing',
296
286
  message: '未配置华为云凭证,需要完成登录后才能使用云能力。',
297
- steps: isCodeArtsContext()
298
- ? [
299
- { action: 'write-import', target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json') },
300
- { action: 'auth_switch', args: { mode: 'import', action: 'persist' } },
301
- ]
302
- : [{ action: 'auth-init', args: {} }],
287
+ steps: buildOnboardingSteps(3, { codearts }),
303
288
  };
304
289
  throw err;
305
290
  }
@@ -320,7 +305,9 @@ export function getParentCwd() {
320
305
  }
321
306
  }
322
307
 
323
- function isCodeArtsContext() {
308
+ // Exported for tests: the failure path and the tests must agree on which
309
+ // onboarding variant (codearts vs plain) the current environment selects.
310
+ export function isCodeArtsContext() {
324
311
  return (
325
312
  existsSync(join(process.cwd(), '.codeartsdoer')) ||
326
313
  existsSync(join(homedir(), '.codeartsdoer')) ||
@@ -0,0 +1,112 @@
1
+ // plugins/huaweicloud-core/src/auth/guidance.mjs
2
+ // Single Source of Truth for onboarding guidance steps and the error surface.
3
+ // Leaf module: imports NOTHING from auth/ siblings. Zero IO — pure builders.
4
+ // Callers: credentials.mjs (failure path), service.mjs (computeOnboarding),
5
+ // credential-validator / reconcile / hcloud-probe (error shapes).
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ export function buildOnboardingSteps(scenario, { codearts = false } = {}) {
10
+ switch (scenario) {
11
+ case 3: {
12
+ const base = [{ order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' }];
13
+ if (codearts) {
14
+ return [
15
+ ...base,
16
+ {
17
+ order: 2,
18
+ action: 'write-import',
19
+ target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
20
+ label: '把 AK/SK 写入 creds-import.json',
21
+ },
22
+ {
23
+ order: 3,
24
+ action: 'auth_switch',
25
+ // QA #905 Required: scenario-3 codearts flow writes creds-import.json
26
+ // in step 2, so the import must READ that file (mode:'import').
27
+ // mode:'mcp-config' reads the CodeArts injected environment, which
28
+ // is empty/placeholder in scenario 3 — the channel would not close.
29
+ args: { mode: 'import', action: 'persist' },
30
+ label: '执行导入完成配置',
31
+ },
32
+ ];
33
+ }
34
+ return [...base, { order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }];
35
+ }
36
+ case 4:
37
+ return [
38
+ {
39
+ order: 1,
40
+ action: 'auth_switch',
41
+ args: { mode: codearts ? 'mcp-config' : 'import', action: 'persist' },
42
+ label: '导入该账号',
43
+ },
44
+ ];
45
+ default:
46
+ return [];
47
+ }
48
+ }
49
+
50
+ // #900 error contract table. messageZh: Chinese, human-readable, no English
51
+ // sentences, no internal codenames. action: directly executable. nextStep is
52
+ // kept as a derived compatibility field for existing consumers.
53
+ export const ERROR_TABLE = {
54
+ HDKIT_CRED_MISSING: {
55
+ messageZh: '未配置华为云凭证,需要先完成登录。',
56
+ action: 'npx huaweicloud-devkit auth init',
57
+ },
58
+ HDKIT_CRED_INVALID: {
59
+ messageZh: '凭证已被华为云拒绝(AK/SK 无效或已过期),请重新生成。',
60
+ action: 'npx huaweicloud-devkit auth status',
61
+ },
62
+ HDKIT_CRED_FORBIDDEN: {
63
+ messageZh: '凭证有效但无权限列出项目,请检查账号授权。',
64
+ action: 'hcloud IAM KeystoneListProjects --cli-region=<region>',
65
+ },
66
+ HDKIT_CRED_REGION_MISSING: {
67
+ messageZh: '缺少 region(区域),登录配置不完整。',
68
+ action: 'hcloud configure set --cli-region=<region>',
69
+ },
70
+ HDKIT_KOOCLI_NOT_READY: {
71
+ messageZh: 'KooCLI(hcloud)未安装或未就绪。',
72
+ action: 'npx huaweicloud-devkit install-hcloud',
73
+ },
74
+ HDKIT_PROJECT_ID_MISSING: {
75
+ messageZh: 'KooCLI 配置缺少 project_id,导致同步失败。',
76
+ action: 'hcloud configure set --cli-project-id=<id>',
77
+ },
78
+ // Runtime credentials (in-process, set via auth_init/auth_switch) suppress
79
+ // auto-sync (rule R10). No `auth sync --force` flag exists; the real unblock
80
+ // is clearing the runtime store first, then re-running the sync.
81
+ HDKIT_SYNC_SKIPPED_RUNTIME: {
82
+ messageZh: '运行时凭证生效,自动同步已跳过。',
83
+ action: 'huaweicloud_auth_switch action=clear',
84
+ },
85
+ // IAM returned something other than 200/401/403 (or the request itself
86
+ // failed). The canonical debug hint lives in the cli-and-auth skill.
87
+ HDKIT_CRED_UNEXPECTED: {
88
+ messageZh: '凭证校验遇到异常响应,请开启调试排查。',
89
+ action: 'hcloud <Service> <Op> --cli-debug=true',
90
+ },
91
+ };
92
+
93
+ export function errorFromTable(code, detail) {
94
+ const e = ERROR_TABLE[code];
95
+ if (!e) return { code, messageZh: detail || String(code), action: '', nextStep: detail || '' };
96
+ return {
97
+ code,
98
+ messageZh: detail ? `${e.messageZh}(${detail})` : e.messageZh,
99
+ action: e.action,
100
+ nextStep: e.action,
101
+ };
102
+ }
103
+
104
+ export function makeError(code, detail) {
105
+ const shape = errorFromTable(code, detail);
106
+ const err = new Error(shape.messageZh);
107
+ err.code = shape.code;
108
+ err.action = shape.action;
109
+ err.nextStep = shape.nextStep;
110
+ err.messageZh = shape.messageZh;
111
+ return err;
112
+ }
@@ -12,6 +12,7 @@ import {
12
12
  readLastSync,
13
13
  resolveCredentialsWithRuntime,
14
14
  } from './credentials.mjs';
15
+ import { errorFromTable } from './guidance.mjs';
15
16
  import { resolveHcloudCommand } from '../hcloud-probe.mjs';
16
17
  import { redactSecrets } from '../safety-policy.mjs';
17
18
 
@@ -43,7 +44,12 @@ export function isManualModified(path) {
43
44
 
44
45
  export function readKooCliProfiles() {
45
46
  const configPath = kooCliConfigPath();
46
- if (!existsSync(configPath)) return { error: 'KooCLI config not found' };
47
+ if (!existsSync(configPath)) {
48
+ // #900: bare string replaced by the unified ERROR_TABLE shape; the old
49
+ // English text survives only as the distinguishing detail argument.
50
+ const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', 'KooCLI config not found');
51
+ return { error: e.messageZh, code: e.code, messageZh: e.messageZh, action: e.action, nextStep: e.nextStep };
52
+ }
47
53
  try {
48
54
  const raw = JSON.parse(readFileSync(configPath, 'utf8'));
49
55
  const current = String(raw.current || 'default');
@@ -72,7 +78,9 @@ export function readKooCliProfiles() {
72
78
  ),
73
79
  };
74
80
  } catch {
75
- return { error: 'KooCLI config parse failed' };
81
+ // #900: bare string replaced by the unified ERROR_TABLE shape.
82
+ const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', 'KooCLI config parse failed');
83
+ return { error: e.messageZh, code: e.code, messageZh: e.messageZh, action: e.action, nextStep: e.nextStep };
76
84
  }
77
85
  }
78
86
 
@@ -221,3 +229,29 @@ export function runHcloudConfigure(profile, ak, sk, region) {
221
229
  ),
222
230
  };
223
231
  }
232
+
233
+ // Delete a KooCLI profile (best-effort). Used by the auth persist path (#920):
234
+ // `configure set` REFUSES an empty --cli-security-token, so a stale/expired
235
+ // token already in the profile can never be overwritten — it poisons every
236
+ // hcloud call with APIGW.0301. The documented manual bypass is
237
+ // `hcloud configure delete --cli-profile=<profile>` followed by a re-configure;
238
+ // this helper makes that path a runtime step instead of user knowledge.
239
+ // A missing profile reports a non-zero status, which callers treat as
240
+ // "nothing to clean" — never as an error.
241
+ export function runHcloudProfileDelete(profile) {
242
+ const { executable, argsPrefix } = resolveHcloudCommand();
243
+ const r = spawnSync(executable, [...argsPrefix, 'configure', 'delete', `--cli-profile=${profile}`], {
244
+ shell: false,
245
+ windowsHide: true,
246
+ stdio: 'pipe',
247
+ timeout: 30000,
248
+ });
249
+ return {
250
+ ok: r.status === 0,
251
+ error: redactSecrets(
252
+ String(r.stderr || '')
253
+ .trim()
254
+ .slice(0, 240),
255
+ ),
256
+ };
257
+ }
@@ -1,11 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
2
 
5
3
  import { getAgentRegistrationStatuses } from './agent-registration.mjs';
6
4
  import { resolveAndApplyProjectId } from './project-id.mjs';
7
5
  import {
8
6
  globalCredentialsPath,
7
+ isCodeArtsContext,
9
8
  isPlaceholder,
10
9
  obsConfigPath,
11
10
  parseStsExpiry,
@@ -15,6 +14,7 @@ import {
15
14
  writeLastSync,
16
15
  writeObsConfig,
17
16
  } from './credentials.mjs';
17
+ import { buildOnboardingSteps, errorFromTable } from './guidance.mjs';
18
18
  import {
19
19
  fingerprint,
20
20
  exportStateForStatus,
@@ -22,15 +22,7 @@ import {
22
22
  resolveManagedProfile,
23
23
  runHcloudConfigure,
24
24
  } from './reconcile.mjs';
25
- import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
26
-
27
- function isCodeArtsHome() {
28
- return (
29
- existsSync(join(process.cwd(), '.codeartsdoer')) ||
30
- existsSync(join(homedir(), '.codeartsdoer')) ||
31
- existsSync(join(homedir(), '.codeartswork'))
32
- );
33
- }
25
+ import { hcloudProbeNextStep, hcloudProbeRaw, probeHcloud } from '../hcloud-probe.mjs';
34
26
 
35
27
  function envHasRealTriplet() {
36
28
  return (
@@ -57,7 +49,9 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
57
49
  const envRealAk = !isPlaceholder(process.env.HW_ACCESS_KEY) && Boolean(process.env.HW_ACCESS_KEY);
58
50
  const envRealSk = !isPlaceholder(process.env.HW_SECRET_KEY) && Boolean(process.env.HW_SECRET_KEY);
59
51
  const envHasCreds = envRealAk && envRealSk;
60
- const codeArts = isCodeArtsHome();
52
+ // Same context detector the resolveCredentials failure path uses (#900): the
53
+ // codearts flag must select the identical builder variant on both paths.
54
+ const codeArts = isCodeArtsContext();
61
55
  const accountHint = s1Has ? fingerprint(creds.ak, creds.sk) : null;
62
56
 
63
57
  let scenario;
@@ -133,32 +127,12 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
133
127
  scenario = 4;
134
128
  reason = 'import-injected';
135
129
  message = '检测到配置中已有有效账号,可导入为正式凭证。';
136
- steps = [
137
- {
138
- order: 1,
139
- action: 'switch-persist',
140
- args: { mode: codeArts ? 'mcp-config' : 'import', action: 'persist' },
141
- label: '导入该账号',
142
- },
143
- ];
130
+ steps = buildOnboardingSteps(4, { codearts: codeArts });
144
131
  } else {
145
132
  scenario = 3;
146
133
  reason = 's1-missing';
147
134
  message = '未配置华为云凭证,需要完成登录后才能使用云能力。';
148
- steps = [
149
- { order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' },
150
- ...(codeArts
151
- ? [
152
- {
153
- order: 2,
154
- action: 'write-import',
155
- target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
156
- label: '把 AK/SK 写入 creds-import.json',
157
- },
158
- { order: 3, action: 'auth_switch', args: { mode: 'import', action: 'persist' }, label: '执行导入完成配置' },
159
- ]
160
- : [{ order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }]),
161
- ];
135
+ steps = buildOnboardingSteps(3, { codearts: codeArts });
162
136
  }
163
137
 
164
138
  return { needsSetup: true, scenario, reason, message, steps, accountHint };
@@ -215,6 +189,7 @@ export function getAuthStatus(target = 'all') {
215
189
  kooCliInstalled: hcloud.installed,
216
190
  kooCliStatus: hcloud.status,
217
191
  kooCliNextStep: hcloudProbeNextStep(hcloud),
192
+ kooCliRaw: hcloudProbeRaw(hcloud),
218
193
  reconciled,
219
194
  onboarding,
220
195
  agents: getAgentRegistrationStatuses(target).agents,
@@ -224,17 +199,28 @@ export function getAuthStatus(target = 'all') {
224
199
  export function syncAuth(target = 'all') {
225
200
  const credentials = readGlobalCredentials();
226
201
  if (!credentials?.ak || !credentials?.sk) {
202
+ // #900: unified error shape from the shared ERROR_TABLE.
203
+ const e = errorFromTable('HDKIT_CRED_MISSING');
227
204
  return {
228
205
  ok: false,
229
- error: 'Global credentials are not configured.',
230
- nextStep: 'Run "npx huaweicloud-devkit auth init" first.',
206
+ error: e.messageZh,
207
+ code: e.code,
208
+ messageZh: e.messageZh,
209
+ action: e.action,
210
+ nextStep: e.nextStep,
231
211
  };
232
212
  }
233
213
  if (hasRuntimeCredentials()) {
214
+ // R10: runtime credentials suppress auto-sync. The codename stays in code
215
+ // comments only, never in user-facing strings (#900).
216
+ const e = errorFromTable('HDKIT_SYNC_SKIPPED_RUNTIME');
234
217
  return {
235
218
  ok: false,
236
- error: 'Runtime credentials are active; auto-sync suppressed (R10).',
237
- nextStep: 'Run huaweicloud_auth_switch action=clear or action=persist first.',
219
+ error: e.messageZh,
220
+ code: e.code,
221
+ messageZh: e.messageZh,
222
+ action: e.action,
223
+ nextStep: e.nextStep,
238
224
  };
239
225
  }
240
226
 
@@ -264,25 +250,47 @@ export function syncAuth(target = 'all') {
264
250
 
265
251
  const hcloud = probeHcloud();
266
252
  if (!hcloud.ok) {
253
+ // #900: three probe states share the table code; the detail distinguishes.
254
+ const detail =
255
+ hcloud.status === 'sandbox_home_failure'
256
+ ? '无法解析用户主目录'
257
+ : hcloud.status === 'privacy_pending'
258
+ ? '隐私协议待同意'
259
+ : '';
260
+ const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', detail);
267
261
  return {
268
262
  ok: false,
269
- error:
270
- hcloud.status === 'sandbox_home_failure'
271
- ? 'KooCLI detected but cannot resolve the user home directory in this agent sandbox.'
272
- : hcloud.status === 'privacy_pending'
273
- ? 'KooCLI privacy agreement is pending.'
274
- : 'KooCLI not ready.',
263
+ error: e.messageZh,
264
+ code: e.code,
265
+ messageZh: e.messageZh,
266
+ action: e.action,
267
+ // #900 issue comment: nextStep is the executable table action; the
268
+ // verbose English guidance survives only in the `raw` side-channel.
275
269
  nextStep: hcloudProbeNextStep(hcloud),
270
+ raw: hcloudProbeRaw(hcloud),
276
271
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
277
272
  };
278
273
  }
279
274
 
280
275
  const { ok, error } = runHcloudConfigure(profile, credentials.ak, credentials.sk, credentials.region);
281
276
  if (!ok) {
277
+ // #900: `hcloud configure set` failures are usually a missing/invalid
278
+ // project_id for the target region; anything else keeps the raw (already
279
+ // redacted) stderr as the detail argument.
280
+ const detail = String(error || '').trim();
281
+ const projectIdCause = /project[_\s-]?id/i.test(detail);
282
+ const e = projectIdCause
283
+ ? errorFromTable('HDKIT_PROJECT_ID_MISSING', detail)
284
+ : errorFromTable('HDKIT_KOOCLI_NOT_READY', detail || 'KooCLI 配置同步失败');
282
285
  return {
283
286
  ok: false,
284
- error: error || 'KooCLI config sync failed',
285
- nextStep: 'Run "npx huaweicloud-devkit auth init" in a real terminal to refresh the KooCLI config.',
287
+ error: e.messageZh,
288
+ code: e.code,
289
+ messageZh: e.messageZh,
290
+ action: e.action,
291
+ nextStep: projectIdCause
292
+ ? e.nextStep
293
+ : 'Run "npx huaweicloud-devkit auth init" in a real terminal to refresh the KooCLI config.',
286
294
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
287
295
  };
288
296
  }
@@ -659,11 +659,39 @@ function runHcloudOnce(plan, options) {
659
659
  });
660
660
  return;
661
661
  }
662
+ // #921: the ok verdict converges on the exit code plus structured error
663
+ // parsing (errorCode-first, handled above). Two false-positive classes
664
+ // are closed here:
665
+ // 1. exit code 0 but the output carries a KooCLI parameter error (e.g.
666
+ // `--delete_ip` instead of `--delete_publicip`) — the command never
667
+ // executed; report ok:false with a PARAM_ERROR errorCode.
668
+ // 2. non-zero exit code with "successfully" in the output — the old
669
+ // broad regex flagged ANY failure mentioning "successfully". It is
670
+ // narrowed to OBS obsutil-style commands (which are known to return
671
+ // non-zero exit codes on success, #57) AND require the output to
672
+ // carry no error indicators.
673
+ const combined = `${stdout}${stderr}`;
674
+ const paramError = combined.match(
675
+ /\[?(?:ERROR|USE_ERROR)\]?[: ]*(?:unknown parameter|unknown option|incorrect (?:option|switch|parameter)|invalid (?:parameter|argument)|参数不存在|参数错误|无效的参数)[^\n]*/i,
676
+ );
677
+ if (paramError) {
678
+ finish({
679
+ ok: false,
680
+ exitCode: code,
681
+ signal,
682
+ errorCode: 'PARAM_ERROR',
683
+ errorMessage: paramError[0].trim(),
684
+ stdout: redactOutput(stdout),
685
+ stderr: redactOutput(stderr),
686
+ plan,
687
+ });
688
+ return;
689
+ }
690
+ const isObsCommand = /^obs\b/i.test(String(plan.rawArgs?.[0] || ''));
691
+ const successText = /successfully/i.test(combined);
692
+ const errorIndicators = /error|failed|失败|invalid|not found|denied|拒绝/i.test(combined);
662
693
  finish({
663
- ok:
664
- code === 0 ||
665
- (code !== 0 &&
666
- /successfully|succ?ess.*\[200\]|create bucket successfully|upload successfully/i.test(stdout + stderr)),
694
+ ok: code === 0 || (isObsCommand && successText && !errorIndicators),
667
695
  exitCode: code,
668
696
  signal,
669
697
  stdout: redactOutput(stdout),
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
 
6
+ import { errorFromTable } from './auth/guidance.mjs';
6
7
  import { compareVersion, getKooCliVersion, parseHcloudVersion } from './koocli-version.mjs';
7
8
  import { kooCliInstallLogPath } from './preflight.mjs';
8
9
  import { redactSecrets } from './safety-policy.mjs';
@@ -140,19 +141,24 @@ export function probeHcloud(options = {}) {
140
141
  };
141
142
  }
142
143
 
143
- export function hcloudProbeNextStep(probe) {
144
- if (probe.status === 'ok')
144
+ // #900 (issue comment Required 1): every not-ready probe state routes through
145
+ // the shared ERROR_TABLE.HDKIT_KOOCLI_NOT_READY so nextStep is always a
146
+ // directly executable command (d11-41-nextstep-actionable). The verbose
147
+ // English guidance is preserved verbatim in hcloudProbeRaw() — a docs/debug
148
+ // side-channel — and must never leak back into the top-level nextStep.
149
+ export function hcloudProbeRaw(probe) {
150
+ if (probe?.status === 'ok')
145
151
  return 'Use huaweicloud_show_profile_redacted to inspect the active KooCLI profile safely.';
146
- if (probe.status === 'version_mismatch') {
152
+ if (probe?.status === 'version_mismatch') {
147
153
  return `KooCLI version mismatch: installed ${probe.installedVersion}, this plugin is paired with ${probe.requiredVersion}. Reinstall the pinned version (see huaweicloud-cli-and-auth skill) and restart the agent.`;
148
154
  }
149
- if (probe.status === 'sandbox_home_failure') {
155
+ if (probe?.status === 'sandbox_home_failure') {
150
156
  return 'KooCLI was found, but this agent sandbox cannot resolve the Windows user home directory. Set HCLOUD_BIN to the full hcloud path, restart Codex, or verify from a normal terminal.';
151
157
  }
152
- if (probe.status === 'privacy_pending') {
158
+ if (probe?.status === 'privacy_pending') {
153
159
  return 'KooCLI requires accepting its one-time privacy agreement. Run hcloud version in a real terminal and accept the prompt, then restart the agent.';
154
160
  }
155
- if (probe.status === 'not_found') {
161
+ if (probe?.status === 'not_found') {
156
162
  return (
157
163
  'KooCLI is not installed yet. Background auto-install may be running — please retry shortly instead of assuming failure. To install now: npx huaweicloud-devkit install-hcloud. DevKit picks up the binary from its fixed install dirs (~/.local/bin on Linux, /usr/local/bin on macOS, ~/hcloud on Windows) or HCLOUD_BIN on the next call — no agent restart needed. Install progress: ' +
158
164
  kooCliInstallLogPath()
@@ -160,3 +166,17 @@ export function hcloudProbeNextStep(probe) {
160
166
  }
161
167
  return 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.';
162
168
  }
169
+
170
+ export function hcloudProbeNextStep(probe) {
171
+ if (probe?.status === 'ok') return 'huaweicloud_show_profile_redacted';
172
+ const detail =
173
+ probe?.status === 'version_mismatch'
174
+ ? `版本不匹配(已装 ${probe?.installedVersion ?? '未知'},插件配套 ${probe?.requiredVersion ?? '未知'})`
175
+ : probe?.status === 'sandbox_home_failure'
176
+ ? '无法解析用户主目录'
177
+ : probe?.status === 'privacy_pending'
178
+ ? '隐私协议待同意'
179
+ : '';
180
+ const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', detail);
181
+ return e.nextStep;
182
+ }