huaweicloud-devkit 1.0.2-next.3 → 1.0.3-dev.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.
Files changed (97) hide show
  1. package/.agents/plugins/marketplace.json +20 -20
  2. package/LICENSE +201 -201
  3. package/README.md +139 -163
  4. package/README.zh-CN.md +139 -161
  5. package/bin/setup.cjs +8 -8
  6. package/integrations/opencode/commands/huaweicloud-doctor.md +14 -14
  7. package/integrations/opencode/opencode.json +14 -14
  8. package/integrations/opencode/skills/huaweicloud-core/SKILL.md +32 -32
  9. package/package.json +44 -47
  10. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +44 -44
  11. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +43 -43
  12. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +44 -44
  13. package/plugins/huaweicloud-core/.mcp.json +15 -15
  14. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +44 -44
  15. package/plugins/huaweicloud-core/hooks/hooks.json +26 -26
  16. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +136 -136
  17. package/plugins/huaweicloud-core/safety/policy.json +99 -99
  18. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +199 -199
  19. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +164 -164
  20. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -48
  21. package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -50
  22. package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +101 -101
  23. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +90 -90
  24. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +190 -190
  25. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +124 -124
  26. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -55
  27. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -70
  28. package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +56 -56
  29. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +97 -97
  30. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +29 -29
  31. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +23 -23
  32. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +185 -185
  33. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +133 -133
  34. package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -31
  35. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +55 -55
  36. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -52
  37. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +131 -131
  38. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +58 -58
  39. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +71 -71
  40. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -119
  41. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -142
  42. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -56
  43. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +65 -65
  44. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -111
  45. package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +122 -122
  46. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +56 -56
  47. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -65
  48. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +127 -133
  49. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +49 -49
  50. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +26 -26
  51. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +100 -100
  52. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -40
  53. package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -56
  54. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +224 -224
  55. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -139
  56. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -69
  57. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +84 -84
  58. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +76 -76
  59. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +59 -59
  60. package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +50 -50
  61. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -63
  62. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +56 -56
  63. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -4
  64. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +47 -51
  65. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -4
  66. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +128 -128
  67. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -4
  68. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +95 -110
  69. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -4
  70. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +19 -19
  71. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +27 -27
  72. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +60 -60
  73. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -4
  74. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -83
  75. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -4
  76. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -105
  77. package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -95
  78. package/plugins/huaweicloud-core/src/auth/service.mjs +63 -63
  79. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +265 -265
  80. package/plugins/huaweicloud-core/src/mcp-server.mjs +129 -129
  81. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +137 -137
  82. package/plugins/huaweicloud-core/src/safety-policy.mjs +302 -302
  83. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -91
  84. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -162
  85. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -112
  86. package/plugins/huaweicloud-core/src/search-market.mjs +169 -174
  87. package/plugins/huaweicloud-core/src/setup-cli.mjs +1594 -2017
  88. package/plugins/huaweicloud-core/src/tools.mjs +993 -1008
  89. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -427
  90. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -132
  91. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -227
  92. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -202
  93. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -158
  94. package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -19
  95. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -338
  96. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +0 -155
  97. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +0 -87
@@ -1,2017 +1,1594 @@
1
- #!/usr/bin/env node
2
-
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
- import { join, dirname, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { homedir, platform } from 'node:os';
7
- import { createInterface } from 'node:readline';
8
- import { spawnSync } from 'node:child_process';
9
- import { getAuthStatus, syncAuth } from './auth/service.mjs';
10
- import { globalCredentialsPath, readGlobalCredentials, writeGlobalCredentials, writeObsConfig } from './auth/credentials.mjs';
11
- import { proxyConfigPath, readProxyConfig, writeProxyConfig, clearProxyConfig, getProxySettings } from './proxy/proxy-config.mjs';
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const PLUGIN_ROOT = resolve(__dirname, '..');
15
- const PACKAGE_ROOT = resolve(PLUGIN_ROOT, '..', '..');
16
-
17
- let pkgVersion = '0.0.0';
18
- try {
19
- pkgVersion = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
20
- } catch {}
21
-
22
- const BANNER = `
23
- ╔══════════════════════════════════════════════╗
24
- HuaweiCloud DevKit v${pkgVersion}${' '.repeat(Math.max(0, 22 - String(pkgVersion).length))}
25
- ║ https://github.com/huaweicloud ║
26
- ╚══════════════════════════════════════════════╝
27
- `;
28
-
29
- function configRoot(target = 'opencode') {
30
- const home = homedir();
31
- return join(home, '.config', target);
32
- }
33
-
34
- function opencodeSkillsDir() { return join(configRoot('opencode'), 'skills'); }
35
- function opencodeCommandsDir() { return join(configRoot('opencode'), 'commands'); }
36
- function opencodePluginsDir() { return join(configRoot('opencode'), 'huaweicloud-plugins'); }
37
- function opencodeConfigFile() {
38
- const jsonc = join(configRoot('opencode'), 'opencode.jsonc');
39
- if (existsSync(jsonc)) return jsonc;
40
- return join(configRoot('opencode'), 'opencode.json');
41
- }
42
-
43
- function codexDesktopSkillsDir() { return join(homedir(), '.agents', 'skills'); }
44
- function codexDesktopCommandsDir() { return join(homedir(), '.agents', 'commands'); }
45
- function codexDesktopPluginsDir() { return join(homedir(), '.agents', 'huaweicloud-plugins'); }
46
- function codexConfigToml() { return join(homedir(), '.codex', 'config.toml'); }
47
-
48
- function codeartsSkillsDir() { return join(homedir(), '.codeartsdoer', 'skills'); }
49
- function codeartsMcpSettingsDir() { return join(homedir(), '.codeartsdoer', 'mcp'); }
50
- function codeartsMcpSettingsFile() { return join(codeartsMcpSettingsDir(), 'mcp_settings.json'); }
51
- function codeartsProjectDir() { return join(process.cwd(), '.codeartsdoer'); }
52
- function codeartsProjectSkillsDir() { return join(codeartsProjectDir(), 'skills'); }
53
- function codeartsProjectMcpSettingsFile() { return join(codeartsProjectDir(), 'mcp', 'mcp_settings.json'); }
54
- function codeartsPluginsDir() { return join(homedir(), '.codeartsdoer', 'huaweicloud-plugins'); }
55
-
56
- function workbuddySkillsDir() { return join(homedir(), '.workbuddy', 'skills'); }
57
- function workbuddyMcpConfigFile() { return join(homedir(), '.workbuddy', 'mcp.json'); }
58
- function workbuddyPluginsDir() { return join(homedir(), '.workbuddy', 'huaweicloud-plugins'); }
59
-
60
- function dshRoot() { return process.env.DSH_HOME || join(homedir(), '.dsh'); }
61
- function dshSkillsDir() { return join(dshRoot(), 'skills'); }
62
- function dshProfileDir() { return join(dshRoot(), 'profiles', 'web'); }
63
- function dshPatchFile() { return join(dshProfileDir(), 'cordis.patch.yml'); }
64
- function dshPluginsDir() { return join(dshRoot(), 'huaweicloud-plugins'); }
65
-
66
- const DSH_MCP_PATCH_START = '# HuaweiCloud DevKit DSH integration start';
67
- const DSH_MCP_PATCH_END = '# HuaweiCloud DevKit DSH integration end';
68
-
69
- // Detect CodeArts sandbox mode (bash_mode in permission config).
70
- function detectCodeartsSandbox() {
71
- try {
72
- const configPath = join(homedir(), '.codeartsdoer', 'codearts-data', 'storage', 'permission', 'config.json');
73
- if (!existsSync(configPath)) return null;
74
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
75
- return typeof config.bash_mode === 'string' ? config.bash_mode : null;
76
- } catch {
77
- return null;
78
- }
79
- }
80
-
81
- // Locate an existing hcloud executable (HCLOUD_BIN, ~/hcloud on Windows, ~/.local/bin elsewhere).
82
- function findHcloudBin() {
83
- if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
84
- const isWin = platform() === 'win32';
85
- const candidates = isWin
86
- ? [join(homedir(), 'hcloud', 'hcloud.exe')]
87
- : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud'), join(homedir(), 'hcloud', 'hcloud.exe')];
88
- for (const c of candidates) {
89
- if (existsSync(c)) return c;
90
- }
91
- return null;
92
- }
93
-
94
- function printSandboxWarning(reason) {
95
- console.log(`\n\x1b[1m\x1b[31m⚠ 检测到码道沙箱模式 (bash_mode: sandbox)\x1b[0m`);
96
- console.log(`\x1b[31m ${reason}\x1b[0m`);
97
- console.log(`\x1b[31m 请任选其一继续:\x1b[0m`);
98
- console.log(`\x1b[31m A. 在码道外的终端安装并使用 KooCLI (推荐):`);
99
- console.log(`\x1b[31m https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`);
100
- console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 权限 → Bash 模式)`);
101
- console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
102
- }
103
-
104
- function checkNode() {
105
- const v = process.versions.node.split('.').map(Number);
106
- if (v[0] < 20) {
107
- console.error(`\x1b[31mNode.js >= 20 required (current: ${process.version})\x1b[0m`);
108
- process.exit(1);
109
- }
110
- console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
111
- }
112
-
113
- function copyDir(src, dest) {
114
- if (!existsSync(src)) return;
115
- mkdirSync(dest, { recursive: true });
116
- for (const entry of readdirSync(src, { withFileTypes: true })) {
117
- const s = join(src, entry.name);
118
- const d = join(dest, entry.name);
119
- if (entry.isDirectory()) {
120
- copyDir(s, d);
121
- } else {
122
- copyFileSync(s, d);
123
- }
124
- }
125
- }
126
-
127
- function removeIfExists(p) {
128
- if (existsSync(p)) {
129
- try {
130
- rmSync(p, { recursive: true, force: true });
131
- return true;
132
- } catch (e) {
133
- console.log(` \x1b[33m[WARN]\x1b[0m Could not remove ${p}: ${e.message}`);
134
- return false;
135
- }
136
- }
137
- return false;
138
- }
139
-
140
- function updateOpenCodeConfig(pluginDir) {
141
- const configPath = opencodeConfigFile();
142
- const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
143
- let config = {};
144
- if (existsSync(configPath)) {
145
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
146
- console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath} (jsonc comments?). Skipping MCP config write; ensure "mcp.huaweicloud-devkit" points to ${mcpPath}.`);
147
- return;
148
- }
149
- const existing = config.mcp?.['huaweicloud-devkit'];
150
- if (existing && existing.type === 'local'
151
- && Array.isArray(existing.command) && existing.command[0] === 'node'
152
- && existing.command[1] === mcpPath) {
153
- console.log(` OpenCode MCP config unchanged: ${configPath}`);
154
- return;
155
- }
156
- }
157
- config.mcp = config.mcp || {};
158
- config.mcp['huaweicloud-devkit'] = {
159
- type: 'local',
160
- command: ['node', mcpPath],
161
- enabled: true,
162
- };
163
- writeFileSync(configPath, JSON.stringify(config, null, 2));
164
- console.log(` OpenCode MCP config updated: ${configPath}`);
165
- }
166
-
167
- function removeOpenCodeConfig() {
168
- const configPath = opencodeConfigFile();
169
- if (!existsSync(configPath)) return;
170
- let config = {};
171
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return; }
172
- if (!config.mcp?.['huaweicloud-devkit']) return;
173
- delete config.mcp['huaweicloud-devkit'];
174
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
175
- writeFileSync(configPath, JSON.stringify(config, null, 2));
176
- console.log(` OpenCode MCP config cleaned: ${configPath}`);
177
- }
178
-
179
- function hasCodexCLI() {
180
- const r = spawnSync('codex --version', [], { shell: true, windowsHide: true, stdio: 'pipe' });
181
- if (r.status === 0 && r.stdout && r.stdout.toString().includes('codex')) return true;
182
- // WindowsApps codex.exe may fail with "Access is denied"
183
- // Fallback: check if codex exists on PATH via where.exe
184
- if (process.platform === 'win32') {
185
- const w = spawnSync('where.exe', ['codex'], { windowsHide: true, stdio: 'pipe' });
186
- if (w.status === 0 && w.stdout.toString().trim()) return true;
187
- }
188
- return false;
189
- }
190
-
191
- function checkHcloud() {
192
- const bin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
193
- if (!existsSync(bin)) return false;
194
- try {
195
- if (statSync(bin).size < 1024) return false;
196
- } catch { return false; }
197
- try {
198
- const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
199
- const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
200
- return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
201
- } catch {
202
- return false;
203
- }
204
- }
205
-
206
- function getMarketplaceName() {
207
- const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
208
- try {
209
- const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
210
- if (manifest.name) return manifest.name;
211
- } catch {}
212
- return 'huaweicloud-devkit';
213
- }
214
-
215
- function installCodex() {
216
- const marketplaceRoot = PACKAGE_ROOT;
217
- const pluginName = 'huaweicloud-core';
218
- const marketplaceName = getMarketplaceName();
219
-
220
- console.log(` Registering Codex marketplace: ${marketplaceRoot}`);
221
- const r1 = spawnSync(`codex plugin marketplace add "${marketplaceRoot}"`, [], {
222
- shell: true, windowsHide: true, stdio: 'pipe',
223
- });
224
- console.log(` ${r1.stdout ? r1.stdout.toString().trim() : r1.stderr.toString().trim()}`);
225
-
226
- if (r1.status !== 0 && /Access is denied/i.test((r1.stderr || '').toString())) {
227
- console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
228
- console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
229
- return false;
230
- }
231
-
232
- console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
233
- const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
234
- shell: true, windowsHide: true, stdio: 'pipe',
235
- });
236
- console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
237
-
238
- if (r2.status !== 0 && /Access is denied/i.test((r2.stderr || '').toString())) {
239
- console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
240
- console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
241
- return false;
242
- }
243
-
244
- return true;
245
- }
246
-
247
- function uninstallCodex() {
248
- const pluginName = 'huaweicloud-core';
249
- const marketplaceName = getMarketplaceName();
250
- console.log(` Removing Codex plugin: ${pluginName}@${marketplaceName}`);
251
- const r = spawnSync(`codex plugin remove "${pluginName}@${marketplaceName}"`, [], {
252
- shell: true, windowsHide: true, stdio: 'pipe',
253
- });
254
- console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
255
-
256
- for (const name of new Set([marketplaceName, 'HuaweiCloud-Devkit'])) {
257
- console.log(` Removing Codex marketplace: ${name}`);
258
- const r2 = spawnSync(`codex plugin marketplace remove "${name}"`, [], {
259
- shell: true, windowsHide: true, stdio: 'pipe',
260
- });
261
- console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
262
- }
263
- }
264
-
265
- function codexStatus() {
266
- const r = spawnSync('codex plugin list', [], { shell: true, windowsHide: true, stdio: 'pipe' });
267
- const out = r.stdout ? r.stdout.toString() : '';
268
- return out.includes('huaweicloud-core');
269
- }
270
-
271
- async function installOpenCode() {
272
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
273
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
274
- const srcDir = join(PLUGIN_ROOT, 'src');
275
- const safetyDir = join(PLUGIN_ROOT, 'safety');
276
- const pluginDest = opencodePluginsDir();
277
-
278
- copyDir(skillsSrc, opencodeSkillsDir());
279
- console.log(` Skills -> ${opencodeSkillsDir()}`);
280
- copyDir(commandsSrc, opencodeCommandsDir());
281
- console.log(` Commands -> ${opencodeCommandsDir()}`);
282
- copyDir(srcDir, join(pluginDest, 'src'));
283
- console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
284
- copyDir(safetyDir, join(pluginDest, 'safety'));
285
- console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
286
- updateOpenCodeConfig(pluginDest);
287
- }
288
-
289
- function uninstallOpenCode() {
290
- let removed = 0;
291
-
292
- const skills = opencodeSkillsDir();
293
- if (existsSync(skills)) {
294
- for (const entry of readdirSync(skills, { withFileTypes: true })) {
295
- if (entry.name.startsWith('huawei')) {
296
- removeIfExists(join(skills, entry.name));
297
- removed++;
298
- }
299
- }
300
- console.log(` Removed ${removed} skills`);
301
- }
302
-
303
- const commands = opencodeCommandsDir();
304
- let cmdRemoved = 0;
305
- if (existsSync(commands)) {
306
- for (const entry of readdirSync(commands, { withFileTypes: true })) {
307
- if (entry.name.startsWith('huawei')) {
308
- removeIfExists(join(commands, entry.name));
309
- cmdRemoved++;
310
- }
311
- }
312
- if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
313
- }
314
-
315
- if (removeIfExists(opencodePluginsDir())) {
316
- console.log(' Removed MCP server and safety policy');
317
- }
318
- removeOpenCodeConfig();
319
- }
320
-
321
- // Remove huawei* entries in targetDir that no longer exist in sourceDir (stale files from an older version).
322
- function pruneStale(targetDir, sourceDir) {
323
- if (!existsSync(targetDir) || !existsSync(sourceDir)) return 0;
324
- const sourceNames = new Set(readdirSync(sourceDir));
325
- let removed = 0;
326
- for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
327
- if (!entry.name.startsWith('huawei')) continue;
328
- if (!sourceNames.has(entry.name)) {
329
- removeIfExists(join(targetDir, entry.name));
330
- removed++;
331
- }
332
- }
333
- return removed;
334
- }
335
-
336
- // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
337
- async function updateOpenCode() {
338
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
339
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
340
- const srcDir = join(PLUGIN_ROOT, 'src');
341
- const safetyDir = join(PLUGIN_ROOT, 'safety');
342
- const pluginDest = opencodePluginsDir();
343
-
344
- copyDir(skillsSrc, opencodeSkillsDir());
345
- const staleSkills = pruneStale(opencodeSkillsDir(), skillsSrc);
346
- console.log(` Skills updated -> ${opencodeSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
347
- copyDir(commandsSrc, opencodeCommandsDir());
348
- const staleCommands = pruneStale(opencodeCommandsDir(), commandsSrc);
349
- console.log(` Commands updated -> ${opencodeCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
350
- copyDir(srcDir, join(pluginDest, 'src'));
351
- console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
352
- copyDir(safetyDir, join(pluginDest, 'safety'));
353
- console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
354
- updateOpenCodeConfig(pluginDest);
355
- mkdirSync(pluginDest, { recursive: true });
356
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
357
- }
358
-
359
- function codexMcpServerPath() {
360
- return join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
361
- }
362
-
363
- function codexConfigSectionText(mcpPath) {
364
- return [
365
- '[mcp_servers.huaweicloud-devkit]',
366
- 'command = "node"',
367
- `args = ["${mcpPath}"]`,
368
- '',
369
- '[mcp_servers.huaweicloud-devkit.env]',
370
- 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
371
- '',
372
- ].join('\n');
373
- }
374
-
375
- // Returns true when the config file was written, false when it was already correct.
376
- function ensureCodexConfigSection(mcpPath) {
377
- const configPath = codexConfigToml();
378
- let existing = '';
379
- if (existsSync(configPath)) {
380
- try { existing = readFileSync(configPath, 'utf8'); } catch {}
381
- if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
382
- if (existing.includes(`args = ["${mcpPath}"]`)) {
383
- console.log(` Config unchanged: ${configPath}`);
384
- return false;
385
- }
386
- removeCodexConfigSection();
387
- existing = '';
388
- if (existsSync(configPath)) {
389
- try { existing = readFileSync(configPath, 'utf8'); } catch {}
390
- }
391
- }
392
- }
393
- mkdirSync(dirname(configPath), { recursive: true });
394
- if (existing && !existing.endsWith('\n')) existing += '\n';
395
- writeFileSync(configPath, existing + codexConfigSectionText(mcpPath));
396
- console.log(` Config updated: ${configPath}`);
397
- return true;
398
- }
399
-
400
- function removeCodexConfigSection() {
401
- const configPath = codexConfigToml();
402
- if (!existsSync(configPath)) return;
403
- const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
404
- const out = [];
405
- let skip = false;
406
- for (const line of lines) {
407
- if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
408
- if (skip && line.startsWith('[')) skip = false;
409
- if (!skip) out.push(line);
410
- }
411
- while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
412
- writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
413
- console.log(' Config cleaned');
414
- }
415
-
416
- async function installCodexDesktop() {
417
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
418
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
419
- const srcDir = join(PLUGIN_ROOT, 'src');
420
- const safetyDir = join(PLUGIN_ROOT, 'safety');
421
-
422
- copyDir(skillsSrc, codexDesktopSkillsDir());
423
- console.log(` Skills -> ${codexDesktopSkillsDir()}`);
424
- copyDir(commandsSrc, codexDesktopCommandsDir());
425
- console.log(` Commands -> ${codexDesktopCommandsDir()}`);
426
- mkdirSync(codexDesktopPluginsDir(), { recursive: true });
427
- copyDir(srcDir, join(codexDesktopPluginsDir(), 'src'));
428
- console.log(` MCP Server -> ${join(codexDesktopPluginsDir(), 'src')}`);
429
- copyDir(safetyDir, join(codexDesktopPluginsDir(), 'safety'));
430
- console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
431
-
432
- // Generate .mcp.json with absolute paths for Codex Desktop MCP server discovery
433
- const mcpServerAbsPath = codexMcpServerPath();
434
- const mcpConfig = {
435
- mcpServers: {
436
- 'huaweicloud-devkit': {
437
- command: 'node',
438
- args: [mcpServerAbsPath],
439
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
440
- },
441
- },
442
- };
443
- writeFileSync(join(codexDesktopPluginsDir(), '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
444
- console.log(` MCP Config -> ${join(codexDesktopPluginsDir(), '.mcp.json')}`);
445
-
446
- // Copy .codex-plugin manifest for Codex Desktop plugin registration
447
- const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
448
- if (existsSync(codexPluginSrc)) {
449
- copyDir(codexPluginSrc, join(codexDesktopPluginsDir(), '.codex-plugin'));
450
- console.log(` Plugin Manifest -> ${join(codexDesktopPluginsDir(), '.codex-plugin')}`);
451
- }
452
-
453
- ensureCodexConfigSection(mcpServerAbsPath);
454
- }
455
-
456
- // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
457
- async function updateCodexDesktop() {
458
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
459
- const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
460
- const srcDir = join(PLUGIN_ROOT, 'src');
461
- const safetyDir = join(PLUGIN_ROOT, 'safety');
462
- const pluginDest = codexDesktopPluginsDir();
463
-
464
- copyDir(skillsSrc, codexDesktopSkillsDir());
465
- const staleSkills = pruneStale(codexDesktopSkillsDir(), skillsSrc);
466
- console.log(` Skills updated -> ${codexDesktopSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
467
- copyDir(commandsSrc, codexDesktopCommandsDir());
468
- const staleCommands = pruneStale(codexDesktopCommandsDir(), commandsSrc);
469
- console.log(` Commands updated -> ${codexDesktopCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
470
- mkdirSync(pluginDest, { recursive: true });
471
- copyDir(srcDir, join(pluginDest, 'src'));
472
- console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
473
- copyDir(safetyDir, join(pluginDest, 'safety'));
474
- console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
475
-
476
- const mcpServerAbsPath = codexMcpServerPath();
477
- const mcpConfig = {
478
- mcpServers: {
479
- 'huaweicloud-devkit': {
480
- command: 'node',
481
- args: [mcpServerAbsPath],
482
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
483
- },
484
- },
485
- };
486
- writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
487
- console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
488
-
489
- const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
490
- if (existsSync(codexPluginSrc)) {
491
- copyDir(codexPluginSrc, join(pluginDest, '.codex-plugin'));
492
- console.log(` Plugin Manifest updated -> ${join(pluginDest, '.codex-plugin')}`);
493
- }
494
-
495
- ensureCodexConfigSection(mcpServerAbsPath);
496
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
497
- }
498
-
499
- function uninstallCodexDesktop() {
500
- const skillsDir = codexDesktopSkillsDir();
501
- let removed = 0;
502
- if (existsSync(skillsDir)) {
503
- for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
504
- if (entry.name.startsWith('huawei')) {
505
- removeIfExists(join(skillsDir, entry.name));
506
- removed++;
507
- }
508
- }
509
- console.log(` Removed ${removed} skills`);
510
- }
511
-
512
- const cmdDir = codexDesktopCommandsDir();
513
- let cmdRemoved = 0;
514
- if (existsSync(cmdDir)) {
515
- for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
516
- if (entry.name.startsWith('huawei')) {
517
- removeIfExists(join(cmdDir, entry.name));
518
- cmdRemoved++;
519
- }
520
- }
521
- if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
522
- }
523
-
524
- if (removeIfExists(codexDesktopPluginsDir())) {
525
- console.log(' Removed MCP server and safety policy');
526
- }
527
- removeCodexConfigSection();
528
- }
529
-
530
- function registerCodeartsMcp(configPath) {
531
- const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
532
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
533
- const hcloudBin = findHcloudBin();
534
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
535
- let config = {};
536
- if (existsSync(configPath)) {
537
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
538
- console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
539
- return;
540
- }
541
- const existing = config.mcpServers?.['huaweicloud-devkit'];
542
- if (existing && existing.command === 'node'
543
- && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
544
- console.log(` MCP config unchanged: ${configPath}`);
545
- return;
546
- }
547
- }
548
- config.mcpServers = config.mcpServers || {};
549
- config.mcpServers['huaweicloud-devkit'] = {
550
- command: 'node',
551
- args: [mcpPath],
552
- env,
553
- enabled: true,
554
- };
555
- mkdirSync(dirname(configPath), { recursive: true });
556
- writeFileSync(configPath, JSON.stringify(config, null, 2));
557
- console.log(` MCP config updated: ${configPath}`);
558
- }
559
-
560
- async function installCodeArts() {
561
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
562
- const srcDir = join(PLUGIN_ROOT, 'src');
563
- const safetyDir = join(PLUGIN_ROOT, 'safety');
564
-
565
- copyDir(skillsSrc, codeartsSkillsDir());
566
- console.log(` Skills -> ${codeartsSkillsDir()}`);
567
- copyDir(skillsSrc, codeartsProjectSkillsDir());
568
- console.log(` Skills -> ${codeartsProjectSkillsDir()}`);
569
-
570
- const pluginDest = codeartsPluginsDir();
571
- copyDir(srcDir, join(pluginDest, 'src'));
572
- console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
573
- copyDir(safetyDir, join(pluginDest, 'safety'));
574
- console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
575
-
576
- registerCodeartsMcp(codeartsMcpSettingsFile());
577
- registerCodeartsMcp(codeartsProjectMcpSettingsFile());
578
- }
579
-
580
- // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
581
- async function updateCodeArts() {
582
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
583
- const srcDir = join(PLUGIN_ROOT, 'src');
584
- const safetyDir = join(PLUGIN_ROOT, 'safety');
585
- const pluginDest = codeartsPluginsDir();
586
-
587
- for (const dir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
588
- copyDir(skillsSrc, dir);
589
- const stale = pruneStale(dir, skillsSrc);
590
- console.log(` Skills updated -> ${dir}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
591
- }
592
- copyDir(srcDir, join(pluginDest, 'src'));
593
- console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
594
- copyDir(safetyDir, join(pluginDest, 'safety'));
595
- console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
596
- registerCodeartsMcp(codeartsMcpSettingsFile());
597
- registerCodeartsMcp(codeartsProjectMcpSettingsFile());
598
- mkdirSync(pluginDest, { recursive: true });
599
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
600
- }
601
-
602
- function uninstallCodeArts() {
603
- let removed = 0;
604
- for (const skillsDir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
605
- if (!existsSync(skillsDir)) continue;
606
- for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
607
- if (entry.name.startsWith('huawei')) {
608
- removeIfExists(join(skillsDir, entry.name));
609
- removed++;
610
- }
611
- }
612
- }
613
- if (removed > 0) console.log(` Removed ${removed} skills`);
614
-
615
- if (removeIfExists(codeartsPluginsDir())) {
616
- console.log(' Removed MCP server and safety policy');
617
- }
618
- for (const configPath of [codeartsMcpSettingsFile(), codeartsProjectMcpSettingsFile()]) {
619
- if (!existsSync(configPath)) continue;
620
- let config = {};
621
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
622
- if (config.mcpServers?.['huaweicloud-devkit']) {
623
- delete config.mcpServers['huaweicloud-devkit'];
624
- if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
625
- writeFileSync(configPath, JSON.stringify(config, null, 2));
626
- console.log(` Config cleaned: ${configPath}`);
627
- }
628
- }
629
- }
630
-
631
- function codeartsStatus() {
632
- const pluginDir = codeartsPluginsDir();
633
- console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
634
- console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
635
- let skillCount = 0;
636
- if (existsSync(codeartsSkillsDir())) {
637
- skillCount = readdirSync(codeartsSkillsDir(), { withFileTypes: true })
638
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
639
- }
640
- console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
641
- if (existsSync(codeartsMcpSettingsFile())) {
642
- try {
643
- const config = JSON.parse(readFileSync(codeartsMcpSettingsFile(), 'utf8'));
644
- console.log(` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
645
- } catch {
646
- console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
647
- }
648
- }
649
- }
650
-
651
- // Returns true when the config file was written, false when it was already correct.
652
- function ensureWorkbuddyMcpConfig() {
653
- const configPath = workbuddyMcpConfigFile();
654
- const mcpPath = join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
655
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
656
- const hcloudBin = findHcloudBin();
657
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
658
- let config = {};
659
- if (existsSync(configPath)) {
660
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
661
- console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
662
- return false;
663
- }
664
- const existing = config.mcpServers?.['huaweicloud-devkit'];
665
- if (existing && existing.command === 'node'
666
- && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
667
- console.log(` MCP config unchanged: ${configPath}`);
668
- return false;
669
- }
670
- }
671
- config.mcpServers = config.mcpServers || {};
672
- config.mcpServers['huaweicloud-devkit'] = {
673
- command: 'node',
674
- args: [mcpPath],
675
- env,
676
- };
677
- mkdirSync(dirname(configPath), { recursive: true });
678
- writeFileSync(configPath, JSON.stringify(config, null, 2));
679
- console.log(` MCP config updated: ${configPath}`);
680
- return true;
681
- }
682
-
683
- async function installWorkBuddy() {
684
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
685
- const srcDir = join(PLUGIN_ROOT, 'src');
686
- const safetyDir = join(PLUGIN_ROOT, 'safety');
687
- const pluginDest = workbuddyPluginsDir();
688
-
689
- copyDir(skillsSrc, workbuddySkillsDir());
690
- console.log(` Skills -> ${workbuddySkillsDir()}`);
691
-
692
- copyDir(srcDir, join(pluginDest, 'src'));
693
- console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
694
- copyDir(safetyDir, join(pluginDest, 'safety'));
695
- console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
696
-
697
- ensureWorkbuddyMcpConfig();
698
- }
699
-
700
- // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
701
- async function updateWorkBuddy() {
702
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
703
- const srcDir = join(PLUGIN_ROOT, 'src');
704
- const safetyDir = join(PLUGIN_ROOT, 'safety');
705
- const pluginDest = workbuddyPluginsDir();
706
-
707
- copyDir(skillsSrc, workbuddySkillsDir());
708
- const stale = pruneStale(workbuddySkillsDir(), skillsSrc);
709
- console.log(` Skills updated -> ${workbuddySkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
710
- copyDir(srcDir, join(pluginDest, 'src'));
711
- console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
712
- copyDir(safetyDir, join(pluginDest, 'safety'));
713
- console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
714
- ensureWorkbuddyMcpConfig();
715
- mkdirSync(pluginDest, { recursive: true });
716
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
717
- }
718
-
719
- function uninstallWorkBuddy() {
720
- const skillsDir = workbuddySkillsDir();
721
- let removed = 0;
722
- if (existsSync(skillsDir)) {
723
- for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
724
- if (entry.name.startsWith('huawei')) {
725
- removeIfExists(join(skillsDir, entry.name));
726
- removed++;
727
- }
728
- }
729
- if (removed > 0) console.log(` Removed ${removed} skills`);
730
- }
731
-
732
- if (removeIfExists(workbuddyPluginsDir())) {
733
- console.log(' Removed MCP server and safety policy');
734
- }
735
-
736
- const configPath = workbuddyMcpConfigFile();
737
- if (existsSync(configPath)) {
738
- let config = {};
739
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
740
- if (config.mcpServers?.['huaweicloud-devkit']) {
741
- delete config.mcpServers['huaweicloud-devkit'];
742
- if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
743
- writeFileSync(configPath, JSON.stringify(config, null, 2));
744
- console.log(` MCP config cleaned: ${configPath}`);
745
- }
746
- }
747
- }
748
-
749
- function workbuddyStatus() {
750
- const pluginDir = workbuddyPluginsDir();
751
- const skillsDir = workbuddySkillsDir();
752
- console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
753
- console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
754
- let skillCount = 0;
755
- if (existsSync(skillsDir)) {
756
- skillCount = readdirSync(skillsDir, { withFileTypes: true })
757
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
758
- }
759
- console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
760
- const configPath = workbuddyMcpConfigFile();
761
- if (existsSync(configPath)) {
762
- try {
763
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
764
- console.log(` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
765
- } catch {
766
- console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
767
- }
768
- }
769
- }
770
-
771
- function dshMcpServerPath() {
772
- return join(dshPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
773
- }
774
-
775
- function dshPatchBlock() {
776
- const hcloudBin = findHcloudBin();
777
- const envLines = [
778
- ' HUAWEICLOUD_AGENT_TOOLKIT_MODE: local',
779
- " HDKITSERVICE_ENDPOINT: ''",
780
- ];
781
- if (hcloudBin) {
782
- envLines.push(` HCLOUD_BIN: '${hcloudBin.replace(/\\/g, '/').replace(/'/g, "''")}'`);
783
- }
784
- return [
785
- DSH_MCP_PATCH_START,
786
- '- insert:',
787
- ' - id: mcp-huaweicloud',
788
- " name: '@deepseek-ai/dsh-mcp-client'",
789
- ' config:',
790
- ' serverName: huaweicloud',
791
- ' transport: stdio',
792
- ' command: node',
793
- ' args:',
794
- ` - '${dshMcpServerPath().replace(/'/g, "''")}'`,
795
- ' env:',
796
- ...envLines,
797
- ' failOnStartupError: false',
798
- DSH_MCP_PATCH_END,
799
- ].join('\n');
800
- }
801
-
802
- function escapeRegExp(value) {
803
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
804
- }
805
-
806
- function removeManagedDshPatchBlock(content) {
807
- const pattern = new RegExp(`\\n?${escapeRegExp(DSH_MCP_PATCH_START)}[\\s\\S]*?${escapeRegExp(DSH_MCP_PATCH_END)}\\s*`, 'g');
808
- return String(content || '').replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
809
- }
810
-
811
- function dshPatchHasOnlyCommentsOrEmptyList(content) {
812
- const meaningful = String(content || '')
813
- .split(/\r?\n/)
814
- .map((line) => line.trim())
815
- .filter((line) => line && !line.startsWith('#'));
816
- return meaningful.length === 0 || (meaningful.length === 1 && meaningful[0] === '[]');
817
- }
818
-
819
- function ensureDshMcpPatch() {
820
- const patchFile = dshPatchFile();
821
- const existing = existsSync(patchFile) ? readFileSync(patchFile, 'utf8') : '';
822
- const cleaned = removeManagedDshPatchBlock(existing);
823
- const block = dshPatchBlock();
824
- let next;
825
- if (dshPatchHasOnlyCommentsOrEmptyList(cleaned)) {
826
- const prefix = cleaned
827
- .split(/\r?\n/)
828
- .filter((line) => line.trim() !== '[]')
829
- .join('\n')
830
- .trimEnd();
831
- next = `${prefix ? `${prefix}\n` : ''}${block}\n`;
832
- } else {
833
- next = `${cleaned}\n\n${block}\n`;
834
- }
835
- if (existing.replace(/\r\n/g, '\n') === next) {
836
- console.log(` DSH patch unchanged: ${patchFile}`);
837
- return false;
838
- }
839
- mkdirSync(dirname(patchFile), { recursive: true });
840
- writeFileSync(patchFile, next);
841
- console.log(` DSH patch updated: ${patchFile}`);
842
- return true;
843
- }
844
-
845
- function removeDshMcpPatch() {
846
- const patchFile = dshPatchFile();
847
- if (!existsSync(patchFile)) return false;
848
- const existing = readFileSync(patchFile, 'utf8');
849
- const cleaned = removeManagedDshPatchBlock(existing);
850
- if (cleaned === existing.trimEnd()) return false;
851
- const prefix = cleaned
852
- .split(/\r?\n/)
853
- .filter((line) => line.trim() !== '[]')
854
- .join('\n')
855
- .trimEnd();
856
- const next = dshPatchHasOnlyCommentsOrEmptyList(cleaned)
857
- ? `${prefix ? `${prefix}\n` : ''}[]\n`
858
- : `${cleaned}\n`;
859
- writeFileSync(patchFile, next);
860
- console.log(` DSH patch cleaned: ${patchFile}`);
861
- return true;
862
- }
863
-
864
- function dshPatchConfigured() {
865
- const patchFile = dshPatchFile();
866
- if (!existsSync(patchFile)) return false;
867
- try {
868
- const patch = readFileSync(patchFile, 'utf8');
869
- return patch.includes('id: mcp-huaweicloud')
870
- && patch.includes("@deepseek-ai/dsh-mcp-client")
871
- && patch.includes('serverName: huaweicloud');
872
- } catch {
873
- return false;
874
- }
875
- }
876
-
877
- function commandAvailable(command, args = ['--version']) {
878
- try {
879
- const r = spawnSync(command, args, { shell: false, windowsHide: true, stdio: 'pipe', timeout: 10000 });
880
- if (r.status === 0) return true;
881
- } catch {}
882
- if (process.platform === 'win32') {
883
- try {
884
- const w = spawnSync('where.exe', [command], { windowsHide: true, stdio: 'pipe', timeout: 10000 });
885
- return w.status === 0 && w.stdout.toString().trim().length > 0;
886
- } catch {}
887
- }
888
- return false;
889
- }
890
-
891
- function dshMcpClientAvailable() {
892
- const modulePath = join('node_modules', '@deepseek-ai', 'dsh-mcp-client', 'package.json');
893
- const candidates = [
894
- join(dshProfileDir(), modulePath),
895
- join(dshRoot(), 'profiles', modulePath),
896
- join(dshRoot(), modulePath),
897
- ];
898
- if (candidates.some((p) => existsSync(p))) return true;
899
- const pkgPath = join(dshProfileDir(), 'package.json');
900
- if (!existsSync(pkgPath)) return false;
901
- try {
902
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
903
- return Boolean(pkg.dependencies?.['@deepseek-ai/dsh-mcp-client']
904
- || pkg.devDependencies?.['@deepseek-ai/dsh-mcp-client']);
905
- } catch {
906
- return false;
907
- }
908
- }
909
-
910
- function tryInstallDshMcpClient() {
911
- if (process.env.HUAWEICLOUD_DEVKIT_SKIP_DSH_PLUGIN_INSTALL === '1') {
912
- console.log(' DSH MCP client install skipped by environment');
913
- return false;
914
- }
915
- if (dshMcpClientAvailable()) {
916
- console.log(' DSH MCP client package detected');
917
- return true;
918
- }
919
- if (commandAvailable('dsh')) {
920
- const r = spawnSync('dsh', ['plugin', '--profile', 'web', 'add', '@deepseek-ai/dsh-mcp-client'], {
921
- env: { ...process.env, DSH_HOME: dshRoot() },
922
- windowsHide: true,
923
- stdio: 'pipe',
924
- timeout: 60000,
925
- });
926
- if (r.status === 0) {
927
- console.log(' DSH MCP client package installed via dsh');
928
- return true;
929
- }
930
- const err = `${r.stderr || ''}${r.stdout || ''}`.trim().split(/\r?\n/).slice(-2).join(' ');
931
- console.log(` \x1b[33m[WARN]\x1b[0m DSH MCP client auto-install failed${err ? `: ${err}` : ''}`);
932
- }
933
- if (commandAvailable('pnpm') && existsSync(join(dshProfileDir(), 'package.json'))) {
934
- const r = spawnSync('pnpm', ['--dir', dshProfileDir(), 'add', '@deepseek-ai/dsh-mcp-client'], {
935
- windowsHide: true,
936
- stdio: 'pipe',
937
- timeout: 60000,
938
- });
939
- if (r.status === 0) {
940
- console.log(' DSH MCP client package installed via pnpm');
941
- return true;
942
- }
943
- }
944
- console.log(' \x1b[33m[WARN]\x1b[0m DSH MCP client package not detected.');
945
- console.log(' Manual: npx @deepseek-ai/dsh plugin --profile web add @deepseek-ai/dsh-mcp-client');
946
- console.log(' If pnpm is missing, run: corepack enable pnpm');
947
- return false;
948
- }
949
-
950
- async function installDsh() {
951
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
952
- const srcDir = join(PLUGIN_ROOT, 'src');
953
- const safetyDir = join(PLUGIN_ROOT, 'safety');
954
- const pluginDest = dshPluginsDir();
955
-
956
- copyDir(skillsSrc, dshSkillsDir());
957
- console.log(` Skills -> ${dshSkillsDir()}`);
958
- copyDir(srcDir, join(pluginDest, 'src'));
959
- console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
960
- copyDir(safetyDir, join(pluginDest, 'safety'));
961
- console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
962
- ensureDshMcpPatch();
963
- tryInstallDshMcpClient();
964
- mkdirSync(pluginDest, { recursive: true });
965
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
966
- }
967
-
968
- async function updateDsh() {
969
- const skillsSrc = join(PLUGIN_ROOT, 'skills');
970
- const srcDir = join(PLUGIN_ROOT, 'src');
971
- const safetyDir = join(PLUGIN_ROOT, 'safety');
972
- const pluginDest = dshPluginsDir();
973
-
974
- copyDir(skillsSrc, dshSkillsDir());
975
- const stale = pruneStale(dshSkillsDir(), skillsSrc);
976
- console.log(` Skills updated -> ${dshSkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
977
- copyDir(srcDir, join(pluginDest, 'src'));
978
- console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
979
- copyDir(safetyDir, join(pluginDest, 'safety'));
980
- console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
981
- ensureDshMcpPatch();
982
- tryInstallDshMcpClient();
983
- mkdirSync(pluginDest, { recursive: true });
984
- writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
985
- }
986
-
987
- function uninstallDsh() {
988
- const skillsDir = dshSkillsDir();
989
- let removed = 0;
990
- if (existsSync(skillsDir)) {
991
- for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
992
- if (entry.name.startsWith('huawei')) {
993
- removeIfExists(join(skillsDir, entry.name));
994
- removed++;
995
- }
996
- }
997
- if (removed > 0) console.log(` Removed ${removed} skills`);
998
- try {
999
- if (readdirSync(skillsDir).length === 0) {
1000
- rmSync(skillsDir, { recursive: true, force: true });
1001
- console.log(` Removed empty skills directory: ${skillsDir}`);
1002
- }
1003
- } catch {}
1004
- }
1005
- if (removeIfExists(dshPluginsDir())) {
1006
- console.log(' Removed MCP server and safety policy');
1007
- }
1008
- removeDshMcpPatch();
1009
- }
1010
-
1011
- function dshStatus() {
1012
- const pluginDir = dshPluginsDir();
1013
- const skillsDir = dshSkillsDir();
1014
- console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
1015
- console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
1016
- let skillCount = 0;
1017
- if (existsSync(skillsDir)) {
1018
- skillCount = readdirSync(skillsDir, { withFileTypes: true })
1019
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
1020
- }
1021
- console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
1022
- console.log(` DSH patch: ${dshPatchConfigured() ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
1023
- console.log(` DSH MCP client package: ${dshMcpClientAvailable() ? '\x1b[32mDetected\x1b[0m' : '\x1b[33mCheck DSH profile\x1b[0m'}`);
1024
- }
1025
-
1026
- function opencodeStatus() {
1027
- const pluginDir = opencodePluginsDir();
1028
- const skillsDir = opencodeSkillsDir();
1029
- console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
1030
- console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
1031
- let skillCount = 0;
1032
- if (existsSync(skillsDir)) {
1033
- skillCount = readdirSync(skillsDir, { withFileTypes: true })
1034
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
1035
- }
1036
- console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
1037
- const configPath = opencodeConfigFile();
1038
- if (existsSync(configPath)) {
1039
- try {
1040
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
1041
- console.log(` MCP config: ${config.mcp?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
1042
- } catch {
1043
- console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
1044
- }
1045
- }
1046
- }
1047
-
1048
- function parseTarget() {
1049
- const idx = process.argv.indexOf('--target');
1050
- if (idx < 0) return 'opencode';
1051
- const val = (process.argv[idx + 1] || '').toLowerCase();
1052
- if (val === 'codex') return 'codex';
1053
- if (val === 'codex-desktop') return 'codex-desktop';
1054
- if (val === 'codearts') return 'codearts';
1055
- if (val === 'workbuddy') return 'workbuddy';
1056
- if (val === 'dsh') return 'dsh';
1057
- if (val === 'all') return 'all';
1058
- return 'opencode';
1059
- }
1060
-
1061
- async function cmdInstall() {
1062
- const target = parseTarget();
1063
- console.log(BANNER);
1064
- console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
1065
- checkNode();
1066
-
1067
- if (target === 'opencode' || target === 'all') {
1068
- console.log('[OpenCode]');
1069
- await installOpenCode();
1070
- }
1071
- if (target === 'codex-desktop' || target === 'all') {
1072
- console.log('\n[Codex Desktop]');
1073
- await installCodexDesktop();
1074
- }
1075
- if (target === 'codearts' || target === 'all') {
1076
- console.log('\n[CodeArts]');
1077
- await installCodeArts();
1078
- }
1079
- if (target === 'workbuddy' || target === 'all') {
1080
- console.log('\n[WorkBuddy]');
1081
- await installWorkBuddy();
1082
- }
1083
- if (target === 'dsh' || target === 'all') {
1084
- console.log('\n[DSH]');
1085
- await installDsh();
1086
- }
1087
- if (target === 'codex' || target === 'all') {
1088
- console.log('\n[Codex]');
1089
- if (!hasCodexCLI()) {
1090
- if (target === 'codex') {
1091
- console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
1092
- if (process.platform === 'win32') {
1093
- console.log(` \x1b[33mTip: Codex Desktop on Windows installs codex.exe under WindowsApps,\x1b[0m`);
1094
- console.log(` \x1b[33m which may fail with "Access is denied". Try instead:\x1b[0m`);
1095
- console.log(` \x1b[33m npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
1096
- }
1097
- console.log(` \x1b[31mOr install Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
1098
- process.exit(1);
1099
- }
1100
- console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
1101
- if (process.platform === 'win32') {
1102
- console.log(' \x1b[33mTip: try --target codex-desktop for Codex Desktop on Windows\x1b[0m');
1103
- } else {
1104
- console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
1105
- }
1106
- } else {
1107
- installCodex();
1108
- }
1109
- }
1110
- console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
1111
- const appName = target === 'codearts' ? 'CodeArts'
1112
- : target === 'codex-desktop' ? 'Codex Desktop'
1113
- : target === 'codex' ? 'Codex'
1114
- : target === 'workbuddy' ? 'WorkBuddy'
1115
- : target === 'dsh' ? 'DSH'
1116
- : 'OpenCode';
1117
- const pad = ' '.repeat(24 - appName.length);
1118
- console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
1119
- console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
1120
- console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 → 重新打开,直接描述华为云任务即可 ║`);
1121
- console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
1122
- console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
1123
-
1124
- const hcloudOk = checkHcloud();
1125
- if (!hcloudOk) {
1126
- console.log(`\n\x1b[33mKooCLI (hcloud) is not installed.`);
1127
- console.log(` Run: npx huaweicloud-devkit install-hcloud\x1b[0m`);
1128
- } else {
1129
- console.log(`\nKooCLI (hcloud) detected.`);
1130
- }
1131
-
1132
- console.log(`\n\x1b[1m下一步:\x1b[0m`);
1133
- console.log(` 1. 配置统一凭据:npx huaweicloud-devkit auth init`);
1134
- console.log(` 2. 重启 ${appName} 会话(MCP 工具重启后生效)`);
1135
- console.log(` 3. 运行自检:npx huaweicloud-devkit doctor`);
1136
-
1137
- // Write install marker for doctor to detect
1138
- const markerDir = target === 'dsh' ? dshPluginsDir()
1139
- : target === 'codearts' ? codeartsPluginsDir()
1140
- : target === 'workbuddy' ? workbuddyPluginsDir()
1141
- : target === 'codex-desktop' ? codexDesktopPluginsDir()
1142
- : opencodePluginsDir();
1143
- mkdirSync(markerDir, { recursive: true });
1144
- writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
1145
- if (target === 'opencode' || target === 'all') {
1146
- console.log('Or describe your Huawei Cloud task in OpenCode');
1147
- }
1148
- if (target === 'codearts' || target === 'all') {
1149
- console.log('Or describe your Huawei Cloud task in CodeArts');
1150
- }
1151
- if (target === 'codex' || target === 'all') {
1152
- console.log('Or mention @huaweicloud-core in Codex');
1153
- }
1154
- if (target === 'workbuddy' || target === 'all') {
1155
- console.log('Or describe your Huawei Cloud task in WorkBuddy');
1156
- }
1157
- if (target === 'dsh' || target === 'all') {
1158
- console.log('Or describe your Huawei Cloud task in DSH');
1159
- }
1160
- }
1161
-
1162
- async function cmdUninstall() {
1163
- const target = parseTarget();
1164
- console.log(BANNER);
1165
- console.log(`Uninstalling HuaweiCloud DevKit${target !== 'opencode' ? ` from ${target}` : ''}...\n`);
1166
-
1167
- if (target === 'opencode' || target === 'all') {
1168
- console.log('[OpenCode]');
1169
- await uninstallOpenCode();
1170
- }
1171
- if (target === 'codearts' || target === 'all') {
1172
- console.log('\n[CodeArts]');
1173
- uninstallCodeArts();
1174
- }
1175
- if (target === 'workbuddy' || target === 'all') {
1176
- console.log('\n[WorkBuddy]');
1177
- uninstallWorkBuddy();
1178
- }
1179
- if (target === 'dsh' || target === 'all') {
1180
- console.log('\n[DSH]');
1181
- uninstallDsh();
1182
- }
1183
- if (target === 'codex-desktop' || target === 'codex' || target === 'all') {
1184
- console.log('\n[Codex]');
1185
- uninstallCodexDesktop();
1186
- if (target === 'codex' || target === 'all') {
1187
- if (!hasCodexCLI()) {
1188
- console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
1189
- } else {
1190
- uninstallCodex();
1191
- }
1192
- }
1193
- }
1194
- if (target === 'all') {
1195
- const vaultPath = globalCredentialsPath();
1196
- if (removeIfExists(vaultPath)) {
1197
- console.log(' Removed credential vault');
1198
- }
1199
- const vaultDir = dirname(vaultPath);
1200
- try {
1201
- if (existsSync(vaultDir) && readdirSync(vaultDir).length === 0) {
1202
- rmSync(vaultDir, { recursive: true, force: true });
1203
- console.log(` Removed empty directory: ${vaultDir}`);
1204
- }
1205
- } catch {}
1206
- }
1207
- console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
1208
- }
1209
-
1210
- async function cmdStatus() {
1211
- const target = parseTarget();
1212
- console.log(BANNER);
1213
- console.log(`HuaweiCloud DevKit Status\n`);
1214
-
1215
- if (target === 'opencode' || target === 'all') {
1216
- console.log('[OpenCode]');
1217
- opencodeStatus();
1218
- }
1219
- if (target === 'codearts' || target === 'all') {
1220
- console.log('\n[CodeArts]');
1221
- codeartsStatus();
1222
- }
1223
- if (target === 'workbuddy' || target === 'all') {
1224
- console.log('\n[WorkBuddy]');
1225
- workbuddyStatus();
1226
- }
1227
- if (target === 'dsh' || target === 'all') {
1228
- console.log('\n[DSH]');
1229
- dshStatus();
1230
- }
1231
- if (target === 'codex' || target === 'all') {
1232
- console.log('\n[Codex]');
1233
- if (!hasCodexCLI()) {
1234
- console.log(' \x1b[33mCodex CLI not found.\x1b[0m');
1235
- } else {
1236
- console.log(` Plugin: ${codexStatus() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
1237
- }
1238
- }
1239
- console.log('\nEnvironment:');
1240
- console.log(` Node.js: ${process.version}`);
1241
- console.log(` Platform: ${platform()}`);
1242
- }
1243
-
1244
- async function cmdDoctor() {
1245
- console.log(BANNER);
1246
- console.log('HuaweiCloud DevKit Doctor\n');
1247
-
1248
- let pass = 0, warn = 0, fail = 0;
1249
-
1250
- function check(label, ok, msg) {
1251
- if (ok) { console.log(` \x1b[32m[PASS]\x1b[0m ${label}`); pass++; }
1252
- else { console.log(` \x1b[31m[FAIL]\x1b[0m ${label} — ${msg}`); fail++; }
1253
- }
1254
-
1255
- // Node.js
1256
- check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
1257
-
1258
- // MCP server — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH paths
1259
- const opencodePluginDir = opencodePluginsDir();
1260
- const codexPluginDir = codexDesktopPluginsDir();
1261
- const codeartsPluginDir = codeartsPluginsDir();
1262
- const workbuddyPluginDir = workbuddyPluginsDir();
1263
- const dshPluginDir = dshPluginsDir();
1264
- const mcpOk = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs'))
1265
- || existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs'))
1266
- || existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs'))
1267
- || existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs'))
1268
- || existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs'));
1269
- const mcpTarget = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ? 'OpenCode'
1270
- : existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ? 'Codex Desktop'
1271
- : existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs')) ? 'CodeArts'
1272
- : existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ? 'WorkBuddy'
1273
- : existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs')) ? 'DSH' : '';
1274
- check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
1275
-
1276
- if (mcpOk) {
1277
- check(`MCP server can start (${mcpTarget})`, true, '');
1278
- }
1279
-
1280
- const safetyOk = existsSync(join(opencodePluginDir, 'safety', 'policy.json'))
1281
- || existsSync(join(codexPluginDir, 'safety', 'policy.json'))
1282
- || existsSync(join(codeartsPluginDir, 'safety', 'policy.json'))
1283
- || existsSync(join(workbuddyPluginDir, 'safety', 'policy.json'))
1284
- || existsSync(join(dshPluginDir, 'safety', 'policy.json'));
1285
- check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
1286
-
1287
- // MCP config — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH
1288
- let mcpConfigured = false;
1289
- let mcpCfgTarget = '';
1290
- const opencodeCfg = opencodeConfigFile();
1291
- if (existsSync(opencodeCfg)) {
1292
- try {
1293
- const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
1294
- if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'OpenCode'; }
1295
- } catch {}
1296
- }
1297
- const codexCfg = codexConfigToml();
1298
- if (!mcpConfigured && existsSync(codexCfg)) {
1299
- try {
1300
- const cfg = readFileSync(codexCfg, 'utf8');
1301
- if (cfg.includes('[mcp_servers.huaweicloud-devkit]')) { mcpConfigured = true; mcpCfgTarget = 'Codex Desktop'; }
1302
- } catch {}
1303
- }
1304
- const codeartsCfg = codeartsMcpSettingsFile();
1305
- if (!mcpConfigured && existsSync(codeartsCfg)) {
1306
- try {
1307
- const cfg = JSON.parse(readFileSync(codeartsCfg, 'utf8'));
1308
- if (cfg.mcpServers && cfg.mcpServers['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'CodeArts'; }
1309
- } catch {}
1310
- }
1311
- const workbuddyCfg = workbuddyMcpConfigFile();
1312
- if (!mcpConfigured && existsSync(workbuddyCfg)) {
1313
- try {
1314
- const cfg = JSON.parse(readFileSync(workbuddyCfg, 'utf8'));
1315
- if (cfg.mcpServers && cfg.mcpServers['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'WorkBuddy'; }
1316
- } catch {}
1317
- }
1318
- if (!mcpConfigured && dshPatchConfigured()) {
1319
- mcpConfigured = true;
1320
- mcpCfgTarget = 'DSH';
1321
- }
1322
- check('MCP configured', mcpConfigured, mcpCfgTarget ? `Found in ${mcpCfgTarget} config` : 'Run: npx huaweicloud-devkit install');
1323
-
1324
- // hcloud CLI
1325
- const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1326
- const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
1327
- const hcloudOut = (hcloudCheck.stdout || '').toString() + (hcloudCheck.stderr || '').toString();
1328
- const hcloudOk = hcloudCheck.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(hcloudOut);
1329
- check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
1330
-
1331
- // CodeArts sandbox mode warning
1332
- const sandboxMode = detectCodeartsSandbox();
1333
- if (sandboxMode === 'sandbox') {
1334
- console.log(` \x1b[33m[WARN]\x1b[0m CodeArts sandbox mode active (bash_mode: sandbox)`);
1335
- console.log(` KooCLI may fail to write config in ~/.hcloud/ and hang on the privacy agreement.`);
1336
- console.log(` Fix: disable sandbox (Settings → Permissions) or use a terminal outside CodeArts.`);
1337
- warn++;
1338
- }
1339
-
1340
- if (hcloudOk) {
1341
- const ver = (hcloudCheck.stdout.toString().match(/(\d+\.\d+\.\d+)/) || [])[1] || 'unknown';
1342
- console.log(` Version: ${ver}`);
1343
-
1344
- // Check auth
1345
- const authCheck = spawnSync(`"${hcloudBin}" configure list`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
1346
- const hasAuth = authCheck.status === 0 && /access.?key/i.test(authCheck.stdout.toString());
1347
- check('hcloud credentials configured', hasAuth, 'Run: npx huaweicloud-devkit auth init');
1348
- }
1349
-
1350
- // Skills
1351
- const skillsOptions = [opencodeSkillsDir(), codexDesktopSkillsDir(), codeartsSkillsDir(), workbuddySkillsDir(), dshSkillsDir()];
1352
- let skillCount = 0, skillsDir = '', missingSkills = [];
1353
- for (const dir of skillsOptions) {
1354
- if (!existsSync(dir)) continue;
1355
- const entries = readdirSync(dir, { withFileTypes: true })
1356
- .filter((d) => d.isDirectory() && d.name.startsWith('huawei'));
1357
- const count = entries.length;
1358
- if (count > skillCount) { skillCount = count; skillsDir = dir; }
1359
- for (const d of entries) {
1360
- if (!existsSync(join(dir, d.name, 'SKILL.md'))) missingSkills.push(d.name);
1361
- }
1362
- }
1363
- const skillsOk = skillCount >= 6;
1364
- check(`Skills installed (${skillCount})`, skillsOk, 'Run: npx huaweicloud-devkit install');
1365
- if (missingSkills.length > 0) {
1366
- console.log(` \x1b[33m[WARN]\x1b[0m ${missingSkills.length} skill(s) missing SKILL.md: ${missingSkills.join(', ')} — Run: npx huaweicloud-devkit install`);
1367
- warn++;
1368
- }
1369
-
1370
- const proxyConfig = readProxyConfig();
1371
- const proxyEnv = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
1372
- if (proxyConfig || proxyEnv) {
1373
- const source = proxyEnv ? 'env' : 'file';
1374
- const proxyUrl = proxyEnv || proxyConfig.https_proxy || proxyConfig.http_proxy;
1375
- console.log(` \x1b[36m[INFO]\x1b[0m Proxy configured (${source}): ${proxyUrl}`);
1376
- }
1377
-
1378
- console.log(`\nResults: ${pass} pass, ${warn} warn, ${fail} fail`);
1379
-
1380
- if (mcpConfigured && !hcloudOk) {
1381
- console.log('\n\x1b[33mMCP is configured but hcloud is not installed. Install hcloud then restart your session.\x1b[0m');
1382
- }
1383
- if (fail > 0) {
1384
- console.log('\x1b[33mFix failures above, then restart your session.\x1b[0m');
1385
- }
1386
- if (fail === 0 && mcpConfigured) {
1387
- console.log('\n\x1b[32mAll checks passed.\x1b[0m Restart your session, then describe your Huawei Cloud task');
1388
- }
1389
-
1390
- // Detect "installed but not restarted" — check all supported agents
1391
- const installedMarkers = [
1392
- { path: join(opencodePluginsDir(), '.installed'), name: 'OpenCode' },
1393
- { path: join(codexDesktopPluginsDir(), '.installed'), name: 'Codex Desktop' },
1394
- { path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
1395
- { path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
1396
- { path: join(dshPluginsDir(), '.installed'), name: 'DSH' },
1397
- ];
1398
- for (const marker of installedMarkers) {
1399
- if (existsSync(marker.path)) {
1400
- console.log(`\n\x1b[1m\x1b[31m╔══════════════════════════════════════════╗`);
1401
- console.log(`\x1b[1m\x1b[31m║ 请重启 ${marker.name}!MCP 工具尚未激活 ║`);
1402
- console.log(`\x1b[1m\x1b[31m║ 关闭当前会话 重新打开即可 ║`);
1403
- console.log(`\x1b[1m\x1b[31m╚══════════════════════════════════════════╝\x1b[0m`);
1404
- break;
1405
- }
1406
- }
1407
- }
1408
-
1409
- async function cmdUpdate() {
1410
- console.log(BANNER);
1411
- const target = parseTarget();
1412
-
1413
- if (target === 'opencode') {
1414
- if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1415
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1416
- return;
1417
- }
1418
- console.log('[OpenCode]');
1419
- await updateOpenCode();
1420
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1421
- console.log(`\x1b[33mMCP 工具在重启 OpenCode 会话后才生效。\x1b[0m`);
1422
- return;
1423
- }
1424
-
1425
- if (target === 'codex-desktop') {
1426
- if (!existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1427
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1428
- return;
1429
- }
1430
- console.log('[Codex Desktop]');
1431
- await updateCodexDesktop();
1432
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1433
- console.log(`\x1b[33mMCP 工具在重启 Codex Desktop 会话后才生效。\x1b[0m`);
1434
- return;
1435
- }
1436
-
1437
- if (target === 'codex') {
1438
- if (!hasCodexCLI()) {
1439
- console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
1440
- if (process.platform === 'win32') {
1441
- console.log(` \x1b[33mTip: use --target codex-desktop for Codex Desktop on Windows\x1b[0m`);
1442
- }
1443
- console.log(` \x1b[31mInstall Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
1444
- process.exitCode = 1;
1445
- return;
1446
- }
1447
- console.log('[Codex]');
1448
- installCodex();
1449
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1450
- console.log(`\x1b[33mRestart the Codex session for changes to take effect.\x1b[0m`);
1451
- return;
1452
- }
1453
-
1454
- if (target === 'codearts') {
1455
- if (!existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1456
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1457
- return;
1458
- }
1459
- console.log('[CodeArts]');
1460
- await updateCodeArts();
1461
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1462
- console.log(`\x1b[33mMCP 工具在重启 CodeArts 会话后才生效。\x1b[0m`);
1463
- return;
1464
- }
1465
-
1466
- if (target === 'workbuddy') {
1467
- if (!existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
1468
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1469
- return;
1470
- }
1471
- console.log('[WorkBuddy]');
1472
- await updateWorkBuddy();
1473
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1474
- console.log(`\x1b[33mMCP 工具在重启 WorkBuddy 会话后才生效。\x1b[0m`);
1475
- return;
1476
- }
1477
-
1478
- if (target === 'dsh') {
1479
- if (!existsSync(join(dshPluginsDir(), 'src', 'mcp-server.mjs'))) {
1480
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1481
- return;
1482
- }
1483
- console.log('[DSH]');
1484
- await updateDsh();
1485
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1486
- console.log(`\x1b[33mRestart the DSH session for changes to take effect.\x1b[0m`);
1487
- return;
1488
- }
1489
-
1490
- if (target === 'all') {
1491
- let updatedAny = false;
1492
- if (existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1493
- console.log('[OpenCode]');
1494
- await updateOpenCode();
1495
- updatedAny = true;
1496
- }
1497
- if (existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1498
- console.log('\n[Codex Desktop]');
1499
- await updateCodexDesktop();
1500
- updatedAny = true;
1501
- }
1502
- if (existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1503
- console.log('\n[CodeArts]');
1504
- await updateCodeArts();
1505
- updatedAny = true;
1506
- }
1507
- if (existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
1508
- console.log('\n[WorkBuddy]');
1509
- await updateWorkBuddy();
1510
- updatedAny = true;
1511
- }
1512
- if (existsSync(join(dshPluginsDir(), 'src', 'mcp-server.mjs'))) {
1513
- console.log('\n[DSH]');
1514
- await updateDsh();
1515
- updatedAny = true;
1516
- }
1517
- if (codexStatus()) {
1518
- console.log('\n[Codex]');
1519
- installCodex();
1520
- updatedAny = true;
1521
- }
1522
- if (!updatedAny) {
1523
- console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1524
- return;
1525
- }
1526
- console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1527
- console.log(`\x1b[33mMCP 工具在重启各 agent 会话后才生效。\x1b[0m`);
1528
- return;
1529
- }
1530
-
1531
- await cmdUninstall();
1532
- console.log('');
1533
- await cmdInstall();
1534
- }
1535
-
1536
- async function cmdReinstall() {
1537
- console.log(BANNER);
1538
- if (!(await confirm('This will remove and reinstall all HuaweiCloud DevKit files. Continue?'))) {
1539
- console.log('Cancelled.');
1540
- return;
1541
- }
1542
- confirmed = true;
1543
- await cmdUninstall();
1544
- console.log('');
1545
- await cmdInstall();
1546
- }
1547
-
1548
- let confirmed = false;
1549
- async function confirm(msg) {
1550
- if (confirmed) return true;
1551
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1552
- return new Promise((ok) => {
1553
- rl.question(`${msg} [y/N] `, (a) => {
1554
- rl.close();
1555
- ok(a.toLowerCase() === 'y' || a.toLowerCase() === 'yes');
1556
- });
1557
- });
1558
- }
1559
-
1560
- async function cmdInstallHcloud() {
1561
- console.log(BANNER);
1562
- console.log('Installing KooCLI (hcloud)...\n');
1563
-
1564
- const os = platform();
1565
- const arch = process.arch;
1566
- const baseUrl = 'https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest';
1567
- const installDir = os === 'win32'
1568
- ? join(homedir(), 'hcloud')
1569
- : join(homedir(), '.local', 'bin');
1570
-
1571
- if (os === 'win32') {
1572
- const url = `${baseUrl}/huaweicloud-cli-windows-amd64.zip`;
1573
- const zipPath = join(installDir, 'hcloud.zip');
1574
-
1575
- console.log(`[Windows] Auto-installing to ${installDir}...`);
1576
-
1577
- try {
1578
- mkdirSync(installDir, { recursive: true });
1579
-
1580
- // Download
1581
- console.log(` Downloading ${url}...`);
1582
- const psCmd = `[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '${url}' -OutFile '${zipPath}' -UseBasicParsing`;
1583
- const dl = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { stdio: 'inherit', windowsHide: true, timeout: 180000 });
1584
- if (dl.status !== 0) throw new Error(`下载失败 (PowerShell exit ${dl.status})`);
1585
-
1586
- // Extract
1587
- console.log(' Extracting...');
1588
- const ex = spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -Path '${zipPath}' -DestinationPath '${installDir}' -Force`], { stdio: 'inherit', windowsHide: true, timeout: 60000 });
1589
- if (ex.status !== 0) throw new Error(`解压失败 (PowerShell exit ${ex.status})`);
1590
- if (!existsSync(join(installDir, 'hcloud.exe'))) throw new Error('hcloud.exe 未生成,可能已被安全软件拦截');
1591
-
1592
- // Clean up zip
1593
- rmSync(zipPath, { force: true });
1594
-
1595
- // Add to user PATH (append + dedupe within the User scope only; never copy
1596
- // session/system entries into the user PATH, and never use setx PATH which
1597
- // overwrites the whole variable and truncates at 1024 chars).
1598
- console.log(' Adding to user PATH...');
1599
- const pathPs = [
1600
- '$ErrorActionPreference = "Stop"',
1601
- `$target = '${installDir.replace(/'/g, "''")}'`,
1602
- '$cur = [Environment]::GetEnvironmentVariable("Path", "User")',
1603
- 'if (-not $cur) { $cur = "" }',
1604
- '$parts = @($cur -split ";" | Where-Object { $_ -ne "" })',
1605
- 'if ($parts -notcontains $target) {',
1606
- ' [Environment]::SetEnvironmentVariable("Path", (@($parts) + $target) -join ";", "User")',
1607
- ' Write-Output " Added to user PATH (deduped): $target"',
1608
- '} else {',
1609
- ' Write-Output " Already in user PATH: $target"',
1610
- '}',
1611
- ].join('; ');
1612
- spawnSync('powershell', ['-NoProfile', '-Command', pathPs], { stdio: 'inherit', windowsHide: true, timeout: 30000 });
1613
-
1614
- console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
1615
- console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
1616
-
1617
- const hcloudBin = join(installDir, 'hcloud.exe');
1618
-
1619
- // Ask user before accepting the privacy agreement — never auto-accept.
1620
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1621
- const agree = await new Promise((resolve) => {
1622
- rl.question('\n KooCLI requires accepting its privacy agreement. Do you accept? (y/N) ', (answer) => {
1623
- rl.close();
1624
- resolve(/^\s*y\s*$/i.test(answer));
1625
- });
1626
- });
1627
- if (agree) {
1628
- const r = spawnSync(hcloudBin, ['version'], { input: 'y\n', encoding: 'utf8', timeout: 10000, windowsHide: true });
1629
- if (r.status === 0) {
1630
- console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
1631
- } else {
1632
- console.log(' \x1b[33m无法写入配置目录。请在码道外终端运行: echo "y" | hcloud version\x1b[0m');
1633
- }
1634
- } else {
1635
- console.log(' \x1b[33m请手动接受隐私协议:在终端运行 hcloud version 并按提示操作\x1b[0m');
1636
- }
1637
-
1638
- console.log(' Or restart terminal and: hcloud version');
1639
- } catch (e) {
1640
- console.log(`\n\x1b[33mAuto-install failed: ${e.message}\x1b[0m`);
1641
- console.log(` Manual: download ${url}, unzip to ${installDir}, add to PATH`);
1642
- console.log(` Guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_01.html`);
1643
- if (detectCodeartsSandbox() === 'sandbox') {
1644
- printSandboxWarning('沙箱模式拦截了 KooCLI 自动安装(无法创建/写入安装目录)。');
1645
- }
1646
- }
1647
- } else if (os === 'linux') {
1648
- console.log('[Linux] One-liner install:');
1649
- console.log(' curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y');
1650
- console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64' : 'AMD64'}`);
1651
- const pkg = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
1652
- console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
1653
- console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
1654
- console.log(` mv hcloud ~/.local/bin/`);
1655
- console.log(` hcloud version`);
1656
- console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_02.html`);
1657
- } else if (os === 'darwin') {
1658
- console.log('[macOS] One-liner install:');
1659
- console.log(' curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y');
1660
- console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64 (Apple Silicon)' : 'AMD64 (Intel)'}`);
1661
- const pkg = arch === 'arm64' ? 'mac-arm64' : 'mac-amd64';
1662
- console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
1663
- console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
1664
- console.log(` mv hcloud /usr/local/bin/`);
1665
- console.log(` hcloud version`);
1666
- console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_03.html`);
1667
- }
1668
-
1669
- console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
1670
- console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
1671
- console.log(' Unified credentials (recommended): npx huaweicloud-devkit auth init');
1672
- console.log(' KooCLI only (alternative): hcloud configure init');
1673
- console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
1674
- console.log('\nThen run: npx huaweicloud-devkit doctor');
1675
- }
1676
-
1677
- function readLineQuestion(prompt) {
1678
- return new Promise((resolve) => {
1679
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1680
- rl.question(prompt, (answer) => {
1681
- rl.close();
1682
- resolve(answer.trim());
1683
- });
1684
- });
1685
- }
1686
-
1687
- async function readSecret(prompt) {
1688
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
1689
- throw new Error(
1690
- `Cannot read "${prompt.trim()}" securely in a non-interactive session. Set HW_ACCESS_KEY/HW_SECRET_KEY environment variables instead, or run "npx huaweicloud-devkit auth init" in a real terminal.`
1691
- );
1692
- }
1693
-
1694
- process.stdout.write(prompt);
1695
- const wasRaw = process.stdin.isRaw;
1696
- process.stdin.setRawMode(true);
1697
- process.stdin.resume();
1698
-
1699
- return new Promise((resolve) => {
1700
- let value = '';
1701
- const onData = (chunk) => {
1702
- for (const ch of chunk.toString('utf8')) {
1703
- if (ch === '\r' || ch === '\n') {
1704
- cleanup();
1705
- resolve(value.trim());
1706
- return;
1707
- }
1708
- if (ch === '\u0003') {
1709
- cleanup();
1710
- process.exit(130);
1711
- }
1712
- if (ch === '\b' || ch === '\u007f') {
1713
- value = value.slice(0, -1);
1714
- continue;
1715
- }
1716
- value += ch;
1717
- }
1718
- };
1719
- const cleanup = () => {
1720
- process.stdin.setRawMode(wasRaw);
1721
- process.stdin.pause();
1722
- process.stdin.off('data', onData);
1723
- process.stdout.write('\n');
1724
- };
1725
- process.stdin.on('data', onData);
1726
- });
1727
- }
1728
-
1729
- function configureHcloud(credentials) {
1730
- const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1731
- const args = [
1732
- 'configure',
1733
- 'set',
1734
- `--cli-access-key=${credentials.ak}`,
1735
- `--cli-secret-key=${credentials.sk}`,
1736
- `--cli-region=${credentials.region || ''}`,
1737
- ];
1738
- const r = spawnSync(hcloudBin, args, {
1739
- shell: false,
1740
- windowsHide: true,
1741
- stdio: 'pipe',
1742
- timeout: 30000,
1743
- });
1744
- return {
1745
- ok: r.status === 0,
1746
- code: r.status,
1747
- error: String(r.stderr || '').trim().slice(0, 240),
1748
- };
1749
- }
1750
-
1751
- function printAuthAgents(agents = {}) {
1752
- for (const [agent, info] of Object.entries(agents)) {
1753
- console.log(` ${agent}: ${info.configured ? '[OK]' : '[MISSING]'}`);
1754
- }
1755
- }
1756
-
1757
- function printAuthStatus(status) {
1758
- console.log(`Credentials vault: ${status.credentialsConfigured ? 'configured' : 'missing'} (${status.credentialsPath})`);
1759
- console.log(`OBS config: ${status.obsConfigured ? 'configured' : 'missing'} (${status.obsConfigPath})`);
1760
- console.log(`KooCLI: ${status.kooCliInstalled ? 'installed' : 'missing'}`);
1761
- console.log('Agent MCP registration:');
1762
- printAuthAgents(status.agents);
1763
- }
1764
-
1765
- async function cmdAuthInit() {
1766
- console.log(BANNER);
1767
- console.log('HuaweiCloud DevKit Unified Authentication Setup\n');
1768
- console.log('\x1b[1m获取 AK/SK(如果还没有):\x1b[0m');
1769
- console.log(' 1. 打开华为云"访问密钥"页签:');
1770
- console.log(' https://console.huaweicloud.com/iam/?region=cn-north-4#/mine/accessKey');
1771
- console.log(' 2. 点击"新增访问密钥",完成身份验证');
1772
- console.log(' 3. 下载凭证文件(内含 AK 和 SK)。');
1773
- console.log(' 注意:SK 只在创建密钥时显示一次,请妥善保存该文件。\n');
1774
-
1775
- let ak = process.env.HW_ACCESS_KEY || '';
1776
- let sk = process.env.HW_SECRET_KEY || '';
1777
- let securityToken = process.env.HW_SECURITY_TOKEN || '';
1778
- let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
1779
-
1780
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
1781
- if (!interactive && (!ak || !sk)) {
1782
- console.error('\x1b[31mNon-interactive session detected. Provide credentials via environment variables instead:\x1b[0m');
1783
- console.error(' HW_ACCESS_KEY, HW_SECRET_KEY');
1784
- console.error(' (Or run "npx huaweicloud-devkit auth init" in a real terminal.)');
1785
- process.exitCode = 1;
1786
- return;
1787
- }
1788
-
1789
- if (!ak) ak = await readLineQuestion('Access Key ID (AK): ');
1790
- if (!sk) sk = await readSecret('Secret Access Key (SK): ');
1791
- if (interactive && !securityToken) securityToken = await readLineQuestion('Security Token (optional, press Enter to skip): ');
1792
- if (!region) region = 'cn-north-4';
1793
-
1794
- if (!ak || !sk) {
1795
- console.error('\nAK and SK are required.');
1796
- process.exitCode = 1;
1797
- return;
1798
- }
1799
-
1800
-
1801
- const vaultPath = writeGlobalCredentials({ ak, sk, securityToken, region });
1802
-
1803
- try {
1804
- writeObsConfig({ ak, sk, securityToken, region });
1805
- } catch (error) {
1806
- console.log(`OBS config sync failed: ${error.message}`);
1807
- }
1808
-
1809
- if (findHcloudBin()) {
1810
- const result = configureHcloud({ ak, sk, region });
1811
- if (!result.ok) console.log(`KooCLI update failed: ${result.error || result.code}`);
1812
- } else {
1813
- console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
1814
- }
1815
-
1816
- console.log('\nCredentials synchronized.');
1817
- console.log('\nNext steps:');
1818
- console.log(' npx huaweicloud-devkit install --target all');
1819
- console.log(' Restart your agent sessions.');
1820
- }
1821
-
1822
- async function cmdAuthSync() {
1823
- const target = parseTarget();
1824
- console.log(BANNER);
1825
- console.log('Synchronizing Huawei Cloud authentication...\n');
1826
-
1827
- const credentials = readGlobalCredentials();
1828
- if (!credentials?.ak || !credentials?.sk) {
1829
- console.error('No global credentials found. Run "npx huaweicloud-devkit auth init" first.');
1830
- process.exitCode = 1;
1831
- return;
1832
- }
1833
-
1834
- const result = syncAuth(target);
1835
- if (result.ok) {
1836
- console.log('Credentials synchronized.');
1837
- } else {
1838
- console.error(result.error);
1839
- }
1840
- }
1841
-
1842
- async function cmdAuthStatus() {
1843
- const target = parseTarget();
1844
- console.log(BANNER);
1845
- console.log('HuaweiCloud DevKit Authentication Status\n');
1846
- printAuthStatus(getAuthStatus(target));
1847
- }
1848
-
1849
- async function cmdAuth() {
1850
- const sub = (process.argv[3] || 'status').toLowerCase();
1851
- if (sub === 'init' || sub === 'setup') return cmdAuthInit();
1852
- if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
1853
- return cmdAuthStatus();
1854
- }
1855
-
1856
- async function cmdProxyInit() {
1857
- console.log(BANNER);
1858
- console.log('HuaweiCloud DevKit Proxy Configuration\n');
1859
- console.log('Configure HTTP/HTTPS proxy for connections to Huawei Cloud services.');
1860
- console.log('Proxy settings are saved to ~/.config/huaweicloud/proxy.json\n');
1861
-
1862
- const existing = readProxyConfig() || {};
1863
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
1864
-
1865
- if (!interactive) {
1866
- console.error('\x1b[31mNon-interactive session. Set proxy via environment variables:\x1b[0m');
1867
- console.error(' HTTPS_PROXY=http://proxy:port');
1868
- console.error(' HTTP_PROXY=http://proxy:port');
1869
- console.error(' NO_PROXY=localhost,127.0.0.1');
1870
- console.error('\nOr run "npx huaweicloud-devkit proxy init" in a real terminal.');
1871
- process.exitCode = 1;
1872
- return;
1873
- }
1874
-
1875
- const httpsProxy = await readLineQuestion(`HTTPS proxy [${existing.https_proxy || 'none'}]: `);
1876
- const httpProxy = await readLineQuestion(`HTTP proxy [${existing.http_proxy || 'none'}]: `);
1877
- const noProxy = await readLineQuestion(`NO_PROXY hosts [${existing.no_proxy || 'localhost,127.0.0.1'}]: `);
1878
-
1879
- const config = {
1880
- https_proxy: httpsProxy || existing.https_proxy || '',
1881
- http_proxy: httpProxy || existing.http_proxy || '',
1882
- no_proxy: noProxy || existing.no_proxy || 'localhost,127.0.0.1',
1883
- };
1884
-
1885
- const path = writeProxyConfig(config);
1886
- console.log(`\nProxy configuration saved to ${path}`);
1887
- console.log('\nEffective settings:');
1888
- console.log(` HTTPS_PROXY: ${config.https_proxy || '(none)'}`);
1889
- console.log(` HTTP_PROXY: ${config.http_proxy || '(none)'}`);
1890
- console.log(` NO_PROXY: ${config.no_proxy || '(none)'}`);
1891
- console.log('\nEnvironment variables (HTTPS_PROXY, HTTP_PROXY, NO_PROXY) override file settings.');
1892
- }
1893
-
1894
- async function cmdProxyShow() {
1895
- console.log(BANNER);
1896
- console.log('HuaweiCloud DevKit Proxy Configuration\n');
1897
-
1898
- const config = readProxyConfig();
1899
- const configPath = proxyConfigPath();
1900
-
1901
- console.log(`Config file: ${configPath}`);
1902
- console.log(`File exists: ${config ? 'yes' : 'no'}\n`);
1903
-
1904
- if (config) {
1905
- console.log('File settings:');
1906
- console.log(` https_proxy: ${config.https_proxy || '(empty)'}`);
1907
- console.log(` http_proxy: ${config.http_proxy || '(empty)'}`);
1908
- console.log(` no_proxy: ${config.no_proxy || '(empty)'}`);
1909
- }
1910
-
1911
- console.log('\nEnvironment variables:');
1912
- console.log(` HTTPS_PROXY: ${process.env.HTTPS_PROXY || process.env.https_proxy || '(not set)'}`);
1913
- console.log(` HTTP_PROXY: ${process.env.HTTP_PROXY || process.env.http_proxy || '(not set)'}`);
1914
- console.log(` NO_PROXY: ${process.env.NO_PROXY || process.env.no_proxy || '(not set)'}`);
1915
-
1916
- const effective = getProxySettings();
1917
- console.log('\nEffective (env > file):');
1918
- if (effective) {
1919
- console.log(` https_proxy: ${effective.https_proxy || '(none)'}`);
1920
- console.log(` http_proxy: ${effective.http_proxy || '(none)'}`);
1921
- console.log(` no_proxy: ${effective.no_proxy || '(none)'}`);
1922
- } else {
1923
- console.log(' (no proxy configured)');
1924
- }
1925
- }
1926
-
1927
- async function cmdProxyClear() {
1928
- const removed = clearProxyConfig();
1929
- if (removed) {
1930
- console.log('Proxy configuration removed.');
1931
- } else {
1932
- console.log('No proxy configuration file found.');
1933
- }
1934
- }
1935
-
1936
- async function cmdProxy() {
1937
- const sub = (process.argv[3] || 'show').toLowerCase();
1938
- if (sub === 'init' || sub === 'setup') return cmdProxyInit();
1939
- if (sub === 'clear' || sub === 'remove' || sub === 'reset') return cmdProxyClear();
1940
- return cmdProxyShow();
1941
- }
1942
-
1943
- async function main() {
1944
- const cmd = process.argv[2] || 'help';
1945
-
1946
- switch (cmd) {
1947
- case 'install':
1948
- case 'i':
1949
- await cmdInstall();
1950
- break;
1951
- case 'uninstall':
1952
- case 'remove':
1953
- await cmdUninstall();
1954
- break;
1955
- case 'update':
1956
- case 'upgrade':
1957
- await cmdUpdate();
1958
- break;
1959
- case 'reinstall':
1960
- await cmdReinstall();
1961
- break;
1962
- case 'status':
1963
- case 'info':
1964
- await cmdStatus();
1965
- break;
1966
- case 'doctor':
1967
- case 'check':
1968
- await cmdDoctor();
1969
- break;
1970
- case 'install-hcloud':
1971
- await cmdInstallHcloud();
1972
- break;
1973
- case 'auth':
1974
- await cmdAuth();
1975
- break;
1976
- case 'proxy':
1977
- await cmdProxy();
1978
- break;
1979
- case 'help':
1980
- case '--help':
1981
- case '-h':
1982
- default:
1983
- console.log(BANNER);
1984
- console.log('Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|all>]\n');
1985
- console.log('Commands:');
1986
- console.log(' install Install skills, MCP server, safety policy');
1987
- console.log(' uninstall Remove installed files');
1988
- console.log(' update Update to latest version');
1989
- console.log(' reinstall Full clean reinstall');
1990
- console.log(' status Show installation status');
1991
- console.log(' doctor Self-check: hcloud, MCP, skills, auth');
1992
- console.log(' install-hcloud Show KooCLI install commands for your OS');
1993
- console.log(' auth Manage unified auth: init | sync | status');
1994
- console.log(' proxy Manage proxy config: init | show | clear');
1995
- console.log(' help Show this help');
1996
- console.log('\nOptions:');
1997
- console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, dsh, all');
1998
- console.log('\nExamples:');
1999
- console.log(' npx huaweicloud-devkit install');
2000
- console.log(' npx huaweicloud-devkit install --target codex');
2001
- console.log(' npx huaweicloud-devkit install --target codearts');
2002
- console.log(' npx huaweicloud-devkit install --target workbuddy');
2003
- console.log(' npx huaweicloud-devkit install --target dsh');
2004
- console.log(' npx huaweicloud-devkit install --target all');
2005
- console.log(' npx huaweicloud-devkit auth init');
2006
- console.log(' npx huaweicloud-devkit auth sync --target all');
2007
- console.log(' npx huaweicloud-devkit auth status --target all');
2008
- console.log(' npx huaweicloud-devkit proxy init');
2009
- console.log(' npx huaweicloud-devkit proxy show');
2010
- break;
2011
- }
2012
- }
2013
-
2014
- main().catch((e) => {
2015
- console.error(`\x1b[31mError: ${e.message}\x1b[0m`);
2016
- process.exit(1);
2017
- });
1
+ #!/usr/bin/env node
2
+
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { join, dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { homedir, platform } from 'node:os';
7
+ import { createInterface } from 'node:readline';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { getAuthStatus, syncAuth } from './auth/service.mjs';
10
+ import { globalCredentialsPath, readGlobalCredentials, writeGlobalCredentials, writeObsConfig } from './auth/credentials.mjs';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const PLUGIN_ROOT = resolve(__dirname, '..');
14
+ const PACKAGE_ROOT = resolve(PLUGIN_ROOT, '..', '..');
15
+
16
+ let pkgVersion = '0.0.0';
17
+ try {
18
+ pkgVersion = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
19
+ } catch {}
20
+
21
+ const BANNER = `
22
+ ╔══════════════════════════════════════════════╗
23
+ ║ HuaweiCloud DevKit v${pkgVersion}${' '.repeat(Math.max(0, 22 - String(pkgVersion).length))}║
24
+ https://github.com/huaweicloud
25
+ ╚══════════════════════════════════════════════╝
26
+ `;
27
+
28
+ function configRoot(target = 'opencode') {
29
+ const home = homedir();
30
+ return join(home, '.config', target);
31
+ }
32
+
33
+ function opencodeSkillsDir() { return join(configRoot('opencode'), 'skills'); }
34
+ function opencodeCommandsDir() { return join(configRoot('opencode'), 'commands'); }
35
+ function opencodePluginsDir() { return join(configRoot('opencode'), 'huaweicloud-plugins'); }
36
+ function opencodeConfigFile() {
37
+ const jsonc = join(configRoot('opencode'), 'opencode.jsonc');
38
+ if (existsSync(jsonc)) return jsonc;
39
+ return join(configRoot('opencode'), 'opencode.json');
40
+ }
41
+
42
+ function codexDesktopSkillsDir() { return join(homedir(), '.agents', 'skills'); }
43
+ function codexDesktopCommandsDir() { return join(homedir(), '.agents', 'commands'); }
44
+ function codexDesktopPluginsDir() { return join(homedir(), '.agents', 'huaweicloud-plugins'); }
45
+ function codexConfigToml() { return join(homedir(), '.codex', 'config.toml'); }
46
+
47
+ function codeartsSkillsDir() { return join(homedir(), '.codeartsdoer', 'skills'); }
48
+ function codeartsMcpSettingsDir() { return join(homedir(), '.codeartsdoer', 'mcp'); }
49
+ function codeartsMcpSettingsFile() { return join(codeartsMcpSettingsDir(), 'mcp_settings.json'); }
50
+ function codeartsProjectDir() { return join(process.cwd(), '.codeartsdoer'); }
51
+ function codeartsProjectSkillsDir() { return join(codeartsProjectDir(), 'skills'); }
52
+ function codeartsProjectMcpSettingsFile() { return join(codeartsProjectDir(), 'mcp', 'mcp_settings.json'); }
53
+ function codeartsPluginsDir() { return join(homedir(), '.codeartsdoer', 'huaweicloud-plugins'); }
54
+
55
+ function workbuddySkillsDir() { return join(homedir(), '.workbuddy', 'skills'); }
56
+ function workbuddyMcpConfigFile() { return join(homedir(), '.workbuddy', 'mcp.json'); }
57
+ function workbuddyPluginsDir() { return join(homedir(), '.workbuddy', 'huaweicloud-plugins'); }
58
+
59
+ // Detect CodeArts sandbox mode (bash_mode in permission config).
60
+ function detectCodeartsSandbox() {
61
+ try {
62
+ const configPath = join(homedir(), '.codeartsdoer', 'codearts-data', 'storage', 'permission', 'config.json');
63
+ if (!existsSync(configPath)) return null;
64
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
65
+ return typeof config.bash_mode === 'string' ? config.bash_mode : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ // Locate an existing hcloud executable (HCLOUD_BIN, ~/hcloud on Windows, ~/.local/bin elsewhere).
72
+ function findHcloudBin() {
73
+ if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
74
+ const isWin = platform() === 'win32';
75
+ const candidates = isWin
76
+ ? [join(homedir(), 'hcloud', 'hcloud.exe')]
77
+ : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud'), join(homedir(), 'hcloud', 'hcloud.exe')];
78
+ for (const c of candidates) {
79
+ if (existsSync(c)) return c;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function printSandboxWarning(reason) {
85
+ console.log(`\n\x1b[1m\x1b[31m⚠ 检测到码道沙箱模式 (bash_mode: sandbox)\x1b[0m`);
86
+ console.log(`\x1b[31m ${reason}\x1b[0m`);
87
+ console.log(`\x1b[31m 请任选其一继续:\x1b[0m`);
88
+ console.log(`\x1b[31m A. 在码道外的终端安装并使用 KooCLI (推荐):`);
89
+ console.log(`\x1b[31m https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`);
90
+ console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 权限 → Bash 模式)`);
91
+ console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
92
+ }
93
+
94
+ function checkNode() {
95
+ const v = process.versions.node.split('.').map(Number);
96
+ if (v[0] < 20) {
97
+ console.error(`\x1b[31mNode.js >= 20 required (current: ${process.version})\x1b[0m`);
98
+ process.exit(1);
99
+ }
100
+ console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
101
+ }
102
+
103
+ function copyDir(src, dest) {
104
+ if (!existsSync(src)) return;
105
+ mkdirSync(dest, { recursive: true });
106
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
107
+ const s = join(src, entry.name);
108
+ const d = join(dest, entry.name);
109
+ if (entry.isDirectory()) {
110
+ copyDir(s, d);
111
+ } else {
112
+ copyFileSync(s, d);
113
+ }
114
+ }
115
+ }
116
+
117
+ function removeIfExists(p) {
118
+ if (existsSync(p)) {
119
+ try {
120
+ rmSync(p, { recursive: true, force: true });
121
+ return true;
122
+ } catch (e) {
123
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not remove ${p}: ${e.message}`);
124
+ return false;
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ function updateOpenCodeConfig(pluginDir) {
131
+ const configPath = opencodeConfigFile();
132
+ const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
133
+ let config = {};
134
+ if (existsSync(configPath)) {
135
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
136
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath} (jsonc comments?). Skipping MCP config write; ensure "mcp.huaweicloud-devkit" points to ${mcpPath}.`);
137
+ return;
138
+ }
139
+ const existing = config.mcp?.['huaweicloud-devkit'];
140
+ if (existing && existing.type === 'local'
141
+ && Array.isArray(existing.command) && existing.command[0] === 'node'
142
+ && existing.command[1] === mcpPath) {
143
+ console.log(` OpenCode MCP config unchanged: ${configPath}`);
144
+ return;
145
+ }
146
+ }
147
+ config.mcp = config.mcp || {};
148
+ config.mcp['huaweicloud-devkit'] = {
149
+ type: 'local',
150
+ command: ['node', mcpPath],
151
+ enabled: true,
152
+ };
153
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
154
+ console.log(` OpenCode MCP config updated: ${configPath}`);
155
+ }
156
+
157
+ function removeOpenCodeConfig() {
158
+ const configPath = opencodeConfigFile();
159
+ if (!existsSync(configPath)) return;
160
+ let config = {};
161
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return; }
162
+ if (!config.mcp?.['huaweicloud-devkit']) return;
163
+ delete config.mcp['huaweicloud-devkit'];
164
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
165
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
166
+ console.log(` OpenCode MCP config cleaned: ${configPath}`);
167
+ }
168
+
169
+ function hasCodexCLI() {
170
+ const r = spawnSync('codex --version', [], { shell: true, windowsHide: true, stdio: 'pipe' });
171
+ if (r.status === 0 && r.stdout && r.stdout.toString().includes('codex')) return true;
172
+ // WindowsApps codex.exe may fail with "Access is denied"
173
+ // Fallback: check if codex exists on PATH via where.exe
174
+ if (process.platform === 'win32') {
175
+ const w = spawnSync('where.exe', ['codex'], { windowsHide: true, stdio: 'pipe' });
176
+ if (w.status === 0 && w.stdout.toString().trim()) return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ function checkHcloud() {
182
+ const bin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
183
+ if (!existsSync(bin)) return false;
184
+ try {
185
+ if (statSync(bin).size < 1024) return false;
186
+ } catch { return false; }
187
+ try {
188
+ const r = spawnSync(`"${bin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
189
+ const out = (r.stdout ? r.stdout.toString() : '') + (r.stderr ? r.stderr.toString() : '');
190
+ return r.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(out);
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ function getMarketplaceName() {
197
+ const marketplacePath = join(PACKAGE_ROOT, '.agents', 'plugins', 'marketplace.json');
198
+ try {
199
+ const manifest = JSON.parse(readFileSync(marketplacePath, 'utf8'));
200
+ if (manifest.name) return manifest.name;
201
+ } catch {}
202
+ return 'huaweicloud-devkit';
203
+ }
204
+
205
+ function installCodex() {
206
+ const marketplaceRoot = PACKAGE_ROOT;
207
+ const pluginName = 'huaweicloud-core';
208
+ const marketplaceName = getMarketplaceName();
209
+
210
+ console.log(` Registering Codex marketplace: ${marketplaceRoot}`);
211
+ const r1 = spawnSync(`codex plugin marketplace add "${marketplaceRoot}"`, [], {
212
+ shell: true, windowsHide: true, stdio: 'pipe',
213
+ });
214
+ console.log(` ${r1.stdout ? r1.stdout.toString().trim() : r1.stderr.toString().trim()}`);
215
+
216
+ if (r1.status !== 0 && /Access is denied/i.test((r1.stderr || '').toString())) {
217
+ console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
218
+ console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
219
+ return false;
220
+ }
221
+
222
+ console.log(` Installing plugin: ${pluginName}@${marketplaceName}`);
223
+ const r2 = spawnSync(`codex plugin add "${pluginName}@${marketplaceName}"`, [], {
224
+ shell: true, windowsHide: true, stdio: 'pipe',
225
+ });
226
+ console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
227
+
228
+ if (r2.status !== 0 && /Access is denied/i.test((r2.stderr || '').toString())) {
229
+ console.log(` \x1b[33mWindowsApps codex.exe permission denied.\x1b[0m`);
230
+ console.log(` \x1b[33mUse: npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
231
+ return false;
232
+ }
233
+
234
+ return true;
235
+ }
236
+
237
+ function uninstallCodex() {
238
+ const pluginName = 'huaweicloud-core';
239
+ const marketplaceName = getMarketplaceName();
240
+ console.log(` Removing Codex plugin: ${pluginName}@${marketplaceName}`);
241
+ const r = spawnSync(`codex plugin remove "${pluginName}@${marketplaceName}"`, [], {
242
+ shell: true, windowsHide: true, stdio: 'pipe',
243
+ });
244
+ console.log(` ${r.stdout ? r.stdout.toString().trim() : r.stderr.toString().trim()}`);
245
+
246
+ for (const name of new Set([marketplaceName, 'HuaweiCloud-Devkit'])) {
247
+ console.log(` Removing Codex marketplace: ${name}`);
248
+ const r2 = spawnSync(`codex plugin marketplace remove "${name}"`, [], {
249
+ shell: true, windowsHide: true, stdio: 'pipe',
250
+ });
251
+ console.log(` ${r2.stdout ? r2.stdout.toString().trim() : r2.stderr.toString().trim()}`);
252
+ }
253
+ }
254
+
255
+ function codexStatus() {
256
+ const r = spawnSync('codex plugin list', [], { shell: true, windowsHide: true, stdio: 'pipe' });
257
+ const out = r.stdout ? r.stdout.toString() : '';
258
+ return out.includes('huaweicloud-core');
259
+ }
260
+
261
+ async function installOpenCode() {
262
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
263
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
264
+ const srcDir = join(PLUGIN_ROOT, 'src');
265
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
266
+ const pluginDest = opencodePluginsDir();
267
+
268
+ copyDir(skillsSrc, opencodeSkillsDir());
269
+ console.log(` Skills -> ${opencodeSkillsDir()}`);
270
+ copyDir(commandsSrc, opencodeCommandsDir());
271
+ console.log(` Commands -> ${opencodeCommandsDir()}`);
272
+ copyDir(srcDir, join(pluginDest, 'src'));
273
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
274
+ copyDir(safetyDir, join(pluginDest, 'safety'));
275
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
276
+ updateOpenCodeConfig(pluginDest);
277
+ }
278
+
279
+ function uninstallOpenCode() {
280
+ let removed = 0;
281
+
282
+ const skills = opencodeSkillsDir();
283
+ if (existsSync(skills)) {
284
+ for (const entry of readdirSync(skills, { withFileTypes: true })) {
285
+ if (entry.name.startsWith('huawei')) {
286
+ removeIfExists(join(skills, entry.name));
287
+ removed++;
288
+ }
289
+ }
290
+ console.log(` Removed ${removed} skills`);
291
+ }
292
+
293
+ const commands = opencodeCommandsDir();
294
+ let cmdRemoved = 0;
295
+ if (existsSync(commands)) {
296
+ for (const entry of readdirSync(commands, { withFileTypes: true })) {
297
+ if (entry.name.startsWith('huawei')) {
298
+ removeIfExists(join(commands, entry.name));
299
+ cmdRemoved++;
300
+ }
301
+ }
302
+ if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
303
+ }
304
+
305
+ if (removeIfExists(opencodePluginsDir())) {
306
+ console.log(' Removed MCP server and safety policy');
307
+ }
308
+ removeOpenCodeConfig();
309
+ }
310
+
311
+ // Remove huawei* entries in targetDir that no longer exist in sourceDir (stale files from an older version).
312
+ function pruneStale(targetDir, sourceDir) {
313
+ if (!existsSync(targetDir) || !existsSync(sourceDir)) return 0;
314
+ const sourceNames = new Set(readdirSync(sourceDir));
315
+ let removed = 0;
316
+ for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
317
+ if (!entry.name.startsWith('huawei')) continue;
318
+ if (!sourceNames.has(entry.name)) {
319
+ removeIfExists(join(targetDir, entry.name));
320
+ removed++;
321
+ }
322
+ }
323
+ return removed;
324
+ }
325
+
326
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
327
+ async function updateOpenCode() {
328
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
329
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
330
+ const srcDir = join(PLUGIN_ROOT, 'src');
331
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
332
+ const pluginDest = opencodePluginsDir();
333
+
334
+ copyDir(skillsSrc, opencodeSkillsDir());
335
+ const staleSkills = pruneStale(opencodeSkillsDir(), skillsSrc);
336
+ console.log(` Skills updated -> ${opencodeSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
337
+ copyDir(commandsSrc, opencodeCommandsDir());
338
+ const staleCommands = pruneStale(opencodeCommandsDir(), commandsSrc);
339
+ console.log(` Commands updated -> ${opencodeCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
340
+ copyDir(srcDir, join(pluginDest, 'src'));
341
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
342
+ copyDir(safetyDir, join(pluginDest, 'safety'));
343
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
344
+ updateOpenCodeConfig(pluginDest);
345
+ mkdirSync(pluginDest, { recursive: true });
346
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
347
+ }
348
+
349
+ function codexMcpServerPath() {
350
+ return join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
351
+ }
352
+
353
+ function codexConfigSectionText(mcpPath) {
354
+ return [
355
+ '[mcp_servers.huaweicloud-devkit]',
356
+ 'command = "node"',
357
+ `args = ["${mcpPath}"]`,
358
+ '',
359
+ '[mcp_servers.huaweicloud-devkit.env]',
360
+ 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
361
+ '',
362
+ ].join('\n');
363
+ }
364
+
365
+ // Returns true when the config file was written, false when it was already correct.
366
+ function ensureCodexConfigSection(mcpPath) {
367
+ const configPath = codexConfigToml();
368
+ let existing = '';
369
+ if (existsSync(configPath)) {
370
+ try { existing = readFileSync(configPath, 'utf8'); } catch {}
371
+ if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
372
+ if (existing.includes(`args = ["${mcpPath}"]`)) {
373
+ console.log(` Config unchanged: ${configPath}`);
374
+ return false;
375
+ }
376
+ removeCodexConfigSection();
377
+ existing = '';
378
+ if (existsSync(configPath)) {
379
+ try { existing = readFileSync(configPath, 'utf8'); } catch {}
380
+ }
381
+ }
382
+ }
383
+ mkdirSync(dirname(configPath), { recursive: true });
384
+ if (existing && !existing.endsWith('\n')) existing += '\n';
385
+ writeFileSync(configPath, existing + codexConfigSectionText(mcpPath));
386
+ console.log(` Config updated: ${configPath}`);
387
+ return true;
388
+ }
389
+
390
+ function removeCodexConfigSection() {
391
+ const configPath = codexConfigToml();
392
+ if (!existsSync(configPath)) return;
393
+ const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
394
+ const out = [];
395
+ let skip = false;
396
+ for (const line of lines) {
397
+ if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
398
+ if (skip && line.startsWith('[')) skip = false;
399
+ if (!skip) out.push(line);
400
+ }
401
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
402
+ writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
403
+ console.log(' Config cleaned');
404
+ }
405
+
406
+ async function installCodexDesktop() {
407
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
408
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
409
+ const srcDir = join(PLUGIN_ROOT, 'src');
410
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
411
+
412
+ copyDir(skillsSrc, codexDesktopSkillsDir());
413
+ console.log(` Skills -> ${codexDesktopSkillsDir()}`);
414
+ copyDir(commandsSrc, codexDesktopCommandsDir());
415
+ console.log(` Commands -> ${codexDesktopCommandsDir()}`);
416
+ mkdirSync(codexDesktopPluginsDir(), { recursive: true });
417
+ copyDir(srcDir, join(codexDesktopPluginsDir(), 'src'));
418
+ console.log(` MCP Server -> ${join(codexDesktopPluginsDir(), 'src')}`);
419
+ copyDir(safetyDir, join(codexDesktopPluginsDir(), 'safety'));
420
+ console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
421
+
422
+ // Generate .mcp.json with absolute paths for Codex Desktop MCP server discovery
423
+ const mcpServerAbsPath = codexMcpServerPath();
424
+ const mcpConfig = {
425
+ mcpServers: {
426
+ 'huaweicloud-devkit': {
427
+ command: 'node',
428
+ args: [mcpServerAbsPath],
429
+ env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
430
+ },
431
+ },
432
+ };
433
+ writeFileSync(join(codexDesktopPluginsDir(), '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
434
+ console.log(` MCP Config -> ${join(codexDesktopPluginsDir(), '.mcp.json')}`);
435
+
436
+ // Copy .codex-plugin manifest for Codex Desktop plugin registration
437
+ const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
438
+ if (existsSync(codexPluginSrc)) {
439
+ copyDir(codexPluginSrc, join(codexDesktopPluginsDir(), '.codex-plugin'));
440
+ console.log(` Plugin Manifest -> ${join(codexDesktopPluginsDir(), '.codex-plugin')}`);
441
+ }
442
+
443
+ ensureCodexConfigSection(mcpServerAbsPath);
444
+ }
445
+
446
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
447
+ async function updateCodexDesktop() {
448
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
449
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
450
+ const srcDir = join(PLUGIN_ROOT, 'src');
451
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
452
+ const pluginDest = codexDesktopPluginsDir();
453
+
454
+ copyDir(skillsSrc, codexDesktopSkillsDir());
455
+ const staleSkills = pruneStale(codexDesktopSkillsDir(), skillsSrc);
456
+ console.log(` Skills updated -> ${codexDesktopSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
457
+ copyDir(commandsSrc, codexDesktopCommandsDir());
458
+ const staleCommands = pruneStale(codexDesktopCommandsDir(), commandsSrc);
459
+ console.log(` Commands updated -> ${codexDesktopCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
460
+ mkdirSync(pluginDest, { recursive: true });
461
+ copyDir(srcDir, join(pluginDest, 'src'));
462
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
463
+ copyDir(safetyDir, join(pluginDest, 'safety'));
464
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
465
+
466
+ const mcpServerAbsPath = codexMcpServerPath();
467
+ const mcpConfig = {
468
+ mcpServers: {
469
+ 'huaweicloud-devkit': {
470
+ command: 'node',
471
+ args: [mcpServerAbsPath],
472
+ env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
473
+ },
474
+ },
475
+ };
476
+ writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
477
+ console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
478
+
479
+ const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
480
+ if (existsSync(codexPluginSrc)) {
481
+ copyDir(codexPluginSrc, join(pluginDest, '.codex-plugin'));
482
+ console.log(` Plugin Manifest updated -> ${join(pluginDest, '.codex-plugin')}`);
483
+ }
484
+
485
+ ensureCodexConfigSection(mcpServerAbsPath);
486
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
487
+ }
488
+
489
+ function uninstallCodexDesktop() {
490
+ const skillsDir = codexDesktopSkillsDir();
491
+ let removed = 0;
492
+ if (existsSync(skillsDir)) {
493
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
494
+ if (entry.name.startsWith('huawei')) {
495
+ removeIfExists(join(skillsDir, entry.name));
496
+ removed++;
497
+ }
498
+ }
499
+ console.log(` Removed ${removed} skills`);
500
+ }
501
+
502
+ const cmdDir = codexDesktopCommandsDir();
503
+ let cmdRemoved = 0;
504
+ if (existsSync(cmdDir)) {
505
+ for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
506
+ if (entry.name.startsWith('huawei')) {
507
+ removeIfExists(join(cmdDir, entry.name));
508
+ cmdRemoved++;
509
+ }
510
+ }
511
+ if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
512
+ }
513
+
514
+ if (removeIfExists(codexDesktopPluginsDir())) {
515
+ console.log(' Removed MCP server and safety policy');
516
+ }
517
+ removeCodexConfigSection();
518
+ }
519
+
520
+ function registerCodeartsMcp(configPath) {
521
+ const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
522
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
523
+ const hcloudBin = findHcloudBin();
524
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
525
+ let config = {};
526
+ if (existsSync(configPath)) {
527
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
528
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
529
+ return;
530
+ }
531
+ const existing = config.mcpServers?.['huaweicloud-devkit'];
532
+ if (existing && existing.command === 'node'
533
+ && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
534
+ console.log(` MCP config unchanged: ${configPath}`);
535
+ return;
536
+ }
537
+ }
538
+ config.mcpServers = config.mcpServers || {};
539
+ config.mcpServers['huaweicloud-devkit'] = {
540
+ command: 'node',
541
+ args: [mcpPath],
542
+ env,
543
+ enabled: true,
544
+ };
545
+ mkdirSync(dirname(configPath), { recursive: true });
546
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
547
+ console.log(` MCP config updated: ${configPath}`);
548
+ }
549
+
550
+ async function installCodeArts() {
551
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
552
+ const srcDir = join(PLUGIN_ROOT, 'src');
553
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
554
+
555
+ copyDir(skillsSrc, codeartsSkillsDir());
556
+ console.log(` Skills -> ${codeartsSkillsDir()}`);
557
+ copyDir(skillsSrc, codeartsProjectSkillsDir());
558
+ console.log(` Skills -> ${codeartsProjectSkillsDir()}`);
559
+
560
+ const pluginDest = codeartsPluginsDir();
561
+ copyDir(srcDir, join(pluginDest, 'src'));
562
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
563
+ copyDir(safetyDir, join(pluginDest, 'safety'));
564
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
565
+
566
+ registerCodeartsMcp(codeartsMcpSettingsFile());
567
+ registerCodeartsMcp(codeartsProjectMcpSettingsFile());
568
+ }
569
+
570
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
571
+ async function updateCodeArts() {
572
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
573
+ const srcDir = join(PLUGIN_ROOT, 'src');
574
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
575
+ const pluginDest = codeartsPluginsDir();
576
+
577
+ for (const dir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
578
+ copyDir(skillsSrc, dir);
579
+ const stale = pruneStale(dir, skillsSrc);
580
+ console.log(` Skills updated -> ${dir}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
581
+ }
582
+ copyDir(srcDir, join(pluginDest, 'src'));
583
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
584
+ copyDir(safetyDir, join(pluginDest, 'safety'));
585
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
586
+ registerCodeartsMcp(codeartsMcpSettingsFile());
587
+ registerCodeartsMcp(codeartsProjectMcpSettingsFile());
588
+ mkdirSync(pluginDest, { recursive: true });
589
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
590
+ }
591
+
592
+ function uninstallCodeArts() {
593
+ let removed = 0;
594
+ for (const skillsDir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
595
+ if (!existsSync(skillsDir)) continue;
596
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
597
+ if (entry.name.startsWith('huawei')) {
598
+ removeIfExists(join(skillsDir, entry.name));
599
+ removed++;
600
+ }
601
+ }
602
+ }
603
+ if (removed > 0) console.log(` Removed ${removed} skills`);
604
+
605
+ if (removeIfExists(codeartsPluginsDir())) {
606
+ console.log(' Removed MCP server and safety policy');
607
+ }
608
+ for (const configPath of [codeartsMcpSettingsFile(), codeartsProjectMcpSettingsFile()]) {
609
+ if (!existsSync(configPath)) continue;
610
+ let config = {};
611
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
612
+ if (config.mcpServers?.['huaweicloud-devkit']) {
613
+ delete config.mcpServers['huaweicloud-devkit'];
614
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
615
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
616
+ console.log(` Config cleaned: ${configPath}`);
617
+ }
618
+ }
619
+ }
620
+
621
+ function codeartsStatus() {
622
+ const pluginDir = codeartsPluginsDir();
623
+ console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
624
+ console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
625
+ let skillCount = 0;
626
+ if (existsSync(codeartsSkillsDir())) {
627
+ skillCount = readdirSync(codeartsSkillsDir(), { withFileTypes: true })
628
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
629
+ }
630
+ console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
631
+ if (existsSync(codeartsMcpSettingsFile())) {
632
+ try {
633
+ const config = JSON.parse(readFileSync(codeartsMcpSettingsFile(), 'utf8'));
634
+ console.log(` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
635
+ } catch {
636
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
637
+ }
638
+ }
639
+ }
640
+
641
+ // Returns true when the config file was written, false when it was already correct.
642
+ function ensureWorkbuddyMcpConfig() {
643
+ const configPath = workbuddyMcpConfigFile();
644
+ const mcpPath = join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
645
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
646
+ const hcloudBin = findHcloudBin();
647
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
648
+ let config = {};
649
+ if (existsSync(configPath)) {
650
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
651
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
652
+ return false;
653
+ }
654
+ const existing = config.mcpServers?.['huaweicloud-devkit'];
655
+ if (existing && existing.command === 'node'
656
+ && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
657
+ console.log(` MCP config unchanged: ${configPath}`);
658
+ return false;
659
+ }
660
+ }
661
+ config.mcpServers = config.mcpServers || {};
662
+ config.mcpServers['huaweicloud-devkit'] = {
663
+ command: 'node',
664
+ args: [mcpPath],
665
+ env,
666
+ };
667
+ mkdirSync(dirname(configPath), { recursive: true });
668
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
669
+ console.log(` MCP config updated: ${configPath}`);
670
+ return true;
671
+ }
672
+
673
+ async function installWorkBuddy() {
674
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
675
+ const srcDir = join(PLUGIN_ROOT, 'src');
676
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
677
+ const pluginDest = workbuddyPluginsDir();
678
+
679
+ copyDir(skillsSrc, workbuddySkillsDir());
680
+ console.log(` Skills -> ${workbuddySkillsDir()}`);
681
+
682
+ copyDir(srcDir, join(pluginDest, 'src'));
683
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
684
+ copyDir(safetyDir, join(pluginDest, 'safety'));
685
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
686
+
687
+ ensureWorkbuddyMcpConfig();
688
+ }
689
+
690
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
691
+ async function updateWorkBuddy() {
692
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
693
+ const srcDir = join(PLUGIN_ROOT, 'src');
694
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
695
+ const pluginDest = workbuddyPluginsDir();
696
+
697
+ copyDir(skillsSrc, workbuddySkillsDir());
698
+ const stale = pruneStale(workbuddySkillsDir(), skillsSrc);
699
+ console.log(` Skills updated -> ${workbuddySkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
700
+ copyDir(srcDir, join(pluginDest, 'src'));
701
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
702
+ copyDir(safetyDir, join(pluginDest, 'safety'));
703
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
704
+ ensureWorkbuddyMcpConfig();
705
+ mkdirSync(pluginDest, { recursive: true });
706
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
707
+ }
708
+
709
+ function uninstallWorkBuddy() {
710
+ const skillsDir = workbuddySkillsDir();
711
+ let removed = 0;
712
+ if (existsSync(skillsDir)) {
713
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
714
+ if (entry.name.startsWith('huawei')) {
715
+ removeIfExists(join(skillsDir, entry.name));
716
+ removed++;
717
+ }
718
+ }
719
+ if (removed > 0) console.log(` Removed ${removed} skills`);
720
+ }
721
+
722
+ if (removeIfExists(workbuddyPluginsDir())) {
723
+ console.log(' Removed MCP server and safety policy');
724
+ }
725
+
726
+ const configPath = workbuddyMcpConfigFile();
727
+ if (existsSync(configPath)) {
728
+ let config = {};
729
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
730
+ if (config.mcpServers?.['huaweicloud-devkit']) {
731
+ delete config.mcpServers['huaweicloud-devkit'];
732
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
733
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
734
+ console.log(` MCP config cleaned: ${configPath}`);
735
+ }
736
+ }
737
+ }
738
+
739
+ function workbuddyStatus() {
740
+ const pluginDir = workbuddyPluginsDir();
741
+ const skillsDir = workbuddySkillsDir();
742
+ console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
743
+ console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
744
+ let skillCount = 0;
745
+ if (existsSync(skillsDir)) {
746
+ skillCount = readdirSync(skillsDir, { withFileTypes: true })
747
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
748
+ }
749
+ console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
750
+ const configPath = workbuddyMcpConfigFile();
751
+ if (existsSync(configPath)) {
752
+ try {
753
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
754
+ console.log(` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
755
+ } catch {
756
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
757
+ }
758
+ }
759
+ }
760
+
761
+ function opencodeStatus() {
762
+ const pluginDir = opencodePluginsDir();
763
+ const skillsDir = opencodeSkillsDir();
764
+ console.log(` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
765
+ console.log(` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
766
+ let skillCount = 0;
767
+ if (existsSync(skillsDir)) {
768
+ skillCount = readdirSync(skillsDir, { withFileTypes: true })
769
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei')).length;
770
+ }
771
+ console.log(` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`);
772
+ const configPath = opencodeConfigFile();
773
+ if (existsSync(configPath)) {
774
+ try {
775
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
776
+ console.log(` MCP config: ${config.mcp?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
777
+ } catch {
778
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
779
+ }
780
+ }
781
+ }
782
+
783
+ function parseTarget() {
784
+ const idx = process.argv.indexOf('--target');
785
+ if (idx < 0) return 'opencode';
786
+ const val = (process.argv[idx + 1] || '').toLowerCase();
787
+ if (val === 'codex') return 'codex';
788
+ if (val === 'codex-desktop') return 'codex-desktop';
789
+ if (val === 'codearts') return 'codearts';
790
+ if (val === 'workbuddy') return 'workbuddy';
791
+ if (val === 'all') return 'all';
792
+ return 'opencode';
793
+ }
794
+
795
+ async function cmdInstall() {
796
+ const target = parseTarget();
797
+ console.log(BANNER);
798
+ console.log(`Installing HuaweiCloud DevKit${target !== 'opencode' ? ` for ${target}` : ''}...\n`);
799
+ checkNode();
800
+
801
+ if (target === 'opencode' || target === 'all') {
802
+ console.log('[OpenCode]');
803
+ await installOpenCode();
804
+ }
805
+ if (target === 'codex-desktop' || target === 'all') {
806
+ console.log('\n[Codex Desktop]');
807
+ await installCodexDesktop();
808
+ }
809
+ if (target === 'codearts' || target === 'all') {
810
+ console.log('\n[CodeArts]');
811
+ await installCodeArts();
812
+ }
813
+ if (target === 'workbuddy' || target === 'all') {
814
+ console.log('\n[WorkBuddy]');
815
+ await installWorkBuddy();
816
+ }
817
+ if (target === 'codex' || target === 'all') {
818
+ console.log('\n[Codex]');
819
+ if (!hasCodexCLI()) {
820
+ if (target === 'codex') {
821
+ console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
822
+ if (process.platform === 'win32') {
823
+ console.log(` \x1b[33mTip: Codex Desktop on Windows installs codex.exe under WindowsApps,\x1b[0m`);
824
+ console.log(` \x1b[33m which may fail with "Access is denied". Try instead:\x1b[0m`);
825
+ console.log(` \x1b[33m npx huaweicloud-devkit install --target codex-desktop\x1b[0m`);
826
+ }
827
+ console.log(` \x1b[31mOr install Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
828
+ process.exit(1);
829
+ }
830
+ console.log(` \x1b[33mCodex CLI not found. Skipping Codex.\x1b[0m`);
831
+ if (process.platform === 'win32') {
832
+ console.log(' \x1b[33mTip: try --target codex-desktop for Codex Desktop on Windows\x1b[0m');
833
+ } else {
834
+ console.log(' Install Codex CLI to enable: npx huaweicloud-devkit install --target codex');
835
+ }
836
+ } else {
837
+ installCodex();
838
+ }
839
+ } console.log(`\n\x1b[32mInstallation complete!\x1b[0m`);
840
+ const appName = target === 'codearts' ? 'CodeArts'
841
+ : target === 'codex-desktop' ? 'Codex Desktop'
842
+ : target === 'codex' ? 'Codex'
843
+ : target === 'workbuddy' ? 'WorkBuddy'
844
+ : 'OpenCode';
845
+ const pad = ' '.repeat(24 - appName.length);
846
+ console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
847
+ console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
848
+ console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 重新打开,直接描述华为云任务即可 ║`);
849
+ console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
850
+ console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
851
+
852
+ const hcloudOk = checkHcloud();
853
+ if (!hcloudOk) {
854
+ console.log(`\n\x1b[33mKooCLI (hcloud) is not installed.`);
855
+ console.log(` Run: npx huaweicloud-devkit install-hcloud\x1b[0m`);
856
+ } else {
857
+ console.log(`\nKooCLI (hcloud) detected.`);
858
+ }
859
+
860
+ console.log(`\n\x1b[1m下一步:\x1b[0m`);
861
+ console.log(` 1. 配置统一凭据:npx huaweicloud-devkit auth init`);
862
+ console.log(` 2. 重启 ${appName} 会话(MCP 工具重启后生效)`);
863
+ console.log(` 3. 运行自检:npx huaweicloud-devkit doctor`);
864
+
865
+ // Write install marker for doctor to detect
866
+ const markerDir = target === 'codearts' ? codeartsPluginsDir()
867
+ : target === 'workbuddy' ? workbuddyPluginsDir()
868
+ : target === 'codex-desktop' ? codexDesktopPluginsDir()
869
+ : opencodePluginsDir();
870
+ mkdirSync(markerDir, { recursive: true });
871
+ writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
872
+ if (target === 'opencode' || target === 'all') {
873
+ console.log('Or describe your Huawei Cloud task in OpenCode');
874
+ }
875
+ if (target === 'codearts' || target === 'all') {
876
+ console.log('Or describe your Huawei Cloud task in CodeArts');
877
+ }
878
+ if (target === 'codex' || target === 'all') {
879
+ console.log('Or mention @huaweicloud-core in Codex');
880
+ }
881
+ if (target === 'workbuddy' || target === 'all') {
882
+ console.log('Or describe your Huawei Cloud task in WorkBuddy');
883
+ }
884
+ }
885
+
886
+ async function cmdUninstall() {
887
+ const target = parseTarget();
888
+ console.log(BANNER);
889
+ console.log(`Uninstalling HuaweiCloud DevKit${target !== 'opencode' ? ` from ${target}` : ''}...\n`);
890
+
891
+ if (target === 'opencode' || target === 'all') {
892
+ console.log('[OpenCode]');
893
+ await uninstallOpenCode();
894
+ }
895
+ if (target === 'codearts' || target === 'all') {
896
+ console.log('\n[CodeArts]');
897
+ uninstallCodeArts();
898
+ }
899
+ if (target === 'workbuddy' || target === 'all') {
900
+ console.log('\n[WorkBuddy]');
901
+ uninstallWorkBuddy();
902
+ }
903
+ if (target === 'codex-desktop' || target === 'codex' || target === 'all') {
904
+ console.log('\n[Codex]');
905
+ uninstallCodexDesktop();
906
+ if (target === 'codex' || target === 'all') {
907
+ if (!hasCodexCLI()) {
908
+ console.log(' \x1b[33mCodex CLI not found. Run "npm uninstall -g codex" to fully remove.\x1b[0m');
909
+ } else {
910
+ uninstallCodex();
911
+ }
912
+ }
913
+ }
914
+ if (target === 'all') {
915
+ const vaultPath = globalCredentialsPath();
916
+ if (removeIfExists(vaultPath)) {
917
+ console.log(' Removed credential vault');
918
+ }
919
+ const vaultDir = dirname(vaultPath);
920
+ try {
921
+ if (existsSync(vaultDir) && readdirSync(vaultDir).length === 0) {
922
+ rmSync(vaultDir, { recursive: true, force: true });
923
+ console.log(` Removed empty directory: ${vaultDir}`);
924
+ }
925
+ } catch {}
926
+ }
927
+ console.log(`\n\x1b[32mUninstall complete.\x1b[0m`);
928
+ }
929
+
930
+ async function cmdStatus() {
931
+ const target = parseTarget();
932
+ console.log(BANNER);
933
+ console.log(`HuaweiCloud DevKit Status\n`);
934
+
935
+ if (target === 'opencode' || target === 'all') {
936
+ console.log('[OpenCode]');
937
+ opencodeStatus();
938
+ }
939
+ if (target === 'codearts' || target === 'all') {
940
+ console.log('\n[CodeArts]');
941
+ codeartsStatus();
942
+ }
943
+ if (target === 'workbuddy' || target === 'all') {
944
+ console.log('\n[WorkBuddy]');
945
+ workbuddyStatus();
946
+ }
947
+ if (target === 'codex' || target === 'all') {
948
+ console.log('\n[Codex]');
949
+ if (!hasCodexCLI()) {
950
+ console.log(' \x1b[33mCodex CLI not found.\x1b[0m');
951
+ } else {
952
+ console.log(` Plugin: ${codexStatus() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`);
953
+ }
954
+ }
955
+ console.log('\nEnvironment:');
956
+ console.log(` Node.js: ${process.version}`);
957
+ console.log(` Platform: ${platform()}`);
958
+ }
959
+
960
+ async function cmdDoctor() {
961
+ console.log(BANNER);
962
+ console.log('HuaweiCloud DevKit Doctor\n');
963
+
964
+ let pass = 0, warn = 0, fail = 0;
965
+
966
+ function check(label, ok, msg) {
967
+ if (ok) { console.log(` \x1b[32m[PASS]\x1b[0m ${label}`); pass++; }
968
+ else { console.log(` \x1b[31m[FAIL]\x1b[0m ${label} — ${msg}`); fail++; }
969
+ }
970
+
971
+ // Node.js
972
+ check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
973
+
974
+ // MCP server — check OpenCode, Codex Desktop, CodeArts, and WorkBuddy paths
975
+ const opencodePluginDir = opencodePluginsDir();
976
+ const codexPluginDir = codexDesktopPluginsDir();
977
+ const workbuddyPluginDir = workbuddyPluginsDir();
978
+ const mcpOk = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs'))
979
+ || existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs'))
980
+ || existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs'));
981
+ const mcpTarget = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ? 'OpenCode'
982
+ : existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ? 'Codex Desktop'
983
+ : existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ? 'WorkBuddy' : '';
984
+ check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
985
+
986
+ if (mcpOk) {
987
+ check(`MCP server can start (${mcpTarget})`, true, '');
988
+ }
989
+
990
+ const safetyOk = existsSync(join(opencodePluginDir, 'safety', 'policy.json'))
991
+ || existsSync(join(codexPluginDir, 'safety', 'policy.json'))
992
+ || existsSync(join(workbuddyPluginDir, 'safety', 'policy.json'));
993
+ check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
994
+
995
+ // MCP config — check OpenCode, Codex Desktop, and WorkBuddy
996
+ let mcpConfigured = false;
997
+ let mcpCfgTarget = '';
998
+ const opencodeCfg = opencodeConfigFile();
999
+ if (existsSync(opencodeCfg)) {
1000
+ try {
1001
+ const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
1002
+ if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'OpenCode'; }
1003
+ } catch {}
1004
+ }
1005
+ const codexCfg = codexConfigToml();
1006
+ if (!mcpConfigured && existsSync(codexCfg)) {
1007
+ try {
1008
+ const cfg = readFileSync(codexCfg, 'utf8');
1009
+ if (cfg.includes('[mcp_servers.huaweicloud-devkit]')) { mcpConfigured = true; mcpCfgTarget = 'Codex Desktop'; }
1010
+ } catch {}
1011
+ }
1012
+ const workbuddyCfg = workbuddyMcpConfigFile();
1013
+ if (!mcpConfigured && existsSync(workbuddyCfg)) {
1014
+ try {
1015
+ const cfg = JSON.parse(readFileSync(workbuddyCfg, 'utf8'));
1016
+ if (cfg.mcpServers && cfg.mcpServers['huaweicloud-devkit']) { mcpConfigured = true; mcpCfgTarget = 'WorkBuddy'; }
1017
+ } catch {}
1018
+ }
1019
+ check('MCP configured', mcpConfigured, mcpCfgTarget ? `Found in ${mcpCfgTarget} config` : 'Run: npx huaweicloud-devkit install');
1020
+
1021
+ // hcloud CLI
1022
+ const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1023
+ const hcloudCheck = spawnSync(`"${hcloudBin}" version`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
1024
+ const hcloudOut = (hcloudCheck.stdout || '').toString() + (hcloudCheck.stderr || '').toString();
1025
+ const hcloudOk = hcloudCheck.status === 0 && /KooCLI|Current.*version|当前KooCLI/i.test(hcloudOut);
1026
+ check('hcloud CLI installed', hcloudOk, 'Run: npx huaweicloud-devkit install-hcloud');
1027
+
1028
+ // CodeArts sandbox mode warning
1029
+ const sandboxMode = detectCodeartsSandbox();
1030
+ if (sandboxMode === 'sandbox') {
1031
+ console.log(` \x1b[33m[WARN]\x1b[0m CodeArts sandbox mode active (bash_mode: sandbox)`);
1032
+ console.log(` KooCLI may fail to write config in ~/.hcloud/ and hang on the privacy agreement.`);
1033
+ console.log(` Fix: disable sandbox (Settings Permissions) or use a terminal outside CodeArts.`);
1034
+ warn++;
1035
+ }
1036
+
1037
+ if (hcloudOk) {
1038
+ const ver = (hcloudCheck.stdout.toString().match(/(\d+\.\d+\.\d+)/) || [])[1] || 'unknown';
1039
+ console.log(` Version: ${ver}`);
1040
+
1041
+ // Check auth
1042
+ const authCheck = spawnSync(`"${hcloudBin}" configure list`, [], { shell: true, windowsHide: true, stdio: 'pipe', timeout: 5000 });
1043
+ const hasAuth = authCheck.status === 0 && /access.?key/i.test(authCheck.stdout.toString());
1044
+ check('hcloud credentials configured', hasAuth, 'Run: npx huaweicloud-devkit auth init');
1045
+ }
1046
+
1047
+ // Skills
1048
+ const skillsOptions = [opencodeSkillsDir(), codexDesktopSkillsDir(), codeartsSkillsDir(), workbuddySkillsDir()];
1049
+ let skillCount = 0, skillsDir = '', missingSkills = [];
1050
+ for (const dir of skillsOptions) {
1051
+ if (!existsSync(dir)) continue;
1052
+ const entries = readdirSync(dir, { withFileTypes: true })
1053
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei'));
1054
+ const count = entries.length;
1055
+ if (count > skillCount) { skillCount = count; skillsDir = dir; }
1056
+ for (const d of entries) {
1057
+ if (!existsSync(join(dir, d.name, 'SKILL.md'))) missingSkills.push(d.name);
1058
+ }
1059
+ }
1060
+ const skillsOk = skillCount >= 6;
1061
+ check(`Skills installed (${skillCount})`, skillsOk, 'Run: npx huaweicloud-devkit install');
1062
+ if (missingSkills.length > 0) {
1063
+ console.log(` \x1b[33m[WARN]\x1b[0m ${missingSkills.length} skill(s) missing SKILL.md: ${missingSkills.join(', ')} — Run: npx huaweicloud-devkit install`);
1064
+ warn++;
1065
+ }
1066
+
1067
+ console.log(`\nResults: ${pass} pass, ${warn} warn, ${fail} fail`);
1068
+
1069
+ if (mcpConfigured && !hcloudOk) {
1070
+ console.log('\n\x1b[33mMCP is configured but hcloud is not installed. Install hcloud then restart your session.\x1b[0m');
1071
+ }
1072
+ if (fail > 0) {
1073
+ console.log('\x1b[33mFix failures above, then restart your session.\x1b[0m');
1074
+ }
1075
+ if (fail === 0 && mcpConfigured) {
1076
+ console.log('\n\x1b[32mAll checks passed.\x1b[0m Restart your session, then describe your Huawei Cloud task');
1077
+ }
1078
+
1079
+ // Detect "installed but not restarted" check all supported agents
1080
+ const installedMarkers = [
1081
+ { path: join(opencodePluginsDir(), '.installed'), name: 'OpenCode' },
1082
+ { path: join(codexDesktopPluginsDir(), '.installed'), name: 'Codex Desktop' },
1083
+ { path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
1084
+ { path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
1085
+ ];
1086
+ for (const marker of installedMarkers) {
1087
+ if (existsSync(marker.path)) {
1088
+ console.log(`\n\x1b[1m\x1b[31m╔══════════════════════════════════════════╗`);
1089
+ console.log(`\x1b[1m\x1b[31m║ 请重启 ${marker.name}!MCP 工具尚未激活 ║`);
1090
+ console.log(`\x1b[1m\x1b[31m║ 关闭当前会话 重新打开即可 ║`);
1091
+ console.log(`\x1b[1m\x1b[31m╚══════════════════════════════════════════╝\x1b[0m`);
1092
+ break;
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ async function cmdUpdate() {
1098
+ console.log(BANNER);
1099
+ const target = parseTarget();
1100
+
1101
+ if (target === 'opencode') {
1102
+ if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1103
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1104
+ return;
1105
+ }
1106
+ console.log('[OpenCode]');
1107
+ await updateOpenCode();
1108
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1109
+ console.log(`\x1b[33mMCP 工具在重启 OpenCode 会话后才生效。\x1b[0m`);
1110
+ return;
1111
+ }
1112
+
1113
+ if (target === 'codex-desktop') {
1114
+ if (!existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1115
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1116
+ return;
1117
+ }
1118
+ console.log('[Codex Desktop]');
1119
+ await updateCodexDesktop();
1120
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1121
+ console.log(`\x1b[33mMCP 工具在重启 Codex Desktop 会话后才生效。\x1b[0m`);
1122
+ return;
1123
+ }
1124
+
1125
+ if (target === 'codex') {
1126
+ if (!hasCodexCLI()) {
1127
+ console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
1128
+ if (process.platform === 'win32') {
1129
+ console.log(` \x1b[33mTip: use --target codex-desktop for Codex Desktop on Windows\x1b[0m`);
1130
+ }
1131
+ console.log(` \x1b[31mInstall Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
1132
+ process.exitCode = 1;
1133
+ return;
1134
+ }
1135
+ console.log('[Codex]');
1136
+ installCodex();
1137
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1138
+ console.log(`\x1b[33mRestart the Codex session for changes to take effect.\x1b[0m`);
1139
+ return;
1140
+ }
1141
+
1142
+ if (target === 'codearts') {
1143
+ if (!existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1144
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1145
+ return;
1146
+ }
1147
+ console.log('[CodeArts]');
1148
+ await updateCodeArts();
1149
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1150
+ console.log(`\x1b[33mMCP 工具在重启 CodeArts 会话后才生效。\x1b[0m`);
1151
+ return;
1152
+ }
1153
+
1154
+ if (target === 'workbuddy') {
1155
+ if (!existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
1156
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1157
+ return;
1158
+ }
1159
+ console.log('[WorkBuddy]');
1160
+ await updateWorkBuddy();
1161
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1162
+ console.log(`\x1b[33mMCP 工具在重启 WorkBuddy 会话后才生效。\x1b[0m`);
1163
+ return;
1164
+ }
1165
+
1166
+ if (target === 'all') {
1167
+ let updatedAny = false;
1168
+ if (existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1169
+ console.log('[OpenCode]');
1170
+ await updateOpenCode();
1171
+ updatedAny = true;
1172
+ }
1173
+ if (existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1174
+ console.log('\n[Codex Desktop]');
1175
+ await updateCodexDesktop();
1176
+ updatedAny = true;
1177
+ }
1178
+ if (existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1179
+ console.log('\n[CodeArts]');
1180
+ await updateCodeArts();
1181
+ updatedAny = true;
1182
+ }
1183
+ if (existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
1184
+ console.log('\n[WorkBuddy]');
1185
+ await updateWorkBuddy();
1186
+ updatedAny = true;
1187
+ }
1188
+ if (codexStatus()) {
1189
+ console.log('\n[Codex]');
1190
+ installCodex();
1191
+ updatedAny = true;
1192
+ }
1193
+ if (!updatedAny) {
1194
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1195
+ return;
1196
+ }
1197
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1198
+ console.log(`\x1b[33mMCP 工具在重启各 agent 会话后才生效。\x1b[0m`);
1199
+ return;
1200
+ }
1201
+
1202
+ await cmdUninstall();
1203
+ console.log('');
1204
+ await cmdInstall();
1205
+ }
1206
+
1207
+ async function cmdReinstall() {
1208
+ console.log(BANNER);
1209
+ if (!(await confirm('This will remove and reinstall all HuaweiCloud DevKit files. Continue?'))) {
1210
+ console.log('Cancelled.');
1211
+ return;
1212
+ }
1213
+ confirmed = true;
1214
+ await cmdUninstall();
1215
+ console.log('');
1216
+ await cmdInstall();
1217
+ }
1218
+
1219
+ let confirmed = false;
1220
+ async function confirm(msg) {
1221
+ if (confirmed) return true;
1222
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1223
+ return new Promise((ok) => {
1224
+ rl.question(`${msg} [y/N] `, (a) => {
1225
+ rl.close();
1226
+ ok(a.toLowerCase() === 'y' || a.toLowerCase() === 'yes');
1227
+ });
1228
+ });
1229
+ }
1230
+
1231
+ async function cmdInstallHcloud() {
1232
+ console.log(BANNER);
1233
+ console.log('Installing KooCLI (hcloud)...\n');
1234
+
1235
+ const os = platform();
1236
+ const arch = process.arch;
1237
+ const baseUrl = 'https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest';
1238
+ const installDir = os === 'win32'
1239
+ ? join(homedir(), 'hcloud')
1240
+ : join(homedir(), '.local', 'bin');
1241
+
1242
+ if (os === 'win32') {
1243
+ const url = `${baseUrl}/huaweicloud-cli-windows-amd64.zip`;
1244
+ const zipPath = join(installDir, 'hcloud.zip');
1245
+
1246
+ console.log(`[Windows] Auto-installing to ${installDir}...`);
1247
+
1248
+ try {
1249
+ mkdirSync(installDir, { recursive: true });
1250
+
1251
+ // Download
1252
+ console.log(` Downloading ${url}...`);
1253
+ const psCmd = `[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '${url}' -OutFile '${zipPath}' -UseBasicParsing`;
1254
+ const dl = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { stdio: 'inherit', windowsHide: true, timeout: 180000 });
1255
+ if (dl.status !== 0) throw new Error(`下载失败 (PowerShell exit ${dl.status})`);
1256
+
1257
+ // Extract
1258
+ console.log(' Extracting...');
1259
+ const ex = spawnSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -Path '${zipPath}' -DestinationPath '${installDir}' -Force`], { stdio: 'inherit', windowsHide: true, timeout: 60000 });
1260
+ if (ex.status !== 0) throw new Error(`解压失败 (PowerShell exit ${ex.status})`);
1261
+ if (!existsSync(join(installDir, 'hcloud.exe'))) throw new Error('hcloud.exe 未生成,可能已被安全软件拦截');
1262
+
1263
+ // Clean up zip
1264
+ rmSync(zipPath, { force: true });
1265
+
1266
+ // Add to user PATH (append + dedupe within the User scope only; never copy
1267
+ // session/system entries into the user PATH, and never use setx PATH which
1268
+ // overwrites the whole variable and truncates at 1024 chars).
1269
+ console.log(' Adding to user PATH...');
1270
+ const pathPs = [
1271
+ '$ErrorActionPreference = "Stop"',
1272
+ `$target = '${installDir.replace(/'/g, "''")}'`,
1273
+ '$cur = [Environment]::GetEnvironmentVariable("Path", "User")',
1274
+ 'if (-not $cur) { $cur = "" }',
1275
+ '$parts = @($cur -split ";" | Where-Object { $_ -ne "" })',
1276
+ 'if ($parts -notcontains $target) {',
1277
+ ' [Environment]::SetEnvironmentVariable("Path", (@($parts) + $target) -join ";", "User")',
1278
+ ' Write-Output " Added to user PATH (deduped): $target"',
1279
+ '} else {',
1280
+ ' Write-Output " Already in user PATH: $target"',
1281
+ '}',
1282
+ ].join('; ');
1283
+ spawnSync('powershell', ['-NoProfile', '-Command', pathPs], { stdio: 'inherit', windowsHide: true, timeout: 30000 });
1284
+
1285
+ console.log(`\n\x1b[32mInstall complete.\x1b[0m`);
1286
+ console.log(` Verify: ${join(installDir, 'hcloud.exe')} version`);
1287
+
1288
+ const hcloudBin = join(installDir, 'hcloud.exe');
1289
+
1290
+ // Ask user before accepting the privacy agreement — never auto-accept.
1291
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1292
+ const agree = await new Promise((resolve) => {
1293
+ rl.question('\n KooCLI requires accepting its privacy agreement. Do you accept? (y/N) ', (answer) => {
1294
+ rl.close();
1295
+ resolve(/^\s*y\s*$/i.test(answer));
1296
+ });
1297
+ });
1298
+ if (agree) {
1299
+ const r = spawnSync(hcloudBin, ['version'], { input: 'y\n', encoding: 'utf8', timeout: 10000, windowsHide: true });
1300
+ if (r.status === 0) {
1301
+ console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
1302
+ } else {
1303
+ console.log(' \x1b[33m无法写入配置目录。请在码道外终端运行: echo "y" | hcloud version\x1b[0m');
1304
+ }
1305
+ } else {
1306
+ console.log(' \x1b[33m请手动接受隐私协议:在终端运行 hcloud version 并按提示操作\x1b[0m');
1307
+ }
1308
+
1309
+ console.log(' Or restart terminal and: hcloud version');
1310
+ } catch (e) {
1311
+ console.log(`\n\x1b[33mAuto-install failed: ${e.message}\x1b[0m`);
1312
+ console.log(` Manual: download ${url}, unzip to ${installDir}, add to PATH`);
1313
+ console.log(` Guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_01.html`);
1314
+ if (detectCodeartsSandbox() === 'sandbox') {
1315
+ printSandboxWarning('沙箱模式拦截了 KooCLI 自动安装(无法创建/写入安装目录)。');
1316
+ }
1317
+ }
1318
+ } else if (os === 'linux') {
1319
+ console.log('[Linux] One-liner install:');
1320
+ console.log(' curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y');
1321
+ console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64' : 'AMD64'}`);
1322
+ const pkg = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
1323
+ console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
1324
+ console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
1325
+ console.log(` mv hcloud ~/.local/bin/`);
1326
+ console.log(` hcloud version`);
1327
+ console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_02.html`);
1328
+ } else if (os === 'darwin') {
1329
+ console.log('[macOS] One-liner install:');
1330
+ console.log(' curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y');
1331
+ console.log(`\nOr manual: ${arch === 'arm64' ? 'ARM64 (Apple Silicon)' : 'AMD64 (Intel)'}`);
1332
+ const pkg = arch === 'arm64' ? 'mac-arm64' : 'mac-amd64';
1333
+ console.log(` curl -LO "${baseUrl}/huaweicloud-cli-${pkg}.tar.gz"`);
1334
+ console.log(` tar -zxvf huaweicloud-cli-${pkg}.tar.gz`);
1335
+ console.log(` mv hcloud /usr/local/bin/`);
1336
+ console.log(` hcloud version`);
1337
+ console.log(`\nFull guide: https://support.huaweicloud.com/qs-hcli/hcli_02_003_03.html`);
1338
+ }
1339
+
1340
+ console.log('\nAfter install, set HCLOUD_BIN if hcloud is not on PATH.');
1341
+ console.log('\n\x1b[1m\x1b[33m=== Configure credentials SAFELY ===\x1b[0m');
1342
+ console.log(' Unified credentials (recommended): npx huaweicloud-devkit auth init');
1343
+ console.log(' KooCLI only (alternative): hcloud configure init');
1344
+ console.log(' NEVER: hcloud configure set --cli-access-key=xxx (AK/SK in shell history!)');
1345
+ console.log('\nThen run: npx huaweicloud-devkit doctor');
1346
+ }
1347
+
1348
+ function readLineQuestion(prompt) {
1349
+ return new Promise((resolve) => {
1350
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1351
+ rl.question(prompt, (answer) => {
1352
+ rl.close();
1353
+ resolve(answer.trim());
1354
+ });
1355
+ });
1356
+ }
1357
+
1358
+ async function readSecret(prompt) {
1359
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1360
+ throw new Error(
1361
+ `Cannot read "${prompt.trim()}" securely in a non-interactive session. Set HW_ACCESS_KEY/HW_SECRET_KEY environment variables instead, or run "npx huaweicloud-devkit auth init" in a real terminal.`
1362
+ );
1363
+ }
1364
+
1365
+ process.stdout.write(prompt);
1366
+ const wasRaw = process.stdin.isRaw;
1367
+ process.stdin.setRawMode(true);
1368
+ process.stdin.resume();
1369
+
1370
+ return new Promise((resolve) => {
1371
+ let value = '';
1372
+ const onData = (chunk) => {
1373
+ for (const ch of chunk.toString('utf8')) {
1374
+ if (ch === '\r' || ch === '\n') {
1375
+ cleanup();
1376
+ resolve(value.trim());
1377
+ return;
1378
+ }
1379
+ if (ch === '\u0003') {
1380
+ cleanup();
1381
+ process.exit(130);
1382
+ }
1383
+ if (ch === '\b' || ch === '\u007f') {
1384
+ value = value.slice(0, -1);
1385
+ continue;
1386
+ }
1387
+ value += ch;
1388
+ }
1389
+ };
1390
+ const cleanup = () => {
1391
+ process.stdin.setRawMode(wasRaw);
1392
+ process.stdin.pause();
1393
+ process.stdin.off('data', onData);
1394
+ process.stdout.write('\n');
1395
+ };
1396
+ process.stdin.on('data', onData);
1397
+ });
1398
+ }
1399
+
1400
+ function configureHcloud(credentials) {
1401
+ const hcloudBin = findHcloudBin() || (process.env.HCLOUD_BIN || 'hcloud');
1402
+ const args = [
1403
+ 'configure',
1404
+ 'set',
1405
+ `--cli-access-key=${credentials.ak}`,
1406
+ `--cli-secret-key=${credentials.sk}`,
1407
+ `--cli-region=${credentials.region || ''}`,
1408
+ ];
1409
+ const r = spawnSync(hcloudBin, args, {
1410
+ shell: false,
1411
+ windowsHide: true,
1412
+ stdio: 'pipe',
1413
+ timeout: 30000,
1414
+ });
1415
+ return {
1416
+ ok: r.status === 0,
1417
+ code: r.status,
1418
+ error: String(r.stderr || '').trim().slice(0, 240),
1419
+ };
1420
+ }
1421
+
1422
+ function printAuthAgents(agents = {}) {
1423
+ for (const [agent, info] of Object.entries(agents)) {
1424
+ console.log(` ${agent}: ${info.configured ? '[OK]' : '[MISSING]'}`);
1425
+ }
1426
+ }
1427
+
1428
+ function printAuthStatus(status) {
1429
+ console.log(`Credentials vault: ${status.credentialsConfigured ? 'configured' : 'missing'} (${status.credentialsPath})`);
1430
+ console.log(`OBS config: ${status.obsConfigured ? 'configured' : 'missing'} (${status.obsConfigPath})`);
1431
+ console.log(`KooCLI: ${status.kooCliInstalled ? 'installed' : 'missing'}`);
1432
+ console.log('Agent MCP registration:');
1433
+ printAuthAgents(status.agents);
1434
+ }
1435
+
1436
+ async function cmdAuthInit() {
1437
+ console.log(BANNER);
1438
+ console.log('HuaweiCloud DevKit Unified Authentication Setup\n');
1439
+ console.log('\x1b[1m获取 AK/SK(如果还没有):\x1b[0m');
1440
+ console.log(' 1. 打开华为云"访问密钥"页签:');
1441
+ console.log(' https://console.huaweicloud.com/iam/?region=cn-north-4#/mine/accessKey');
1442
+ console.log(' 2. 点击"新增访问密钥",完成身份验证');
1443
+ console.log(' 3. 下载凭证文件(内含 AK 和 SK)。');
1444
+ console.log(' 注意:SK 只在创建密钥时显示一次,请妥善保存该文件。\n');
1445
+
1446
+ let ak = process.env.HW_ACCESS_KEY || '';
1447
+ let sk = process.env.HW_SECRET_KEY || '';
1448
+ let securityToken = process.env.HW_SECURITY_TOKEN || '';
1449
+ let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
1450
+
1451
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
1452
+ if (!interactive && (!ak || !sk)) {
1453
+ console.error('\x1b[31mNon-interactive session detected. Provide credentials via environment variables instead:\x1b[0m');
1454
+ console.error(' HW_ACCESS_KEY, HW_SECRET_KEY');
1455
+ console.error(' (Or run "npx huaweicloud-devkit auth init" in a real terminal.)');
1456
+ process.exitCode = 1;
1457
+ return;
1458
+ }
1459
+
1460
+ if (!ak) ak = await readLineQuestion('Access Key ID (AK): ');
1461
+ if (!sk) sk = await readSecret('Secret Access Key (SK): ');
1462
+ if (interactive && !securityToken) securityToken = await readLineQuestion('Security Token (optional, press Enter to skip): ');
1463
+ if (!region) region = 'cn-north-4';
1464
+
1465
+ if (!ak || !sk) {
1466
+ console.error('\nAK and SK are required.');
1467
+ process.exitCode = 1;
1468
+ return;
1469
+ }
1470
+
1471
+
1472
+ const vaultPath = writeGlobalCredentials({ ak, sk, securityToken, region });
1473
+
1474
+ try {
1475
+ writeObsConfig({ ak, sk, securityToken, region });
1476
+ } catch (error) {
1477
+ console.log(`OBS config sync failed: ${error.message}`);
1478
+ }
1479
+
1480
+ if (findHcloudBin()) {
1481
+ const result = configureHcloud({ ak, sk, region });
1482
+ if (!result.ok) console.log(`KooCLI update failed: ${result.error || result.code}`);
1483
+ } else {
1484
+ console.log('KooCLI not found. Run "npx huaweicloud-devkit install-hcloud" and then "auth sync".');
1485
+ }
1486
+
1487
+ console.log('\nCredentials synchronized.');
1488
+ console.log('\nNext steps:');
1489
+ console.log(' npx huaweicloud-devkit install --target all');
1490
+ console.log(' Restart your agent sessions.');
1491
+ }
1492
+
1493
+ async function cmdAuthSync() {
1494
+ const target = parseTarget();
1495
+ console.log(BANNER);
1496
+ console.log('Synchronizing Huawei Cloud authentication...\n');
1497
+
1498
+ const credentials = readGlobalCredentials();
1499
+ if (!credentials?.ak || !credentials?.sk) {
1500
+ console.error('No global credentials found. Run "npx huaweicloud-devkit auth init" first.');
1501
+ process.exitCode = 1;
1502
+ return;
1503
+ }
1504
+
1505
+ const result = syncAuth(target);
1506
+ if (result.ok) {
1507
+ console.log('Credentials synchronized.');
1508
+ } else {
1509
+ console.error(result.error);
1510
+ }
1511
+ }
1512
+
1513
+ async function cmdAuthStatus() {
1514
+ const target = parseTarget();
1515
+ console.log(BANNER);
1516
+ console.log('HuaweiCloud DevKit Authentication Status\n');
1517
+ printAuthStatus(getAuthStatus(target));
1518
+ }
1519
+
1520
+ async function cmdAuth() {
1521
+ const sub = (process.argv[3] || 'status').toLowerCase();
1522
+ if (sub === 'init' || sub === 'setup') return cmdAuthInit();
1523
+ if (sub === 'sync' || sub === 'refresh') return cmdAuthSync();
1524
+ return cmdAuthStatus();
1525
+ }
1526
+
1527
+ async function main() {
1528
+ const cmd = process.argv[2] || 'help';
1529
+
1530
+ switch (cmd) {
1531
+ case 'install':
1532
+ case 'i':
1533
+ await cmdInstall();
1534
+ break;
1535
+ case 'uninstall':
1536
+ case 'remove':
1537
+ await cmdUninstall();
1538
+ break;
1539
+ case 'update':
1540
+ case 'upgrade':
1541
+ await cmdUpdate();
1542
+ break;
1543
+ case 'reinstall':
1544
+ await cmdReinstall();
1545
+ break;
1546
+ case 'status':
1547
+ case 'info':
1548
+ await cmdStatus();
1549
+ break;
1550
+ case 'doctor':
1551
+ case 'check':
1552
+ await cmdDoctor();
1553
+ break;
1554
+ case 'install-hcloud':
1555
+ await cmdInstallHcloud();
1556
+ break;
1557
+ case 'auth':
1558
+ await cmdAuth();
1559
+ break;
1560
+ case 'help':
1561
+ case '--help':
1562
+ case '-h':
1563
+ default:
1564
+ console.log(BANNER);
1565
+ console.log('Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|all>]\n');
1566
+ console.log('Commands:');
1567
+ console.log(' install Install skills, MCP server, safety policy');
1568
+ console.log(' uninstall Remove installed files');
1569
+ console.log(' update Update to latest version');
1570
+ console.log(' reinstall Full clean reinstall');
1571
+ console.log(' status Show installation status');
1572
+ console.log(' doctor Self-check: hcloud, MCP, skills, auth');
1573
+ console.log(' install-hcloud Show KooCLI install commands for your OS');
1574
+ console.log(' auth Manage unified auth: init | sync | status');
1575
+ console.log(' help Show this help');
1576
+ console.log('\nOptions:');
1577
+ console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, all');
1578
+ console.log('\nExamples:');
1579
+ console.log(' npx huaweicloud-devkit install');
1580
+ console.log(' npx huaweicloud-devkit install --target codex');
1581
+ console.log(' npx huaweicloud-devkit install --target codearts');
1582
+ console.log(' npx huaweicloud-devkit install --target workbuddy');
1583
+ console.log(' npx huaweicloud-devkit install --target all');
1584
+ console.log(' npx huaweicloud-devkit auth init');
1585
+ console.log(' npx huaweicloud-devkit auth sync --target all');
1586
+ console.log(' npx huaweicloud-devkit auth status --target all');
1587
+ break;
1588
+ }
1589
+ }
1590
+
1591
+ main().catch((e) => {
1592
+ console.error(`\x1b[31mError: ${e.message}\x1b[0m`);
1593
+ process.exit(1);
1594
+ });